Skip to content

Commit 3326a3e

Browse files
committed
feat(node): Add release health feature to Node.js
Captures request mode sessions associated with a specific release, and sends them to the server as part of the release health functionality. Also added functionality that captures application mode sessions in Node.js
1 parent 6587399 commit 3326a3e

31 files changed

+1856
-76
lines changed

packages/core/src/baseclient.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Integration,
88
IntegrationClass,
99
Options,
10+
RequestSessionStatus,
1011
SessionStatus,
1112
Severity,
1213
} from '@sentry/types';
@@ -100,6 +101,24 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
100101
public captureException(exception: any, hint?: EventHint, scope?: Scope): string | undefined {
101102
let eventId: string | undefined = hint && hint.event_id;
102103

104+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
105+
if (this._options.autoSessionTracking && scope) {
106+
const requestSession = (scope as any)._requestSession;
107+
108+
// Necessary check to ensure this is code block is executed only within a request
109+
if (requestSession.status !== undefined) {
110+
const scopeExtras = (scope as any)._extra;
111+
// Set status to crashed, if captureException call is made from either onunhandledrejection handler
112+
// or the onuncaughtexception handler
113+
if (scopeExtras.unhandledPromiseRejection || scopeExtras.onUncaughtException) {
114+
requestSession.status = RequestSessionStatus.Crashed;
115+
} else {
116+
requestSession.status = RequestSessionStatus.Errored;
117+
}
118+
}
119+
}
120+
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
121+
103122
this._process(
104123
this._getBackend()
105124
.eventFromException(exception, hint)
@@ -139,6 +158,24 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
139158
public captureEvent(event: Event, hint?: EventHint, scope?: Scope): string | undefined {
140159
let eventId: string | undefined = hint && hint.event_id;
141160

161+
if (this._options.autoSessionTracking) {
162+
const eventType = event.type || 'event';
163+
const isException = eventType === 'event';
164+
165+
// If the event is of type Exception, then a request session should be captured
166+
if (isException && scope) {
167+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
168+
const requestSession = (scope as any)._requestSession;
169+
170+
// Ensure that this is happening within a request, and make sure not to override if Errored/Crashed
171+
if (requestSession.status === RequestSessionStatus.Ok) {
172+
requestSession.status = RequestSessionStatus.Errored;
173+
}
174+
175+
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
176+
}
177+
}
178+
142179
this._process(
143180
this._captureEvent(event, hint, scope).then(result => {
144181
eventId = result;

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export { addGlobalEventProcessor, getCurrentHub, getHubFromCarrier, Hub, makeMai
1717
export { API } from './api';
1818
export { BaseClient } from './baseclient';
1919
export { BackendClass, BaseBackend } from './basebackend';
20-
export { eventToSentryRequest, sessionToSentryRequest } from './request';
20+
export { eventToSentryRequest, sessionToSentryRequest, aggregateSessionsToSentryRequest } from './request';
2121
export { initAndBind, ClientClass } from './sdk';
2222
export { NoopTransport } from './transports/noop';
2323
export { SDK_VERSION } from './version';

packages/core/src/request.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Event, SdkInfo, SentryRequest, Session } from '@sentry/types';
1+
import { AggregatedSessions, Event, SdkInfo, SentryRequest, Session } from '@sentry/types';
22

33
import { API } from './api';
44

@@ -49,6 +49,26 @@ export function sessionToSentryRequest(session: Session, api: API): SentryReques
4949
};
5050
}
5151

52+
/** Creates a SentryRequest from an Aggregate of Request mode sessions */
53+
export function aggregateSessionsToSentryRequest(aggregatedSessions: AggregatedSessions, api: API): SentryRequest {
54+
const sdkInfo = getSdkMetadataForEnvelopeHeader(api);
55+
const envelopeHeaders = JSON.stringify({
56+
sent_at: new Date().toISOString(),
57+
...(sdkInfo && { sdk: sdkInfo }),
58+
});
59+
// The server expects the headers for request mode sessions to be `sessions` while the SDK considers
60+
// request mode sessions to be of type `session`
61+
const itemHeaders = JSON.stringify({
62+
type: 'sessions',
63+
});
64+
65+
return {
66+
body: `${envelopeHeaders}\n${itemHeaders}\n${JSON.stringify(aggregatedSessions)}`,
67+
type: 'session',
68+
url: api.getEnvelopeEndpointWithUrlEncodedAuth(),
69+
};
70+
}
71+
5272
/** Creates a SentryRequest from an event. */
5373
export function eventToSentryRequest(event: Event, api: API): SentryRequest {
5474
const sdkInfo = getSdkMetadataForEnvelopeHeader(api);

packages/core/test/lib/base.test.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Hub, Scope } from '@sentry/hub';
2-
import { Event, Severity, Span } from '@sentry/types';
2+
import { Event, RequestSessionStatus, Severity, Span } from '@sentry/types';
33
import { logger, SentryError, SyncPromise } from '@sentry/utils';
44

55
import { TestBackend } from '../mocks/backend';
@@ -238,6 +238,67 @@ describe('BaseClient', () => {
238238
}),
239239
);
240240
});
241+
242+
test('when autoSessionTracking is disabled, does not set requestSession status', () => {
243+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: false });
244+
const scope = new Scope();
245+
const requestSession = (scope as any)._requestSession;
246+
requestSession.status = RequestSessionStatus.Ok;
247+
client.captureException(new Error('test exception'), {}, scope);
248+
expect(requestSession).toEqual({ status: RequestSessionStatus.Ok });
249+
});
250+
251+
test('Does not set requestSession status when an exception occurs outside of a request', () => {
252+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
253+
const scope = new Scope();
254+
client.captureException(new Error('test exception'), {}, scope);
255+
expect((scope as any)._requestSession).toEqual({});
256+
});
257+
258+
test('Sets requestSession status to Errored when an exception occurs within a request', () => {
259+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
260+
const scope = new Scope();
261+
const requestSession = (scope as any)._requestSession;
262+
requestSession.status = RequestSessionStatus.Ok;
263+
client.captureException(new Error('test exception'), {}, scope);
264+
expect(requestSession).toEqual({ status: RequestSessionStatus.Errored });
265+
});
266+
267+
test('Sets requestSession status to Crashed when an onuncaughtexception occurs within a request', () => {
268+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
269+
const scope = new Scope();
270+
const requestSession = (scope as any)._requestSession;
271+
requestSession.status = RequestSessionStatus.Ok;
272+
scope.setExtra('onUncaughtException', true);
273+
client.captureException(new Error('test exception'), {}, scope);
274+
expect(requestSession).toEqual({ status: RequestSessionStatus.Crashed });
275+
});
276+
277+
test('Does not set requestSession status when an onunhandledrejection occurs outside a request', () => {
278+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
279+
const scope = new Scope();
280+
const requestSession = (scope as any)._requestSession;
281+
requestSession.status = RequestSessionStatus.Ok;
282+
scope.setExtra('unhandledPromiseRejection', true);
283+
client.captureException(new Error('test exception'), {}, scope);
284+
expect(requestSession).toEqual({ status: RequestSessionStatus.Crashed });
285+
});
286+
287+
test('Does not set requestSession status when an onuncaughtexception occurs outside a request', () => {
288+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
289+
const scope = new Scope();
290+
scope.setExtra('onUncaughtException', true);
291+
client.captureException(new Error('test exception'), {}, scope);
292+
expect((scope as any)._requestSession).toEqual({});
293+
});
294+
295+
test('Sets requestSession status to Crashed when an onunhandledrejection occurs within a request', () => {
296+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
297+
const scope = new Scope();
298+
scope.setExtra('unhandledPromiseRejection', true);
299+
client.captureException(new Error('test exception'), {}, scope);
300+
expect((scope as any)._requestSession).toEqual({});
301+
});
241302
});
242303

243304
describe('captureMessage', () => {
@@ -477,6 +538,49 @@ describe('BaseClient', () => {
477538
});
478539
});
479540

541+
test('If autoSessionTracking is disabled, requestSession status should not be set', () => {
542+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: false });
543+
const scope = new Scope();
544+
const requestSession = (scope as any)._requestSession;
545+
requestSession.status = RequestSessionStatus.Ok;
546+
client.captureEvent({ message: 'message' }, undefined, scope);
547+
expect(requestSession).toEqual({ status: RequestSessionStatus.Ok });
548+
});
549+
550+
test('When captureEvent is called with an exception, requestSession status should be set to Errored', () => {
551+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
552+
const scope = new Scope();
553+
const requestSession = (scope as any)._requestSession;
554+
requestSession.status = RequestSessionStatus.Ok;
555+
client.captureEvent({ message: 'message' }, undefined, scope);
556+
expect(requestSession).toEqual({ status: RequestSessionStatus.Errored });
557+
});
558+
559+
test('When captureEvent is called with an exception and, requestSession status is Crashed, it should not be overridden', () => {
560+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
561+
const scope = new Scope();
562+
const requestSession = (scope as any)._requestSession;
563+
requestSession.status = RequestSessionStatus.Crashed;
564+
client.captureEvent({ message: 'message' }, undefined, scope);
565+
expect(requestSession).toEqual({ status: RequestSessionStatus.Crashed });
566+
});
567+
568+
test('When captureEvent is called with an exception but outside of a request, then requestStatus should not be set', () => {
569+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
570+
const scope = new Scope();
571+
client.captureEvent({ message: 'message' }, undefined, scope);
572+
expect((scope as any)._requestSession).toEqual({});
573+
});
574+
575+
test('When captureEvent is called with a transaction, then requestStatus should not be set', () => {
576+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
577+
const scope = new Scope();
578+
const requestSession = (scope as any)._requestSession;
579+
requestSession.status = RequestSessionStatus.Ok;
580+
client.captureEvent({ message: 'message', type: 'transaction' }, undefined, scope);
581+
expect(requestSession).toEqual({ status: RequestSessionStatus.Ok });
582+
});
583+
480584
test('normalizes event with default depth of 3', () => {
481585
expect.assertions(1);
482586
const client = new TestClient({ dsn: PUBLIC_DSN });

packages/core/test/lib/request.test.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
import { DebugMeta, Event, SentryRequest, TransactionSamplingMethod } from '@sentry/types';
22

33
import { API } from '../../src/api';
4-
import { eventToSentryRequest } from '../../src/request';
4+
import { aggregateSessionsToSentryRequest, eventToSentryRequest } from '../../src/request';
5+
6+
const api = new API('https://[email protected]/12312012', {
7+
sdk: {
8+
integrations: ['AWSLambda'],
9+
name: 'sentry.javascript.browser',
10+
version: `12.31.12`,
11+
packages: [{ name: 'npm:@sentry/browser', version: `12.31.12` }],
12+
},
13+
});
514

615
describe('eventToSentryRequest', () => {
16+
let event: Event;
717
function parseEnvelopeRequest(request: SentryRequest): any {
818
const [envelopeHeaderString, itemHeaderString, eventString] = request.body.split('\n');
919

@@ -14,16 +24,6 @@ describe('eventToSentryRequest', () => {
1424
};
1525
}
1626

17-
const api = new API('https://[email protected]/12312012', {
18-
sdk: {
19-
integrations: ['AWSLambda'],
20-
name: 'sentry.javascript.browser',
21-
version: `12.31.12`,
22-
packages: [{ name: 'npm:@sentry/browser', version: `12.31.12` }],
23-
},
24-
});
25-
let event: Event;
26-
2727
beforeEach(() => {
2828
event = {
2929
contexts: { trace: { trace_id: '1231201211212012', span_id: '12261980', op: 'pageload' } },
@@ -125,3 +125,32 @@ describe('eventToSentryRequest', () => {
125125
);
126126
});
127127
});
128+
129+
describe('aggregateSessionsToSentryRequest', () => {
130+
it('test envelope creation for aggregateSessions', () => {
131+
const aggregatedSession = {
132+
attrs: { release: '1.0.x', environment: 'prod' },
133+
aggregates: [{ started: '2021-04-08T12:18:00.000Z', exited: 2 }],
134+
};
135+
const result = aggregateSessionsToSentryRequest(aggregatedSession, api);
136+
137+
const [envelopeHeaderString, itemHeaderString, sessionString] = result.body.split('\n');
138+
139+
expect(JSON.parse(envelopeHeaderString)).toEqual(
140+
expect.objectContaining({
141+
sdk: { name: 'sentry.javascript.browser', version: '12.31.12' },
142+
}),
143+
);
144+
expect(JSON.parse(itemHeaderString)).toEqual(
145+
expect.objectContaining({
146+
type: 'sessions',
147+
}),
148+
);
149+
expect(JSON.parse(sessionString)).toEqual(
150+
expect.objectContaining({
151+
attrs: { release: '1.0.x', environment: 'prod' },
152+
aggregates: [{ started: '2021-04-08T12:18:00.000Z', exited: 2 }],
153+
}),
154+
);
155+
});
156+
});

packages/hub/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// eslint-disable-next-line deprecation/deprecation
22
export { Carrier, DomainAsCarrier, Layer } from './interfaces';
33
export { addGlobalEventProcessor, Scope } from './scope';
4-
export { Session } from './session';
4+
export { Session, SessionFlusher } from './session';
55
export {
66
// eslint-disable-next-line deprecation/deprecation
77
getActiveDomain,

packages/hub/src/scope.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Extra,
1111
Extras,
1212
Primitive,
13+
RequestSessionStatus,
1314
Scope as ScopeInterface,
1415
ScopeContext,
1516
Severity,
@@ -65,6 +66,9 @@ export class Scope implements ScopeInterface {
6566
/** Session */
6667
protected _session?: Session;
6768

69+
/** Request Mode Session Status */
70+
protected _requestSession: { status?: RequestSessionStatus } = {};
71+
6872
/**
6973
* Inherit values from the parent scope.
7074
* @param scope to clone.
@@ -83,6 +87,9 @@ export class Scope implements ScopeInterface {
8387
newScope._transactionName = scope._transactionName;
8488
newScope._fingerprint = scope._fingerprint;
8589
newScope._eventProcessors = [...scope._eventProcessors];
90+
if (scope._requestSession) {
91+
newScope._requestSession = scope._requestSession;
92+
}
8693
}
8794
return newScope;
8895
}
@@ -288,6 +295,7 @@ export class Scope implements ScopeInterface {
288295
this._tags = { ...this._tags, ...captureContext._tags };
289296
this._extra = { ...this._extra, ...captureContext._extra };
290297
this._contexts = { ...this._contexts, ...captureContext._contexts };
298+
this._requestSession = { ...this._requestSession, ...captureContext._requestSession };
291299
if (captureContext._user && Object.keys(captureContext._user).length) {
292300
this._user = captureContext._user;
293301
}
@@ -312,6 +320,9 @@ export class Scope implements ScopeInterface {
312320
if (captureContext.fingerprint) {
313321
this._fingerprint = captureContext.fingerprint;
314322
}
323+
if (captureContext.requestSession) {
324+
this._requestSession = captureContext.requestSession;
325+
}
315326
}
316327

317328
return this;
@@ -326,6 +337,7 @@ export class Scope implements ScopeInterface {
326337
this._extra = {};
327338
this._user = {};
328339
this._contexts = {};
340+
this._requestSession = {};
329341
this._level = undefined;
330342
this._transactionName = undefined;
331343
this._fingerprint = undefined;

0 commit comments

Comments
 (0)