Skip to content

Commit 812f317

Browse files
committed
feat(node): Application mode sessions
Add API to capture application mode sessions associated with a specific release, and send them to Sentry as part of the Release Health functionality.
1 parent 10938a7 commit 812f317

File tree

7 files changed

+171
-25
lines changed

7 files changed

+171
-25
lines changed

packages/node/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,12 @@
5656
"fix": "run-s fix:eslint fix:prettier",
5757
"fix:prettier": "prettier --write \"{src,test}/**/*.ts\"",
5858
"fix:eslint": "eslint . --format stylish --fix",
59-
"test": "run-s test:jest test:express test:webpack",
59+
"test": "run-s test:jest test:express test:webpack test:release-health",
6060
"test:jest": "jest",
6161
"test:watch": "jest --watch",
6262
"test:express": "node test/manual/express-scope-separation/start.js",
6363
"test:webpack": "cd test/manual/webpack-domain/ && yarn && node npm-build.js",
64+
"test:release-health": "node test/manual/release-health/*.js",
6465
"pack": "npm pack"
6566
},
6667
"volta": {

packages/node/src/transports/base.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { API, eventToSentryRequest, SDK_VERSION } from '@sentry/core';
2-
import { Event, Response, Status, Transport, TransportOptions } from '@sentry/types';
1+
import { API, SDK_VERSION } from '@sentry/core';
2+
import { Event, Response, SentryRequest, Status, Transport, TransportOptions } from '@sentry/types';
33
import { logger, parseRetryAfterHeader, PromiseBuffer, SentryError } from '@sentry/utils';
44
import * as fs from 'fs';
55
import * as http from 'http';
@@ -96,7 +96,10 @@ export abstract class BaseTransport implements Transport {
9696
}
9797

9898
/** JSDoc */
99-
protected async _sendWithModule(httpModule: HTTPModule, event: Event): Promise<Response> {
99+
protected async _send(sentryReq: SentryRequest): Promise<Response> {
100+
if (!this.module) {
101+
throw new SentryError('No module available');
102+
}
100103
if (new Date(Date.now()) < this._disabledUntil) {
101104
return Promise.reject(new SentryError(`Transport locked till ${this._disabledUntil} due to too many requests.`));
102105
}
@@ -106,10 +109,11 @@ export abstract class BaseTransport implements Transport {
106109
}
107110
return this._buffer.add(
108111
new Promise<Response>((resolve, reject) => {
109-
const sentryReq = eventToSentryRequest(event, this._api);
112+
if (!this.module) {
113+
throw new SentryError('No module available');
114+
}
110115
const options = this._getRequestOptions(new url.URL(sentryReq.url));
111-
112-
const req = httpModule.request(options, (res: http.IncomingMessage) => {
116+
const req = this.module.request(options, (res: http.IncomingMessage) => {
113117
const statusCode = res.statusCode || 500;
114118
const status = Status.fromHttpCode(statusCode);
115119

packages/node/src/transports/http.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Event, Response, TransportOptions } from '@sentry/types';
2-
import { SentryError } from '@sentry/utils';
1+
import { eventToSentryRequest, sessionToSentryRequest } from '@sentry/core';
2+
import { Event, Response, Session, TransportOptions } from '@sentry/types';
33
import * as http from 'http';
44

55
import { BaseTransport } from './base';
@@ -20,9 +20,13 @@ export class HTTPTransport extends BaseTransport {
2020
* @inheritDoc
2121
*/
2222
public sendEvent(event: Event): Promise<Response> {
23-
if (!this.module) {
24-
throw new SentryError('No module available in HTTPTransport');
25-
}
26-
return this._sendWithModule(this.module, event);
23+
return this._send(eventToSentryRequest(event, this._api));
24+
}
25+
26+
/**
27+
* @inheritDoc
28+
*/
29+
public sendSession(session: Session): PromiseLike<Response> {
30+
return this._send(sessionToSentryRequest(session, this._api));
2731
}
2832
}

packages/node/src/transports/https.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Event, Response, TransportOptions } from '@sentry/types';
2-
import { SentryError } from '@sentry/utils';
1+
import { eventToSentryRequest, sessionToSentryRequest } from '@sentry/core';
2+
import { Event, Response, Session, TransportOptions } from '@sentry/types';
33
import * as https from 'https';
44

55
import { BaseTransport } from './base';
@@ -20,9 +20,13 @@ export class HTTPSTransport extends BaseTransport {
2020
* @inheritDoc
2121
*/
2222
public sendEvent(event: Event): Promise<Response> {
23-
if (!this.module) {
24-
throw new SentryError('No module available in HTTPSTransport');
25-
}
26-
return this._sendWithModule(this.module, event);
23+
return this._send(eventToSentryRequest(event, this._api));
24+
}
25+
26+
/**
27+
* @inheritDoc
28+
*/
29+
public sendSession(session: Session): PromiseLike<Response> {
30+
return this._send(sessionToSentryRequest(session, this._api));
2731
}
2832
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const http = require('http');
2+
const express = require('express');
3+
const app = express();
4+
const Sentry = require('../../../dist');
5+
6+
function assertSessions(actual, expected) {
7+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
8+
console.error('FAILED: Sessions do not match');
9+
process.exit(1);
10+
}
11+
}
12+
13+
function constructStrippedSessionObject(actual) {
14+
const { init, status, errors, release } = actual;
15+
return { init, status, errors, release };
16+
}
17+
18+
let remaining = 2;
19+
20+
class DummyTransport {
21+
sendSession(session) {
22+
if (session.did === 'ahmed') {
23+
assertSessions(constructStrippedSessionObject(session),
24+
{
25+
init: true,
26+
status: "ok",
27+
errors: 0,
28+
release: "1.1"
29+
}
30+
)
31+
}
32+
else if (session.did === 'ahmed2') {
33+
assertSessions(constructStrippedSessionObject(session),
34+
{
35+
init: true,
36+
status: "ok",
37+
errors: 1,
38+
release: "1.1"
39+
}
40+
)
41+
}
42+
--remaining;
43+
44+
if (!remaining) {
45+
console.error('SUCCESS: All application mode sessions were sent to node transport as expected');
46+
server.close();
47+
process.exit(0);
48+
}
49+
50+
return Promise.resolve({
51+
status: 'success',
52+
});
53+
}
54+
}
55+
56+
Sentry.init({
57+
dsn: 'http://[email protected]/1337',
58+
release: '1.1',
59+
transport: DummyTransport,
60+
});
61+
62+
63+
app.use(Sentry.Handlers.requestHandler());
64+
65+
app.get('/foo', (req) => {
66+
const currentHub = Sentry.getCurrentHub();
67+
currentHub.startSession({user: {username: 'ahmed'}});
68+
currentHub.captureSession();
69+
});
70+
71+
app.get('/bar', req => {
72+
const currentHub = Sentry.getCurrentHub();
73+
currentHub.startSession({user: {username: 'ahmed2'}});
74+
throw new Error('bar');
75+
currentHub.captureSession();
76+
});
77+
78+
app.use(Sentry.Handlers.errorHandler());
79+
80+
const server = app.listen(0, () => {
81+
const port = server.address().port;
82+
http.get(`http://localhost:${port}/foo`);
83+
http.get(`http://localhost:${port}/bar`);
84+
});
85+

packages/node/test/transports/http.test.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Session } from '@sentry/hub';
12
import { TransportOptions } from '@sentry/types';
23
import { SentryError } from '@sentry/utils';
34
import * as HttpsProxyAgent from 'https-proxy-agent';
@@ -6,7 +7,8 @@ import { HTTPTransport } from '../../src/transports/http';
67

78
const mockSetEncoding = jest.fn();
89
const dsn = 'http://[email protected]:8989/mysubpath/50622';
9-
const transportPath = '/mysubpath/api/50622/store/';
10+
const storePath = '/mysubpath/api/50622/store/';
11+
const envelopePath = '/mysubpath/api/50622/envelope/';
1012
let mockReturnCode = 200;
1113
let mockHeaders = {};
1214

@@ -27,12 +29,12 @@ function createTransport(options: TransportOptions): HTTPTransport {
2729
return transport;
2830
}
2931

30-
function assertBasicOptions(options: any): void {
32+
function assertBasicOptions(options: any, useEnvelope: boolean = false): void {
3133
expect(options.headers['X-Sentry-Auth']).toContain('sentry_version');
3234
expect(options.headers['X-Sentry-Auth']).toContain('sentry_client');
3335
expect(options.headers['X-Sentry-Auth']).toContain('sentry_key');
3436
expect(options.port).toEqual('8989');
35-
expect(options.path).toEqual(transportPath);
37+
expect(options.path).toEqual(useEnvelope ? envelopePath : storePath);
3638
expect(options.hostname).toEqual('sentry.io');
3739
}
3840

@@ -69,6 +71,28 @@ describe('HTTPTransport', () => {
6971
}
7072
});
7173

74+
test('send 200 session', async () => {
75+
const transport = createTransport({ dsn });
76+
await transport.sendSession(new Session());
77+
78+
const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0];
79+
assertBasicOptions(requestOptions, true);
80+
expect(mockSetEncoding).toHaveBeenCalled();
81+
});
82+
83+
test('send 400 session', async () => {
84+
mockReturnCode = 400;
85+
const transport = createTransport({ dsn });
86+
87+
try {
88+
await transport.sendSession(new Session());
89+
} catch (e) {
90+
const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0];
91+
assertBasicOptions(requestOptions, true);
92+
expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`));
93+
}
94+
});
95+
7296
test('send x-sentry-error header', async () => {
7397
mockReturnCode = 429;
7498
mockHeaders = {

packages/node/test/transports/https.test.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Session } from '@sentry/hub';
12
import { TransportOptions } from '@sentry/types';
23
import { SentryError } from '@sentry/utils';
34
import * as HttpsProxyAgent from 'https-proxy-agent';
@@ -6,7 +7,8 @@ import { HTTPSTransport } from '../../src/transports/https';
67

78
const mockSetEncoding = jest.fn();
89
const dsn = 'https://[email protected]:8989/mysubpath/50622';
9-
const transportPath = '/mysubpath/api/50622/store/';
10+
const storePath = '/mysubpath/api/50622/store/';
11+
const envelopePath = '/mysubpath/api/50622/envelope/';
1012
let mockReturnCode = 200;
1113
let mockHeaders = {};
1214

@@ -33,12 +35,12 @@ function createTransport(options: TransportOptions): HTTPSTransport {
3335
return transport;
3436
}
3537

36-
function assertBasicOptions(options: any): void {
38+
function assertBasicOptions(options: any, useEnvelope: boolean = false): void {
3739
expect(options.headers['X-Sentry-Auth']).toContain('sentry_version');
3840
expect(options.headers['X-Sentry-Auth']).toContain('sentry_client');
3941
expect(options.headers['X-Sentry-Auth']).toContain('sentry_key');
4042
expect(options.port).toEqual('8989');
41-
expect(options.path).toEqual(transportPath);
43+
expect(options.path).toEqual(useEnvelope ? envelopePath : storePath);
4244
expect(options.hostname).toEqual('sentry.io');
4345
}
4446

@@ -75,6 +77,28 @@ describe('HTTPSTransport', () => {
7577
}
7678
});
7779

80+
test('send 200 session', async () => {
81+
const transport = createTransport({ dsn });
82+
await transport.sendSession(new Session());
83+
84+
const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0];
85+
assertBasicOptions(requestOptions, true);
86+
expect(mockSetEncoding).toHaveBeenCalled();
87+
});
88+
89+
test('send 400 session', async () => {
90+
mockReturnCode = 400;
91+
const transport = createTransport({ dsn });
92+
93+
try {
94+
await transport.sendSession(new Session());
95+
} catch (e) {
96+
const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0];
97+
assertBasicOptions(requestOptions, true);
98+
expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`));
99+
}
100+
});
101+
78102
test('send x-sentry-error header', async () => {
79103
mockReturnCode = 429;
80104
mockHeaders = {

0 commit comments

Comments
 (0)