diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js new file mode 100644 index 000000000000..936f84aa375e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app-default.js @@ -0,0 +1,46 @@ +/* eslint-disable no-unused-vars */ + +const Sentry = require('@sentry/node'); +// const { loggingTransport } = require('@sentry-internal/node-integration-tests'); is throwing error that package not found, so using relative path +const { loggingTransport } = require('../../../src/index.ts'); + +// make sure to create the following file with the following content: +// function out_of_app_function() { +// const outOfAppVar = 'out of app value'; +// throw new Error('out-of-app error'); +// } + +// module.exports = { out_of_app_function }; + +const { out_of_app_function } = require('./node_modules/test-module/out-of-app-function.js'); + +function in_app_function() { + const inAppVar = 'in app value'; + out_of_app_function(); +} + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + includeLocalVariables: true, + // either set each frame's in_app flag manually or import the `out_of_app_function` from a node_module directory + // beforeSend: (event) => { + // event.exception?.values?.[0]?.stacktrace?.frames?.forEach(frame => { + // if (frame.function === 'out_of_app_function') { + // frame.in_app = false; + // } + // }); + // return event; + // }, +}); + +setTimeout(async () => { + try { + in_app_function(); + } catch (e) { + Sentry.captureException(e); + await Sentry.flush(); + + return null; + } +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js new file mode 100644 index 000000000000..4e923391dd37 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-out-of-app.js @@ -0,0 +1,42 @@ +/* eslint-disable no-unused-vars */ + +const Sentry = require('@sentry/node'); +// const { loggingTransport } = require('@sentry-internal/node-integration-tests'); is throwing error that package not found, so using relative path +const { loggingTransport } = require('../../../src/index.ts'); + +const { out_of_app_function } = require('./node_modules/test-module/out-of-app-function.js'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + includeLocalVariables: true, + integrations: [ + Sentry.localVariablesIntegration({ + includeOutOfAppFrames: true, + }), + ], + // either set each frame's in_app flag manually or import the `out_of_app_function` from a node_module directory + // beforeSend: (event) => { + // event.exception?.values?.[0]?.stacktrace?.frames?.forEach(frame => { + // if (frame.function === 'out_of_app_function') { + // frame.in_app = false; + // } + // }); + // return event; + // }, +}); + +function in_app_function() { + const inAppVar = 'in app value'; + out_of_app_function(); +} + +setTimeout(async () => { + try { + in_app_function(); + } catch (e) { + Sentry.captureException(e); + await Sentry.flush(); + return null; + } +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts index 2c87d14c2b45..a19e1a803b7d 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -127,4 +127,44 @@ describe('LocalVariables integration', () => { .start() .completed(); }); + + test('adds local variables to out of app frames when includeOutOfAppFrames is true', async () => { + await createRunner(__dirname, 'local-variables-out-of-app.js') + .expect({ + event: event => { + const frames = event.exception?.values?.[0]?.stacktrace?.frames || []; + + const inAppFrame = frames.find(frame => frame.function === 'in_app_function'); + const outOfAppFrame = frames.find(frame => frame.function === 'out_of_app_function'); + + expect(inAppFrame?.vars).toEqual({ inAppVar: 'in app value' }); + expect(inAppFrame?.in_app).toEqual(true); + + expect(outOfAppFrame?.vars).toEqual({ outOfAppVar: 'out of app value' }); + expect(outOfAppFrame?.in_app).toEqual(false); + }, + }) + .start() + .completed(); + }); + + test('does not add local variables to out of app frames by default', async () => { + await createRunner(__dirname, 'local-variables-out-of-app-default.js') + .expect({ + event: event => { + const frames = event.exception?.values?.[0]?.stacktrace?.frames || []; + + const inAppFrame = frames.find(frame => frame.function === 'in_app_function'); + const outOfAppFrame = frames.find(frame => frame.function === 'out_of_app_function'); + + expect(inAppFrame?.vars).toEqual({ inAppVar: 'in app value' }); + expect(inAppFrame?.in_app).toEqual(true); + + expect(outOfAppFrame?.vars).toBeUndefined(); + expect(outOfAppFrame?.in_app).toEqual(false); + }, + }) + .start() + .completed(); + }); }); diff --git a/packages/node-core/src/integrations/local-variables/common.ts b/packages/node-core/src/integrations/local-variables/common.ts index 471fa1a69864..f86988b4cbfc 100644 --- a/packages/node-core/src/integrations/local-variables/common.ts +++ b/packages/node-core/src/integrations/local-variables/common.ts @@ -99,6 +99,12 @@ export interface LocalVariablesIntegrationOptions { * Maximum number of exceptions to capture local variables for per second before rate limiting is triggered. */ maxExceptionsPerSecond?: number; + /** + * When true, local variables will be captured for all frames, including those that are not in_app. + * + * Defaults to `false`. + */ + includeOutOfAppFrames?: boolean; } export interface LocalVariablesWorkerArgs extends LocalVariablesIntegrationOptions { diff --git a/packages/node-core/src/integrations/local-variables/local-variables-async.ts b/packages/node-core/src/integrations/local-variables/local-variables-async.ts index 32fff66bab4e..7bad543c2588 100644 --- a/packages/node-core/src/integrations/local-variables/local-variables-async.ts +++ b/packages/node-core/src/integrations/local-variables/local-variables-async.ts @@ -39,8 +39,8 @@ export const localVariablesAsyncIntegration = defineIntegration((( if ( // We need to have vars to add frameLocalVariables.vars === undefined || - // We're not interested in frames that are not in_app because the vars are not relevant - frame.in_app === false || + // Only skip out-of-app frames if includeOutOfAppFrames is not true + (frame.in_app === false && integrationOptions.includeOutOfAppFrames !== true) || // The function names need to match !functionNamesMatch(frame.function, frameLocalVariables.function) ) { diff --git a/packages/node-core/src/integrations/local-variables/local-variables-sync.ts b/packages/node-core/src/integrations/local-variables/local-variables-sync.ts index 7de91a54276e..b2af37b0c7fb 100644 --- a/packages/node-core/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node-core/src/integrations/local-variables/local-variables-sync.ts @@ -268,8 +268,8 @@ const _localVariablesSyncIntegration = (( if ( // We need to have vars to add cachedFrameVariable.vars === undefined || - // We're not interested in frames that are not in_app because the vars are not relevant - frameVariable.in_app === false || + // Only skip out-of-app frames if includeOutOfAppFrames is not true + (frameVariable.in_app === false && options.includeOutOfAppFrames !== true) || // The function names need to match !functionNamesMatch(frameVariable.function, cachedFrameVariable.function) ) { @@ -288,122 +288,128 @@ const _localVariablesSyncIntegration = (( return event; } - return { - name: INTEGRATION_NAME, - async setupOnce() { - const client = getClient(); - const clientOptions = client?.getOptions(); + let setupPromise: Promise | undefined; - if (!clientOptions?.includeLocalVariables) { - return; - } + async function setup(): Promise { + const client = getClient(); + const clientOptions = client?.getOptions(); - // Only setup this integration if the Node version is>= v18 - // https://github.com/getsentry/sentry-javascript/issues/7697 - const unsupportedNodeVersion = NODE_MAJOR < 18; + if (!clientOptions?.includeLocalVariables) { + return; + } - if (unsupportedNodeVersion) { - debug.log('The `LocalVariables` integration is only supported on Node>= v18.'); - return; - } + // Only setup this integration if the Node version is>= v18 + // https://github.com/getsentry/sentry-javascript/issues/7697 + const unsupportedNodeVersion = NODE_MAJOR < 18; - if (await isDebuggerEnabled()) { - debug.warn('Local variables capture has been disabled because the debugger was already enabled'); - return; - } + if (unsupportedNodeVersion) { + debug.log('The `LocalVariables` integration is only supported on Node>= v18.'); + return; + } - AsyncSession.create(sessionOverride).then( - session => { - function handlePaused( - stackParser: StackParser, - { params: { reason, data, callFrames } }: InspectorNotification, - complete: () => void, - ): void { - if (reason !== 'exception' && reason !== 'promiseRejection') { - complete(); - return; - } - - rateLimiter?.(); - - // data.description contains the original error.stack - const exceptionHash = hashFromStack(stackParser, data.description); - - if (exceptionHash == undefined) { - complete(); - return; - } - - const { add, next } = createCallbackList(frames => { - cachedFrames.set(exceptionHash, frames); - complete(); - }); + if (await isDebuggerEnabled()) { + debug.warn('Local variables capture has been disabled because the debugger was already enabled'); + return; + } - // Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack - // For this reason we only attempt to get local variables for the first 5 frames - for (let i = 0; i < Math.min(callFrames.length, 5); i++) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { scopeChain, functionName, this: obj } = callFrames[i]!; - - const localScope = scopeChain.find(scope => scope.type === 'local'); - - // obj.className is undefined in ESM modules - const fn = - obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`; - - if (localScope?.object.objectId === undefined) { - add(frames => { - frames[i] = { function: fn }; - next(frames); - }); - } else { - const id = localScope.object.objectId; - add(frames => - session.getLocalVariables(id, vars => { - frames[i] = { function: fn, vars }; - next(frames); - }), - ); - } - } - - next([]); - } + try { + const session = await AsyncSession.create(sessionOverride); + + const handlePaused = ( + stackParser: StackParser, + { params: { reason, data, callFrames } }: InspectorNotification, + complete: () => void, + ): void => { + if (reason !== 'exception' && reason !== 'promiseRejection') { + complete(); + return; + } + + rateLimiter?.(); + + // data.description contains the original error.stack + const exceptionHash = hashFromStack(stackParser, data.description); + + if (exceptionHash == undefined) { + complete(); + return; + } - const captureAll = options.captureAllExceptions !== false; - - session.configureAndConnect( - (ev, complete) => - handlePaused(clientOptions.stackParser, ev as InspectorNotification, complete), - captureAll, - ); - - if (captureAll) { - const max = options.maxExceptionsPerSecond || 50; - - rateLimiter = createRateLimiter( - max, - () => { - debug.log('Local variables rate-limit lifted.'); - session.setPauseOnExceptions(true); - }, - seconds => { - debug.log( - `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, - ); - session.setPauseOnExceptions(false); - }, + const { add, next } = createCallbackList(frames => { + cachedFrames.set(exceptionHash, frames); + complete(); + }); + + // Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack + // For this reason we only attempt to get local variables for the first 5 frames + for (let i = 0; i < Math.min(callFrames.length, 5); i++) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { scopeChain, functionName, this: obj } = callFrames[i]!; + + const localScope = scopeChain.find(scope => scope.type === 'local'); + + // obj.className is undefined in ESM modules + const fn = obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`; + + if (localScope?.object.objectId === undefined) { + add(frames => { + frames[i] = { function: fn }; + next(frames); + }); + } else { + const id = localScope.object.objectId; + add(frames => + session.getLocalVariables(id, vars => { + frames[i] = { function: fn, vars }; + next(frames); + }), ); } + } + + next([]); + }; - shouldProcessEvent = true; - }, - error => { - debug.log('The `LocalVariables` integration failed to start.', error); - }, + const captureAll = options.captureAllExceptions !== false; + + session.configureAndConnect( + (ev, complete) => + handlePaused(clientOptions.stackParser, ev as InspectorNotification, complete), + captureAll, ); + + if (captureAll) { + const max = options.maxExceptionsPerSecond || 50; + + rateLimiter = createRateLimiter( + max, + () => { + debug.log('Local variables rate-limit lifted.'); + session.setPauseOnExceptions(true); + }, + seconds => { + debug.log( + `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, + ); + session.setPauseOnExceptions(false); + }, + ); + } + + shouldProcessEvent = true; + } catch (error) { + debug.log('The `LocalVariables` integration failed to start.', error); + } + } + + return { + name: INTEGRATION_NAME, + setupOnce() { + setupPromise = setup(); }, - processEvent(event: Event): Event { + async processEvent(event: Event): Promise { + await setupPromise; + if (shouldProcessEvent) { return addLocalVariablesToEvent(event); }

AltStyle によって変換されたページ (->オリジナル) /