Skip to content

Commit d471dcf

Browse files
authored
fix(node): LocalVariables integration should use setupOnce (#10307)
Closes #10278 This integration incorrectly used `setup` which then caused issues when the core code was changed. It should have been using `setupOnce`. Unfortunately, `setupOnce` causes issues with the unit tests since it then relies on `getClient` to get the client instance for the client options. I've tried using jest to mock `getClient` but this doesn't work which is likely due to `installedIntegrations`. Because we already have [extensive integration tests for LocalVariables](https://github.com/getsentry/sentry-javascript/tree/develop/dev-packages/node-integration-tests/suites/public-api/LocalVariables) I have removed the unit tests because they are becoming a maintenance burden and actually test less than the integration tests. After v8, I plan to move Local variables lookup to the worker thread at which point we can revisit how these might be better tested in unit tests.
1 parent a2f0bac commit d471dcf

File tree

2 files changed

+6
-351
lines changed

2 files changed

+6
-351
lines changed

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable max-lines */
2-
import { convertIntegrationFnToClass } from '@sentry/core';
2+
import { convertIntegrationFnToClass, getClient } from '@sentry/core';
33
import type { Event, Exception, Integration, IntegrationClass, IntegrationFn, StackParser } from '@sentry/types';
44
import { LRUMap, logger } from '@sentry/utils';
55
import type { Debugger, InspectorNotification, Runtime, Session } from 'inspector';
@@ -326,12 +326,11 @@ const localVariablesSyncIntegration = ((
326326

327327
return {
328328
name: INTEGRATION_NAME,
329-
// TODO v8: Remove this
330-
setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function
331-
setup(client: NodeClient) {
332-
const clientOptions = client.getOptions();
329+
setupOnce() {
330+
const client = getClient<NodeClient>();
331+
const clientOptions = client?.getOptions();
333332

334-
if (session && clientOptions.includeLocalVariables) {
333+
if (session && clientOptions?.includeLocalVariables) {
335334
// Only setup this integration if the Node version is >= v18
336335
// https://github.com/getsentry/sentry-javascript/issues/7697
337336
const unsupportedNodeVersion = NODE_VERSION.major < 18;

packages/node/test/integrations/localvariables.test.ts

Lines changed: 1 addition & 345 deletions
Original file line numberDiff line numberDiff line change
@@ -1,356 +1,12 @@
1-
import type { Debugger, InspectorNotification } from 'inspector';
2-
3-
import { NodeClient, defaultStackParser } from '../../src';
41
import { createRateLimiter } from '../../src/integrations/local-variables/common';
5-
import type { FrameVariables } from '../../src/integrations/local-variables/common';
6-
import type { DebugSession } from '../../src/integrations/local-variables/local-variables-sync';
7-
import { LocalVariablesSync, createCallbackList } from '../../src/integrations/local-variables/local-variables-sync';
2+
import { createCallbackList } from '../../src/integrations/local-variables/local-variables-sync';
83
import { NODE_VERSION } from '../../src/nodeVersion';
9-
import { getDefaultNodeClientOptions } from '../../test/helper/node-client-options';
104

115
jest.setTimeout(20_000);
126

137
const describeIf = (condition: boolean) => (condition ? describe : describe.skip);
148

15-
interface ThrowOn {
16-
configureAndConnect?: boolean;
17-
getLocalVariables?: boolean;
18-
}
19-
20-
class MockDebugSession implements DebugSession {
21-
private _onPause?: (message: InspectorNotification<Debugger.PausedEventDataType>, callback: () => void) => void;
22-
23-
constructor(private readonly _vars: Record<string, Record<string, unknown>>, private readonly _throwOn?: ThrowOn) {}
24-
25-
public configureAndConnect(
26-
onPause: (message: InspectorNotification<Debugger.PausedEventDataType>, callback: () => void) => void,
27-
_captureAll: boolean,
28-
): void {
29-
if (this._throwOn?.configureAndConnect) {
30-
throw new Error('configureAndConnect should not be called');
31-
}
32-
33-
this._onPause = onPause;
34-
}
35-
36-
public setPauseOnExceptions(_: boolean): void {}
37-
38-
public getLocalVariables(objectId: string, callback: (vars: Record<string, unknown>) => void): void {
39-
if (this._throwOn?.getLocalVariables) {
40-
throw new Error('getLocalVariables should not be called');
41-
}
42-
43-
callback(this._vars[objectId]);
44-
}
45-
46-
public runPause(message: InspectorNotification<Debugger.PausedEventDataType>): Promise<void> {
47-
return new Promise(resolve => {
48-
this._onPause?.(message, resolve);
49-
});
50-
}
51-
}
52-
53-
interface LocalVariablesPrivate {
54-
_getCachedFramesCount(): number;
55-
_getFirstCachedFrame(): FrameVariables[] | undefined;
56-
}
57-
58-
const exceptionEvent = {
59-
method: 'Debugger.paused',
60-
params: {
61-
reason: 'exception',
62-
data: {
63-
description:
64-
'Error: Some error\n' +
65-
' at two (/dist/javascript/src/main.js:23:9)\n' +
66-
' at one (/dist/javascript/src/main.js:19:3)\n' +
67-
' at Timeout._onTimeout (/dist/javascript/src/main.js:40:5)\n' +
68-
' at listOnTimeout (node:internal/timers:559:17)\n' +
69-
' at process.processTimers (node:internal/timers:502:7)',
70-
},
71-
callFrames: [
72-
{
73-
callFrameId: '-6224981551105448869.1.0',
74-
functionName: 'two',
75-
location: { scriptId: '134', lineNumber: 22 },
76-
url: '',
77-
scopeChain: [
78-
{
79-
type: 'local',
80-
object: {
81-
type: 'object',
82-
className: 'Object',
83-
objectId: '-6224981551105448869.1.2',
84-
},
85-
name: 'two',
86-
},
87-
],
88-
this: {
89-
type: 'object',
90-
className: 'global',
91-
},
92-
},
93-
{
94-
callFrameId: '-6224981551105448869.1.1',
95-
functionName: 'one',
96-
location: { scriptId: '134', lineNumber: 18 },
97-
url: '',
98-
scopeChain: [
99-
{
100-
type: 'local',
101-
object: {
102-
type: 'object',
103-
className: 'Object',
104-
objectId: '-6224981551105448869.1.6',
105-
},
106-
name: 'one',
107-
},
108-
],
109-
this: {
110-
type: 'object',
111-
className: 'global',
112-
},
113-
},
114-
],
115-
},
116-
};
117-
118-
const exceptionEvent100Frames = {
119-
method: 'Debugger.paused',
120-
params: {
121-
reason: 'exception',
122-
data: {
123-
description:
124-
'Error: Some error\n' +
125-
' at two (/dist/javascript/src/main.js:23:9)\n' +
126-
' at one (/dist/javascript/src/main.js:19:3)\n' +
127-
' at Timeout._onTimeout (/dist/javascript/src/main.js:40:5)\n' +
128-
' at listOnTimeout (node:internal/timers:559:17)\n' +
129-
' at process.processTimers (node:internal/timers:502:7)',
130-
},
131-
callFrames: new Array(100).fill({
132-
callFrameId: '-6224981551105448869.1.0',
133-
functionName: 'two',
134-
location: { scriptId: '134', lineNumber: 22 },
135-
url: '',
136-
scopeChain: [
137-
{
138-
type: 'local',
139-
object: {
140-
type: 'object',
141-
className: 'Object',
142-
objectId: '-6224981551105448869.1.2',
143-
},
144-
name: 'two',
145-
},
146-
],
147-
this: {
148-
type: 'object',
149-
className: 'global',
150-
},
151-
}),
152-
},
153-
};
154-
1559
describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => {
156-
it('Adds local variables to stack frames', async () => {
157-
const session = new MockDebugSession({
158-
'-6224981551105448869.1.2': { name: 'tim' },
159-
'-6224981551105448869.1.6': { arr: [1, 2, 3] },
160-
});
161-
const localVariables = new LocalVariablesSync({}, session);
162-
const options = getDefaultNodeClientOptions({
163-
stackParser: defaultStackParser,
164-
includeLocalVariables: true,
165-
integrations: [],
166-
});
167-
168-
const client = new NodeClient(options);
169-
client.addIntegration(localVariables);
170-
171-
const eventProcessors = client['_eventProcessors'];
172-
const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables');
173-
174-
expect(eventProcessor).toBeDefined();
175-
176-
await session.runPause(exceptionEvent);
177-
178-
expect((localVariables as unknown as LocalVariablesPrivate)._getCachedFramesCount()).toBe(1);
179-
180-
const frames = (localVariables as unknown as LocalVariablesPrivate)._getFirstCachedFrame();
181-
182-
expect(frames).toBeDefined();
183-
184-
const vars = frames as FrameVariables[];
185-
186-
expect(vars).toEqual([
187-
{ function: 'two', vars: { name: 'tim' } },
188-
{ function: 'one', vars: { arr: [1, 2, 3] } },
189-
]);
190-
191-
const event = await eventProcessor!(
192-
{
193-
event_id: '9cbf882ade9a415986632ac4e16918eb',
194-
platform: 'node',
195-
timestamp: 1671113680.306,
196-
level: 'fatal',
197-
exception: {
198-
values: [
199-
{
200-
type: 'Error',
201-
value: 'Some error',
202-
stacktrace: {
203-
frames: [
204-
{
205-
function: 'process.processTimers',
206-
lineno: 502,
207-
colno: 7,
208-
in_app: false,
209-
},
210-
{
211-
function: 'listOnTimeout',
212-
lineno: 559,
213-
colno: 17,
214-
in_app: false,
215-
},
216-
{
217-
function: 'Timeout._onTimeout',
218-
lineno: 40,
219-
colno: 5,
220-
in_app: true,
221-
},
222-
{
223-
function: 'one',
224-
lineno: 19,
225-
colno: 3,
226-
in_app: true,
227-
},
228-
{
229-
function: 'two',
230-
lineno: 23,
231-
colno: 9,
232-
in_app: true,
233-
},
234-
],
235-
},
236-
mechanism: { type: 'generic', handled: true },
237-
},
238-
],
239-
},
240-
},
241-
{},
242-
);
243-
244-
expect(event?.exception?.values?.[0].stacktrace?.frames?.[3]?.vars).toEqual({ arr: [1, 2, 3] });
245-
expect(event?.exception?.values?.[0].stacktrace?.frames?.[4]?.vars).toEqual({ name: 'tim' });
246-
247-
expect((localVariables as unknown as LocalVariablesPrivate)._getCachedFramesCount()).toBe(0);
248-
});
249-
250-
it('Only considers the first 5 frames', async () => {
251-
const session = new MockDebugSession({});
252-
const localVariables = new LocalVariablesSync({}, session);
253-
const options = getDefaultNodeClientOptions({
254-
stackParser: defaultStackParser,
255-
includeLocalVariables: true,
256-
integrations: [],
257-
});
258-
259-
const client = new NodeClient(options);
260-
client.addIntegration(localVariables);
261-
262-
await session.runPause(exceptionEvent100Frames);
263-
264-
expect((localVariables as unknown as LocalVariablesPrivate)._getCachedFramesCount()).toBe(1);
265-
266-
const frames = (localVariables as unknown as LocalVariablesPrivate)._getFirstCachedFrame();
267-
268-
expect(frames).toBeDefined();
269-
270-
const vars = frames as FrameVariables[];
271-
272-
expect(vars.length).toEqual(5);
273-
});
274-
275-
it('Should not lookup variables for non-exception reasons', async () => {
276-
const session = new MockDebugSession({}, { getLocalVariables: true });
277-
const localVariables = new LocalVariablesSync({}, session);
278-
const options = getDefaultNodeClientOptions({
279-
stackParser: defaultStackParser,
280-
includeLocalVariables: true,
281-
integrations: [],
282-
});
283-
284-
const client = new NodeClient(options);
285-
client.addIntegration(localVariables);
286-
287-
const nonExceptionEvent = {
288-
method: exceptionEvent.method,
289-
params: { ...exceptionEvent.params, reason: 'non-exception-reason' },
290-
};
291-
292-
await session.runPause(nonExceptionEvent);
293-
294-
expect((localVariables as unknown as LocalVariablesPrivate)._getCachedFramesCount()).toBe(0);
295-
});
296-
297-
it('Should not initialize when disabled', async () => {
298-
const session = new MockDebugSession({}, { configureAndConnect: true });
299-
const localVariables = new LocalVariablesSync({}, session);
300-
const options = getDefaultNodeClientOptions({
301-
stackParser: defaultStackParser,
302-
integrations: [],
303-
});
304-
305-
const client = new NodeClient(options);
306-
client.addIntegration(localVariables);
307-
308-
const eventProcessors = client['_eventProcessors'];
309-
const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables');
310-
311-
expect(eventProcessor).toBeDefined();
312-
});
313-
314-
it('Should not initialize when inspector not loaded', async () => {
315-
const localVariables = new LocalVariablesSync({}, undefined);
316-
const options = getDefaultNodeClientOptions({
317-
stackParser: defaultStackParser,
318-
integrations: [],
319-
});
320-
321-
const client = new NodeClient(options);
322-
client.addIntegration(localVariables);
323-
324-
const eventProcessors = client['_eventProcessors'];
325-
const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables');
326-
327-
expect(eventProcessor).toBeDefined();
328-
});
329-
330-
it('Should cache identical uncaught exception events', async () => {
331-
const session = new MockDebugSession({
332-
'-6224981551105448869.1.2': { name: 'tim' },
333-
'-6224981551105448869.1.6': { arr: [1, 2, 3] },
334-
});
335-
const localVariables = new LocalVariablesSync({}, session);
336-
const options = getDefaultNodeClientOptions({
337-
stackParser: defaultStackParser,
338-
includeLocalVariables: true,
339-
integrations: [],
340-
});
341-
342-
const client = new NodeClient(options);
343-
client.addIntegration(localVariables);
344-
345-
await session.runPause(exceptionEvent);
346-
await session.runPause(exceptionEvent);
347-
await session.runPause(exceptionEvent);
348-
await session.runPause(exceptionEvent);
349-
await session.runPause(exceptionEvent);
350-
351-
expect((localVariables as unknown as LocalVariablesPrivate)._getCachedFramesCount()).toBe(1);
352-
});
353-
35410
describe('createCallbackList', () => {
35511
it('Should call callbacks in reverse order', done => {
35612
const log: number[] = [];

0 commit comments

Comments
 (0)