Skip to content

Commit c8a4edb

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 10938a7 commit c8a4edb

File tree

5 files changed

+82
-22
lines changed

5 files changed

+82
-22
lines changed

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 { 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';
@@ -96,7 +96,10 @@ export abstract class BaseTransport implements Transport {
9696
}
9797

9898
/** JSDoc */
99-
protected async _sendWithModule(httpModule: HTTPModule, event: Event): Promise<Response> {
99+
protected async _send(sentryReq: SentryRequest): Promise<Response> {
100+
if (!this.module) {
101+
throw new SentryError('No module available');
102+
}
100103
if (new Date(Date.now()) < this._disabledUntil) {
101104
return Promise.reject(new SentryError(`Transport locked till ${this._disabledUntil} due to too many requests.`));
102105
}
@@ -106,10 +109,11 @@ export abstract class BaseTransport implements Transport {
106109
}
107110
return this._buffer.add(
108111
new Promise<Response>((resolve, reject) => {
109-
const sentryReq = eventToSentryRequest(event, this._api);
112+
if (!this.module) {
113+
throw new SentryError('No module available');
114+
}
110115
const options = this._getRequestOptions(new url.URL(sentryReq.url));
111-
112-
const req = httpModule.request(options, (res: http.IncomingMessage) => {
116+
const req = this.module.request(options, (res: http.IncomingMessage) => {
113117
const statusCode = res.statusCode || 500;
114118
const status = Status.fromHttpCode(statusCode);
115119

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
}

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)