diff --git a/dev-packages/node-integration-tests/suites/anr/basic-session.js b/dev-packages/node-integration-tests/suites/anr/basic-session.js index 153acf83f16f..5661e08b850b 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic-session.js +++ b/dev-packages/node-integration-tests/suites/anr/basic-session.js @@ -15,6 +15,9 @@ Sentry.init({ autoSessionTracking: true, }); +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + function longWork() { for (let i = 0; i < 20; i++) { const salt = crypto.randomBytes(128).toString('base64'); diff --git a/dev-packages/node-integration-tests/suites/anr/basic.js b/dev-packages/node-integration-tests/suites/anr/basic.js index 712e0e26a3f8..d98b18216703 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.js +++ b/dev-packages/node-integration-tests/suites/anr/basic.js @@ -15,6 +15,9 @@ Sentry.init({ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], }); +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + function longWork() { for (let i = 0; i < 20; i++) { const salt = crypto.randomBytes(128).toString('base64'); diff --git a/dev-packages/node-integration-tests/suites/anr/basic.mjs b/dev-packages/node-integration-tests/suites/anr/basic.mjs index 0184ca9583f7..77bb9ae3626d 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.mjs +++ b/dev-packages/node-integration-tests/suites/anr/basic.mjs @@ -15,6 +15,9 @@ Sentry.init({ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], }); +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + function longWork() { for (let i = 0; i < 20; i++) { const salt = crypto.randomBytes(128).toString('base64'); diff --git a/dev-packages/node-integration-tests/suites/anr/forked.js b/dev-packages/node-integration-tests/suites/anr/forked.js index 0db282eacb07..06529096cca5 100644 --- a/dev-packages/node-integration-tests/suites/anr/forked.js +++ b/dev-packages/node-integration-tests/suites/anr/forked.js @@ -15,6 +15,9 @@ Sentry.init({ integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], }); +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + function longWork() { for (let i = 0; i < 20; i++) { const salt = crypto.randomBytes(128).toString('base64'); diff --git a/dev-packages/node-integration-tests/suites/anr/isolated.mjs b/dev-packages/node-integration-tests/suites/anr/isolated.mjs new file mode 100644 index 000000000000..d9b179c63e71 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/anr/isolated.mjs @@ -0,0 +1,53 @@ +import * as assert from 'assert'; +import * as crypto from 'crypto'; + +import * as Sentry from '@sentry/node'; + +setTimeout(() => { + process.exit(); +}, 10000); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + debug: true, + autoSessionTracking: false, + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], +}); + +async function longWork() { + await new Promise(resolve => setTimeout(resolve, 1000)); + + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +function neverResolve() { + return new Promise(() => { + // + }); +} + +const fns = [ + neverResolve, + neverResolve, + neverResolve, + neverResolve, + neverResolve, + longWork, // [5] + neverResolve, + neverResolve, + neverResolve, + neverResolve, +]; + +for (let id = 0; id < 10; id++) { + Sentry.withIsolationScope(async () => { + Sentry.setUser({ id }); + + await fns[id](); + }); +} diff --git a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js index 9de453abf23d..4f9fc9bc64db 100644 --- a/dev-packages/node-integration-tests/suites/anr/stop-and-start.js +++ b/dev-packages/node-integration-tests/suites/anr/stop-and-start.js @@ -17,6 +17,9 @@ Sentry.init({ integrations: [anr], }); +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + function longWorkIgnored() { for (let i = 0; i < 20; i++) { const salt = crypto.randomBytes(128).toString('base64'); diff --git a/dev-packages/node-integration-tests/suites/anr/test.ts b/dev-packages/node-integration-tests/suites/anr/test.ts index 7ace974d6170..b0299f4a038d 100644 --- a/dev-packages/node-integration-tests/suites/anr/test.ts +++ b/dev-packages/node-integration-tests/suites/anr/test.ts @@ -21,6 +21,15 @@ const EXPECTED_ANR_EVENT = { timezone: expect.any(String), }, }, + user: { + email: 'person@home.com', + }, + breadcrumbs: [ + { + timestamp: expect.any(Number), + message: 'important message!', + }, + ], // and an exception that is our ANR exception: { values: [ @@ -105,4 +114,34 @@ conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => test('worker can be stopped and restarted', done => { createRunner(__dirname, 'stop-and-start.js').expect({ event: EXPECTED_ANR_EVENT }).start(done); }); + + const EXPECTED_ISOLATED_EVENT = { + user: { + id: 5, + }, + exception: { + values: [ + { + type: 'ApplicationNotResponding', + value: 'Application Not Responding for at least 100 ms', + mechanism: { type: 'ANR' }, + stacktrace: { + frames: expect.arrayContaining([ + { + colno: expect.any(Number), + lineno: expect.any(Number), + filename: expect.stringMatching(/isolated.mjs$/), + function: 'longWork', + in_app: true, + }, + ]), + }, + }, + ], + }, + }; + + test('fetches correct isolated scope', done => { + createRunner(__dirname, 'isolated.mjs').expect({ event: EXPECTED_ISOLATED_EVENT }).start(done); + }); }); diff --git a/packages/node-experimental/src/integrations/anr/index.ts b/packages/node-experimental/src/integrations/anr/index.ts index c1bc03bc5a86..7dbe9e905cb4 100644 --- a/packages/node-experimental/src/integrations/anr/index.ts +++ b/packages/node-experimental/src/integrations/anr/index.ts @@ -1,8 +1,9 @@ -import { defineIntegration, getCurrentScope } from '@sentry/core'; -import type { Contexts, Event, EventHint, Integration, IntegrationFn } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { defineIntegration, mergeScopeData } from '@sentry/core'; +import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/types'; +import { GLOBAL_OBJ, logger } from '@sentry/utils'; import * as inspector from 'inspector'; import { Worker } from 'worker_threads'; +import { getCurrentScope, getGlobalScope, getIsolationScope } from '../..'; import { NODE_VERSION } from '../../nodeVersion'; import type { NodeClient } from '../../sdk/client'; import type { AnrIntegrationOptions, WorkerStartData } from './common'; @@ -15,8 +16,26 @@ function log(message: string, ...args: unknown[]): void { logger.log(`[ANR] ${message}`, ...args); } +function globalWithScopeFetchFn(): typeof GLOBAL_OBJ & { __SENTRY_GET_SCOPES__?: () => ScopeData } { + return GLOBAL_OBJ; +} + +/** Fetches merged scope data */ +function getScopeData(): ScopeData { + const scope = getGlobalScope().getScopeData(); + mergeScopeData(scope, getIsolationScope().getScopeData()); + mergeScopeData(scope, getCurrentScope().getScopeData()); + + // We remove attachments because they likely won't serialize well as json + scope.attachments = []; + // We can't serialize event processor functions + scope.eventProcessors = []; + + return scope; +} + /** - * Gets contexts by calling all event processors. This relies on being called after all integrations are setup + * Gets contexts by calling all event processors. This shouldn't be called until all integrations are setup */ async function getContexts(client: NodeClient): Promise { let event: Event | null = { message: 'ANR' }; @@ -35,9 +54,18 @@ const INTEGRATION_NAME = 'Anr'; type AnrInternal = { startWorker: () => void; stopWorker: () => void }; const _anrIntegration = ((options: Partial = {}) => { + if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { + throw new Error('ANR detection requires Node 16.17.0 or later'); + } + let worker: Promise<() => void> | undefined; let client: NodeClient | undefined; + // Hookup the scope fetch function to the global object so that it can be called from the worker thread via the + // debugger when it pauses + const gbl = globalWithScopeFetchFn(); + gbl.__SENTRY_GET_SCOPES__ = getScopeData; + return { name: INTEGRATION_NAME, startWorker: () => { @@ -59,10 +87,6 @@ const _anrIntegration = ((options: Partial = {}) => { } }, setup(initClient: NodeClient) { - if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { - throw new Error('ANR detection requires Node 16.17.0 or later'); - } - client = initClient; // setImmediate is used to ensure that all other integrations have had their setup called first. diff --git a/packages/node-experimental/src/integrations/anr/worker.ts b/packages/node-experimental/src/integrations/anr/worker.ts index 9f8147a280cf..21bdcbbb0631 100644 --- a/packages/node-experimental/src/integrations/anr/worker.ts +++ b/packages/node-experimental/src/integrations/anr/worker.ts @@ -1,11 +1,12 @@ import { + applyScopeDataToEvent, createEventEnvelope, createSessionEnvelope, getEnvelopeEndpointWithUrlEncodedAuth, makeSession, updateSession, } from '@sentry/core'; -import type { Event, Session, StackFrame, TraceContext } from '@sentry/types'; +import type { Event, ScopeData, Session, StackFrame } from '@sentry/types'; import { callFrameToStackFrame, normalizeUrlToBase, @@ -86,7 +87,23 @@ function prepareStackFrames(stackFrames: StackFrame[] | undefined): StackFrame[] return strippedFrames; } -async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): Promise { +function applyScopeToEvent(event: Event, scope: ScopeData): void { + applyScopeDataToEvent(event, scope); + + if (!event.contexts?.trace) { + const { traceId, spanId, parentSpanId } = scope.propagationContext; + event.contexts = { + trace: { + trace_id: traceId, + span_id: spanId, + parent_span_id: parentSpanId, + }, + ...event.contexts, + }; + } +} + +async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise { if (hasSentAnrEvent) { return; } @@ -99,7 +116,7 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): const event: Event = { event_id: uuid4(), - contexts: { ...options.contexts, trace: traceContext }, + contexts: options.contexts, release: options.release, environment: options.environment, dist: options.dist, @@ -119,6 +136,10 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): tags: options.staticTags, }; + if (scope) { + applyScopeToEvent(event, scope); + } + const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata, options.tunnel); // Log the envelope to aid in testing log(JSON.stringify(envelope)); @@ -171,20 +192,23 @@ if (options.captureStackTrace) { 'Runtime.evaluate', { // Grab the trace context from the current scope - expression: - 'var __sentry_ctx = __SENTRY__.acs?.getCurrentScope().getPropagationContext() || {}; __sentry_ctx.traceId + "-" + __sentry_ctx.spanId + "-" + __sentry_ctx.parentSpanId', + expression: 'global.__SENTRY_GET_SCOPES__();', // Don't re-trigger the debugger if this causes an error silent: true, + // Serialize the result to json otherwise only primitives are supported + returnByValue: true, }, - (_, param) => { - const traceId = param && param.result ? (param.result.value as string) : '--'; - const [trace_id, span_id, parent_span_id] = traceId.split('-') as (string | undefined)[]; + (err, param) => { + if (err) { + log(`Error executing script: '${err.message}'`); + } + + const scopes = param && param.result ? (param.result.value as ScopeData) : undefined; session.post('Debugger.resume'); session.post('Debugger.disable'); - const context = trace_id?.length && span_id?.length ? { trace_id, span_id, parent_span_id } : undefined; - sendAnrEvent(stackFrames, context).then(null, () => { + sendAnrEvent(stackFrames, scopes).then(null, () => { log('Sending ANR event failed.'); }); },