Skip to content

Commit 38a5058

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 4bbb553 commit 38a5058

File tree

14 files changed

+340
-31
lines changed

14 files changed

+340
-31
lines changed

packages/browser/src/backend.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,6 @@ export interface BrowserOptions extends Options {
2929

3030
/** @deprecated use {@link Options.denyUrls} instead. */
3131
blacklistUrls?: Array<string | RegExp>;
32-
33-
/**
34-
* A flag enabling Sessions Tracking feature.
35-
* By default, Sessions Tracking is enabled.
36-
*/
37-
autoSessionTracking?: boolean;
3832
}
3933

4034
/**

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/single-session/healthy-session.js && node test/manual/release-health/single-session/caught-exception-errored-session.js && node test/manual/release-health/single-session/uncaught-exception-crashed-session.js && node test/manual/release-health/single-session/unhandled-rejection-crashed-session.js",
6465
"pack": "npm pack"
6566
},
6667
"volta": {

packages/node/src/sdk.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,16 @@ export function init(options: NodeOptions = {}): void {
9696
const detectedRelease = getSentryRelease();
9797
if (detectedRelease !== undefined) {
9898
options.release = detectedRelease;
99+
} else {
100+
// If release is not provided, then we should disable autoSessionTracking
101+
options.autoSessionTracking = false;
99102
}
100103
}
101104

105+
if (options.autoSessionTracking === undefined) {
106+
options.autoSessionTracking = true;
107+
}
108+
102109
if (options.environment === undefined && process.env.SENTRY_ENVIRONMENT) {
103110
options.environment = process.env.SENTRY_ENVIRONMENT;
104111
}
@@ -109,6 +116,10 @@ export function init(options: NodeOptions = {}): void {
109116
}
110117

111118
initAndBind(NodeClient, options);
119+
120+
if (options.autoSessionTracking) {
121+
startSessionTracking();
122+
}
112123
}
113124

114125
/**
@@ -148,6 +159,20 @@ export async function close(timeout?: number): Promise<boolean> {
148159
return Promise.reject(false);
149160
}
150161

162+
/**
163+
* Function that takes an instance of NodeClient and checks if autoSessionTracking option is enabled for that client
164+
*/
165+
export function isAutoSessionTrackingEnabled(client?: NodeClient): boolean {
166+
if (client === undefined) {
167+
return false;
168+
}
169+
const clientOptions: NodeOptions = client && client.getOptions();
170+
if (clientOptions && clientOptions.autoSessionTracking !== undefined) {
171+
return clientOptions.autoSessionTracking;
172+
}
173+
return false;
174+
}
175+
151176
/**
152177
* Returns a release dynamically from environment variables.
153178
*/
@@ -180,3 +205,18 @@ export function getSentryRelease(fallback?: string): string | undefined {
180205
fallback
181206
);
182207
}
208+
209+
/**
210+
* Enable automatic Session Tracking for the node process.
211+
*/
212+
function startSessionTracking(): void {
213+
const hub = getCurrentHub();
214+
hub.startSession();
215+
// Emitted in the case of healthy sessions, error of `mechanism.handled: true` and unhandledrejections because
216+
// The 'beforeExit' event is not emitted for conditions causing explicit termination,
217+
// such as calling process.exit() or uncaught exceptions.
218+
// Ref: https://nodejs.org/api/process.html#process_event_beforeexit
219+
process.on('beforeExit', () => {
220+
hub.endSession();
221+
});
222+
}

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 { DsnProtocol, Event, Response, Status, Transport, TransportOptions } from '@sentry/types';
1+
import { API, SDK_VERSION } from '@sentry/core';
2+
import { DsnProtocol, 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';
@@ -124,7 +124,10 @@ export abstract class BaseTransport implements Transport {
124124
}
125125

126126
/** JSDoc */
127-
protected async _sendWithModule(httpModule: HTTPModule, event: Event): Promise<Response> {
127+
protected async _send(sentryReq: SentryRequest): Promise<Response> {
128+
if (!this.module) {
129+
throw new SentryError('No module available');
130+
}
128131
if (new Date(Date.now()) < this._disabledUntil) {
129132
return Promise.reject(new SentryError(`Transport locked till ${this._disabledUntil} due to too many requests.`));
130133
}
@@ -134,10 +137,11 @@ export abstract class BaseTransport implements Transport {
134137
}
135138
return this._buffer.add(
136139
new Promise<Response>((resolve, reject) => {
137-
const sentryReq = eventToSentryRequest(event, this._api);
140+
if (!this.module) {
141+
throw new SentryError('No module available');
142+
}
138143
const options = this._getRequestOptions(new url.URL(sentryReq.url));
139-
140-
const req = httpModule.request(options, (res: http.IncomingMessage) => {
144+
const req = this.module.request(options, (res: http.IncomingMessage) => {
141145
const statusCode = res.statusCode || 500;
142146
const status = Status.fromHttpCode(statusCode);
143147

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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const Sentry = require('../../../../dist');
2+
const { assertSessions, constructStrippedSessionObject, BaseDummyTransport } = require('./test-utils');
3+
4+
let sessionCounter = 0;
5+
process.on('exit', ()=> {
6+
console.log('SUCCESS: All application mode sessions were sent to node transport as expected');
7+
})
8+
9+
class DummyTransport extends BaseDummyTransport {
10+
sendSession(session) {
11+
sessionCounter++;
12+
if (sessionCounter === 1) {
13+
assertSessions(constructStrippedSessionObject(session),
14+
{
15+
init: true,
16+
status: 'ok',
17+
errors: 1,
18+
release: '1.1'
19+
}
20+
)
21+
}
22+
else if (sessionCounter === 2) {
23+
assertSessions(constructStrippedSessionObject(session),
24+
{
25+
init: false,
26+
status: 'exited',
27+
errors: 1,
28+
release: '1.1'
29+
}
30+
)
31+
}
32+
else {
33+
console.log('FAIL: Received way too many Sessions!');
34+
process.exit(1);
35+
}
36+
return super.sendSession(session);
37+
}
38+
}
39+
40+
Sentry.init({
41+
dsn: 'http://[email protected]/1337',
42+
release: '1.1',
43+
transport: DummyTransport,
44+
});
45+
46+
/**
47+
* The following code snippet will capture exceptions of `mechanism.handled` equal to `true`, and so these sessions
48+
* are treated as Errored Sessions.
49+
* In this case, we have two session updates sent; First Session sent is due to the call to CaptureException that
50+
* extracts event data and uses it to update the Session and sends it. The second session update is sent on the
51+
* `beforeExit` event which happens right before the process exits.
52+
*/
53+
try {
54+
throw new Error('hey there')
55+
}
56+
catch(e) {
57+
Sentry.captureException(e);
58+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const Sentry = require('../../../../dist');
2+
const { assertSessions, constructStrippedSessionObject, BaseDummyTransport } = require('./test-utils');
3+
4+
class DummyTransport extends BaseDummyTransport {
5+
sendSession(session) {
6+
assertSessions(constructStrippedSessionObject(session),
7+
{
8+
init: true,
9+
status: 'exited',
10+
errors: 0,
11+
release: '1.1'
12+
}
13+
)
14+
console.log('SUCCESS: All application mode sessions were sent to node transport as expected');
15+
return super.sendSession(session);
16+
}
17+
}
18+
19+
Sentry.init({
20+
dsn: 'http://[email protected]/1337',
21+
release: '1.1',
22+
transport: DummyTransport
23+
});
24+
25+
/**
26+
* This script or process, start a Session on init object, and calls endSession on `beforeExit` of the process, which
27+
* sends a healthy session to the Server.
28+
*/
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
function assertSessions(actual, expected) {
2+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
3+
console.error('FAILED: Sessions do not match');
4+
process.exit(1);
5+
}
6+
}
7+
8+
function constructStrippedSessionObject(actual) {
9+
const { init, status, errors, release, did } = actual;
10+
return { init, status, errors, release, did };
11+
}
12+
13+
class BaseDummyTransport {
14+
sendEvent(event) {
15+
return Promise.resolve({
16+
status: 'success',
17+
});
18+
}
19+
sendSession(session) {
20+
return Promise.resolve({
21+
status: 'success',
22+
});
23+
}
24+
close(timeout) {
25+
return Promise.resolve(true);
26+
}
27+
}
28+
29+
module.exports = { assertSessions, constructStrippedSessionObject, BaseDummyTransport };
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const Sentry = require('../../../../dist');
2+
const { assertSessions, constructStrippedSessionObject, BaseDummyTransport } = require('./test-utils');
3+
4+
process.on('exit', ()=> {
5+
console.log('SUCCESS: All application mode sessions were sent to node transport as expected');
6+
})
7+
8+
class DummyTransport extends BaseDummyTransport {
9+
sendSession(session) {
10+
assertSessions(constructStrippedSessionObject(session),
11+
{
12+
init: true,
13+
status: 'crashed',
14+
errors: 1,
15+
release: '1.1'
16+
}
17+
)
18+
process.exit(0);
19+
}
20+
}
21+
22+
Sentry.init({
23+
dsn: 'http://[email protected]/1337',
24+
release: '1.1',
25+
transport: DummyTransport
26+
});
27+
/**
28+
* The following code snippet will throw an exception of `mechanism.handled` equal to `false`, and so this session
29+
* is considered a Crashed Session.
30+
* In this case, we have only session update that is sent, which is sent due to the call to CaptureException that
31+
* extracts event data and uses it to update the Session and send it. No secondary session update in this case because
32+
* we explicitly exit the process in the onUncaughtException handler and so the `beforeExit` event is not fired.
33+
*/
34+
throw new Error('test error')
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const Sentry = require('../../../../dist');
2+
const { assertSessions, constructStrippedSessionObject, BaseDummyTransport } = require('./test-utils');
3+
4+
let sessionCounter = 0;
5+
process.on('exit', ()=> {
6+
console.log('SUCCESS: All application mode sessions were sent to node transport as expected');
7+
})
8+
9+
class DummyTransport extends BaseDummyTransport {
10+
sendSession(session) {
11+
sessionCounter++;
12+
13+
if (sessionCounter === 1) {
14+
assertSessions(constructStrippedSessionObject(session),
15+
{
16+
init: true,
17+
status: 'crashed',
18+
errors: 1,
19+
release: '1.1'
20+
}
21+
)
22+
}
23+
else if (sessionCounter === 2){
24+
assertSessions(constructStrippedSessionObject(session),
25+
{
26+
init: false,
27+
status: 'crashed',
28+
errors: 1,
29+
release: '1.1'
30+
}
31+
)
32+
}
33+
else {
34+
console.log('FAIL: Received way too many Sessions!');
35+
process.exit(1);
36+
}
37+
38+
return super.sendSession(session);
39+
}
40+
}
41+
42+
Sentry.init({
43+
dsn: 'http://[email protected]/1337',
44+
release: '1.1',
45+
transport: DummyTransport
46+
});
47+
48+
/**
49+
* The following code snippet will throw an exception of `mechanism.handled` equal to `false`, and so this session
50+
* is treated as a Crashed Session.
51+
* In this case, we have two session updates sent; First Session sent is due to the call to CaptureException that
52+
* extracts event data and uses it to update the Session and sends it. The second session update is sent on the
53+
* `beforeExit` event which happens right before the process exits.
54+
*/
55+
new Promise(function(resolve, reject) {
56+
reject();
57+
}).then(function() {
58+
console.log('Promise Resolved');
59+
});

0 commit comments

Comments
 (0)