@sentry/node
Version:
Official Sentry SDK for Node.js
386 lines (321 loc) • 12.7 kB
JavaScript
var {
_optionalChain
} = require('@sentry/utils/cjs/buildPolyfills');
Object.defineProperty(exports, '__esModule', { value: true });
const utils = require('@sentry/utils');
const lru_map = require('lru_map');
const nodeVersion = require('../nodeVersion.js');
/** Creates a container for callbacks to be called sequentially */
function createCallbackList(complete) {
// A collection of callbacks to be executed last to first
let callbacks = [];
let completedCalled = false;
function checkedComplete(result) {
callbacks = [];
if (completedCalled) {
return;
}
completedCalled = true;
complete(result);
}
// complete should be called last
callbacks.push(checkedComplete);
function add(fn) {
callbacks.push(fn);
}
function next(result) {
const popped = callbacks.pop() || checkedComplete;
try {
popped(result);
} catch (_) {
// If there is an error, we still want to call the complete callback
checkedComplete(result);
}
}
return { add, next };
}
/**
* Promise API is available as `Experimental` and in Node 19 only.
*
* Callback-based API is `Stable` since v14 and `Experimental` since v8.
* Because of that, we are creating our own `AsyncSession` class.
*
* https://nodejs.org/docs/latest-v19.x/api/inspector.html#promises-api
* https://nodejs.org/docs/latest-v14.x/api/inspector.html
*/
class AsyncSession {
/** Throws if inspector API is not available */
constructor() {
/*
TODO: We really should get rid of this require statement below for a couple of reasons:
1. It makes the integration unusable in the SvelteKit SDK, as it's not possible to use `require`
in SvelteKit server code (at least not by default).
2. Throwing in a constructor is bad practice
More context for a future attempt to fix this:
We already tried replacing it with import but didn't get it to work because of async problems.
We still called import in the constructor but assigned to a promise which we "awaited" in
`configureAndConnect`. However, this broke the Node integration tests as no local variables
were reported any more. We probably missed a place where we need to await the promise, too.
*/
// Node can be built without inspector support so this can throw
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { Session } = require('inspector');
this._session = new Session();
}
/** @inheritdoc */
configureAndConnect(onPause, captureAll) {
this._session.connect();
this._session.on('Debugger.paused', event => {
onPause(event, () => {
// After the pause work is complete, resume execution or the exception context memory is leaked
this._session.post('Debugger.resume');
});
});
this._session.post('Debugger.enable');
this._session.post('Debugger.setPauseOnExceptions', { state: captureAll ? 'all' : 'uncaught' });
}
/** @inheritdoc */
getLocalVariables(objectId, complete) {
this._getProperties(objectId, props => {
const { add, next } = createCallbackList(complete);
for (const prop of props) {
if (_optionalChain([prop, 'optionalAccess', _2 => _2.value, 'optionalAccess', _3 => _3.objectId]) && _optionalChain([prop, 'optionalAccess', _4 => _4.value, 'access', _5 => _5.className]) === 'Array') {
const id = prop.value.objectId;
add(vars => this._unrollArray(id, prop.name, vars, next));
} else if (_optionalChain([prop, 'optionalAccess', _6 => _6.value, 'optionalAccess', _7 => _7.objectId]) && _optionalChain([prop, 'optionalAccess', _8 => _8.value, 'optionalAccess', _9 => _9.className]) === 'Object') {
const id = prop.value.objectId;
add(vars => this._unrollObject(id, prop.name, vars, next));
} else if (_optionalChain([prop, 'optionalAccess', _10 => _10.value, 'optionalAccess', _11 => _11.value]) || _optionalChain([prop, 'optionalAccess', _12 => _12.value, 'optionalAccess', _13 => _13.description])) {
add(vars => this._unrollOther(prop, vars, next));
}
}
next({});
});
}
/**
* Gets all the PropertyDescriptors of an object
*/
_getProperties(objectId, next) {
this._session.post(
'Runtime.getProperties',
{
objectId,
ownProperties: true,
},
(err, params) => {
if (err) {
next([]);
} else {
next(params.result);
}
},
);
}
/**
* Unrolls an array property
*/
_unrollArray(objectId, name, vars, next) {
this._getProperties(objectId, props => {
vars[name] = props
.filter(v => v.name !== 'length' && !isNaN(parseInt(v.name, 10)))
.sort((a, b) => parseInt(a.name, 10) - parseInt(b.name, 10))
.map(v => _optionalChain([v, 'optionalAccess', _14 => _14.value, 'optionalAccess', _15 => _15.value]));
next(vars);
});
}
/**
* Unrolls an object property
*/
_unrollObject(objectId, name, vars, next) {
this._getProperties(objectId, props => {
vars[name] = props
.map(v => [v.name, _optionalChain([v, 'optionalAccess', _16 => _16.value, 'optionalAccess', _17 => _17.value])])
.reduce((obj, [key, val]) => {
obj[key] = val;
return obj;
}, {} );
next(vars);
});
}
/**
* Unrolls other properties
*/
_unrollOther(prop, vars, next) {
if (_optionalChain([prop, 'optionalAccess', _18 => _18.value, 'optionalAccess', _19 => _19.value])) {
vars[prop.name] = prop.value.value;
} else if (_optionalChain([prop, 'optionalAccess', _20 => _20.value, 'optionalAccess', _21 => _21.description]) && _optionalChain([prop, 'optionalAccess', _22 => _22.value, 'optionalAccess', _23 => _23.type]) !== 'function') {
vars[prop.name] = `<${prop.value.description}>`;
}
next(vars);
}
}
/**
* When using Vercel pkg, the inspector module is not available.
* https://github.com/getsentry/sentry-javascript/issues/6769
*/
function tryNewAsyncSession() {
try {
return new AsyncSession();
} catch (e) {
return undefined;
}
}
// Add types for the exception event data
/** Could this be an anonymous function? */
function isAnonymous(name) {
return name !== undefined && ['', '?', '<anonymous>'].includes(name);
}
/** Do the function names appear to match? */
function functionNamesMatch(a, b) {
return a === b || (isAnonymous(a) && isAnonymous(b));
}
/** Creates a unique hash from stack frames */
function hashFrames(frames) {
if (frames === undefined) {
return;
}
// Only hash the 10 most recent frames (ie. the last 10)
return frames.slice(-10).reduce((acc, frame) => `${acc},${frame.function},${frame.lineno},${frame.colno}`, '');
}
/**
* We use the stack parser to create a unique hash from the exception stack trace
* This is used to lookup vars when the exception passes through the event processor
*/
function hashFromStack(stackParser, stack) {
if (stack === undefined) {
return undefined;
}
return hashFrames(stackParser(stack, 1));
}
/**
* Adds local variables to exception frames
*/
class LocalVariables {
static __initStatic() {this.id = 'LocalVariables';}
__init() {this.name = LocalVariables.id;}
__init2() {this._cachedFrames = new lru_map.LRUMap(20);}
constructor(
_options = {},
_session = tryNewAsyncSession(),
) {this._options = _options;this._session = _session;LocalVariables.prototype.__init.call(this);LocalVariables.prototype.__init2.call(this);}
/**
* @inheritDoc
*/
setupOnce(addGlobalEventProcessor, getCurrentHub) {
this._setup(addGlobalEventProcessor, _optionalChain([getCurrentHub, 'call', _24 => _24(), 'access', _25 => _25.getClient, 'call', _26 => _26(), 'optionalAccess', _27 => _27.getOptions, 'call', _28 => _28()]));
}
/** Setup in a way that's easier to call from tests */
_setup(
addGlobalEventProcessor,
clientOptions,
) {
if (this._session && _optionalChain([clientOptions, 'optionalAccess', _29 => _29.includeLocalVariables])) {
// Only setup this integration if the Node version is >= v18
// https://github.com/getsentry/sentry-javascript/issues/7697
const unsupportedNodeVersion = (nodeVersion.NODE_VERSION.major || 0) < 18;
if (unsupportedNodeVersion) {
utils.logger.log('The `LocalVariables` integration is only supported on Node >= v18.');
return;
}
this._session.configureAndConnect(
(ev, complete) =>
this._handlePaused(clientOptions.stackParser, ev , complete),
!!this._options.captureAllExceptions,
);
addGlobalEventProcessor(async event => this._addLocalVariables(event));
}
}
/**
* Handle the pause event
*/
_handlePaused(
stackParser,
{ params: { reason, data, callFrames } },
complete,
) {
if (reason !== 'exception' && reason !== 'promiseRejection') {
complete();
return;
}
// data.description contains the original error.stack
const exceptionHash = hashFromStack(stackParser, _optionalChain([data, 'optionalAccess', _30 => _30.description]));
if (exceptionHash == undefined) {
complete();
return;
}
const { add, next } = createCallbackList(frames => {
this._cachedFrames.set(exceptionHash, frames);
complete();
});
// Because we're queuing up and making all these calls synchronously, we can potentially overflow the stack
// For this reason we only attempt to get local variables for the first 5 frames
for (let i = 0; i < Math.min(callFrames.length, 5); i++) {
const { scopeChain, functionName, this: obj } = callFrames[i];
const localScope = scopeChain.find(scope => scope.type === 'local');
// obj.className is undefined in ESM modules
const fn = obj.className === 'global' || !obj.className ? functionName : `${obj.className}.${functionName}`;
if (_optionalChain([localScope, 'optionalAccess', _31 => _31.object, 'access', _32 => _32.objectId]) === undefined) {
add(frames => {
frames[i] = { function: fn };
next(frames);
});
} else {
const id = localScope.object.objectId;
add(frames =>
_optionalChain([this, 'access', _33 => _33._session, 'optionalAccess', _34 => _34.getLocalVariables, 'call', _35 => _35(id, vars => {
frames[i] = { function: fn, vars };
next(frames);
})]),
);
}
}
next([]);
}
/**
* Adds local variables event stack frames.
*/
_addLocalVariables(event) {
for (const exception of _optionalChain([event, 'optionalAccess', _36 => _36.exception, 'optionalAccess', _37 => _37.values]) || []) {
this._addLocalVariablesToException(exception);
}
return event;
}
/**
* Adds local variables to the exception stack frames.
*/
_addLocalVariablesToException(exception) {
const hash = hashFrames(_optionalChain([exception, 'optionalAccess', _38 => _38.stacktrace, 'optionalAccess', _39 => _39.frames]));
if (hash === undefined) {
return;
}
// Check if we have local variables for an exception that matches the hash
// delete is identical to get but also removes the entry from the cache
const cachedFrames = this._cachedFrames.delete(hash);
if (cachedFrames === undefined) {
return;
}
const frameCount = _optionalChain([exception, 'access', _40 => _40.stacktrace, 'optionalAccess', _41 => _41.frames, 'optionalAccess', _42 => _42.length]) || 0;
for (let i = 0; i < frameCount; i++) {
// Sentry frames are in reverse order
const frameIndex = frameCount - i - 1;
// Drop out if we run out of frames to match up
if (!_optionalChain([exception, 'optionalAccess', _43 => _43.stacktrace, 'optionalAccess', _44 => _44.frames, 'optionalAccess', _45 => _45[frameIndex]]) || !cachedFrames[i]) {
break;
}
if (
// We need to have vars to add
cachedFrames[i].vars === undefined ||
// We're not interested in frames that are not in_app because the vars are not relevant
exception.stacktrace.frames[frameIndex].in_app === false ||
// The function names need to match
!functionNamesMatch(exception.stacktrace.frames[frameIndex].function, cachedFrames[i].function)
) {
continue;
}
exception.stacktrace.frames[frameIndex].vars = cachedFrames[i].vars;
}
}
}LocalVariables.__initStatic();
exports.LocalVariables = LocalVariables;
exports.createCallbackList = createCallbackList;
//# sourceMappingURL=localvariables.js.map