diff --git a/dev-packages/node-integration-tests/suites/anr/basic.js b/dev-packages/node-integration-tests/suites/anr/basic.js index b1dddf958d46..e2adf0e8c60f 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.js +++ b/dev-packages/node-integration-tests/suites/anr/basic.js @@ -3,6 +3,8 @@ const assert = require('assert'); const Sentry = require('@sentry/node'); +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + setTimeout(() => { process.exit(); }, 10000); diff --git a/dev-packages/node-integration-tests/suites/anr/basic.mjs b/dev-packages/node-integration-tests/suites/anr/basic.mjs index c3e74222f587..18777e5ecdbd 100644 --- a/dev-packages/node-integration-tests/suites/anr/basic.mjs +++ b/dev-packages/node-integration-tests/suites/anr/basic.mjs @@ -3,6 +3,8 @@ import * as crypto from 'crypto'; import * as Sentry from '@sentry/node'; +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + setTimeout(() => { process.exit(); }, 10000); diff --git a/dev-packages/node-integration-tests/suites/anr/test.ts b/dev-packages/node-integration-tests/suites/anr/test.ts index 78f89d7451c0..0352212a8293 100644 --- a/dev-packages/node-integration-tests/suites/anr/test.ts +++ b/dev-packages/node-integration-tests/suites/anr/test.ts @@ -1,3 +1,4 @@ +import type { Event } from '@sentry/types'; import { conditionalTest } from '../../utils'; import { cleanupChildProcesses, createRunner } from '../../utils/runner'; @@ -64,17 +65,33 @@ const ANR_EVENT_WITH_SCOPE = { ]), }; +const ANR_EVENT_WITH_DEBUG_META: Event = { + ...ANR_EVENT_WITH_SCOPE, + debug_meta: { + images: [ + { + type: 'sourcemap', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + code_file: expect.stringContaining('basic.'), + }, + ], + }, +}; + conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => { afterAll(() => { cleanupChildProcesses(); }); test('CJS', done => { - createRunner(__dirname, 'basic.js').withMockSentryServer().expect({ event: ANR_EVENT_WITH_SCOPE }).start(done); + createRunner(__dirname, 'basic.js').withMockSentryServer().expect({ event: ANR_EVENT_WITH_DEBUG_META }).start(done); }); test('ESM', done => { - createRunner(__dirname, 'basic.mjs').withMockSentryServer().expect({ event: ANR_EVENT_WITH_SCOPE }).start(done); + createRunner(__dirname, 'basic.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_DEBUG_META }) + .start(done); }); test('blocked indefinitely', done => { diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 92a4078aa766..c5f5b28e0888 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -1,7 +1,8 @@ +import * as diagnosticsChannel from 'node:diagnostics_channel'; import { Worker } from 'node:worker_threads'; import { defineIntegration, getCurrentScope, getGlobalScope, getIsolationScope, mergeScopeData } from '@sentry/core'; import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/types'; -import { GLOBAL_OBJ, logger } from '@sentry/utils'; +import { GLOBAL_OBJ, getFilenameToDebugIdMap, logger } from '@sentry/utils'; import { NODE_VERSION } from '../../nodeVersion'; import type { NodeClient } from '../../sdk/client'; import type { AnrIntegrationOptions, WorkerStartData } from './common'; @@ -100,6 +101,13 @@ type AnrReturn = (options?: Partial) => Integration & Anr export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn; +function onModuleLoad(callback: () => void): void { + // eslint-disable-next-line deprecation/deprecation + diagnosticsChannel.channel('module.require.end').subscribe(() => callback()); + // eslint-disable-next-line deprecation/deprecation + diagnosticsChannel.channel('module.import.asyncEnd').subscribe(() => callback()); +} + /** * Starts the ANR worker thread * @@ -153,6 +161,12 @@ async function _startWorker( } } + let debugImages: Record = getFilenameToDebugIdMap(initOptions.stackParser); + + onModuleLoad(() => { + debugImages = getFilenameToDebugIdMap(initOptions.stackParser); + }); + const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), { workerData: options, // We don't want any Node args to be passed to the worker @@ -171,7 +185,7 @@ async function _startWorker( // serialized without making it a SerializedSession const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; // message the worker to tell it the main event loop is still running - worker.postMessage({ session }); + worker.postMessage({ session, debugImages }); } catch (_) { // } diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts index 67532435a39e..8e20fbeeb39a 100644 --- a/packages/node/src/integrations/anr/worker.ts +++ b/packages/node/src/integrations/anr/worker.ts @@ -8,7 +8,7 @@ import { makeSession, updateSession, } from '@sentry/core'; -import type { Event, ScopeData, Session, StackFrame } from '@sentry/types'; +import type { DebugImage, Event, ScopeData, Session, StackFrame } from '@sentry/types'; import { callFrameToStackFrame, normalizeUrlToBase, @@ -26,6 +26,7 @@ type VoidFunction = () => void; const options: WorkerStartData = workerData; let session: Session | undefined; let hasSentAnrEvent = false; +let mainDebugImages: Record = {}; function log(msg: string): void { if (options.debug) { @@ -87,6 +88,35 @@ function prepareStackFrames(stackFrames: StackFrame[] | undefined): StackFrame[] return strippedFrames; } +function applyDebugMeta(event: Event): void { + if (Object.keys(mainDebugImages).length === 0) { + return; + } + + const filenameToDebugId = new Map(); + + for (const exception of event.exception?.values || []) { + for (const frame of exception.stacktrace?.frames || []) { + const filename = frame.abs_path || frame.filename; + if (filename && mainDebugImages[filename]) { + filenameToDebugId.set(filename, mainDebugImages[filename] as string); + } + } + } + + if (filenameToDebugId.size > 0) { + const images: DebugImage[] = []; + for (const [filename, debugId] of filenameToDebugId.entries()) { + images.push({ + type: 'sourcemap', + code_file: filename, + debug_id: debugId, + }); + } + event.debug_meta = { images }; + } +} + function applyScopeToEvent(event: Event, scope: ScopeData): void { applyScopeDataToEvent(event, scope); @@ -140,6 +170,8 @@ async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise { +parentPort?.on('message', (msg: { session: Session | undefined; debugImages?: Record }) => { if (msg.session) { session = makeSession(msg.session); } + if (msg.debugImages) { + mainDebugImages = msg.debugImages; + } + poll(); });