From 197cf805bee0845d966caa5154c4d0618365dcf2 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 6 Oct 2023 14:39:10 +0200 Subject: [PATCH 1/4] feat(electron): Move common ANR code to utils --- packages/node/src/anr/debugger.ts | 56 +++------------- packages/node/src/anr/index.ts | 36 +--------- packages/utils/src/anr.ts | 106 ++++++++++++++++++++++++++++++ packages/utils/src/index.ts | 1 + 4 files changed, 118 insertions(+), 81 deletions(-) create mode 100644 packages/utils/src/anr.ts diff --git a/packages/node/src/anr/debugger.ts b/packages/node/src/anr/debugger.ts index 4d4a2799fa64..fc4f20bcf5ef 100644 --- a/packages/node/src/anr/debugger.ts +++ b/packages/node/src/anr/debugger.ts @@ -1,33 +1,10 @@ import type { StackFrame } from '@sentry/types'; -import { dropUndefinedKeys, filenameIsInApp } from '@sentry/utils'; import type { Debugger } from 'inspector'; +import { createDebugPauseMessageHandler } from '@sentry/utils'; import { getModuleFromFilename } from '../module'; import { createWebSocketClient } from './websocket'; -/** - * Converts Debugger.CallFrame to Sentry StackFrame - */ -function callFrameToStackFrame( - frame: Debugger.CallFrame, - filenameFromScriptId: (id: string) => string | undefined, -): StackFrame { - const filename = filenameFromScriptId(frame.location.scriptId)?.replace(/^file:\/\//, ''); - - // CallFrame row/col are 0 based, whereas StackFrame are 1 based - const colno = frame.location.columnNumber ? frame.location.columnNumber + 1 : undefined; - const lineno = frame.location.lineNumber ? frame.location.lineNumber + 1 : undefined; - - return dropUndefinedKeys({ - filename, - module: getModuleFromFilename(filename), - function: frame.functionName || '?', - colno, - lineno, - in_app: filename ? filenameIsInApp(filename) : undefined, - }); -} - // The only messages we care about type DebugMessage = | { @@ -45,7 +22,7 @@ type DebugMessage = async function webSocketDebugger( url: string, onMessage: (message: DebugMessage) => void, -): Promise<(method: string, params?: unknown) => void> { +): Promise<(method: string) => void> { let id = 0; const webSocket = await createWebSocketClient(url); @@ -54,8 +31,8 @@ async function webSocketDebugger( onMessage(message); }); - return (method: string, params?: unknown) => { - webSocket.send(JSON.stringify({ id: id++, method, params })); + return (method: string) => { + webSocket.send(JSON.stringify({ id: id++, method })); }; } @@ -66,27 +43,10 @@ async function webSocketDebugger( * @returns A function that triggers the debugger to pause and capture a stack trace */ export async function captureStackTrace(url: string, callback: (frames: StackFrame[]) => void): Promise<() => void> { - // Collect scriptId -> url map so we can look up the filenames later - const scripts = new Map(); - - const sendCommand = await webSocketDebugger(url, message => { - if (message.method === 'Debugger.scriptParsed') { - scripts.set(message.params.scriptId, message.params.url); - } else if (message.method === 'Debugger.paused') { - // copy the frames - const callFrames = [...message.params.callFrames]; - // and resume immediately! - sendCommand('Debugger.resume'); - sendCommand('Debugger.disable'); - - const frames = callFrames - .map(frame => callFrameToStackFrame(frame, id => scripts.get(id))) - // Sentry expects the frames to be in the opposite order - .reverse(); - - callback(frames); - } - }); + const sendCommand: (method: string) => void = await webSocketDebugger( + url, + createDebugPauseMessageHandler(cmd => sendCommand(cmd), getModuleFromFilename, callback), + ); return () => { sendCommand('Debugger.enable'); diff --git a/packages/node/src/anr/index.ts b/packages/node/src/anr/index.ts index 99bb5901b4ea..38aa697457dd 100644 --- a/packages/node/src/anr/index.ts +++ b/packages/node/src/anr/index.ts @@ -1,5 +1,5 @@ import type { Event, StackFrame } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { logger, watchdogTimer } from '@sentry/utils'; import { spawn } from 'child_process'; import { addGlobalEventProcessor, captureEvent, flush } from '..'; @@ -8,36 +8,6 @@ import { captureStackTrace } from './debugger'; const DEFAULT_INTERVAL = 50; const DEFAULT_HANG_THRESHOLD = 5000; -/** - * A node.js watchdog timer - * @param pollInterval The interval that we expect to get polled at - * @param anrThreshold The threshold for when we consider ANR - * @param callback The callback to call for ANR - * @returns A function to call to reset the timer - */ -function watchdogTimer(pollInterval: number, anrThreshold: number, callback: () => void): () => void { - let lastPoll = process.hrtime(); - let triggered = false; - - setInterval(() => { - const [seconds, nanoSeconds] = process.hrtime(lastPoll); - const diffMs = Math.floor(seconds * 1e3 + nanoSeconds / 1e6); - - if (triggered === false && diffMs > pollInterval + anrThreshold) { - triggered = true; - callback(); - } - - if (diffMs < pollInterval + anrThreshold) { - triggered = false; - } - }, 20); - - return () => { - lastPoll = process.hrtime(); - }; -} - interface Options { /** * The app entry script. This is used to run the same script as the child process. @@ -216,10 +186,10 @@ function handleChildProcess(options: Options): void { } } - const ping = watchdogTimer(options.pollInterval, options.anrThreshold, watchdogTimeout); + const { poll } = watchdogTimer(options.pollInterval, options.anrThreshold, watchdogTimeout); process.on('message', () => { - ping(); + poll(); }); } diff --git a/packages/utils/src/anr.ts b/packages/utils/src/anr.ts new file mode 100644 index 000000000000..4aadd51545eb --- /dev/null +++ b/packages/utils/src/anr.ts @@ -0,0 +1,106 @@ +import { StackFrame } from '@sentry/types'; +import type { Debugger } from 'inspector'; +import { dropUndefinedKeys } from './object'; +import { filenameIsInApp, stripSentryFramesAndReverse } from './stacktrace'; + +type WatchdogReturn = { poll: () => void; enabled: (state: boolean) => void }; + +/** + * A node.js watchdog timer + * @param pollInterval The interval that we expect to get polled at + * @param anrThreshold The threshold for when we consider ANR + * @param callback The callback to call for ANR + * @returns A function to call to reset the timer + */ +export function watchdogTimer(pollInterval: number, anrThreshold: number, callback: () => void): WatchdogReturn { + let lastPoll = process.hrtime(); + let triggered = false; + let enabled = true; + + setInterval(() => { + const [seconds, nanoSeconds] = process.hrtime(lastPoll); + const diffMs = Math.floor(seconds * 1e3 + nanoSeconds / 1e6); + + if (triggered === false && diffMs > pollInterval + anrThreshold) { + triggered = true; + if (enabled) { + callback(); + } + } + + if (diffMs < pollInterval + anrThreshold) { + triggered = false; + } + }, 20); + + return { + poll: () => { + lastPoll = process.hrtime(); + }, + enabled: (state: boolean) => { + enabled = state; + }, + }; +} + +/** + * Converts Debugger.CallFrame to Sentry StackFrame + */ +function callFrameToStackFrame( + frame: Debugger.CallFrame, + getModuleFromFilename: (filename: string | undefined) => string | undefined, + filenameFromScriptId: (id: string) => string | undefined, +): StackFrame { + const filename = filenameFromScriptId(frame.location.scriptId)?.replace(/^file:\/\//, ''); + + // CallFrame row/col are 0 based, whereas StackFrame are 1 based + const colno = frame.location.columnNumber ? frame.location.columnNumber + 1 : undefined; + const lineno = frame.location.lineNumber ? frame.location.lineNumber + 1 : undefined; + + return dropUndefinedKeys({ + filename, + module: getModuleFromFilename(filename), + function: frame.functionName || '?', + colno, + lineno, + in_app: filename ? filenameIsInApp(filename) : undefined, + }); +} + +// The only messages we care about +type DebugMessage = + | { + method: 'Debugger.scriptParsed'; + params: Debugger.ScriptParsedEventDataType; + } + | { method: 'Debugger.paused'; params: Debugger.PausedEventDataType }; + +/** + * Creates a message handler from the v8 debugger protocol and passed stack frames to the callback when paused. + */ +export function createDebugPauseMessageHandler( + sendCommand: (message: string) => void, + getModuleFromFilename: (filename?: string) => string | undefined, + pausedStackFrames: (frames: StackFrame[]) => void, +): (message: DebugMessage) => void { + // Collect scriptId -> url map so we can look up the filenames later + const scripts = new Map(); + + return message => { + if (message.method === 'Debugger.scriptParsed') { + scripts.set(message.params.scriptId, message.params.url); + } else if (message.method === 'Debugger.paused') { + // copy the frames + const callFrames = [...message.params.callFrames]; + // and resume immediately + sendCommand('Debugger.resume'); + sendCommand('Debugger.disable'); + + const frames = stripSentryFramesAndReverse( + callFrames.map(frame => callFrameToStackFrame(frame, getModuleFromFilename, id => scripts.get(id))), + ); + + pausedStackFrames(frames); + } + }; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 8de4941f6b96..81f4d947cd0d 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -31,3 +31,4 @@ export * from './url'; export * from './userIntegrations'; export * from './cache'; export * from './eventbuilder'; +export * from './anr'; From 33177c26e90d649b824cc7462ea1e5c31575f8f8 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 6 Oct 2023 16:15:36 +0200 Subject: [PATCH 2/4] Fix linting --- packages/node/src/anr/debugger.ts | 2 +- packages/utils/src/anr.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/node/src/anr/debugger.ts b/packages/node/src/anr/debugger.ts index fc4f20bcf5ef..01b2a90fe0f9 100644 --- a/packages/node/src/anr/debugger.ts +++ b/packages/node/src/anr/debugger.ts @@ -1,6 +1,6 @@ import type { StackFrame } from '@sentry/types'; -import type { Debugger } from 'inspector'; import { createDebugPauseMessageHandler } from '@sentry/utils'; +import type { Debugger } from 'inspector'; import { getModuleFromFilename } from '../module'; import { createWebSocketClient } from './websocket'; diff --git a/packages/utils/src/anr.ts b/packages/utils/src/anr.ts index 4aadd51545eb..5bb9eb670062 100644 --- a/packages/utils/src/anr.ts +++ b/packages/utils/src/anr.ts @@ -1,5 +1,6 @@ -import { StackFrame } from '@sentry/types'; +import type { StackFrame } from '@sentry/types'; import type { Debugger } from 'inspector'; + import { dropUndefinedKeys } from './object'; import { filenameIsInApp, stripSentryFramesAndReverse } from './stacktrace'; @@ -51,7 +52,10 @@ function callFrameToStackFrame( getModuleFromFilename: (filename: string | undefined) => string | undefined, filenameFromScriptId: (id: string) => string | undefined, ): StackFrame { - const filename = filenameFromScriptId(frame.location.scriptId)?.replace(/^file:\/\//, ''); + let filename = filenameFromScriptId(frame.location.scriptId); + if (filename) { + filename = filename.replace(/^file:\/\//, ''); + } // CallFrame row/col are 0 based, whereas StackFrame are 1 based const colno = frame.location.columnNumber ? frame.location.columnNumber + 1 : undefined; From b5b8f4e5c28fc817b9476b82883486cd8831d4bc Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 6 Oct 2023 18:40:38 +0200 Subject: [PATCH 3/4] Vendor node types --- packages/utils/src/anr.ts | 48 +++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/utils/src/anr.ts b/packages/utils/src/anr.ts index 5bb9eb670062..94c992515048 100644 --- a/packages/utils/src/anr.ts +++ b/packages/utils/src/anr.ts @@ -1,5 +1,4 @@ import type { StackFrame } from '@sentry/types'; -import type { Debugger } from 'inspector'; import { dropUndefinedKeys } from './object'; import { filenameIsInApp, stripSentryFramesAndReverse } from './stacktrace'; @@ -44,18 +43,38 @@ export function watchdogTimer(pollInterval: number, anrThreshold: number, callba }; } +// types copied from inspector.d.ts +interface Location { + scriptId: string; + lineNumber: number; + columnNumber?: number; +} + +interface CallFrame { + functionName: string; + location: Location; + url: string; +} + +interface ScriptParsedEventDataType { + scriptId: string; + url: string; +} + +interface PausedEventDataType { + callFrames: CallFrame[]; + reason: string; +} + /** * Converts Debugger.CallFrame to Sentry StackFrame */ function callFrameToStackFrame( - frame: Debugger.CallFrame, + frame: CallFrame, + url: string | undefined, getModuleFromFilename: (filename: string | undefined) => string | undefined, - filenameFromScriptId: (id: string) => string | undefined, ): StackFrame { - let filename = filenameFromScriptId(frame.location.scriptId); - if (filename) { - filename = filename.replace(/^file:\/\//, ''); - } + const filename = url ? url.replace(/^file:\/\//, '') : undefined; // CallFrame row/col are 0 based, whereas StackFrame are 1 based const colno = frame.location.columnNumber ? frame.location.columnNumber + 1 : undefined; @@ -73,11 +92,8 @@ function callFrameToStackFrame( // The only messages we care about type DebugMessage = - | { - method: 'Debugger.scriptParsed'; - params: Debugger.ScriptParsedEventDataType; - } - | { method: 'Debugger.paused'; params: Debugger.PausedEventDataType }; + | { method: 'Debugger.scriptParsed'; params: ScriptParsedEventDataType } + | { method: 'Debugger.paused'; params: PausedEventDataType }; /** * Creates a message handler from the v8 debugger protocol and passed stack frames to the callback when paused. @@ -100,11 +116,13 @@ export function createDebugPauseMessageHandler( sendCommand('Debugger.resume'); sendCommand('Debugger.disable'); - const frames = stripSentryFramesAndReverse( - callFrames.map(frame => callFrameToStackFrame(frame, getModuleFromFilename, id => scripts.get(id))), + const stackFrames = stripSentryFramesAndReverse( + callFrames.map(frame => + callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), getModuleFromFilename), + ), ); - pausedStackFrames(frames); + pausedStackFrames(stackFrames); } }; } From 430291f2e8b466f54dc022377a66f56b990c8f9f Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 9 Oct 2023 11:52:33 +0200 Subject: [PATCH 4/4] Fix jsdoc --- packages/utils/src/anr.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/anr.ts b/packages/utils/src/anr.ts index 94c992515048..b007c1355cb7 100644 --- a/packages/utils/src/anr.ts +++ b/packages/utils/src/anr.ts @@ -3,14 +3,19 @@ import type { StackFrame } from '@sentry/types'; import { dropUndefinedKeys } from './object'; import { filenameIsInApp, stripSentryFramesAndReverse } from './stacktrace'; -type WatchdogReturn = { poll: () => void; enabled: (state: boolean) => void }; +type WatchdogReturn = { + /** Resets the watchdog timer */ + poll: () => void; + /** Enables or disables the watchdog timer */ + enabled: (state: boolean) => void; +}; /** * A node.js watchdog timer * @param pollInterval The interval that we expect to get polled at * @param anrThreshold The threshold for when we consider ANR * @param callback The callback to call for ANR - * @returns A function to call to reset the timer + * @returns An object with `poll` and `enabled` functions {@link WatchdogReturn} */ export function watchdogTimer(pollInterval: number, anrThreshold: number, callback: () => void): WatchdogReturn { let lastPoll = process.hrtime();