Skip to content

Commit

Permalink
fix(node): Include debug_meta with ANR events (#14203)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
timfish authored Nov 11, 2024
1 parent 8532e25 commit fac7209
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 6 deletions.
2 changes: 2 additions & 0 deletions dev-packages/node-integration-tests/suites/anr/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions dev-packages/node-integration-tests/suites/anr/basic.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
21 changes: 19 additions & 2 deletions dev-packages/node-integration-tests/suites/anr/test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Event } from '@sentry/types';
import { conditionalTest } from '../../utils';
import { cleanupChildProcesses, createRunner } from '../../utils/runner';

Expand Down Expand Up @@ -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 => {
Expand Down
18 changes: 16 additions & 2 deletions packages/node/src/integrations/anr/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -100,6 +101,13 @@ type AnrReturn = (options?: Partial<AnrIntegrationOptions>) => 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
*
Expand Down Expand Up @@ -153,6 +161,12 @@ async function _startWorker(
}
}

let debugImages: Record<string, string> = 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
Expand All @@ -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 (_) {
//
}
Expand Down
40 changes: 38 additions & 2 deletions packages/node/src/integrations/anr/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +26,7 @@ type VoidFunction = () => void;
const options: WorkerStartData = workerData;
let session: Session | undefined;
let hasSentAnrEvent = false;
let mainDebugImages: Record<string, string> = {};

function log(msg: string): void {
if (options.debug) {
Expand Down Expand Up @@ -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<string, string>();

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);

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

applyDebugMeta(event);

const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata, options.tunnel);
// Log the envelope to aid in testing
log(JSON.stringify(envelope));
Expand Down Expand Up @@ -272,10 +304,14 @@ function watchdogTimeout(): void {

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

parentPort?.on('message', (msg: { session: Session | undefined }) => {
parentPort?.on('message', (msg: { session: Session | undefined; debugImages?: Record<string, string> }) => {
if (msg.session) {
session = makeSession(msg.session);
}

if (msg.debugImages) {
mainDebugImages = msg.debugImages;
}

poll();
});

0 comments on commit fac7209

Please sign in to comment.