1+ /* eslint-disable max-lines */
12import type { Event , EventProcessor , Exception , Hub , Integration , StackFrame , StackParser } from '@sentry/types' ;
23import { logger } from '@sentry/utils' ;
34import type { Debugger , InspectorNotification , Runtime , Session } from 'inspector' ;
@@ -11,6 +12,8 @@ type OnPauseEvent = InspectorNotification<Debugger.PausedEventDataType>;
1112export interface DebugSession {
1213 /** Configures and connects to the debug session */
1314 configureAndConnect ( onPause : ( message : OnPauseEvent , complete : ( ) => void ) => void , captureAll : boolean ) : void ;
15+ /** Updates which kind of exceptions to capture */
16+ setPauseOnExceptions ( captureAll : boolean ) : void ;
1417 /** Gets local variables for an objectId */
1518 getLocalVariables ( objectId : string , callback : ( vars : Variables ) => void ) : void ;
1619}
@@ -19,6 +22,52 @@ type Next<T> = (result: T) => void;
1922type Add < T > = ( fn : Next < T > ) => void ;
2023type CallbackWrapper < T > = { add : Add < T > ; next : Next < T > } ;
2124
25+ type RateLimitIncrement = ( ) => void ;
26+
27+ /**
28+ * Creates a rate limiter
29+ * @param maxPerSecond Maximum number of calls per second
30+ * @param enable Callback to enable capture
31+ * @param disable Callback to disable capture
32+ * @returns A function to call to increment the rate limiter count
33+ */
34+ export function createRateLimiter (
35+ maxPerSecond : number ,
36+ enable : ( ) => void ,
37+ disable : ( seconds : number ) => void ,
38+ ) : RateLimitIncrement {
39+ let count = 0 ;
40+ let retrySeconds = 5 ;
41+ let disabledTimeout = 0 ;
42+
43+ setInterval ( ( ) => {
44+ if ( disabledTimeout === 0 ) {
45+ if ( count > maxPerSecond ) {
46+ retrySeconds *= 2 ;
47+ disable ( retrySeconds ) ;
48+
49+ // Cap at one day
50+ if ( retrySeconds > 86400 ) {
51+ retrySeconds = 86400 ;
52+ }
53+ disabledTimeout = retrySeconds ;
54+ }
55+ } else {
56+ disabledTimeout -= 1 ;
57+
58+ if ( disabledTimeout === 0 ) {
59+ enable ( ) ;
60+ }
61+ }
62+
63+ count = 0 ;
64+ } , 1_000 ) . unref ( ) ;
65+
66+ return ( ) => {
67+ count += 1 ;
68+ } ;
69+ }
70+
2271/** Creates a container for callbacks to be called sequentially */
2372export function createCallbackList < T > ( complete : Next < T > ) : CallbackWrapper < T > {
2473 // A collection of callbacks to be executed last to first
@@ -103,6 +152,10 @@ class AsyncSession implements DebugSession {
103152 this . _session . post ( 'Debugger.setPauseOnExceptions' , { state : captureAll ? 'all' : 'uncaught' } ) ;
104153 }
105154
155+ public setPauseOnExceptions ( captureAll : boolean ) : void {
156+ this . _session . post ( 'Debugger.setPauseOnExceptions' , { state : captureAll ? 'all' : 'uncaught' } ) ;
157+ }
158+
106159 /** @inheritdoc */
107160 public getLocalVariables ( objectId : string , complete : ( vars : Variables ) => void ) : void {
108161 this . _getProperties ( objectId , props => {
@@ -245,26 +298,41 @@ export interface FrameVariables {
245298 vars ?: Variables ;
246299}
247300
248- /** There are no options yet. This allows them to be added later without breaking changes */
249- // eslint-disable-next-line @typescript-eslint/no-empty-interface
250301interface Options {
251302 /**
252- * Capture local variables for both handled and unhandled exceptions
303+ * Capture local variables for both caught and uncaught exceptions
304+ *
305+ * - When false, only uncaught exceptions will have local variables
306+ * - When true, both caught and uncaught exceptions will have local variables.
307+ *
308+ * Defaults to `true`.
253309 *
254- * Default: false - Only captures local variables for uncaught exceptions
310+ * Capturing local variables for all exceptions can be expensive since the debugger pauses for every throw to collect
311+ * local variables.
312+ *
313+ * To reduce the likelihood of this feature impacting app performance or throughput, this feature is rate-limited.
314+ * Once the rate limit is reached, local variables will only be captured for uncaught exceptions until a timeout has
315+ * been reached.
255316 */
256317 captureAllExceptions ?: boolean ;
318+ /**
319+ * Maximum number of exceptions to capture local variables for per second before rate limiting is triggered.
320+ */
321+ maxExceptionsPerSecond ?: number ;
257322}
258323
259324/**
260325 * Adds local variables to exception frames
326+ *
327+ * Default: 50
261328 */
262329export class LocalVariables implements Integration {
263330 public static id : string = 'LocalVariables' ;
264331
265332 public readonly name : string = LocalVariables . id ;
266333
267334 private readonly _cachedFrames : LRUMap < string , FrameVariables [ ] > = new LRUMap ( 20 ) ;
335+ private _rateLimiter : RateLimitIncrement | undefined ;
268336
269337 public constructor (
270338 private readonly _options : Options = { } ,
@@ -293,12 +361,32 @@ export class LocalVariables implements Integration {
293361 return ;
294362 }
295363
364+ const captureAll = this . _options . captureAllExceptions !== false ;
365+
296366 this . _session . configureAndConnect (
297367 ( ev , complete ) =>
298368 this . _handlePaused ( clientOptions . stackParser , ev as InspectorNotification < PausedExceptionEvent > , complete ) ,
299- ! ! this . _options . captureAllExceptions ,
369+ captureAll ,
300370 ) ;
301371
372+ if ( captureAll ) {
373+ const max = this . _options . maxExceptionsPerSecond || 50 ;
374+
375+ this . _rateLimiter = createRateLimiter (
376+ max ,
377+ ( ) => {
378+ logger . log ( 'Local variables rate-limit lifted.' ) ;
379+ this . _session ?. setPauseOnExceptions ( true ) ;
380+ } ,
381+ seconds => {
382+ logger . log (
383+ `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${ seconds } seconds.` ,
384+ ) ;
385+ this . _session ?. setPauseOnExceptions ( false ) ;
386+ } ,
387+ ) ;
388+ }
389+
302390 addGlobalEventProcessor ( async event => this . _addLocalVariables ( event ) ) ;
303391 }
304392 }
@@ -316,6 +404,8 @@ export class LocalVariables implements Integration {
316404 return ;
317405 }
318406
407+ this . _rateLimiter ?.( ) ;
408+
319409 // data.description contains the original error.stack
320410 const exceptionHash = hashFromStack ( stackParser , data ?. description ) ;
321411
0 commit comments