Skip to content

Commit fac7209

Browse files
authored
fix(node): Include debug_meta with ANR events (#14203)
Sends a map of filename -> debug ids to the ANR worker thread and sends an updated list every time more modules are loaded. These are then used and included with events so they can be symbolicated
1 parent 8532e25 commit fac7209

File tree

5 files changed

+77
-6
lines changed

5 files changed

+77
-6
lines changed

dev-packages/node-integration-tests/suites/anr/basic.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const assert = require('assert');
33

44
const Sentry = require('@sentry/node');
55

6+
global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' };
7+
68
setTimeout(() => {
79
process.exit();
810
}, 10000);

dev-packages/node-integration-tests/suites/anr/basic.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import * as crypto from 'crypto';
33

44
import * as Sentry from '@sentry/node';
55

6+
global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' };
7+
68
setTimeout(() => {
79
process.exit();
810
}, 10000);

dev-packages/node-integration-tests/suites/anr/test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Event } from '@sentry/types';
12
import { conditionalTest } from '../../utils';
23
import { cleanupChildProcesses, createRunner } from '../../utils/runner';
34

@@ -64,17 +65,33 @@ const ANR_EVENT_WITH_SCOPE = {
6465
]),
6566
};
6667

68+
const ANR_EVENT_WITH_DEBUG_META: Event = {
69+
...ANR_EVENT_WITH_SCOPE,
70+
debug_meta: {
71+
images: [
72+
{
73+
type: 'sourcemap',
74+
debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
75+
code_file: expect.stringContaining('basic.'),
76+
},
77+
],
78+
},
79+
};
80+
6781
conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => {
6882
afterAll(() => {
6983
cleanupChildProcesses();
7084
});
7185

7286
test('CJS', done => {
73-
createRunner(__dirname, 'basic.js').withMockSentryServer().expect({ event: ANR_EVENT_WITH_SCOPE }).start(done);
87+
createRunner(__dirname, 'basic.js').withMockSentryServer().expect({ event: ANR_EVENT_WITH_DEBUG_META }).start(done);
7488
});
7589

7690
test('ESM', done => {
77-
createRunner(__dirname, 'basic.mjs').withMockSentryServer().expect({ event: ANR_EVENT_WITH_SCOPE }).start(done);
91+
createRunner(__dirname, 'basic.mjs')
92+
.withMockSentryServer()
93+
.expect({ event: ANR_EVENT_WITH_DEBUG_META })
94+
.start(done);
7895
});
7996

8097
test('blocked indefinitely', done => {

packages/node/src/integrations/anr/index.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import * as diagnosticsChannel from 'node:diagnostics_channel';
12
import { Worker } from 'node:worker_threads';
23
import { defineIntegration, getCurrentScope, getGlobalScope, getIsolationScope, mergeScopeData } from '@sentry/core';
34
import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/types';
4-
import { GLOBAL_OBJ, logger } from '@sentry/utils';
5+
import { GLOBAL_OBJ, getFilenameToDebugIdMap, logger } from '@sentry/utils';
56
import { NODE_VERSION } from '../../nodeVersion';
67
import type { NodeClient } from '../../sdk/client';
78
import type { AnrIntegrationOptions, WorkerStartData } from './common';
@@ -100,6 +101,13 @@ type AnrReturn = (options?: Partial<AnrIntegrationOptions>) => Integration & Anr
100101

101102
export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn;
102103

104+
function onModuleLoad(callback: () => void): void {
105+
// eslint-disable-next-line deprecation/deprecation
106+
diagnosticsChannel.channel('module.require.end').subscribe(() => callback());
107+
// eslint-disable-next-line deprecation/deprecation
108+
diagnosticsChannel.channel('module.import.asyncEnd').subscribe(() => callback());
109+
}
110+
103111
/**
104112
* Starts the ANR worker thread
105113
*
@@ -153,6 +161,12 @@ async function _startWorker(
153161
}
154162
}
155163

164+
let debugImages: Record<string, string> = getFilenameToDebugIdMap(initOptions.stackParser);
165+
166+
onModuleLoad(() => {
167+
debugImages = getFilenameToDebugIdMap(initOptions.stackParser);
168+
});
169+
156170
const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), {
157171
workerData: options,
158172
// We don't want any Node args to be passed to the worker
@@ -171,7 +185,7 @@ async function _startWorker(
171185
// serialized without making it a SerializedSession
172186
const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined;
173187
// message the worker to tell it the main event loop is still running
174-
worker.postMessage({ session });
188+
worker.postMessage({ session, debugImages });
175189
} catch (_) {
176190
//
177191
}

packages/node/src/integrations/anr/worker.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
makeSession,
99
updateSession,
1010
} from '@sentry/core';
11-
import type { Event, ScopeData, Session, StackFrame } from '@sentry/types';
11+
import type { DebugImage, Event, ScopeData, Session, StackFrame } from '@sentry/types';
1212
import {
1313
callFrameToStackFrame,
1414
normalizeUrlToBase,
@@ -26,6 +26,7 @@ type VoidFunction = () => void;
2626
const options: WorkerStartData = workerData;
2727
let session: Session | undefined;
2828
let hasSentAnrEvent = false;
29+
let mainDebugImages: Record<string, string> = {};
2930

3031
function log(msg: string): void {
3132
if (options.debug) {
@@ -87,6 +88,35 @@ function prepareStackFrames(stackFrames: StackFrame[] | undefined): StackFrame[]
8788
return strippedFrames;
8889
}
8990

91+
function applyDebugMeta(event: Event): void {
92+
if (Object.keys(mainDebugImages).length === 0) {
93+
return;
94+
}
95+
96+
const filenameToDebugId = new Map<string, string>();
97+
98+
for (const exception of event.exception?.values || []) {
99+
for (const frame of exception.stacktrace?.frames || []) {
100+
const filename = frame.abs_path || frame.filename;
101+
if (filename && mainDebugImages[filename]) {
102+
filenameToDebugId.set(filename, mainDebugImages[filename] as string);
103+
}
104+
}
105+
}
106+
107+
if (filenameToDebugId.size > 0) {
108+
const images: DebugImage[] = [];
109+
for (const [filename, debugId] of filenameToDebugId.entries()) {
110+
images.push({
111+
type: 'sourcemap',
112+
code_file: filename,
113+
debug_id: debugId,
114+
});
115+
}
116+
event.debug_meta = { images };
117+
}
118+
}
119+
90120
function applyScopeToEvent(event: Event, scope: ScopeData): void {
91121
applyScopeDataToEvent(event, scope);
92122

@@ -140,6 +170,8 @@ async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise<v
140170
applyScopeToEvent(event, scope);
141171
}
142172

173+
applyDebugMeta(event);
174+
143175
const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata, options.tunnel);
144176
// Log the envelope to aid in testing
145177
log(JSON.stringify(envelope));
@@ -272,10 +304,14 @@ function watchdogTimeout(): void {
272304

273305
const { poll } = watchdogTimer(createHrTimer, options.pollInterval, options.anrThreshold, watchdogTimeout);
274306

275-
parentPort?.on('message', (msg: { session: Session | undefined }) => {
307+
parentPort?.on('message', (msg: { session: Session | undefined; debugImages?: Record<string, string> }) => {
276308
if (msg.session) {
277309
session = makeSession(msg.session);
278310
}
279311

312+
if (msg.debugImages) {
313+
mainDebugImages = msg.debugImages;
314+
}
315+
280316
poll();
281317
});

0 commit comments

Comments
 (0)