Skip to content

Commit 93cb95e

Browse files
committed
Add unit tests
1 parent c842fb3 commit 93cb95e

File tree

2 files changed

+358
-60
lines changed

2 files changed

+358
-60
lines changed

packages/node/src/integrations/localvariables.ts

Lines changed: 89 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
1-
import { Event, EventProcessor, Exception, Hub, Integration, StackFrame, StackParser } from '@sentry/types';
1+
import {
2+
ClientOptions,
3+
Event,
4+
EventProcessor,
5+
Exception,
6+
Hub,
7+
Integration,
8+
StackFrame,
9+
StackParser,
10+
} from '@sentry/types';
211
import { Debugger, InspectorNotification, Runtime, Session } from 'inspector';
312
import { LRUMap } from 'lru_map';
413

14+
export interface DebugSession {
15+
/** Configures and connects to the debug session */
16+
configureAndConnect(onPause: (message: InspectorNotification<Debugger.PausedEventDataType>) => void): void;
17+
/** Gets local variables for an objectId */
18+
getLocalVariables(objectId: string): Promise<Record<string, unknown>>;
19+
}
20+
521
/**
622
* Promise API is available as `Experimental` and in Node 19 only.
723
*
@@ -11,8 +27,38 @@ import { LRUMap } from 'lru_map';
1127
* https://nodejs.org/docs/latest-v19.x/api/inspector.html#promises-api
1228
* https://nodejs.org/docs/latest-v14.x/api/inspector.html
1329
*/
14-
class AsyncSession extends Session {
15-
public getProperties(objectId: string): Promise<Runtime.PropertyDescriptor[]> {
30+
class AsyncSession extends Session implements DebugSession {
31+
/** @inheritdoc */
32+
public configureAndConnect(onPause: (message: InspectorNotification<Debugger.PausedEventDataType>) => void): void {
33+
this.connect();
34+
this.on('Debugger.paused', onPause);
35+
this.post('Debugger.enable');
36+
// We only want to pause on uncaught exceptions
37+
this.post('Debugger.setPauseOnExceptions', { state: 'uncaught' });
38+
}
39+
40+
/** @inheritdoc */
41+
public async getLocalVariables(objectId: string): Promise<Record<string, unknown>> {
42+
const props = await this._getProperties(objectId);
43+
const unrolled: Record<string, unknown> = {};
44+
45+
for (const prop of props) {
46+
if (prop?.value?.objectId && prop?.value.className === 'Array') {
47+
unrolled[prop.name] = await this._unrollArray(prop.value.objectId);
48+
} else if (prop?.value?.objectId && prop?.value?.className === 'Object') {
49+
unrolled[prop.name] = await this._unrollObject(prop.value.objectId);
50+
} else if (prop?.value?.value || prop?.value?.description) {
51+
unrolled[prop.name] = prop.value.value || `<${prop.value.description}>`;
52+
}
53+
}
54+
55+
return unrolled;
56+
}
57+
58+
/**
59+
* Gets all the PropertyDescriptors of an object
60+
*/
61+
private _getProperties(objectId: string): Promise<Runtime.PropertyDescriptor[]> {
1662
return new Promise((resolve, reject) => {
1763
this.post(
1864
'Runtime.getProperties',
@@ -30,6 +76,30 @@ class AsyncSession extends Session {
3076
);
3177
});
3278
}
79+
80+
/**
81+
* Unrolls an array property
82+
*/
83+
private async _unrollArray(objectId: string): Promise<unknown> {
84+
const props = await this._getProperties(objectId);
85+
return props
86+
.filter(v => v.name !== 'length' && !isNaN(parseInt(v.name, 10)))
87+
.sort((a, b) => parseInt(a.name, 10) - parseInt(b.name, 10))
88+
.map(v => v?.value?.value);
89+
}
90+
91+
/**
92+
* Unrolls an object property
93+
*/
94+
private async _unrollObject(objectId: string): Promise<Record<string, unknown>> {
95+
const props = await this._getProperties(objectId);
96+
return props
97+
.map<[string, unknown]>(v => [v.name, v?.value?.value])
98+
.reduce((obj, [key, val]) => {
99+
obj[key] = val;
100+
return obj;
101+
}, {} as Record<string, unknown>);
102+
}
33103
}
34104

35105
// Add types for the exception event data
@@ -60,7 +130,7 @@ function hashFrames(frames: StackFrame[] | undefined): string | undefined {
60130
return frames.slice(-10).reduce((acc, frame) => `${acc},${frame.function},${frame.lineno},${frame.colno}`, '');
61131
}
62132

63-
interface FrameVariables {
133+
export interface FrameVariables {
64134
function: string;
65135
vars?: Record<string, unknown>;
66136
}
@@ -73,24 +143,27 @@ export class LocalVariables implements Integration {
73143

74144
public readonly name: string = LocalVariables.id;
75145

76-
private readonly _session: AsyncSession = new AsyncSession();
77146
private readonly _cachedFrames: LRUMap<string, Promise<FrameVariables[]>> = new LRUMap(20);
78147
private _stackParser: StackParser | undefined;
79148

149+
public constructor(private readonly _session: DebugSession = new AsyncSession()) {}
150+
80151
/**
81152
* @inheritDoc
82153
*/
83154
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
84-
const options = getCurrentHub().getClient()?.getOptions();
155+
this._setup(addGlobalEventProcessor, getCurrentHub().getClient()?.getOptions());
156+
}
85157

86-
if (options?._experiments?.includeStackLocals) {
87-
this._stackParser = options.stackParser;
158+
/** Setup in a way that's easier to call from tests */
159+
private _setup(
160+
addGlobalEventProcessor: (callback: EventProcessor) => void,
161+
clientOptions: ClientOptions | undefined,
162+
): void {
163+
if (clientOptions?._experiments?.includeStackLocals) {
164+
this._stackParser = clientOptions.stackParser;
88165

89-
this._session.connect();
90-
this._session.on('Debugger.paused', this._handlePaused.bind(this));
91-
this._session.post('Debugger.enable');
92-
// We only want to pause on uncaught exceptions
93-
this._session.post('Debugger.setPauseOnExceptions', { state: 'uncaught' });
166+
this._session.configureAndConnect(this._handlePaused.bind(this));
94167

95168
addGlobalEventProcessor(async event => this._addLocalVariables(event));
96169
}
@@ -134,7 +207,7 @@ export class LocalVariables implements Integration {
134207
return { function: fn };
135208
}
136209

137-
const vars = await this._unrollProps(await this._session.getProperties(localScope.object.objectId));
210+
const vars = await this._session.getLocalVariables(localScope.object.objectId);
138211

139212
return { function: fn, vars };
140213
});
@@ -144,49 +217,6 @@ export class LocalVariables implements Integration {
144217
this._cachedFrames.set(exceptionHash, Promise.all(framePromises));
145218
}
146219

147-
/**
148-
* Unrolls all the properties
149-
*/
150-
private async _unrollProps(props: Runtime.PropertyDescriptor[]): Promise<Record<string, unknown>> {
151-
const unrolled: Record<string, unknown> = {};
152-
153-
for (const prop of props) {
154-
if (prop?.value?.objectId && prop?.value.className === 'Array') {
155-
unrolled[prop.name] = await this._unrollArray(prop.value.objectId);
156-
} else if (prop?.value?.objectId && prop?.value?.className === 'Object') {
157-
unrolled[prop.name] = await this._unrollObject(prop.value.objectId);
158-
} else if (prop?.value?.value || prop?.value?.description) {
159-
unrolled[prop.name] = prop.value.value || `<${prop.value.description}>`;
160-
}
161-
}
162-
163-
return unrolled;
164-
}
165-
166-
/**
167-
* Unrolls an array property
168-
*/
169-
private async _unrollArray(objectId: string): Promise<unknown> {
170-
const props = await this._session.getProperties(objectId);
171-
return props
172-
.filter(v => v.name !== 'length' && !isNaN(parseInt(v.name, 10)))
173-
.sort((a, b) => parseInt(a.name, 10) - parseInt(b.name, 10))
174-
.map(v => v?.value?.value);
175-
}
176-
177-
/**
178-
* Unrolls an object property
179-
*/
180-
private async _unrollObject(objectId: string): Promise<Record<string, unknown>> {
181-
const props = await this._session.getProperties(objectId);
182-
return props
183-
.map<[string, unknown]>(v => [v.name, v?.value?.value])
184-
.reduce((obj, [key, val]) => {
185-
obj[key] = val;
186-
return obj;
187-
}, {} as Record<string, unknown>);
188-
}
189-
190220
/**
191221
* Adds local variables event stack frames.
192222
*/
@@ -209,14 +239,13 @@ export class LocalVariables implements Integration {
209239
}
210240

211241
// Check if we have local variables for an exception that matches the hash
212-
const cachedFrames = await this._cachedFrames.get(hash);
242+
// delete is identical to get but also removes the entry from the cache
243+
const cachedFrames = await this._cachedFrames.delete(hash);
213244

214245
if (cachedFrames === undefined) {
215246
return;
216247
}
217248

218-
await this._cachedFrames.delete(hash);
219-
220249
const frameCount = exception.stacktrace?.frames?.length || 0;
221250

222251
for (let i = 0; i < frameCount; i++) {

0 commit comments

Comments
 (0)