1
+ /* eslint-disable max-lines */
1
2
import type { Event , EventProcessor , Exception , Hub , Integration , StackFrame , StackParser } from '@sentry/types' ;
2
3
import { logger } from '@sentry/utils' ;
3
4
import type { Debugger , InspectorNotification , Runtime , Session } from 'inspector' ;
@@ -11,6 +12,8 @@ type OnPauseEvent = InspectorNotification<Debugger.PausedEventDataType>;
11
12
export interface DebugSession {
12
13
/** Configures and connects to the debug session */
13
14
configureAndConnect ( onPause : ( message : OnPauseEvent , complete : ( ) => void ) => void , captureAll : boolean ) : void ;
15
+ /** Updates which kind of exceptions to capture */
16
+ setPauseOnExceptions ( captureAll : boolean ) : void ;
14
17
/** Gets local variables for an objectId */
15
18
getLocalVariables ( objectId : string , callback : ( vars : Variables ) => void ) : void ;
16
19
}
@@ -19,6 +22,52 @@ type Next<T> = (result: T) => void;
19
22
type Add < T > = ( fn : Next < T > ) => void ;
20
23
type CallbackWrapper < T > = { add : Add < T > ; next : Next < T > } ;
21
24
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
+
22
71
/** Creates a container for callbacks to be called sequentially */
23
72
export function createCallbackList < T > ( complete : Next < T > ) : CallbackWrapper < T > {
24
73
// A collection of callbacks to be executed last to first
@@ -103,6 +152,10 @@ class AsyncSession implements DebugSession {
103
152
this . _session . post ( 'Debugger.setPauseOnExceptions' , { state : captureAll ? 'all' : 'uncaught' } ) ;
104
153
}
105
154
155
+ public setPauseOnExceptions ( captureAll : boolean ) : void {
156
+ this . _session . post ( 'Debugger.setPauseOnExceptions' , { state : captureAll ? 'all' : 'uncaught' } ) ;
157
+ }
158
+
106
159
/** @inheritdoc */
107
160
public getLocalVariables ( objectId : string , complete : ( vars : Variables ) => void ) : void {
108
161
this . _getProperties ( objectId , props => {
@@ -245,26 +298,41 @@ export interface FrameVariables {
245
298
vars ?: Variables ;
246
299
}
247
300
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
250
301
interface Options {
251
302
/**
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`.
253
309
*
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.
255
316
*/
256
317
captureAllExceptions ?: boolean ;
318
+ /**
319
+ * Maximum number of exceptions to capture local variables for per second before rate limiting is triggered.
320
+ */
321
+ maxExceptionsPerSecond ?: number ;
257
322
}
258
323
259
324
/**
260
325
* Adds local variables to exception frames
326
+ *
327
+ * Default: 50
261
328
*/
262
329
export class LocalVariables implements Integration {
263
330
public static id : string = 'LocalVariables' ;
264
331
265
332
public readonly name : string = LocalVariables . id ;
266
333
267
334
private readonly _cachedFrames : LRUMap < string , FrameVariables [ ] > = new LRUMap ( 20 ) ;
335
+ private _rateLimiter : RateLimitIncrement | undefined ;
268
336
269
337
public constructor (
270
338
private readonly _options : Options = { } ,
@@ -293,12 +361,32 @@ export class LocalVariables implements Integration {
293
361
return ;
294
362
}
295
363
364
+ const captureAll = this . _options . captureAllExceptions !== false ;
365
+
296
366
this . _session . configureAndConnect (
297
367
( ev , complete ) =>
298
368
this . _handlePaused ( clientOptions . stackParser , ev as InspectorNotification < PausedExceptionEvent > , complete ) ,
299
- ! ! this . _options . captureAllExceptions ,
369
+ captureAll ,
300
370
) ;
301
371
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
+
302
390
addGlobalEventProcessor ( async event => this . _addLocalVariables ( event ) ) ;
303
391
}
304
392
}
@@ -316,6 +404,8 @@ export class LocalVariables implements Integration {
316
404
return ;
317
405
}
318
406
407
+ this . _rateLimiter ?.( ) ;
408
+
319
409
// data.description contains the original error.stack
320
410
const exceptionHash = hashFromStack ( stackParser , data ?. description ) ;
321
411
0 commit comments