diff --git a/packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs b/packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs new file mode 100644 index 000000000000..175ff0702d30 --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs @@ -0,0 +1,36 @@ +/* eslint-disable no-unused-vars */ +import * as Sentry from '@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 649f7cc9ba06..aae488d50475 100644 --- a/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -1,7 +1,12 @@ import type { Event } from '@sentry/node'; +import { parseSemver } from '@sentry/utils'; import * as childProcess from 'child_process'; import * as path from 'path'; +const nodeMajor = parseSemver(process.version.slice(1)).major || 1; + +const testIf = (condition: boolean, t: jest.It) => (condition ? t : t.skip); + describe('LocalVariables integration', () => { test('Should not include local variables by default', done => { expect.assertions(2); @@ -52,6 +57,34 @@ describe('LocalVariables integration', () => { }); }); + testIf(nodeMajor > 10, test)('Should include local variables with ESM', done => { + expect.assertions(4); + + const testScriptPath = path.resolve(__dirname, 'local-variables-caught.mjs'); + + 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(); + }); + }); + test('Includes local variables for caught exceptions when enabled', done => { expect.assertions(4); diff --git a/packages/node/src/integrations/localvariables.ts b/packages/node/src/integrations/localvariables.ts index ac23e67af910..2423cfa30c70 100644 --- a/packages/node/src/integrations/localvariables.ts +++ b/packages/node/src/integrations/localvariables.ts @@ -237,7 +237,8 @@ export class LocalVariables implements Integration { const framePromises = callFrames.map(async ({ scopeChain, functionName, this: obj }) => { const localScope = scopeChain.find(scope => scope.type === 'local'); - const fn = obj.className === 'global' ? functionName : `${obj.className}.${functionName}`; + // obj.className is undefined in ESM modules + const fn = obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`; if (localScope?.object.objectId === undefined) { return { function: fn };