|
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