Skip to content

Commit 2d91524

Browse files
committed
fix(node): Include debug_meta with ANR events
1 parent b2605be commit 2d91524

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,10 +1,11 @@
11
import { Worker } from 'node:worker_threads';
22
import { defineIntegration, getCurrentScope, getGlobalScope, getIsolationScope, mergeScopeData } from '@sentry/core';
33
import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/types';
4-
import { GLOBAL_OBJ, logger } from '@sentry/utils';
4+
import { getFilenameToDebugIdMap, GLOBAL_OBJ, logger } from '@sentry/utils';
55
import { NODE_VERSION } from '../../nodeVersion';
66
import type { NodeClient } from '../../sdk/client';
77
import type { AnrIntegrationOptions, WorkerStartData } from './common';
8+
import * as diagnosticsChannel from 'node:diagnostics_channel';
89

910
// This string is a placeholder that gets overwritten with the worker code.
1011
export const base64WorkerScript = '###AnrWorkerScript###';
@@ -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)