Skip to content

Commit 9a8f0dc

Browse files
committed
Improve
1 parent 5c03a9d commit 9a8f0dc

File tree

1 file changed

+88
-64
lines changed

1 file changed

+88
-64
lines changed

packages/node/src/integrations/localvariables.ts

Lines changed: 88 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,6 @@ import { Event, EventProcessor, Hub, Integration, StackFrame, StackParser } from
1010
import { Debugger, InspectorNotification, Runtime, Session } from 'inspector';
1111
import { LRUMap } from 'lru_map';
1212

13-
interface ExceptionData {
14-
description: string;
15-
}
16-
17-
interface FrameVars {
18-
function: string;
19-
vars?: Record<string, unknown>;
20-
}
21-
22-
/**
23-
* Creates a unique hash from the stack frames
24-
*/
25-
function hashFrames(frames: StackFrame[] | undefined): string | undefined {
26-
if (frames === undefined) {
27-
return;
28-
}
29-
30-
return frames.reduce((acc, frame) => `${acc},${frame.function},${frame.lineno},${frame.colno}`, '');
31-
}
32-
3313
/**
3414
* Promise API is available as `Experimental` and in Node 19 only.
3515
*
@@ -60,81 +40,124 @@ class AsyncSession extends Session {
6040
}
6141
}
6242

43+
// Add types for the exception event data
44+
type PausedExceptionEvent = Debugger.PausedEventDataType & {
45+
data: {
46+
// This contains error.stack
47+
description: string;
48+
};
49+
};
50+
51+
/** Could this be an anonymous function? */
52+
function isAnonymous(name: string | undefined): boolean {
53+
return !!name && ['', '?', '<anonymous>'].includes(name);
54+
}
55+
56+
/** Do the function names appear to match? */
57+
function functionNamesMatch(a: string | undefined, b: string | undefined): boolean {
58+
return a === b || (isAnonymous(a) && isAnonymous(b));
59+
}
60+
61+
/** Creates a unique hash from stack frames */
62+
function hashFrames(frames: StackFrame[] | undefined): string | undefined {
63+
if (frames === undefined) {
64+
return;
65+
}
66+
67+
return frames.reduce((acc, frame) => `${acc},${frame.function},${frame.lineno},${frame.colno}`, '');
68+
}
69+
70+
type HashFromStackFn = (stack: string | undefined) => string | undefined;
71+
72+
/**
73+
* Creates a function used to hash stack strings
74+
*
75+
* We use the stack parser to create a unique hash from the exception stack trace
76+
* This is used to lookup vars when the exception passes through the event processor
77+
*/
78+
function createHashFn(stackParser: StackParser | undefined): HashFromStackFn {
79+
return (stack: string | undefined) => {
80+
if (stackParser === undefined || stack === undefined) {
81+
return undefined;
82+
}
83+
84+
return hashFrames(stackParser(stack, 1));
85+
};
86+
}
87+
88+
interface FrameVariables {
89+
function: string;
90+
vars?: Record<string, unknown>;
91+
}
92+
6393
/**
6494
* Adds local variables to exception frames
6595
*/
6696
export class LocalVariables implements Integration {
6797
public static id: string = 'LocalVariables';
6898

69-
public name: string = LocalVariables.id;
99+
public readonly name: string = LocalVariables.id;
70100

71-
private readonly _session: AsyncSession;
72-
private readonly _cachedFrameVars: LRUMap<string, Promise<FrameVars[]>> = new LRUMap(50);
73-
// We use the stack parser to create a unique hash from the exception stack trace
74-
// This is used to lookup vars when
75-
private _stackParser: StackParser | undefined;
101+
private readonly _session: AsyncSession = new AsyncSession();
102+
private readonly _cachedFrames: LRUMap<string, Promise<FrameVariables[]>> = new LRUMap(50);
76103

77104
public constructor() {
78-
this._session = new AsyncSession();
79105
this._session.connect();
80106
this._session.on('Debugger.paused', this._handlePaused.bind(this));
81107
this._session.post('Debugger.enable');
82-
// We only care about uncaught exceptions
108+
// We only want to pause on uncaught exceptions
83109
this._session.post('Debugger.setPauseOnExceptions', { state: 'uncaught' });
84110
}
85111

86112
/**
87113
* @inheritDoc
88114
*/
89115
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
90-
this._stackParser = getCurrentHub().getClient()?.getOptions().stackParser;
116+
this._stackHasher = createHashFn(getCurrentHub().getClient()?.getOptions().stackParser);
91117

92118
addGlobalEventProcessor(async event => this._addLocalVariables(event));
93119
}
94120

121+
/**
122+
* We use the stack parser to create a unique hash from the exception stack trace
123+
* This is used to lookup vars when
124+
*/
125+
private _stackHasher: HashFromStackFn = _ => undefined;
126+
95127
/**
96128
* Handle the pause event
97129
*/
98-
private async _handlePaused(event: InspectorNotification<Debugger.PausedEventDataType>): Promise<void> {
99-
// We only care about exceptions for now
100-
if (event.params.reason !== 'exception') {
130+
private async _handlePaused({
131+
params: { reason, data, callFrames },
132+
}: InspectorNotification<PausedExceptionEvent>): Promise<void> {
133+
if (reason !== 'exception' && reason !== 'promiseRejection') {
101134
return;
102135
}
103136

104-
const exceptionData = event.params?.data as ExceptionData | undefined;
105-
106-
// event.params.data.description contains the original error.stack
107-
const exceptionHash =
108-
exceptionData?.description && this._stackParser
109-
? hashFrames(this._stackParser(exceptionData.description, 1))
110-
: undefined;
137+
// data.description contains the original error.stack
138+
const exceptionHash = this._stackHasher(data?.description);
111139

112140
if (exceptionHash == undefined) {
113141
return;
114142
}
115143

116-
// We add the un-awaited promise to the cache rather than await here otherwise the event processor
117-
// can be called before we're finished getting all the vars
118-
const framesPromise: Promise<FrameVars[]> = Promise.all(
119-
event.params.callFrames.map(async callFrame => {
120-
const localScope = callFrame.scopeChain.find(scope => scope.type === 'local');
144+
const framePromises = callFrames.map(async ({ scopeChain, functionName, this: obj }) => {
145+
const localScope = scopeChain.find(scope => scope.type === 'local');
121146

122-
if (localScope?.object.objectId) {
123-
const vars = await this._unrollProps(await this._session.getProperties(localScope.object.objectId));
147+
const fn = obj.className !== 'global' ? `${obj.className}.${functionName}` : functionName;
124148

125-
const fn =
126-
callFrame.this.className !== 'global'
127-
? `${callFrame.this.className}.${callFrame.functionName}`
128-
: callFrame.functionName;
149+
if (localScope?.object.objectId === undefined) {
150+
return { function: fn };
151+
}
129152

130-
return { function: fn, vars };
131-
}
153+
const vars = await this._unrollProps(await this._session.getProperties(localScope.object.objectId));
132154

133-
return { function: callFrame.functionName };
134-
}),
135-
);
155+
return { function: fn, vars };
156+
});
136157

137-
this._cachedFrameVars.set(exceptionHash, framesPromise);
158+
// We add the un-awaited promise to the cache rather than await here otherwise the event processor
159+
// can be called before we're finished getting all the vars
160+
this._cachedFrames.set(exceptionHash, Promise.all(framePromises));
138161
}
139162

140163
/**
@@ -191,34 +214,35 @@ export class LocalVariables implements Integration {
191214
}
192215

193216
// Check if we have local variables for an exception that matches the hash
194-
const cachedFrameVars = await this._cachedFrameVars.get(hash);
217+
const cachedFrames = await this._cachedFrames.get(hash);
195218

196-
if (cachedFrameVars === undefined) {
219+
if (cachedFrames === undefined) {
197220
return event;
198221
}
199222

200223
const frameCount = event?.exception?.values?.[0]?.stacktrace?.frames?.length || 0;
201224

202225
for (let i = 0; i < frameCount; i++) {
226+
// Sentry frames are already in reverse order
203227
const frameIndex = frameCount - i - 1;
204228

205-
// Drop out if we run out of frames to match
206-
if (!event?.exception?.values?.[0]?.stacktrace?.frames?.[frameIndex] || !cachedFrameVars[i]) {
229+
// Drop out if we run out of frames to match up
230+
if (!event?.exception?.values?.[0]?.stacktrace?.frames?.[frameIndex] || !cachedFrames[i]) {
207231
break;
208232
}
209233

210234
if (
235+
// We need to have vars to add
236+
cachedFrames[i].vars === undefined ||
211237
// We're not interested in frames that are not in_app because the vars are not relevant
212238
event.exception.values[0].stacktrace.frames[frameIndex].in_app === false ||
213239
// The function names need to match
214-
event.exception.values[0].stacktrace.frames[frameIndex].function !== cachedFrameVars[i].function ||
215-
// We need to have vars to add
216-
cachedFrameVars[i].vars === undefined
240+
!functionNamesMatch(event.exception.values[0].stacktrace.frames[frameIndex].function, cachedFrames[i].function)
217241
) {
218242
continue;
219243
}
220244

221-
event.exception.values[0].stacktrace.frames[frameIndex].vars = cachedFrameVars[i].vars;
245+
event.exception.values[0].stacktrace.frames[frameIndex].vars = cachedFrames[i].vars;
222246
}
223247

224248
return event;

0 commit comments

Comments
 (0)