Skip to content

Commit 388220d

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 e5a1771 commit 388220d

File tree

6 files changed

+152
-24
lines changed

6 files changed

+152
-24
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 { DsnProtocol, Event, Response, Status, Transport, TransportOptions } from '@sentry/types';
1+
import { API, SDK_VERSION } from '@sentry/core';
2+
import { DsnProtocol, Event, Response, SentryRequest, 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';
@@ -124,7 +124,10 @@ export abstract class BaseTransport implements Transport {
124124
}
125125

126126
/** JSDoc */
127-
protected async _sendWithModule(httpModule: HTTPModule, event: Event): Promise<Response> {
127+
protected async _send(sentryReq: SentryRequest): Promise<Response> {
128+
if (!this.module) {
129+
throw new SentryError('No module available');
130+
}
128131
if (new Date(Date.now()) < this._disabledUntil) {
129132
return Promise.reject(new SentryError(`Transport locked till ${this._disabledUntil} due to too many requests.`));
130133
}
@@ -134,10 +137,11 @@ export abstract class BaseTransport implements Transport {
134137
}
135138
return this._buffer.add(
136139
new Promise<Response>((resolve, reject) => {
137-
const sentryReq = eventToSentryRequest(event, this._api);
140+
if (!this.module) {
141+
throw new SentryError('No module available');
142+
}
138143
const options = this._getRequestOptions(new url.URL(sentryReq.url));
139-
140-
const req = httpModule.request(options, (res: http.IncomingMessage) => {
144+
const req = this.module.request(options, (res: http.IncomingMessage) => {
141145
const statusCode = res.statusCode || 500;
142146
const status = Status.fromHttpCode(statusCode);
143147

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
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const express = require('express');
2+
const app = express();
3+
const Sentry = require('../../../dist');
4+
5+
function assertSessions(actual, expected) {
6+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
7+
console.error('FAILED: Sessions do not match');
8+
process.exit(1);
9+
}
10+
}
11+
12+
function constructStrippedSessionObject(actual) {
13+
const { init, status, errors, release } = actual;
14+
return { init, status, errors, release };
15+
}
16+
17+
let remaining = 2;
18+
19+
class DummyTransport {
20+
sendSession(session) {
21+
if (session.did === 'ahmed') {
22+
assertSessions(constructStrippedSessionObject(session),
23+
{
24+
init: true,
25+
status: "exited",
26+
errors: 0,
27+
release: "1.1"
28+
}
29+
)
30+
}
31+
else if (session.did === 'ahmed2') {
32+
assertSessions(constructStrippedSessionObject(session),
33+
{
34+
init: true,
35+
status: "ok",
36+
errors: 1,
37+
release: "1.1"
38+
}
39+
)
40+
}
41+
--remaining;
42+
43+
if (!remaining) {
44+
console.error('SUCCESS: All application mode sessions were sent to node transport as expected');
45+
process.exit(0);
46+
}
47+
48+
return Promise.resolve({
49+
status: 'success',
50+
});
51+
}
52+
}
53+
54+
Sentry.init({
55+
dsn: 'http://[email protected]/1337',
56+
release: '1.1',
57+
transport: DummyTransport,
58+
});
59+
60+
const currentHub = Sentry.getCurrentHub();
61+
currentHub.startSession({user: {username: 'ahmed'}});
62+
currentHub.captureSession(true);
63+
64+
const currentHub2 = Sentry.getCurrentHub();
65+
currentHub2.startSession({user: {username: 'ahmed2'}});
66+
throw new Error('bar');
67+
68+
app.use(Sentry.Handlers.errorHandler());

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

Lines changed: 27 additions & 3 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 http from 'http';
@@ -7,7 +8,8 @@ import { HTTPTransport } from '../../src/transports/http';
78

89
const mockSetEncoding = jest.fn();
910
const dsn = 'http://[email protected]:8989/mysubpath/50622';
10-
const transportPath = '/mysubpath/api/50622/store/';
11+
const storePath = '/mysubpath/api/50622/store/';
12+
const envelopePath = '/mysubpath/api/50622/envelope/';
1113
let mockReturnCode = 200;
1214
let mockHeaders = {};
1315

@@ -28,12 +30,12 @@ function createTransport(options: TransportOptions): HTTPTransport {
2830
return transport;
2931
}
3032

31-
function assertBasicOptions(options: any): void {
33+
function assertBasicOptions(options: any, useEnvelope: boolean = false): void {
3234
expect(options.headers['X-Sentry-Auth']).toContain('sentry_version');
3335
expect(options.headers['X-Sentry-Auth']).toContain('sentry_client');
3436
expect(options.headers['X-Sentry-Auth']).toContain('sentry_key');
3537
expect(options.port).toEqual('8989');
36-
expect(options.path).toEqual(transportPath);
38+
expect(options.path).toEqual(useEnvelope ? envelopePath : storePath);
3739
expect(options.hostname).toEqual('sentry.io');
3840
}
3941

@@ -70,6 +72,28 @@ describe('HTTPTransport', () => {
7072
}
7173
});
7274

75+
test('send 200 session', async () => {
76+
const transport = createTransport({ dsn });
77+
await transport.sendSession(new Session());
78+
79+
const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0];
80+
assertBasicOptions(requestOptions, true);
81+
expect(mockSetEncoding).toHaveBeenCalled();
82+
});
83+
84+
test('send 400 session', async () => {
85+
mockReturnCode = 400;
86+
const transport = createTransport({ dsn });
87+
88+
try {
89+
await transport.sendSession(new Session());
90+
} catch (e) {
91+
const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0];
92+
assertBasicOptions(requestOptions, true);
93+
expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`));
94+
}
95+
});
96+
7397
test('send x-sentry-error header', async () => {
7498
mockReturnCode = 429;
7599
mockHeaders = {

packages/node/test/transports/https.test.ts

Lines changed: 27 additions & 3 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 https from 'https';
@@ -7,7 +8,8 @@ import { HTTPSTransport } from '../../src/transports/https';
78

89
const mockSetEncoding = jest.fn();
910
const dsn = 'https://[email protected]:8989/mysubpath/50622';
10-
const transportPath = '/mysubpath/api/50622/store/';
11+
const storePath = '/mysubpath/api/50622/store/';
12+
const envelopePath = '/mysubpath/api/50622/envelope/';
1113
let mockReturnCode = 200;
1214
let mockHeaders = {};
1315

@@ -34,12 +36,12 @@ function createTransport(options: TransportOptions): HTTPSTransport {
3436
return transport;
3537
}
3638

37-
function assertBasicOptions(options: any): void {
39+
function assertBasicOptions(options: any, useEnvelope: boolean = false): void {
3840
expect(options.headers['X-Sentry-Auth']).toContain('sentry_version');
3941
expect(options.headers['X-Sentry-Auth']).toContain('sentry_client');
4042
expect(options.headers['X-Sentry-Auth']).toContain('sentry_key');
4143
expect(options.port).toEqual('8989');
42-
expect(options.path).toEqual(transportPath);
44+
expect(options.path).toEqual(useEnvelope ? envelopePath : storePath);
4345
expect(options.hostname).toEqual('sentry.io');
4446
}
4547

@@ -76,6 +78,28 @@ describe('HTTPSTransport', () => {
7678
}
7779
});
7880

81+
test('send 200 session', async () => {
82+
const transport = createTransport({ dsn });
83+
await transport.sendSession(new Session());
84+
85+
const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0];
86+
assertBasicOptions(requestOptions, true);
87+
expect(mockSetEncoding).toHaveBeenCalled();
88+
});
89+
90+
test('send 400 session', async () => {
91+
mockReturnCode = 400;
92+
const transport = createTransport({ dsn });
93+
94+
try {
95+
await transport.sendSession(new Session());
96+
} catch (e) {
97+
const requestOptions = (transport.module!.request as jest.Mock).mock.calls[0][0];
98+
assertBasicOptions(requestOptions, true);
99+
expect(e).toEqual(new SentryError(`HTTP Error (${mockReturnCode})`));
100+
}
101+
});
102+
79103
test('send x-sentry-error header', async () => {
80104
mockReturnCode = 429;
81105
mockHeaders = {

0 commit comments

Comments
 (0)