Skip to content

Commit cb82655

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 00a5fce commit cb82655

File tree

6 files changed

+103
-12
lines changed

6 files changed

+103
-12
lines changed

packages/node/src/client.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { BaseClient, Scope, SDK_VERSION } from '@sentry/core';
2+
import { Session } from '@sentry/hub';
23
import { Event, EventHint } from '@sentry/types';
4+
import { logger } from '@sentry/utils';
35

46
import { NodeBackend, NodeOptions } from './backend';
57

@@ -30,6 +32,19 @@ export class NodeClient extends BaseClient<NodeBackend, NodeOptions> {
3032
super(NodeBackend, options);
3133
}
3234

35+
/**
36+
* @inheritDoc
37+
*/
38+
public captureSession(session: Session): void {
39+
if (!session.release) {
40+
logger.warn('Discarded session because of missing release');
41+
} else {
42+
this._sendSession(session);
43+
// After sending, we set init false to inidcate it's not the first occurence
44+
session.update({ init: false });
45+
}
46+
}
47+
3348
/**
3449
* @inheritDoc
3550
*/

packages/node/src/transports/base.ts

Lines changed: 10 additions & 4 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, Session, 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';
@@ -63,6 +63,13 @@ export abstract class BaseTransport implements Transport {
6363
throw new SentryError('Transport Class has to implement `sendEvent` method.');
6464
}
6565

66+
/**
67+
* @inheritDoc
68+
*/
69+
public sendSession(_: Session): PromiseLike<Response> {
70+
throw new SentryError('Transport Class has to implement `sendSession` method.');
71+
}
72+
6673
/**
6774
* @inheritDoc
6875
*/
@@ -96,7 +103,7 @@ export abstract class BaseTransport implements Transport {
96103
}
97104

98105
/** JSDoc */
99-
protected async _sendWithModule(httpModule: HTTPModule, event: Event): Promise<Response> {
106+
protected async _sendWithModule(httpModule: HTTPModule, sentryReq: SentryRequest): Promise<Response> {
100107
if (new Date(Date.now()) < this._disabledUntil) {
101108
return Promise.reject(new SentryError(`Transport locked till ${this._disabledUntil} due to too many requests.`));
102109
}
@@ -106,7 +113,6 @@ export abstract class BaseTransport implements Transport {
106113
}
107114
return this._buffer.add(
108115
new Promise<Response>((resolve, reject) => {
109-
const sentryReq = eventToSentryRequest(event, this._api);
110116
const options = this._getRequestOptions(new url.URL(sentryReq.url));
111117

112118
const req = httpModule.request(options, (res: http.IncomingMessage) => {

packages/node/src/transports/http.ts

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

@@ -23,6 +24,16 @@ export class HTTPTransport extends BaseTransport {
2324
if (!this.module) {
2425
throw new SentryError('No module available in HTTPTransport');
2526
}
26-
return this._sendWithModule(this.module, event);
27+
return this._sendWithModule(this.module, eventToSentryRequest(event, this._api));
28+
}
29+
30+
/**
31+
* @inheritDoc
32+
*/
33+
public sendSession(session: Session): PromiseLike<Response> {
34+
if (!this.module) {
35+
throw new SentryError('No module available in HTTPTransport');
36+
}
37+
return this._sendWithModule(this.module, sessionToSentryRequest(session, this._api));
2738
}
2839
}

packages/node/src/transports/https.ts

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

@@ -23,6 +24,16 @@ export class HTTPSTransport extends BaseTransport {
2324
if (!this.module) {
2425
throw new SentryError('No module available in HTTPSTransport');
2526
}
26-
return this._sendWithModule(this.module, event);
27+
return this._sendWithModule(this.module, eventToSentryRequest(event, this._api));
28+
}
29+
30+
/**
31+
* @inheritDoc
32+
*/
33+
public sendSession(session: Session): PromiseLike<Response> {
34+
if (!this.module) {
35+
throw new SentryError('No module available in HTTPTransport');
36+
}
37+
return this._sendWithModule(this.module, sessionToSentryRequest(session, this._api));
2738
}
2839
}

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

Lines changed: 26 additions & 2 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';
@@ -7,6 +8,7 @@ import { HTTPTransport } from '../../src/transports/http';
78
const mockSetEncoding = jest.fn();
89
const dsn = 'http://[email protected]:8989/mysubpath/50622';
910
const transportPath = '/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 : transportPath);
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: 26 additions & 2 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';
@@ -7,6 +8,7 @@ import { HTTPSTransport } from '../../src/transports/https';
78
const mockSetEncoding = jest.fn();
89
const dsn = 'https://[email protected]:8989/mysubpath/50622';
910
const transportPath = '/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 : transportPath);
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)