-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(node): Local variables via async inspector in node 19+ (#9962)
This PR creates a new `LocalVariables` integration that uses the async inspector API. Rather than create a huge messy integration that supports both the sync (node 18) and async (node >= v19) APIs, I created a new integration and wrapped both the sync and async integrations with user facing integration that switches depending on node version. The async API doesn't require the stacking of callbacks that risks stack overflows and limits the number of frames we dare to evaluate. When we tried wrapping the sync API with promises, memory was leaked at an alarming rate! The inspector APIs are not available on all builds of Node so we have to lazily load it and catch any exceptions. I've had to use `dynamicRequire` because webpack picks up `import()` and reports missing dependency when bundling for older versions of node.
- Loading branch information
Showing
12 changed files
with
565 additions
and
258 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,4 +32,6 @@ function one(name) { | |
ty.two(name); | ||
} | ||
|
||
one('some name'); | ||
setTimeout(() => { | ||
one('some name'); | ||
}, 1000); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,4 +31,6 @@ function one(name) { | |
ty.two(name); | ||
} | ||
|
||
one('some name'); | ||
setTimeout(() => { | ||
one('some name'); | ||
}, 1000); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
packages/node/src/integrations/local-variables/common.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import type { StackFrame, StackParser } from '@sentry/types'; | ||
import type { Debugger } from 'inspector'; | ||
|
||
export type Variables = Record<string, unknown>; | ||
|
||
export type RateLimitIncrement = () => void; | ||
|
||
/** | ||
* Creates a rate limiter that will call the disable callback when the rate limit is reached and the enable callback | ||
* when a timeout has occurred. | ||
* @param maxPerSecond Maximum number of calls per second | ||
* @param enable Callback to enable capture | ||
* @param disable Callback to disable capture | ||
* @returns A function to call to increment the rate limiter count | ||
*/ | ||
export function createRateLimiter( | ||
maxPerSecond: number, | ||
enable: () => void, | ||
disable: (seconds: number) => void, | ||
): RateLimitIncrement { | ||
let count = 0; | ||
let retrySeconds = 5; | ||
let disabledTimeout = 0; | ||
|
||
setInterval(() => { | ||
if (disabledTimeout === 0) { | ||
if (count > maxPerSecond) { | ||
retrySeconds *= 2; | ||
disable(retrySeconds); | ||
|
||
// Cap at one day | ||
if (retrySeconds > 86400) { | ||
retrySeconds = 86400; | ||
} | ||
disabledTimeout = retrySeconds; | ||
} | ||
} else { | ||
disabledTimeout -= 1; | ||
|
||
if (disabledTimeout === 0) { | ||
enable(); | ||
} | ||
} | ||
|
||
count = 0; | ||
}, 1_000).unref(); | ||
|
||
return () => { | ||
count += 1; | ||
}; | ||
} | ||
|
||
// Add types for the exception event data | ||
export type PausedExceptionEvent = Debugger.PausedEventDataType & { | ||
data: { | ||
// This contains error.stack | ||
description: string; | ||
}; | ||
}; | ||
|
||
/** Could this be an anonymous function? */ | ||
export function isAnonymous(name: string | undefined): boolean { | ||
return name !== undefined && (name.length === 0 || name === '?' || name === '<anonymous>'); | ||
} | ||
|
||
/** Do the function names appear to match? */ | ||
export function functionNamesMatch(a: string | undefined, b: string | undefined): boolean { | ||
return a === b || (isAnonymous(a) && isAnonymous(b)); | ||
} | ||
|
||
/** Creates a unique hash from stack frames */ | ||
export function hashFrames(frames: StackFrame[] | undefined): string | undefined { | ||
if (frames === undefined) { | ||
return; | ||
} | ||
|
||
// Only hash the 10 most recent frames (ie. the last 10) | ||
return frames.slice(-10).reduce((acc, frame) => `${acc},${frame.function},${frame.lineno},${frame.colno}`, ''); | ||
} | ||
|
||
/** | ||
* We use the stack parser to create a unique hash from the exception stack trace | ||
* This is used to lookup vars when the exception passes through the event processor | ||
*/ | ||
export function hashFromStack(stackParser: StackParser, stack: string | undefined): string | undefined { | ||
if (stack === undefined) { | ||
return undefined; | ||
} | ||
|
||
return hashFrames(stackParser(stack, 1)); | ||
} | ||
|
||
export interface FrameVariables { | ||
function: string; | ||
vars?: Variables; | ||
} | ||
|
||
export interface Options { | ||
/** | ||
* Capture local variables for both caught and uncaught exceptions | ||
* | ||
* - When false, only uncaught exceptions will have local variables | ||
* - When true, both caught and uncaught exceptions will have local variables. | ||
* | ||
* Defaults to `true`. | ||
* | ||
* Capturing local variables for all exceptions can be expensive since the debugger pauses for every throw to collect | ||
* local variables. | ||
* | ||
* To reduce the likelihood of this feature impacting app performance or throughput, this feature is rate-limited. | ||
* Once the rate limit is reached, local variables will only be captured for uncaught exceptions until a timeout has | ||
* been reached. | ||
*/ | ||
captureAllExceptions?: boolean; | ||
/** | ||
* Maximum number of exceptions to capture local variables for per second before rate limiting is triggered. | ||
*/ | ||
maxExceptionsPerSecond?: number; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { convertIntegrationFnToClass } from '@sentry/core'; | ||
import type { IntegrationFn } from '@sentry/types'; | ||
import { NODE_VERSION } from '../../nodeVersion'; | ||
import type { Options } from './common'; | ||
import { localVariablesAsync } from './local-variables-async'; | ||
import { localVariablesSync } from './local-variables-sync'; | ||
|
||
const INTEGRATION_NAME = 'LocalVariables'; | ||
|
||
/** | ||
* Adds local variables to exception frames | ||
*/ | ||
const localVariables: IntegrationFn = (options: Options = {}) => { | ||
return NODE_VERSION.major < 19 ? localVariablesSync(options) : localVariablesAsync(options); | ||
}; | ||
|
||
/** | ||
* Adds local variables to exception frames | ||
*/ | ||
// eslint-disable-next-line deprecation/deprecation | ||
export const LocalVariables = convertIntegrationFnToClass(INTEGRATION_NAME, localVariables); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.