|
1 | | -import type { Debugger, InspectorNotification, Runtime } from 'node:inspector'; |
2 | | -import { Session } from 'node:inspector'; |
| 1 | +import type { Debugger, InspectorNotification, Runtime, Session } from 'node:inspector'; |
3 | 2 | import { defineIntegration, getClient } from '@sentry/core'; |
4 | 3 | import type { Event, Exception, IntegrationFn, StackParser } from '@sentry/types'; |
5 | 4 | import { LRUMap, logger } from '@sentry/utils'; |
@@ -75,11 +74,18 @@ export function createCallbackList<T>(complete: Next<T>): CallbackWrapper<T> { |
75 | 74 | * https://nodejs.org/docs/latest-v14.x/api/inspector.html |
76 | 75 | */ |
77 | 76 | class AsyncSession implements DebugSession { |
78 | | - private readonly _session: Session; |
79 | | - |
80 | 77 | /** Throws if inspector API is not available */ |
81 | | - public constructor() { |
82 | | - this._session = new Session(); |
| 78 | + private constructor(private readonly _session: Session) { |
| 79 | + // |
| 80 | + } |
| 81 | + |
| 82 | + public static async create(orDefault?: DebugSession | undefined): Promise<DebugSession> { |
| 83 | + if (orDefault) { |
| 84 | + return orDefault; |
| 85 | + } |
| 86 | + |
| 87 | + const inspector = await import('node:inspector'); |
| 88 | + return new AsyncSession(new inspector.Session()); |
83 | 89 | } |
84 | 90 |
|
85 | 91 | /** @inheritdoc */ |
@@ -194,85 +200,19 @@ class AsyncSession implements DebugSession { |
194 | 200 | } |
195 | 201 | } |
196 | 202 |
|
197 | | -/** |
198 | | - * When using Vercel pkg, the inspector module is not available. |
199 | | - * https://github.com/getsentry/sentry-javascript/issues/6769 |
200 | | - */ |
201 | | -function tryNewAsyncSession(): AsyncSession | undefined { |
202 | | - try { |
203 | | - return new AsyncSession(); |
204 | | - } catch (e) { |
205 | | - return undefined; |
206 | | - } |
207 | | -} |
208 | | - |
209 | 203 | const INTEGRATION_NAME = 'LocalVariables'; |
210 | 204 |
|
211 | 205 | /** |
212 | 206 | * Adds local variables to exception frames |
213 | 207 | */ |
214 | 208 | const _localVariablesSyncIntegration = (( |
215 | 209 | options: LocalVariablesIntegrationOptions = {}, |
216 | | - session: DebugSession | undefined = tryNewAsyncSession(), |
| 210 | + sessionOverride?: DebugSession, |
217 | 211 | ) => { |
218 | 212 | const cachedFrames: LRUMap<string, FrameVariables[]> = new LRUMap(20); |
219 | 213 | let rateLimiter: RateLimitIncrement | undefined; |
220 | 214 | let shouldProcessEvent = false; |
221 | 215 |
|
222 | | - function handlePaused( |
223 | | - stackParser: StackParser, |
224 | | - { params: { reason, data, callFrames } }: InspectorNotification<PausedExceptionEvent>, |
225 | | - complete: () => void, |
226 | | - ): void { |
227 | | - if (reason !== 'exception' && reason !== 'promiseRejection') { |
228 | | - complete(); |
229 | | - return; |
230 | | - } |
231 | | - |
232 | | - rateLimiter?.(); |
233 | | - |
234 | | - // data.description contains the original error.stack |
235 | | - const exceptionHash = hashFromStack(stackParser, data?.description); |
236 | | - |
237 | | - if (exceptionHash == undefined) { |
238 | | - complete(); |
239 | | - return; |
240 | | - } |
241 | | - |
242 | | - const { add, next } = createCallbackList<FrameVariables[]>(frames => { |
243 | | - cachedFrames.set(exceptionHash, frames); |
244 | | - complete(); |
245 | | - }); |
246 | | - |
247 | | - // Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack |
248 | | - // For this reason we only attempt to get local variables for the first 5 frames |
249 | | - for (let i = 0; i < Math.min(callFrames.length, 5); i++) { |
250 | | - const { scopeChain, functionName, this: obj } = callFrames[i]; |
251 | | - |
252 | | - const localScope = scopeChain.find(scope => scope.type === 'local'); |
253 | | - |
254 | | - // obj.className is undefined in ESM modules |
255 | | - const fn = obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`; |
256 | | - |
257 | | - if (localScope?.object.objectId === undefined) { |
258 | | - add(frames => { |
259 | | - frames[i] = { function: fn }; |
260 | | - next(frames); |
261 | | - }); |
262 | | - } else { |
263 | | - const id = localScope.object.objectId; |
264 | | - add(frames => |
265 | | - session?.getLocalVariables(id, vars => { |
266 | | - frames[i] = { function: fn, vars }; |
267 | | - next(frames); |
268 | | - }), |
269 | | - ); |
270 | | - } |
271 | | - } |
272 | | - |
273 | | - next([]); |
274 | | - } |
275 | | - |
276 | 216 | function addLocalVariablesToException(exception: Exception): void { |
277 | 217 | const hash = hashFrames(exception?.stacktrace?.frames); |
278 | 218 |
|
@@ -330,44 +270,108 @@ const _localVariablesSyncIntegration = (( |
330 | 270 | const client = getClient<NodeClient>(); |
331 | 271 | const clientOptions = client?.getOptions(); |
332 | 272 |
|
333 | | - if (session && clientOptions?.includeLocalVariables) { |
334 | | - // Only setup this integration if the Node version is >= v18 |
335 | | - // https://github.com/getsentry/sentry-javascript/issues/7697 |
336 | | - const unsupportedNodeVersion = NODE_MAJOR < 18; |
| 273 | + if (!clientOptions?.includeLocalVariables) { |
| 274 | + return; |
| 275 | + } |
337 | 276 |
|
338 | | - if (unsupportedNodeVersion) { |
339 | | - logger.log('The `LocalVariables` integration is only supported on Node >= v18.'); |
340 | | - return; |
341 | | - } |
| 277 | + // Only setup this integration if the Node version is >= v18 |
| 278 | + // https://github.com/getsentry/sentry-javascript/issues/7697 |
| 279 | + const unsupportedNodeVersion = NODE_MAJOR < 18; |
342 | 280 |
|
343 | | - const captureAll = options.captureAllExceptions !== false; |
344 | | - |
345 | | - session.configureAndConnect( |
346 | | - (ev, complete) => |
347 | | - handlePaused(clientOptions.stackParser, ev as InspectorNotification<PausedExceptionEvent>, complete), |
348 | | - captureAll, |
349 | | - ); |
350 | | - |
351 | | - if (captureAll) { |
352 | | - const max = options.maxExceptionsPerSecond || 50; |
353 | | - |
354 | | - rateLimiter = createRateLimiter( |
355 | | - max, |
356 | | - () => { |
357 | | - logger.log('Local variables rate-limit lifted.'); |
358 | | - session?.setPauseOnExceptions(true); |
359 | | - }, |
360 | | - seconds => { |
361 | | - logger.log( |
362 | | - `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, |
363 | | - ); |
364 | | - session?.setPauseOnExceptions(false); |
365 | | - }, |
| 281 | + if (unsupportedNodeVersion) { |
| 282 | + logger.log('The `LocalVariables` integration is only supported on Node >= v18.'); |
| 283 | + return; |
| 284 | + } |
| 285 | + |
| 286 | + AsyncSession.create(sessionOverride).then( |
| 287 | + session => { |
| 288 | + function handlePaused( |
| 289 | + stackParser: StackParser, |
| 290 | + { params: { reason, data, callFrames } }: InspectorNotification<PausedExceptionEvent>, |
| 291 | + complete: () => void, |
| 292 | + ): void { |
| 293 | + if (reason !== 'exception' && reason !== 'promiseRejection') { |
| 294 | + complete(); |
| 295 | + return; |
| 296 | + } |
| 297 | + |
| 298 | + rateLimiter?.(); |
| 299 | + |
| 300 | + // data.description contains the original error.stack |
| 301 | + const exceptionHash = hashFromStack(stackParser, data?.description); |
| 302 | + |
| 303 | + if (exceptionHash == undefined) { |
| 304 | + complete(); |
| 305 | + return; |
| 306 | + } |
| 307 | + |
| 308 | + const { add, next } = createCallbackList<FrameVariables[]>(frames => { |
| 309 | + cachedFrames.set(exceptionHash, frames); |
| 310 | + complete(); |
| 311 | + }); |
| 312 | + |
| 313 | + // Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack |
| 314 | + // For this reason we only attempt to get local variables for the first 5 frames |
| 315 | + for (let i = 0; i < Math.min(callFrames.length, 5); i++) { |
| 316 | + const { scopeChain, functionName, this: obj } = callFrames[i]; |
| 317 | + |
| 318 | + const localScope = scopeChain.find(scope => scope.type === 'local'); |
| 319 | + |
| 320 | + // obj.className is undefined in ESM modules |
| 321 | + const fn = |
| 322 | + obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`; |
| 323 | + |
| 324 | + if (localScope?.object.objectId === undefined) { |
| 325 | + add(frames => { |
| 326 | + frames[i] = { function: fn }; |
| 327 | + next(frames); |
| 328 | + }); |
| 329 | + } else { |
| 330 | + const id = localScope.object.objectId; |
| 331 | + add(frames => |
| 332 | + session?.getLocalVariables(id, vars => { |
| 333 | + frames[i] = { function: fn, vars }; |
| 334 | + next(frames); |
| 335 | + }), |
| 336 | + ); |
| 337 | + } |
| 338 | + } |
| 339 | + |
| 340 | + next([]); |
| 341 | + } |
| 342 | + |
| 343 | + const captureAll = options.captureAllExceptions !== false; |
| 344 | + |
| 345 | + session.configureAndConnect( |
| 346 | + (ev, complete) => |
| 347 | + handlePaused(clientOptions.stackParser, ev as InspectorNotification<PausedExceptionEvent>, complete), |
| 348 | + captureAll, |
366 | 349 | ); |
367 | | - } |
368 | 350 |
|
369 | | - shouldProcessEvent = true; |
370 | | - } |
| 351 | + if (captureAll) { |
| 352 | + const max = options.maxExceptionsPerSecond || 50; |
| 353 | + |
| 354 | + rateLimiter = createRateLimiter( |
| 355 | + max, |
| 356 | + () => { |
| 357 | + logger.log('Local variables rate-limit lifted.'); |
| 358 | + session?.setPauseOnExceptions(true); |
| 359 | + }, |
| 360 | + seconds => { |
| 361 | + logger.log( |
| 362 | + `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, |
| 363 | + ); |
| 364 | + session?.setPauseOnExceptions(false); |
| 365 | + }, |
| 366 | + ); |
| 367 | + } |
| 368 | + |
| 369 | + shouldProcessEvent = true; |
| 370 | + }, |
| 371 | + error => { |
| 372 | + logger.log('The `LocalVariables` integration failed to start.', error); |
| 373 | + }, |
| 374 | + ); |
371 | 375 | }, |
372 | 376 | processEvent(event: Event): Event { |
373 | 377 | if (shouldProcessEvent) { |
|
0 commit comments