Skip to content

Commit dc88889

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 fbdc875 commit dc88889

33 files changed

+1862
-75
lines changed

packages/browser/src/transports/base.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const CATEGORY_MAPPING: {
1515
event: 'error',
1616
transaction: 'transaction',
1717
session: 'session',
18+
sessions: 'sessions',
1819
};
1920

2021
/** Base Transport class implementation */

packages/browser/test/unit/transports/fetch.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ describe('FetchTransport', () => {
366366
.returns(afterLimit);
367367

368368
const headers = new Headers();
369-
headers.set('X-Sentry-Rate-Limits', `${retryAfterSeconds}:error;transaction:scope`);
369+
headers.set('X-Sentry-Rate-Limits', `${retryAfterSeconds}::scope`);
370370
fetch.returns(Promise.resolve({ status: 429, headers }));
371371

372372
try {

packages/core/src/baseclient.ts

Lines changed: 25 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,19 @@ 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+
if (this._options.autoSessionTracking && scope) {
105+
const requestSession = scope.getRequestSession();
106+
// Necessary check to ensure this is code block is executed only within a request
107+
if (requestSession.status !== undefined) {
108+
const scopeExtras = scope.getExtras();
109+
if (scopeExtras.unhandledPromiseRejection || scopeExtras.onUncaughtException) {
110+
requestSession.status = RequestSessionStatus.Crashed;
111+
} else {
112+
requestSession.status = RequestSessionStatus.Errored;
113+
}
114+
}
115+
}
116+
103117
this._process(
104118
this._getBackend()
105119
.eventFromException(exception, hint)
@@ -139,6 +153,17 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
139153
public captureEvent(event: Event, hint?: EventHint, scope?: Scope): string | undefined {
140154
let eventId: string | undefined = hint && hint.event_id;
141155

156+
if (this._options.autoSessionTracking) {
157+
const isTransaction = event.type === 'transaction';
158+
159+
if (!isTransaction && scope) {
160+
const requestSession = scope.getRequestSession();
161+
if (requestSession.status === RequestSessionStatus.Ok) {
162+
requestSession.status = RequestSessionStatus.Errored;
163+
}
164+
}
165+
}
166+
142167
this._process(
143168
this._captureEvent(event, hint, scope).then(result => {
144169
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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Event, SdkInfo, SentryRequest, Session } from '@sentry/types';
1+
import { AggregatedSessions, Event, SdkInfo, SentryRequest, Session } from '@sentry/types';
2+
import { logger } from '@sentry/utils';
23

34
import { API } from './api';
45

@@ -31,7 +32,7 @@ function enhanceEventWithSdkInfo(event: Event, sdkInfo?: SdkInfo): Event {
3132
return event;
3233
}
3334

34-
/** Creates a SentryRequest from an event. */
35+
/** Creates a SentryRequest from a Session. */
3536
export function sessionToSentryRequest(session: Session, api: API): SentryRequest {
3637
const sdkInfo = getSdkMetadataForEnvelopeHeader(api);
3738
const envelopeHeaders = JSON.stringify({
@@ -49,6 +50,24 @@ export function sessionToSentryRequest(session: Session, api: API): SentryReques
4950
};
5051
}
5152

53+
/** Creates a SentryRequest from an Aggregate of Request mode sessions */
54+
export function aggregateSessionsToSentryRequest(aggregatedSessions: AggregatedSessions, api: API): SentryRequest {
55+
const sdkInfo = getSdkMetadataForEnvelopeHeader(api);
56+
const envelopeHeaders = JSON.stringify({
57+
sent_at: new Date().toISOString(),
58+
...(sdkInfo && { sdk: sdkInfo }),
59+
});
60+
const itemHeaders = JSON.stringify({
61+
type: 'sessions',
62+
});
63+
64+
return {
65+
body: `${envelopeHeaders}\n${itemHeaders}\n${JSON.stringify(aggregatedSessions)}`,
66+
type: 'sessions',
67+
url: api.getEnvelopeEndpointWithUrlEncodedAuth(),
68+
};
69+
}
70+
5271
/** Creates a SentryRequest from an event. */
5372
export function eventToSentryRequest(event: Event, api: API): SentryRequest {
5473
const sdkInfo = getSdkMetadataForEnvelopeHeader(api);

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

Lines changed: 97 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,63 @@ 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+
scope.setRequestSession(RequestSessionStatus.Ok);
246+
client.captureException(new Error('test exception'), {}, scope);
247+
expect(scope.getRequestSession()).toEqual({ status: RequestSessionStatus.Ok });
248+
});
249+
250+
test('Does not set requestSession status when an exception occurs outside of a request', () => {
251+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
252+
const scope = new Scope();
253+
client.captureException(new Error('test exception'), {}, scope);
254+
expect(scope.getRequestSession()).toEqual({});
255+
});
256+
257+
test('Sets requestSession status to Errored when an exception occurs within a request', () => {
258+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
259+
const scope = new Scope();
260+
scope.setRequestSession(RequestSessionStatus.Ok);
261+
client.captureException(new Error('test exception'), {}, scope);
262+
expect(scope.getRequestSession()).toEqual({ status: RequestSessionStatus.Errored });
263+
});
264+
265+
test('Sets requestSession status to Crashed when an onuncaughtexception occurs within a request', () => {
266+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
267+
const scope = new Scope();
268+
scope.setRequestSession(RequestSessionStatus.Ok);
269+
scope.setExtra('onUncaughtException', true);
270+
client.captureException(new Error('test exception'), {}, scope);
271+
expect(scope.getRequestSession()).toEqual({ status: RequestSessionStatus.Crashed });
272+
});
273+
274+
test('Does not set requestSession status when an onunhandledrejection occurs outside a request', () => {
275+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
276+
const scope = new Scope();
277+
scope.setRequestSession(RequestSessionStatus.Ok);
278+
scope.setExtra('unhandledPromiseRejection', true);
279+
client.captureException(new Error('test exception'), {}, scope);
280+
expect(scope.getRequestSession()).toEqual({ status: RequestSessionStatus.Crashed });
281+
});
282+
283+
test('Does not set requestSession status when an onuncaughtexception occurs outside a request', () => {
284+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
285+
const scope = new Scope();
286+
scope.setExtra('onUncaughtException', true);
287+
client.captureException(new Error('test exception'), {}, scope);
288+
expect(scope.getRequestSession()).toEqual({});
289+
});
290+
291+
test('Sets requestSession status to Crashed when an onunhandledrejection occurs within a request', () => {
292+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
293+
const scope = new Scope();
294+
scope.setExtra('unhandledPromiseRejection', true);
295+
client.captureException(new Error('test exception'), {}, scope);
296+
expect(scope.getRequestSession()).toEqual({});
297+
});
241298
});
242299

243300
describe('captureMessage', () => {
@@ -477,6 +534,45 @@ describe('BaseClient', () => {
477534
});
478535
});
479536

537+
test('If autoSessionTracking is disabled, requestSession status should not be set', () => {
538+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: false });
539+
const scope = new Scope();
540+
scope.setRequestSession(RequestSessionStatus.Ok);
541+
client.captureEvent({ message: 'message' }, undefined, scope);
542+
expect(scope.getRequestSession()).toEqual({ status: RequestSessionStatus.Ok });
543+
});
544+
545+
test('When captureEvent is called with an exception, requestSession status should be set to Errored', () => {
546+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
547+
const scope = new Scope();
548+
scope.setRequestSession(RequestSessionStatus.Ok);
549+
client.captureEvent({ message: 'message' }, undefined, scope);
550+
expect(scope.getRequestSession()).toEqual({ status: RequestSessionStatus.Errored });
551+
});
552+
553+
test('When captureEvent is called with an exception and, requestSession status is Crashed, it should not be overridden', () => {
554+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
555+
const scope = new Scope();
556+
scope.setRequestSession(RequestSessionStatus.Crashed);
557+
client.captureEvent({ message: 'message' }, undefined, scope);
558+
expect(scope.getRequestSession()).toEqual({ status: RequestSessionStatus.Crashed });
559+
});
560+
561+
test('When captureEvent is called with an exception but outside of a request, then requestStatus should not be set', () => {
562+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
563+
const scope = new Scope();
564+
client.captureEvent({ message: 'message' }, undefined, scope);
565+
expect(scope.getRequestSession()).toEqual({});
566+
});
567+
568+
test('When captureEvent is called with a transaction, then requestStatus should not be set', () => {
569+
const client = new TestClient({ dsn: PUBLIC_DSN, autoSessionTracking: true });
570+
const scope = new Scope();
571+
scope.setRequestSession(RequestSessionStatus.Ok);
572+
client.captureEvent({ message: 'message', type: 'transaction' }, undefined, scope);
573+
expect(scope.getRequestSession()).toEqual({ status: RequestSessionStatus.Ok });
574+
});
575+
480576
test('normalizes event with default depth of 3', () => {
481577
expect.assertions(1);
482578
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,

0 commit comments

Comments
 (0)