diff --git a/packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js b/packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js new file mode 100644 index 000000000000..823fa966aa2a --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js @@ -0,0 +1,36 @@ +/* eslint-disable no-unused-vars */ +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + includeLocalVariables: true, + integrations: [new Sentry.Integrations.LocalVariables({ captureAllExceptions: true })], + beforeSend: event => { + // eslint-disable-next-line no-console + console.log(JSON.stringify(event)); + }, +}); + +class Some { + two(name) { + throw new Error('Enough!'); + } +} + +function one(name) { + const arr = [1, '2', null]; + const obj = { + name, + num: 5, + }; + + const ty = new Some(); + + ty.two(name); +} + +try { + one('some name'); +} catch (e) { + Sentry.captureException(e); +} diff --git a/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts b/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts index 6603202a3013..649f7cc9ba06 100644 --- a/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -51,4 +51,32 @@ describe('LocalVariables integration', () => { done(); }); }); + + test('Includes local variables for caught exceptions when enabled', done => { + expect.assertions(4); + + const testScriptPath = path.resolve(__dirname, 'local-variables-caught.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { + const event = JSON.parse(stdout) as Event; + + const frames = event.exception?.values?.[0].stacktrace?.frames || []; + const lastFrame = frames[frames.length - 1]; + + expect(lastFrame.function).toBe('Some.two'); + expect(lastFrame.vars).toEqual({ name: 'some name' }); + + const penultimateFrame = frames[frames.length - 2]; + + expect(penultimateFrame.function).toBe('one'); + expect(penultimateFrame.vars).toEqual({ + name: 'some name', + arr: [1, '2', null], + obj: { name: 'some name', num: 5 }, + ty: '', + }); + + done(); + }); + }); }); diff --git a/packages/node/src/integrations/localvariables.ts b/packages/node/src/integrations/localvariables.ts index 2f28c548fa85..ac23e67af910 100644 --- a/packages/node/src/integrations/localvariables.ts +++ b/packages/node/src/integrations/localvariables.ts @@ -6,7 +6,10 @@ import type { NodeClientOptions } from '../types'; export interface DebugSession { /** Configures and connects to the debug session */ - configureAndConnect(onPause: (message: InspectorNotification) => void): void; + configureAndConnect( + onPause: (message: InspectorNotification) => void, + captureAll: boolean, + ): void; /** Gets local variables for an objectId */ getLocalVariables(objectId: string): Promise>; } @@ -32,12 +35,15 @@ class AsyncSession implements DebugSession { } /** @inheritdoc */ - public configureAndConnect(onPause: (message: InspectorNotification) => void): void { + public configureAndConnect( + onPause: (message: InspectorNotification) => void, + captureAll: boolean, + ): void { this._session.connect(); this._session.on('Debugger.paused', onPause); this._session.post('Debugger.enable'); // We only want to pause on uncaught exceptions - this._session.post('Debugger.setPauseOnExceptions', { state: 'uncaught' }); + this._session.post('Debugger.setPauseOnExceptions', { state: captureAll ? 'all' : 'uncaught' }); } /** @inheritdoc */ @@ -164,7 +170,14 @@ export interface FrameVariables { /** There are no options yet. This allows them to be added later without breaking changes */ // eslint-disable-next-line @typescript-eslint/no-empty-interface -interface Options {} +interface Options { + /** + * Capture local variables for both handled and unhandled exceptions + * + * Default: false - Only captures local variables for uncaught exceptions + */ + captureAllExceptions?: boolean; +} /** * Adds local variables to exception frames @@ -177,7 +190,7 @@ export class LocalVariables implements Integration { private readonly _cachedFrames: LRUMap> = new LRUMap(20); public constructor( - _options: Options = {}, + private readonly _options: Options = {}, private readonly _session: DebugSession | undefined = tryNewAsyncSession(), ) {} @@ -194,8 +207,9 @@ export class LocalVariables implements Integration { clientOptions: NodeClientOptions | undefined, ): void { if (this._session && clientOptions?.includeLocalVariables) { - this._session.configureAndConnect(ev => - this._handlePaused(clientOptions.stackParser, ev as InspectorNotification), + this._session.configureAndConnect( + ev => this._handlePaused(clientOptions.stackParser, ev as InspectorNotification), + !!this._options.captureAllExceptions, ); addGlobalEventProcessor(async event => this._addLocalVariables(event)); diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index 6d0124296ea3..8653b048b257 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -17,7 +17,10 @@ class MockDebugSession implements DebugSession { constructor(private readonly _vars: Record>, private readonly _throwOn?: ThrowOn) {} - public configureAndConnect(onPause: (message: InspectorNotification) => void): void { + public configureAndConnect( + onPause: (message: InspectorNotification) => void, + _captureAll: boolean, + ): void { if (this._throwOn?.configureAndConnect) { throw new Error('configureAndConnect should not be called'); }