Skip to content

Commit f629f06

Browse files
committed
Use functional integrations API
1 parent 8c3c01f commit f629f06

File tree

7 files changed

+217
-252
lines changed

7 files changed

+217
-252
lines changed

packages/node/src/integrations/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export { Modules } from './modules';
66
export { ContextLines } from './contextlines';
77
export { Context } from './context';
88
export { RequestData } from '@sentry/core';
9-
export { LocalVariables } from './localvariables';
9+
export { LocalVariables } from './local-variables';
1010
export { Undici } from './undici';
1111
export { Spotlight } from './spotlight';
1212
export { Anr } from './anr';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { convertIntegrationFnToClass } from '@sentry/core';
2+
import type { IntegrationFn } from '@sentry/types';
3+
import { NODE_VERSION } from '../../nodeVersion';
4+
import type { Options } from './common';
5+
import { localVariablesAsync } from './local-variables-async';
6+
import { localVariablesSync } from './local-variables-sync';
7+
8+
const INTEGRATION_NAME = 'LocalVariables';
9+
10+
/**
11+
* Adds local variables to exception frames
12+
*/
13+
const localVariables: IntegrationFn = (options: Options = {}) => {
14+
return NODE_VERSION.major < 19 ? localVariablesSync(options) : localVariablesAsync(options);
15+
};
16+
17+
/**
18+
* Adds local variables to exception frames
19+
*/
20+
// eslint-disable-next-line deprecation/deprecation
21+
export const LocalVariables = convertIntegrationFnToClass(INTEGRATION_NAME, localVariables);
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { Session } from 'node:inspector/promises';
2-
import type { Event, EventProcessor, Exception, Hub, Integration, StackParser } from '@sentry/types';
3-
import { LRUMap, logger } from '@sentry/utils';
2+
import { convertIntegrationFnToClass } from '@sentry/core';
3+
import type { Event, Exception, IntegrationFn, StackParser } from '@sentry/types';
4+
import { LRUMap, dynamicRequire, logger } from '@sentry/utils';
45
import type { Debugger, InspectorNotification, Runtime } from 'inspector';
5-
import type { NodeClient } from '../../client';
66

7+
import type { NodeClient } from '../../client';
78
import type { NodeClientOptions } from '../../types';
89
import type { FrameVariables, Options, PausedExceptionEvent, RateLimitIncrement, Variables } from './common';
910
import { createRateLimiter, functionNamesMatch, hashFrames, hashFromStack } from './common';
@@ -64,53 +65,56 @@ async function getLocalVariables(session: Session, objectId: string): Promise<Va
6465
return variables;
6566
}
6667

68+
const INTEGRATION_NAME = 'LocalVariablesAsync';
69+
6770
/**
6871
* Adds local variables to exception frames
69-
*
70-
* Default: 50
7172
*/
72-
export class LocalVariablesAsync implements Integration {
73-
public static id: string = 'LocalVariablesAsync';
74-
75-
public readonly name: string = LocalVariablesAsync.id;
73+
export const localVariablesAsync: IntegrationFn = (options: Options = {}) => {
74+
const cachedFrames: LRUMap<string, FrameVariables[]> = new LRUMap(20);
75+
let rateLimiter: RateLimitIncrement | undefined;
76+
let shouldProcessEvent = false;
7677

77-
private readonly _cachedFrames: LRUMap<string, FrameVariables[]> = new LRUMap(20);
78-
private _rateLimiter: RateLimitIncrement | undefined;
79-
private _shouldProcessEvent = false;
80-
81-
public constructor(private readonly _options: Options = {}) {}
78+
async function handlePaused(
79+
session: Session,
80+
stackParser: StackParser,
81+
{ reason, data, callFrames }: PausedExceptionEvent,
82+
): Promise<void> {
83+
if (reason !== 'exception' && reason !== 'promiseRejection') {
84+
return;
85+
}
8286

83-
/**
84-
* @inheritDoc
85-
*/
86-
public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void {
87-
// noop
88-
}
87+
rateLimiter?.();
8988

90-
/** @inheritdoc */
91-
public setup(client: NodeClient): void {
92-
const clientOptions = client.getOptions();
89+
// data.description contains the original error.stack
90+
const exceptionHash = hashFromStack(stackParser, data?.description);
9391

94-
if (!clientOptions.includeLocalVariables) {
92+
if (exceptionHash == undefined) {
9593
return;
9694
}
9795

98-
import(/* webpackIgnore: true */ 'node:inspector/promises')
99-
.then(({ Session }) => this._startDebugger(new Session(), clientOptions))
100-
.catch(e => logger.error('Failed to load inspector API', e));
101-
}
96+
const frames = [];
97+
98+
for (let i = 0; i < callFrames.length; i++) {
99+
const { scopeChain, functionName, this: obj } = callFrames[i];
100+
101+
const localScope = scopeChain.find(scope => scope.type === 'local');
102+
103+
// obj.className is undefined in ESM modules
104+
const fn = obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`;
102105

103-
/** @inheritdoc */
104-
public processEvent(event: Event): Event {
105-
if (this._shouldProcessEvent) {
106-
return this._addLocalVariables(event);
106+
if (localScope?.object.objectId === undefined) {
107+
frames[i] = { function: fn };
108+
} else {
109+
const vars = await getLocalVariables(session, localScope.object.objectId);
110+
frames[i] = { function: fn, vars };
111+
}
107112
}
108113

109-
return event;
114+
cachedFrames.set(exceptionHash, frames);
110115
}
111116

112-
/** Start and configures the debugger to capture local variables */
113-
private async _startDebugger(session: Session, options: NodeClientOptions): Promise<void> {
117+
async function startDebugger(session: Session, clientOptions: NodeClientOptions): Promise<void> {
114118
session.connect();
115119

116120
let isPaused = false;
@@ -122,25 +126,26 @@ export class LocalVariablesAsync implements Integration {
122126
session.on('Debugger.paused', (event: InspectorNotification<Debugger.PausedEventDataType>) => {
123127
isPaused = true;
124128

125-
this._handlePaused(session, options.stackParser, event.params as PausedExceptionEvent)
126-
.then(() => {
129+
handlePaused(session, clientOptions.stackParser, event.params as PausedExceptionEvent).then(
130+
() => {
127131
// After the pause work is complete, resume execution!
128132
return isPaused ? session.post('Debugger.resume') : Promise.resolve();
129-
})
130-
.catch(_ => {
131-
//
132-
});
133+
},
134+
_ => {
135+
// ignore
136+
},
137+
);
133138
});
134139

135140
await session.post('Debugger.enable');
136141

137-
const captureAll = this._options.captureAllExceptions !== false;
142+
const captureAll = options.captureAllExceptions !== false;
138143
await session.post('Debugger.setPauseOnExceptions', { state: captureAll ? 'all' : 'uncaught' });
139144

140145
if (captureAll) {
141-
const max = this._options.maxExceptionsPerSecond || 50;
146+
const max = options.maxExceptionsPerSecond || 50;
142147

143-
this._rateLimiter = createRateLimiter(
148+
rateLimiter = createRateLimiter(
144149
max,
145150
() => {
146151
logger.log('Local variables rate-limit lifted.');
@@ -155,68 +160,10 @@ export class LocalVariablesAsync implements Integration {
155160
);
156161
}
157162

158-
this._shouldProcessEvent = true;
159-
}
160-
161-
/**
162-
* Handle the pause event
163-
*/
164-
private async _handlePaused(
165-
session: Session,
166-
stackParser: StackParser,
167-
{ reason, data, callFrames }: PausedExceptionEvent,
168-
): Promise<void> {
169-
if (reason !== 'exception' && reason !== 'promiseRejection') {
170-
return;
171-
}
172-
173-
this._rateLimiter?.();
174-
175-
// data.description contains the original error.stack
176-
const exceptionHash = hashFromStack(stackParser, data?.description);
177-
178-
if (exceptionHash == undefined) {
179-
return;
180-
}
181-
182-
const frames = [];
183-
184-
// Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack
185-
// For this reason we only attempt to get local variables for the first 5 frames
186-
for (let i = 0; i < callFrames.length; i++) {
187-
const { scopeChain, functionName, this: obj } = callFrames[i];
188-
189-
const localScope = scopeChain.find(scope => scope.type === 'local');
190-
191-
// obj.className is undefined in ESM modules
192-
const fn = obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`;
193-
194-
if (localScope?.object.objectId === undefined) {
195-
frames[i] = { function: fn };
196-
} else {
197-
const vars = await getLocalVariables(session, localScope.object.objectId);
198-
frames[i] = { function: fn, vars };
199-
}
200-
}
201-
202-
this._cachedFrames.set(exceptionHash, frames);
203-
}
204-
205-
/**
206-
* Adds local variables event stack frames.
207-
*/
208-
private _addLocalVariables(event: Event): Event {
209-
for (const exception of event.exception?.values || []) {
210-
this._addLocalVariablesToException(exception);
211-
}
212-
213-
return event;
163+
shouldProcessEvent = true;
214164
}
215165

216-
/**
217-
* Adds local variables to the exception stack frames.
218-
*/
219-
private _addLocalVariablesToException(exception: Exception): void {
166+
function addLocalVariablesToException(exception: Exception): void {
220167
const hash = hashFrames(exception.stacktrace?.frames);
221168

222169
if (hash === undefined) {
@@ -225,9 +172,9 @@ export class LocalVariablesAsync implements Integration {
225172

226173
// Check if we have local variables for an exception that matches the hash
227174
// remove is identical to get but also removes the entry from the cache
228-
const cachedFrames = this._cachedFrames.remove(hash);
175+
const cachedFrame = cachedFrames.remove(hash);
229176

230-
if (cachedFrames === undefined) {
177+
if (cachedFrame === undefined) {
231178
return;
232179
}
233180

@@ -238,22 +185,68 @@ export class LocalVariablesAsync implements Integration {
238185
const frameIndex = frameCount - i - 1;
239186

240187
// Drop out if we run out of frames to match up
241-
if (!exception.stacktrace?.frames?.[frameIndex] || !cachedFrames[i]) {
188+
if (!exception.stacktrace?.frames?.[frameIndex] || !cachedFrame[i]) {
242189
break;
243190
}
244191

245192
if (
246193
// We need to have vars to add
247-
cachedFrames[i].vars === undefined ||
194+
cachedFrame[i].vars === undefined ||
248195
// We're not interested in frames that are not in_app because the vars are not relevant
249196
exception.stacktrace.frames[frameIndex].in_app === false ||
250197
// The function names need to match
251-
!functionNamesMatch(exception.stacktrace.frames[frameIndex].function, cachedFrames[i].function)
198+
!functionNamesMatch(exception.stacktrace.frames[frameIndex].function, cachedFrame[i].function)
252199
) {
253200
continue;
254201
}
255202

256-
exception.stacktrace.frames[frameIndex].vars = cachedFrames[i].vars;
203+
exception.stacktrace.frames[frameIndex].vars = cachedFrame[i].vars;
257204
}
258205
}
259-
}
206+
207+
function addLocalVariablesToEvent(event: Event): Event {
208+
for (const exception of event.exception?.values || []) {
209+
addLocalVariablesToException(exception);
210+
}
211+
212+
return event;
213+
}
214+
215+
return {
216+
name: INTEGRATION_NAME,
217+
setup(client: NodeClient) {
218+
const clientOptions = client.getOptions();
219+
220+
if (!clientOptions.includeLocalVariables) {
221+
return;
222+
}
223+
224+
try {
225+
// TODO: Use import()...
226+
// It would be nice to use import() here, but this built-in library is not in Node <19 so webpack will pick it
227+
// up and report it as a missing dependency
228+
const { Session } = dynamicRequire(module, 'node:inspector/promises');
229+
230+
startDebugger(new Session(), clientOptions).catch(e => {
231+
logger.error('Failed to start inspector session', e);
232+
});
233+
} catch (e) {
234+
logger.error('Failed to load inspector API', e);
235+
return;
236+
}
237+
},
238+
processEvent(event: Event): Event {
239+
if (shouldProcessEvent) {
240+
return addLocalVariablesToEvent(event);
241+
}
242+
243+
return event;
244+
},
245+
};
246+
};
247+
248+
/**
249+
* Adds local variables to exception frames
250+
*/
251+
// eslint-disable-next-line deprecation/deprecation
252+
export const LocalVariablesAsync = convertIntegrationFnToClass(INTEGRATION_NAME, localVariablesAsync);

0 commit comments

Comments
 (0)