11import type { Session } from 'node:inspector/promises' ;
2- import type { Event , EventProcessor , Exception , Hub , Integration , StackParser } from '@sentry/types' ;
3- import { LRUMap , logger } from '@sentry/utils' ;
2+ import { convertIntegrationFnToClass } from '@sentry/core' ;
3+ import type { Event , Exception , IntegrationFn , StackParser } from '@sentry/types' ;
4+ import { LRUMap , dynamicRequire , logger } from '@sentry/utils' ;
45import type { Debugger , InspectorNotification , Runtime } from 'inspector' ;
5- import type { NodeClient } from '../../client' ;
66
7+ import type { NodeClient } from '../../client' ;
78import type { NodeClientOptions } from '../../types' ;
89import type { FrameVariables , Options , PausedExceptionEvent , RateLimitIncrement , Variables } from './common' ;
910import { createRateLimiter , functionNamesMatch , hashFrames , hashFromStack } from './common' ;
@@ -64,53 +65,56 @@ async function getLocalVariables(session: Session, objectId: string): Promise<Va
6465 return variables ;
6566}
6667
68+ const INTEGRATION_NAME = 'LocalVariablesAsync' ;
69+
6770/**
6871 * Adds local variables to exception frames
69- *
70- * Default: 50
7172 */
72- export class LocalVariablesAsync implements Integration {
73- public static id : string = 'LocalVariablesAsync' ;
74-
75- public readonly name : string = LocalVariablesAsync . id ;
73+ export const localVariablesAsync : IntegrationFn = ( options : Options = { } ) => {
74+ const cachedFrames : LRUMap < string , FrameVariables [ ] > = new LRUMap ( 20 ) ;
75+ let rateLimiter : RateLimitIncrement | undefined ;
76+ let shouldProcessEvent = false ;
7677
77- private readonly _cachedFrames : LRUMap < string , FrameVariables [ ] > = new LRUMap ( 20 ) ;
78- private _rateLimiter : RateLimitIncrement | undefined ;
79- private _shouldProcessEvent = false ;
80-
81- public constructor ( private readonly _options : Options = { } ) { }
78+ async function handlePaused (
79+ session : Session ,
80+ stackParser : StackParser ,
81+ { reason, data, callFrames } : PausedExceptionEvent ,
82+ ) : Promise < void > {
83+ if ( reason !== 'exception' && reason !== 'promiseRejection' ) {
84+ return ;
85+ }
8286
83- /**
84- * @inheritDoc
85- */
86- public setupOnce ( _addGlobalEventProcessor : ( callback : EventProcessor ) => void , _getCurrentHub : ( ) => Hub ) : void {
87- // noop
88- }
87+ rateLimiter ?.( ) ;
8988
90- /** @inheritdoc */
91- public setup ( client : NodeClient ) : void {
92- const clientOptions = client . getOptions ( ) ;
89+ // data.description contains the original error.stack
90+ const exceptionHash = hashFromStack ( stackParser , data ?. description ) ;
9391
94- if ( ! clientOptions . includeLocalVariables ) {
92+ if ( exceptionHash == undefined ) {
9593 return ;
9694 }
9795
98- import ( /* webpackIgnore: true */ 'node:inspector/promises' )
99- . then ( ( { Session } ) => this . _startDebugger ( new Session ( ) , clientOptions ) )
100- . catch ( e => logger . error ( 'Failed to load inspector API' , e ) ) ;
101- }
96+ const frames = [ ] ;
97+
98+ for ( let i = 0 ; i < callFrames . length ; i ++ ) {
99+ const { scopeChain, functionName, this : obj } = callFrames [ i ] ;
100+
101+ const localScope = scopeChain . find ( scope => scope . type === 'local' ) ;
102+
103+ // obj.className is undefined in ESM modules
104+ const fn = obj . className === 'global' || ! obj . className ? functionName : `${ obj . className } .${ functionName } ` ;
102105
103- /** @inheritdoc */
104- public processEvent ( event : Event ) : Event {
105- if ( this . _shouldProcessEvent ) {
106- return this . _addLocalVariables ( event ) ;
106+ if ( localScope ?. object . objectId === undefined ) {
107+ frames [ i ] = { function : fn } ;
108+ } else {
109+ const vars = await getLocalVariables ( session , localScope . object . objectId ) ;
110+ frames [ i ] = { function : fn , vars } ;
111+ }
107112 }
108113
109- return event ;
114+ cachedFrames . set ( exceptionHash , frames ) ;
110115 }
111116
112- /** Start and configures the debugger to capture local variables */
113- private async _startDebugger ( session : Session , options : NodeClientOptions ) : Promise < void > {
117+ async function startDebugger ( session : Session , clientOptions : NodeClientOptions ) : Promise < void > {
114118 session . connect ( ) ;
115119
116120 let isPaused = false ;
@@ -122,25 +126,26 @@ export class LocalVariablesAsync implements Integration {
122126 session . on ( 'Debugger.paused' , ( event : InspectorNotification < Debugger . PausedEventDataType > ) => {
123127 isPaused = true ;
124128
125- this . _handlePaused ( session , options . stackParser , event . params as PausedExceptionEvent )
126- . then ( ( ) => {
129+ handlePaused ( session , clientOptions . stackParser , event . params as PausedExceptionEvent ) . then (
130+ ( ) => {
127131 // After the pause work is complete, resume execution!
128132 return isPaused ? session . post ( 'Debugger.resume' ) : Promise . resolve ( ) ;
129- } )
130- . catch ( _ => {
131- //
132- } ) ;
133+ } ,
134+ _ => {
135+ // ignore
136+ } ,
137+ ) ;
133138 } ) ;
134139
135140 await session . post ( 'Debugger.enable' ) ;
136141
137- const captureAll = this . _options . captureAllExceptions !== false ;
142+ const captureAll = options . captureAllExceptions !== false ;
138143 await session . post ( 'Debugger.setPauseOnExceptions' , { state : captureAll ? 'all' : 'uncaught' } ) ;
139144
140145 if ( captureAll ) {
141- const max = this . _options . maxExceptionsPerSecond || 50 ;
146+ const max = options . maxExceptionsPerSecond || 50 ;
142147
143- this . _rateLimiter = createRateLimiter (
148+ rateLimiter = createRateLimiter (
144149 max ,
145150 ( ) => {
146151 logger . log ( 'Local variables rate-limit lifted.' ) ;
@@ -155,68 +160,10 @@ export class LocalVariablesAsync implements Integration {
155160 ) ;
156161 }
157162
158- this . _shouldProcessEvent = true ;
159- }
160-
161- /**
162- * Handle the pause event
163- */
164- private async _handlePaused (
165- session : Session ,
166- stackParser : StackParser ,
167- { reason, data, callFrames } : PausedExceptionEvent ,
168- ) : Promise < void > {
169- if ( reason !== 'exception' && reason !== 'promiseRejection' ) {
170- return ;
171- }
172-
173- this . _rateLimiter ?.( ) ;
174-
175- // data.description contains the original error.stack
176- const exceptionHash = hashFromStack ( stackParser , data ?. description ) ;
177-
178- if ( exceptionHash == undefined ) {
179- return ;
180- }
181-
182- const frames = [ ] ;
183-
184- // Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack
185- // For this reason we only attempt to get local variables for the first 5 frames
186- for ( let i = 0 ; i < callFrames . length ; i ++ ) {
187- const { scopeChain, functionName, this : obj } = callFrames [ i ] ;
188-
189- const localScope = scopeChain . find ( scope => scope . type === 'local' ) ;
190-
191- // obj.className is undefined in ESM modules
192- const fn = obj . className === 'global' || ! obj . className ? functionName : `${ obj . className } .${ functionName } ` ;
193-
194- if ( localScope ?. object . objectId === undefined ) {
195- frames [ i ] = { function : fn } ;
196- } else {
197- const vars = await getLocalVariables ( session , localScope . object . objectId ) ;
198- frames [ i ] = { function : fn , vars } ;
199- }
200- }
201-
202- this . _cachedFrames . set ( exceptionHash , frames ) ;
203- }
204-
205- /**
206- * Adds local variables event stack frames.
207- */
208- private _addLocalVariables ( event : Event ) : Event {
209- for ( const exception of event . exception ?. values || [ ] ) {
210- this . _addLocalVariablesToException ( exception ) ;
211- }
212-
213- return event ;
163+ shouldProcessEvent = true ;
214164 }
215165
216- /**
217- * Adds local variables to the exception stack frames.
218- */
219- private _addLocalVariablesToException ( exception : Exception ) : void {
166+ function addLocalVariablesToException ( exception : Exception ) : void {
220167 const hash = hashFrames ( exception . stacktrace ?. frames ) ;
221168
222169 if ( hash === undefined ) {
@@ -225,9 +172,9 @@ export class LocalVariablesAsync implements Integration {
225172
226173 // Check if we have local variables for an exception that matches the hash
227174 // remove is identical to get but also removes the entry from the cache
228- const cachedFrames = this . _cachedFrames . remove ( hash ) ;
175+ const cachedFrame = cachedFrames . remove ( hash ) ;
229176
230- if ( cachedFrames === undefined ) {
177+ if ( cachedFrame === undefined ) {
231178 return ;
232179 }
233180
@@ -238,22 +185,68 @@ export class LocalVariablesAsync implements Integration {
238185 const frameIndex = frameCount - i - 1 ;
239186
240187 // Drop out if we run out of frames to match up
241- if ( ! exception . stacktrace ?. frames ?. [ frameIndex ] || ! cachedFrames [ i ] ) {
188+ if ( ! exception . stacktrace ?. frames ?. [ frameIndex ] || ! cachedFrame [ i ] ) {
242189 break ;
243190 }
244191
245192 if (
246193 // We need to have vars to add
247- cachedFrames [ i ] . vars === undefined ||
194+ cachedFrame [ i ] . vars === undefined ||
248195 // We're not interested in frames that are not in_app because the vars are not relevant
249196 exception . stacktrace . frames [ frameIndex ] . in_app === false ||
250197 // The function names need to match
251- ! functionNamesMatch ( exception . stacktrace . frames [ frameIndex ] . function , cachedFrames [ i ] . function )
198+ ! functionNamesMatch ( exception . stacktrace . frames [ frameIndex ] . function , cachedFrame [ i ] . function )
252199 ) {
253200 continue ;
254201 }
255202
256- exception . stacktrace . frames [ frameIndex ] . vars = cachedFrames [ i ] . vars ;
203+ exception . stacktrace . frames [ frameIndex ] . vars = cachedFrame [ i ] . vars ;
257204 }
258205 }
259- }
206+
207+ function addLocalVariablesToEvent ( event : Event ) : Event {
208+ for ( const exception of event . exception ?. values || [ ] ) {
209+ addLocalVariablesToException ( exception ) ;
210+ }
211+
212+ return event ;
213+ }
214+
215+ return {
216+ name : INTEGRATION_NAME ,
217+ setup ( client : NodeClient ) {
218+ const clientOptions = client . getOptions ( ) ;
219+
220+ if ( ! clientOptions . includeLocalVariables ) {
221+ return ;
222+ }
223+
224+ try {
225+ // TODO: Use import()...
226+ // It would be nice to use import() here, but this built-in library is not in Node <19 so webpack will pick it
227+ // up and report it as a missing dependency
228+ const { Session } = dynamicRequire ( module , 'node:inspector/promises' ) ;
229+
230+ startDebugger ( new Session ( ) , clientOptions ) . catch ( e => {
231+ logger . error ( 'Failed to start inspector session' , e ) ;
232+ } ) ;
233+ } catch ( e ) {
234+ logger . error ( 'Failed to load inspector API' , e ) ;
235+ return ;
236+ }
237+ } ,
238+ processEvent ( event : Event ) : Event {
239+ if ( shouldProcessEvent ) {
240+ return addLocalVariablesToEvent ( event ) ;
241+ }
242+
243+ return event ;
244+ } ,
245+ } ;
246+ } ;
247+
248+ /**
249+ * Adds local variables to exception frames
250+ */
251+ // eslint-disable-next-line deprecation/deprecation
252+ export const LocalVariablesAsync = convertIntegrationFnToClass ( INTEGRATION_NAME , localVariablesAsync ) ;
0 commit comments