Skip to content

Commit 5282753

Browse files
authored
fix: Only import inspector asynchronously (#12231)
Closes #12223 I've added a test that uses `import-in-the-middle` to throw if `inspector` is imported without enabling the `LocalVariables` integration so this should not regress in the future. Note that we don't need to import asynchronously in the worker scripts since these are not evaluated if the features aren't used.
1 parent 1f8f1b8 commit 5282753

File tree

6 files changed

+135
-110
lines changed

6 files changed

+135
-110
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Sentry from '@sentry/node';
2+
import Hook from 'import-in-the-middle';
3+
4+
Hook((_, name) => {
5+
if (name === 'inspector') {
6+
throw new Error('No inspector!');
7+
}
8+
if (name === 'node:inspector') {
9+
throw new Error('No inspector!');
10+
}
11+
});
12+
13+
Sentry.init({});

dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ conditionalTest({ min: 18 })('LocalVariables integration', () => {
7676
.start(done);
7777
});
7878

79+
test('Should not import inspector when not in use', done => {
80+
createRunner(__dirname, 'deny-inspector.mjs')
81+
.withFlags('--import=@sentry/node/import')
82+
.ensureNoErrorOutput()
83+
.ignore('session')
84+
.start(done);
85+
});
86+
7987
test('Includes local variables for caught exceptions when enabled', done => {
8088
createRunner(__dirname, 'local-variables-caught.js')
8189
.ignore('session')

packages/node/src/integrations/anr/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as inspector from 'node:inspector';
21
import { Worker } from 'node:worker_threads';
32
import { defineIntegration, getCurrentScope, getGlobalScope, getIsolationScope, mergeScopeData } from '@sentry/core';
43
import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/types';
@@ -148,6 +147,7 @@ async function _startWorker(
148147
};
149148

150149
if (options.captureStackTrace) {
150+
const inspector = await import('node:inspector');
151151
if (!inspector.url()) {
152152
inspector.open(0);
153153
}

packages/node/src/integrations/anr/worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Session as InspectorSession } from 'node:inspector';
12
import { parentPort, workerData } from 'node:worker_threads';
23
import {
34
applyScopeDataToEvent,
@@ -15,7 +16,6 @@ import {
1516
uuid4,
1617
watchdogTimer,
1718
} from '@sentry/utils';
18-
import { Session as InspectorSession } from 'inspector';
1919

2020
import { makeNodeTransport } from '../../transports';
2121
import { createGetModuleFromFilename } from '../../utils/module';

packages/node/src/integrations/local-variables/local-variables-async.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export const localVariablesAsyncIntegration = defineIntegration(((
7575

7676
async function startInspector(): Promise<void> {
7777
// We load inspector dynamically because on some platforms Node is built without inspector support
78-
const inspector = await import('inspector');
78+
const inspector = await import('node:inspector');
7979
if (!inspector.url()) {
8080
inspector.open(0);
8181
}

packages/node/src/integrations/local-variables/local-variables-sync.ts

Lines changed: 111 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
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';
32
import { defineIntegration, getClient } from '@sentry/core';
43
import type { Event, Exception, IntegrationFn, StackParser } from '@sentry/types';
54
import { LRUMap, logger } from '@sentry/utils';
@@ -75,11 +74,18 @@ export function createCallbackList<T>(complete: Next<T>): CallbackWrapper<T> {
7574
* https://nodejs.org/docs/latest-v14.x/api/inspector.html
7675
*/
7776
class AsyncSession implements DebugSession {
78-
private readonly _session: Session;
79-
8077
/** 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());
8389
}
8490

8591
/** @inheritdoc */
@@ -194,85 +200,19 @@ class AsyncSession implements DebugSession {
194200
}
195201
}
196202

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-
209203
const INTEGRATION_NAME = 'LocalVariables';
210204

211205
/**
212206
* Adds local variables to exception frames
213207
*/
214208
const _localVariablesSyncIntegration = ((
215209
options: LocalVariablesIntegrationOptions = {},
216-
session: DebugSession | undefined = tryNewAsyncSession(),
210+
sessionOverride?: DebugSession,
217211
) => {
218212
const cachedFrames: LRUMap<string, FrameVariables[]> = new LRUMap(20);
219213
let rateLimiter: RateLimitIncrement | undefined;
220214
let shouldProcessEvent = false;
221215

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-
276216
function addLocalVariablesToException(exception: Exception): void {
277217
const hash = hashFrames(exception?.stacktrace?.frames);
278218

@@ -330,44 +270,108 @@ const _localVariablesSyncIntegration = ((
330270
const client = getClient<NodeClient>();
331271
const clientOptions = client?.getOptions();
332272

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+
}
337276

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;
342280

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,
366349
);
367-
}
368350

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+
);
371375
},
372376
processEvent(event: Event): Event {
373377
if (shouldProcessEvent) {

0 commit comments

Comments
 (0)