UNPKG

@sentry/node

Version:
451 lines (377 loc) 14.5 kB
import { _optionalChain } from '@sentry/utils'; import { LRUMap, logger } from '@sentry/utils'; import { NODE_VERSION } from '../nodeVersion.js'; /** * Creates a rate limiter * @param maxPerSecond Maximum number of calls per second * @param enable Callback to enable capture * @param disable Callback to disable capture * @returns A function to call to increment the rate limiter count */ function createRateLimiter( maxPerSecond, enable, disable, ) { let count = 0; let retrySeconds = 5; let disabledTimeout = 0; setInterval(() => { if (disabledTimeout === 0) { if (count > maxPerSecond) { retrySeconds *= 2; disable(retrySeconds); // Cap at one day if (retrySeconds > 86400) { retrySeconds = 86400; } disabledTimeout = retrySeconds; } } else { disabledTimeout -= 1; if (disabledTimeout === 0) { enable(); } } count = 0; }, 1000).unref(); return () => { count += 1; }; } /** 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' }); } setPauseOnExceptions(captureAll) { 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 * * Default: 50 */ class LocalVariables { static __initStatic() {this.id = 'LocalVariables';} __init() {this.name = LocalVariables.id;} __init2() {this._cachedFrames = new 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 = (NODE_VERSION.major || 0) < 18; if (unsupportedNodeVersion) { logger.log('The `LocalVariables` integration is only supported on Node >= v18.'); return; } const captureAll = this._options.captureAllExceptions !== false; this._session.configureAndConnect( (ev, complete) => this._handlePaused(clientOptions.stackParser, ev , complete), captureAll, ); if (captureAll) { const max = this._options.maxExceptionsPerSecond || 50; this._rateLimiter = createRateLimiter( max, () => { logger.log('Local variables rate-limit lifted.'); _optionalChain([this, 'access', _30 => _30._session, 'optionalAccess', _31 => _31.setPauseOnExceptions, 'call', _32 => _32(true)]); }, seconds => { logger.log( `Local variables rate-limit exceeded. Disabling capturing of caught exceptions for ${seconds} seconds.`, ); _optionalChain([this, 'access', _33 => _33._session, 'optionalAccess', _34 => _34.setPauseOnExceptions, 'call', _35 => _35(false)]); }, ); } addGlobalEventProcessor(async event => this._addLocalVariables(event)); } } /** * Handle the pause event */ _handlePaused( stackParser, { params: { reason, data, callFrames } }, complete, ) { if (reason !== 'exception' && reason !== 'promiseRejection') { complete(); return; } _optionalChain([this, 'access', _36 => _36._rateLimiter, 'optionalCall', _37 => _37()]); // data.description contains the original error.stack const exceptionHash = hashFromStack(stackParser, _optionalChain([data, 'optionalAccess', _38 => _38.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', _39 => _39.object, 'access', _40 => _40.objectId]) === undefined) { add(frames => { frames[i] = { function: fn }; next(frames); }); } else { const id = localScope.object.objectId; add(frames => _optionalChain([this, 'access', _41 => _41._session, 'optionalAccess', _42 => _42.getLocalVariables, 'call', _43 => _43(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', _44 => _44.exception, 'optionalAccess', _45 => _45.values]) || []) { this._addLocalVariablesToException(exception); } return event; } /** * Adds local variables to the exception stack frames. */ _addLocalVariablesToException(exception) { const hash = hashFrames(_optionalChain([exception, 'optionalAccess', _46 => _46.stacktrace, 'optionalAccess', _47 => _47.frames])); if (hash === undefined) { return; } // Check if we have local variables for an exception that matches the hash // remove is identical to get but also removes the entry from the cache const cachedFrames = this._cachedFrames.remove(hash); if (cachedFrames === undefined) { return; } const frameCount = _optionalChain([exception, 'access', _48 => _48.stacktrace, 'optionalAccess', _49 => _49.frames, 'optionalAccess', _50 => _50.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', _51 => _51.stacktrace, 'optionalAccess', _52 => _52.frames, 'optionalAccess', _53 => _53[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(); export { LocalVariables, createCallbackList, createRateLimiter }; //# sourceMappingURL=localvariables.js.map