Skip to content

Commit b80cf1b

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 d98f5d6 commit b80cf1b

27 files changed

+1714
-73
lines changed

packages/core/src/baseclient.ts

Lines changed: 19 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';
@@ -139,6 +140,24 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
139140
public captureEvent(event: Event, hint?: EventHint, scope?: Scope): string | undefined {
140141
let eventId: string | undefined = hint && hint.event_id;
141142

143+
if (this._options.autoSessionTracking) {
144+
const eventType = event.type || 'event';
145+
const isException = eventType === 'event';
146+
147+
// If the event is of type Exception, then a request session should be captured
148+
if (isException && scope) {
149+
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
150+
const requestSessionStatus = (scope as any)._requestSessionStatus;
151+
152+
// Ensure that this is happening within a request, and make sure not to override if Errored/Crashed
153+
if (requestSessionStatus !== undefined) {
154+
(scope as any)._requestSessionStatus = RequestSessionStatus.Errored;
155+
}
156+
157+
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
158+
}
159+
}
160+
142161
this._process(
143162
this._captureEvent(event, hint, scope).then(result => {
144163
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: 40 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';
@@ -477,6 +477,45 @@ describe('BaseClient', () => {
477477
});
478478
});
479479

480+
test('If autoSessionTracking is disabled, requestSession status should not be set', () => {
481+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: false });
482+
const scope = new Scope();
483+
(scope as any)._requestSessionStatus = RequestSessionStatus.Ok;
484+
client.captureEvent({ message: 'message' }, undefined, scope);
485+
486+
const requestSessionStatus = (scope as any)._requestSessionStatus;
487+
expect(requestSessionStatus).toEqual(RequestSessionStatus.Ok);
488+
});
489+
490+
test('When captureEvent is called with an exception, requestSession status should be set to Errored', () => {
491+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
492+
const scope = new Scope();
493+
(scope as any)._requestSessionStatus = RequestSessionStatus.Ok;
494+
client.captureEvent({ message: 'message' }, undefined, scope);
495+
496+
const requestSessionStatus = (scope as any)._requestSessionStatus;
497+
expect(requestSessionStatus).toEqual(RequestSessionStatus.Errored);
498+
});
499+
500+
test('When captureEvent is called with an exception but outside of a request, then requestStatus should not be set', () => {
501+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
502+
const scope = new Scope();
503+
504+
client.captureEvent({ message: 'message' }, undefined, scope);
505+
506+
expect((scope as any)._requestSessionStatus).toEqual(undefined);
507+
});
508+
509+
test('When captureEvent is called with a transaction, then requestStatus should not be set', () => {
510+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
511+
const scope = new Scope();
512+
(scope as any)._requestSessionStatus = RequestSessionStatus.Ok;
513+
client.captureEvent({ message: 'message', type: 'transaction' }, undefined, scope);
514+
515+
const requestSessionStatus = (scope as any)._requestSessionStatus;
516+
expect(requestSessionStatus).toEqual(RequestSessionStatus.Ok);
517+
});
518+
480519
test('normalizes event with default depth of 3', () => {
481520
expect.assertions(1);
482521
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 _requestSessionStatus?: RequestSessionStatus;
71+
6872
/**
6973
* Inherit values from the parent scope.
7074
* @param scope to clone.
@@ -83,6 +87,7 @@ export class Scope implements ScopeInterface {
8387
newScope._transactionName = scope._transactionName;
8488
newScope._fingerprint = scope._fingerprint;
8589
newScope._eventProcessors = [...scope._eventProcessors];
90+
newScope._requestSessionStatus = scope._requestSessionStatus;
8691
}
8792
return newScope;
8893
}
@@ -288,6 +293,9 @@ export class Scope implements ScopeInterface {
288293
this._tags = { ...this._tags, ...captureContext._tags };
289294
this._extra = { ...this._extra, ...captureContext._extra };
290295
this._contexts = { ...this._contexts, ...captureContext._contexts };
296+
if (captureContext._requestSessionStatus) {
297+
this._requestSessionStatus = captureContext._requestSessionStatus;
298+
}
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.requestSessionStatus) {
324+
this._requestSessionStatus = captureContext.requestSessionStatus;
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._requestSessionStatus = undefined;
329341
this._level = undefined;
330342
this._transactionName = undefined;
331343
this._fingerprint = undefined;

0 commit comments

Comments
 (0)