@@ -10,26 +10,6 @@ import { Event, EventProcessor, Hub, Integration, StackFrame, StackParser } from
10
10
import { Debugger , InspectorNotification , Runtime , Session } from 'inspector' ;
11
11
import { LRUMap } from 'lru_map' ;
12
12
13
- interface ExceptionData {
14
- description : string ;
15
- }
16
-
17
- interface FrameVars {
18
- function : string ;
19
- vars ?: Record < string , unknown > ;
20
- }
21
-
22
- /**
23
- * Creates a unique hash from the stack frames
24
- */
25
- function hashFrames ( frames : StackFrame [ ] | undefined ) : string | undefined {
26
- if ( frames === undefined ) {
27
- return ;
28
- }
29
-
30
- return frames . reduce ( ( acc , frame ) => `${ acc } ,${ frame . function } ,${ frame . lineno } ,${ frame . colno } ` , '' ) ;
31
- }
32
-
33
13
/**
34
14
* Promise API is available as `Experimental` and in Node 19 only.
35
15
*
@@ -60,81 +40,124 @@ class AsyncSession extends Session {
60
40
}
61
41
}
62
42
43
+ // Add types for the exception event data
44
+ type PausedExceptionEvent = Debugger . PausedEventDataType & {
45
+ data : {
46
+ // This contains error.stack
47
+ description : string ;
48
+ } ;
49
+ } ;
50
+
51
+ /** Could this be an anonymous function? */
52
+ function isAnonymous ( name : string | undefined ) : boolean {
53
+ return ! ! name && [ '' , '?' , '<anonymous>' ] . includes ( name ) ;
54
+ }
55
+
56
+ /** Do the function names appear to match? */
57
+ function functionNamesMatch ( a : string | undefined , b : string | undefined ) : boolean {
58
+ return a === b || ( isAnonymous ( a ) && isAnonymous ( b ) ) ;
59
+ }
60
+
61
+ /** Creates a unique hash from stack frames */
62
+ function hashFrames ( frames : StackFrame [ ] | undefined ) : string | undefined {
63
+ if ( frames === undefined ) {
64
+ return ;
65
+ }
66
+
67
+ return frames . reduce ( ( acc , frame ) => `${ acc } ,${ frame . function } ,${ frame . lineno } ,${ frame . colno } ` , '' ) ;
68
+ }
69
+
70
+ type HashFromStackFn = ( stack : string | undefined ) => string | undefined ;
71
+
72
+ /**
73
+ * Creates a function used to hash stack strings
74
+ *
75
+ * We use the stack parser to create a unique hash from the exception stack trace
76
+ * This is used to lookup vars when the exception passes through the event processor
77
+ */
78
+ function createHashFn ( stackParser : StackParser | undefined ) : HashFromStackFn {
79
+ return ( stack : string | undefined ) => {
80
+ if ( stackParser === undefined || stack === undefined ) {
81
+ return undefined ;
82
+ }
83
+
84
+ return hashFrames ( stackParser ( stack , 1 ) ) ;
85
+ } ;
86
+ }
87
+
88
+ interface FrameVariables {
89
+ function : string ;
90
+ vars ?: Record < string , unknown > ;
91
+ }
92
+
63
93
/**
64
94
* Adds local variables to exception frames
65
95
*/
66
96
export class LocalVariables implements Integration {
67
97
public static id : string = 'LocalVariables' ;
68
98
69
- public name : string = LocalVariables . id ;
99
+ public readonly name : string = LocalVariables . id ;
70
100
71
- private readonly _session : AsyncSession ;
72
- private readonly _cachedFrameVars : LRUMap < string , Promise < FrameVars [ ] > > = new LRUMap ( 50 ) ;
73
- // We use the stack parser to create a unique hash from the exception stack trace
74
- // This is used to lookup vars when
75
- private _stackParser : StackParser | undefined ;
101
+ private readonly _session : AsyncSession = new AsyncSession ( ) ;
102
+ private readonly _cachedFrames : LRUMap < string , Promise < FrameVariables [ ] > > = new LRUMap ( 50 ) ;
76
103
77
104
public constructor ( ) {
78
- this . _session = new AsyncSession ( ) ;
79
105
this . _session . connect ( ) ;
80
106
this . _session . on ( 'Debugger.paused' , this . _handlePaused . bind ( this ) ) ;
81
107
this . _session . post ( 'Debugger.enable' ) ;
82
- // We only care about uncaught exceptions
108
+ // We only want to pause on uncaught exceptions
83
109
this . _session . post ( 'Debugger.setPauseOnExceptions' , { state : 'uncaught' } ) ;
84
110
}
85
111
86
112
/**
87
113
* @inheritDoc
88
114
*/
89
115
public setupOnce ( addGlobalEventProcessor : ( callback : EventProcessor ) => void , getCurrentHub : ( ) => Hub ) : void {
90
- this . _stackParser = getCurrentHub ( ) . getClient ( ) ?. getOptions ( ) . stackParser ;
116
+ this . _stackHasher = createHashFn ( getCurrentHub ( ) . getClient ( ) ?. getOptions ( ) . stackParser ) ;
91
117
92
118
addGlobalEventProcessor ( async event => this . _addLocalVariables ( event ) ) ;
93
119
}
94
120
121
+ /**
122
+ * We use the stack parser to create a unique hash from the exception stack trace
123
+ * This is used to lookup vars when
124
+ */
125
+ private _stackHasher : HashFromStackFn = _ => undefined ;
126
+
95
127
/**
96
128
* Handle the pause event
97
129
*/
98
- private async _handlePaused ( event : InspectorNotification < Debugger . PausedEventDataType > ) : Promise < void > {
99
- // We only care about exceptions for now
100
- if ( event . params . reason !== 'exception' ) {
130
+ private async _handlePaused ( {
131
+ params : { reason, data, callFrames } ,
132
+ } : InspectorNotification < PausedExceptionEvent > ) : Promise < void > {
133
+ if ( reason !== 'exception' && reason !== 'promiseRejection' ) {
101
134
return ;
102
135
}
103
136
104
- const exceptionData = event . params ?. data as ExceptionData | undefined ;
105
-
106
- // event.params.data.description contains the original error.stack
107
- const exceptionHash =
108
- exceptionData ?. description && this . _stackParser
109
- ? hashFrames ( this . _stackParser ( exceptionData . description , 1 ) )
110
- : undefined ;
137
+ // data.description contains the original error.stack
138
+ const exceptionHash = this . _stackHasher ( data ?. description ) ;
111
139
112
140
if ( exceptionHash == undefined ) {
113
141
return ;
114
142
}
115
143
116
- // We add the un-awaited promise to the cache rather than await here otherwise the event processor
117
- // can be called before we're finished getting all the vars
118
- const framesPromise : Promise < FrameVars [ ] > = Promise . all (
119
- event . params . callFrames . map ( async callFrame => {
120
- const localScope = callFrame . scopeChain . find ( scope => scope . type === 'local' ) ;
144
+ const framePromises = callFrames . map ( async ( { scopeChain, functionName, this : obj } ) => {
145
+ const localScope = scopeChain . find ( scope => scope . type === 'local' ) ;
121
146
122
- if ( localScope ?. object . objectId ) {
123
- const vars = await this . _unrollProps ( await this . _session . getProperties ( localScope . object . objectId ) ) ;
147
+ const fn = obj . className !== 'global' ? `${ obj . className } .${ functionName } ` : functionName ;
124
148
125
- const fn =
126
- callFrame . this . className !== 'global'
127
- ? `${ callFrame . this . className } .${ callFrame . functionName } `
128
- : callFrame . functionName ;
149
+ if ( localScope ?. object . objectId === undefined ) {
150
+ return { function : fn } ;
151
+ }
129
152
130
- return { function : fn , vars } ;
131
- }
153
+ const vars = await this . _unrollProps ( await this . _session . getProperties ( localScope . object . objectId ) ) ;
132
154
133
- return { function : callFrame . functionName } ;
134
- } ) ,
135
- ) ;
155
+ return { function : fn , vars } ;
156
+ } ) ;
136
157
137
- this . _cachedFrameVars . set ( exceptionHash , framesPromise ) ;
158
+ // We add the un-awaited promise to the cache rather than await here otherwise the event processor
159
+ // can be called before we're finished getting all the vars
160
+ this . _cachedFrames . set ( exceptionHash , Promise . all ( framePromises ) ) ;
138
161
}
139
162
140
163
/**
@@ -191,34 +214,35 @@ export class LocalVariables implements Integration {
191
214
}
192
215
193
216
// Check if we have local variables for an exception that matches the hash
194
- const cachedFrameVars = await this . _cachedFrameVars . get ( hash ) ;
217
+ const cachedFrames = await this . _cachedFrames . get ( hash ) ;
195
218
196
- if ( cachedFrameVars === undefined ) {
219
+ if ( cachedFrames === undefined ) {
197
220
return event ;
198
221
}
199
222
200
223
const frameCount = event ?. exception ?. values ?. [ 0 ] ?. stacktrace ?. frames ?. length || 0 ;
201
224
202
225
for ( let i = 0 ; i < frameCount ; i ++ ) {
226
+ // Sentry frames are already in reverse order
203
227
const frameIndex = frameCount - i - 1 ;
204
228
205
- // Drop out if we run out of frames to match
206
- if ( ! event ?. exception ?. values ?. [ 0 ] ?. stacktrace ?. frames ?. [ frameIndex ] || ! cachedFrameVars [ i ] ) {
229
+ // Drop out if we run out of frames to match up
230
+ if ( ! event ?. exception ?. values ?. [ 0 ] ?. stacktrace ?. frames ?. [ frameIndex ] || ! cachedFrames [ i ] ) {
207
231
break ;
208
232
}
209
233
210
234
if (
235
+ // We need to have vars to add
236
+ cachedFrames [ i ] . vars === undefined ||
211
237
// We're not interested in frames that are not in_app because the vars are not relevant
212
238
event . exception . values [ 0 ] . stacktrace . frames [ frameIndex ] . in_app === false ||
213
239
// The function names need to match
214
- event . exception . values [ 0 ] . stacktrace . frames [ frameIndex ] . function !== cachedFrameVars [ i ] . function ||
215
- // We need to have vars to add
216
- cachedFrameVars [ i ] . vars === undefined
240
+ ! functionNamesMatch ( event . exception . values [ 0 ] . stacktrace . frames [ frameIndex ] . function , cachedFrames [ i ] . function )
217
241
) {
218
242
continue ;
219
243
}
220
244
221
- event . exception . values [ 0 ] . stacktrace . frames [ frameIndex ] . vars = cachedFrameVars [ i ] . vars ;
245
+ event . exception . values [ 0 ] . stacktrace . frames [ frameIndex ] . vars = cachedFrames [ i ] . vars ;
222
246
}
223
247
224
248
return event ;
0 commit comments