Skip to content

feat(node): Application mode sessions #3423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions packages/browser/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@ export interface BrowserOptions extends Options {

/** @deprecated use {@link Options.denyUrls} instead. */
blacklistUrls?: Array<string | RegExp>;

/**
* A flag enabling Sessions Tracking feature.
* By default, Sessions Tracking is enabled.
*/
autoSessionTracking?: boolean;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,12 @@
"fix": "run-s fix:eslint fix:prettier",
"fix:prettier": "prettier --write \"{src,test}/**/*.ts\"",
"fix:eslint": "eslint . --format stylish --fix",
"test": "run-s test:jest test:express test:webpack",
"test": "run-s test:jest test:express test:webpack test:release-health",
"test:jest": "jest",
"test:watch": "jest --watch",
"test:express": "node test/manual/express-scope-separation/start.js",
"test:webpack": "cd test/manual/webpack-domain/ && yarn && node npm-build.js",
"test:release-health": "node test/manual/release-health/single-session/healthy-session.js && node test/manual/release-health/single-session/caught-exception-errored-session.js && node test/manual/release-health/single-session/uncaught-exception-crashed-session.js && node test/manual/release-health/single-session/unhandled-rejection-crashed-session.js",
"pack": "npm pack"
},
"volta": {
Expand Down
47 changes: 47 additions & 0 deletions packages/node/src/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getCurrentHub, initAndBind, Integrations as CoreIntegrations } from '@sentry/core';
import { getMainCarrier, setHubOnCarrier } from '@sentry/hub';
import { SessionStatus } from '@sentry/types';
import { getGlobalObject } from '@sentry/utils';
import * as domain from 'domain';

Expand Down Expand Up @@ -96,9 +97,16 @@ export function init(options: NodeOptions = {}): void {
const detectedRelease = getSentryRelease();
if (detectedRelease !== undefined) {
options.release = detectedRelease;
} else {
// If release is not provided, then we should disable autoSessionTracking
options.autoSessionTracking = false;
}
}

if (options.autoSessionTracking === undefined) {
options.autoSessionTracking = true;
}

if (options.environment === undefined && process.env.SENTRY_ENVIRONMENT) {
options.environment = process.env.SENTRY_ENVIRONMENT;
}
Expand All @@ -109,6 +117,10 @@ export function init(options: NodeOptions = {}): void {
}

initAndBind(NodeClient, options);

if (options.autoSessionTracking) {
startSessionTracking();
}
}

/**
Expand Down Expand Up @@ -148,6 +160,20 @@ export async function close(timeout?: number): Promise<boolean> {
return Promise.reject(false);
}

/**
* Function that takes an instance of NodeClient and checks if autoSessionTracking option is enabled for that client
*/
export function isAutoSessionTrackingEnabled(client?: NodeClient): boolean {
if (client === undefined) {
return false;
}
const clientOptions: NodeOptions = client && client.getOptions();
if (clientOptions && clientOptions.autoSessionTracking !== undefined) {
return clientOptions.autoSessionTracking;
}
return false;
}

/**
* Returns a release dynamically from environment variables.
*/
Expand Down Expand Up @@ -180,3 +206,24 @@ export function getSentryRelease(fallback?: string): string | undefined {
fallback
);
}

/**
* Enable automatic Session Tracking for the node process.
*/
function startSessionTracking(): void {
const hub = getCurrentHub();
hub.startSession();
// Emitted in the case of healthy sessions, error of `mechanism.handled: true` and unhandledrejections because
// The 'beforeExit' event is not emitted for conditions causing explicit termination,
// such as calling process.exit() or uncaught exceptions.
// Ref: https://nodejs.org/api/process.html#process_event_beforeexit
process.on('beforeExit', () => {
const session = hub.getScope()?.getSession();
const terminalStates = [SessionStatus.Exited, SessionStatus.Crashed];
// Only call endSession, if the Session exists on Scope and SessionStatus is not a
// Terminal Status i.e. Exited or Crashed because
// "When a session is moved away from ok it must not be updated anymore."
// Ref: https://develop.sentry.dev/sdk/sessions/
if (session && !terminalStates.includes(session.status)) hub.endSession();
});
}
16 changes: 10 additions & 6 deletions packages/node/src/transports/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { API, eventToSentryRequest, SDK_VERSION } from '@sentry/core';
import { DsnProtocol, Event, Response, Status, Transport, TransportOptions } from '@sentry/types';
import { API, SDK_VERSION } from '@sentry/core';
import { DsnProtocol, Event, Response, SentryRequest, Status, Transport, TransportOptions } from '@sentry/types';
import { logger, parseRetryAfterHeader, PromiseBuffer, SentryError } from '@sentry/utils';
import * as fs from 'fs';
import * as http from 'http';
Expand Down Expand Up @@ -124,7 +124,10 @@ export abstract class BaseTransport implements Transport {
}

/** JSDoc */
protected async _sendWithModule(httpModule: HTTPModule, event: Event): Promise<Response> {
protected async _send(sentryReq: SentryRequest): Promise<Response> {
if (!this.module) {
throw new SentryError('No module available');
}
if (new Date(Date.now()) < this._disabledUntil) {
return Promise.reject(new SentryError(`Transport locked till ${this._disabledUntil} due to too many requests.`));
}
Expand All @@ -134,10 +137,11 @@ export abstract class BaseTransport implements Transport {
}
return this._buffer.add(
new Promise<Response>((resolve, reject) => {
const sentryReq = eventToSentryRequest(event, this._api);
if (!this.module) {
throw new SentryError('No module available');
}
const options = this._getRequestOptions(new url.URL(sentryReq.url));

const req = httpModule.request(options, (res: http.IncomingMessage) => {
const req = this.module.request(options, (res: http.IncomingMessage) => {
const statusCode = res.statusCode || 500;
const status = Status.fromHttpCode(statusCode);

Expand Down
16 changes: 10 additions & 6 deletions packages/node/src/transports/http.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Event, Response, TransportOptions } from '@sentry/types';
import { SentryError } from '@sentry/utils';
import { eventToSentryRequest, sessionToSentryRequest } from '@sentry/core';
import { Event, Response, Session, TransportOptions } from '@sentry/types';
import * as http from 'http';

import { BaseTransport } from './base';
Expand All @@ -20,9 +20,13 @@ export class HTTPTransport extends BaseTransport {
* @inheritDoc
*/
public sendEvent(event: Event): Promise<Response> {
if (!this.module) {
throw new SentryError('No module available in HTTPTransport');
}
return this._sendWithModule(this.module, event);
return this._send(eventToSentryRequest(event, this._api));
}

/**
* @inheritDoc
*/
public sendSession(session: Session): PromiseLike<Response> {
return this._send(sessionToSentryRequest(session, this._api));
}
}
16 changes: 10 additions & 6 deletions packages/node/src/transports/https.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Event, Response, TransportOptions } from '@sentry/types';
import { SentryError } from '@sentry/utils';
import { eventToSentryRequest, sessionToSentryRequest } from '@sentry/core';
import { Event, Response, Session, TransportOptions } from '@sentry/types';
import * as https from 'https';

import { BaseTransport } from './base';
Expand All @@ -20,9 +20,13 @@ export class HTTPSTransport extends BaseTransport {
* @inheritDoc
*/
public sendEvent(event: Event): Promise<Response> {
if (!this.module) {
throw new SentryError('No module available in HTTPSTransport');
}
return this._sendWithModule(this.module, event);
return this._send(eventToSentryRequest(event, this._api));
}

/**
* @inheritDoc
*/
public sendSession(session: Session): PromiseLike<Response> {
return this._send(sessionToSentryRequest(session, this._api));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const Sentry = require('../../../../dist');
const { assertSessions, constructStrippedSessionObject, BaseDummyTransport } = require('./test-utils');

let sessionCounter = 0;
process.on('exit', ()=> {
if (process.exitCode !== 1) {
console.log('SUCCESS: All application mode sessions were sent to node transport as expected');
}
})

class DummyTransport extends BaseDummyTransport {
sendSession(session) {
sessionCounter++;
if (sessionCounter === 1) {
assertSessions(constructStrippedSessionObject(session),
{
init: true,
status: 'ok',
errors: 1,
release: '1.1'
}
)
}
else if (sessionCounter === 2) {
assertSessions(constructStrippedSessionObject(session),
{
init: false,
status: 'exited',
errors: 1,
release: '1.1'
}
)
}
else {
console.log('FAIL: Received way too many Sessions!');
process.exit(1);
}
return super.sendSession(session);
}
}

Sentry.init({
dsn: 'http://[email protected]/1337',
release: '1.1',
transport: DummyTransport,
});

/**
* The following code snippet will capture exceptions of `mechanism.handled` equal to `true`, and so these sessions
* are treated as Errored Sessions.
* In this case, we have two session updates sent; First Session sent is due to the call to CaptureException that
* extracts event data and uses it to update the Session and sends it. The second session update is sent on the
* `beforeExit` event which happens right before the process exits.
*/
try {
throw new Error('hey there')
}
catch(e) {
Sentry.captureException(e);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const Sentry = require('../../../../dist');
const { assertSessions, constructStrippedSessionObject, BaseDummyTransport } = require('./test-utils');

let sessionCounter = 0;
process.on('exit', ()=> {
if (process.exitCode !== 1) {
console.log('SUCCESS: All application mode sessions were sent to node transport as expected');
}
})

class DummyTransport extends BaseDummyTransport {
sendSession(session) {
sessionCounter++;
if (sessionCounter === 1) {
assertSessions(constructStrippedSessionObject(session),
{
init: true,
status: 'exited',
errors: 0,
release: '1.1'
}
)
}
else {
console.log('FAIL: Received way too many Sessions!');
process.exit(1);
}
return super.sendSession(session);
}
}

Sentry.init({
dsn: 'http://[email protected]/1337',
release: '1.1',
transport: DummyTransport
});

/**
* This script or process, start a Session on init object, and calls endSession on `beforeExit` of the process, which
* sends a healthy session to the Server.
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
function assertSessions(actual, expected) {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
console.error('FAILED: Sessions do not match');
process.exit(1);
}
}

function constructStrippedSessionObject(actual) {
const { init, status, errors, release, did } = actual;
return { init, status, errors, release, did };
}

class BaseDummyTransport {
sendEvent(event) {
return Promise.resolve({
status: 'success',
});
}
sendSession(session) {
return Promise.resolve({
status: 'success',
});
}
close(timeout) {
return Promise.resolve(true);
}
}

module.exports = { assertSessions, constructStrippedSessionObject, BaseDummyTransport };
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const Sentry = require('../../../../dist');
const { assertSessions, constructStrippedSessionObject, BaseDummyTransport } = require('./test-utils');

process.on('exit', ()=> {
if (process.exitCode !== 1) {
console.log('SUCCESS: All application mode sessions were sent to node transport as expected');
}
})

class DummyTransport extends BaseDummyTransport {
sendSession(session) {
assertSessions(constructStrippedSessionObject(session),
{
init: true,
status: 'crashed',
errors: 1,
release: '1.1'
}
)
process.exit(0);
}
}

Sentry.init({
dsn: 'http://[email protected]/1337',
release: '1.1',
transport: DummyTransport
});
/**
* The following code snippet will throw an exception of `mechanism.handled` equal to `false`, and so this session
* is considered a Crashed Session.
* In this case, we have only session update that is sent, which is sent due to the call to CaptureException that
* extracts event data and uses it to update the Session and send it. No secondary session update in this case because
* we explicitly exit the process in the onUncaughtException handler and so the `beforeExit` event is not fired.
*/
throw new Error('test error')
Loading