import { defineIntegration, convertIntegrationFnToClass, getClient, captureException } from '@sentry/core'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build.js'; import { logAndExitProcess } from './utils/errorhandling.js'; const INTEGRATION_NAME = 'OnUncaughtException'; const _onUncaughtExceptionIntegration = ((options = {}) => { const _options = { exitEvenIfOtherHandlersAreRegistered: true, ...options, }; return { name: INTEGRATION_NAME, // TODO v8: Remove this setupOnce() {}, // eslint-disable-line @typescript-eslint/no-empty-function setup(client) { global.process.on('uncaughtException', makeErrorHandler(client, _options)); }, }; }) ; const onUncaughtExceptionIntegration = defineIntegration(_onUncaughtExceptionIntegration); /** * Global Exception handler. * @deprecated Use `onUncaughtExceptionIntegration()` instead. */ // eslint-disable-next-line deprecation/deprecation const OnUncaughtException = convertIntegrationFnToClass( INTEGRATION_NAME, onUncaughtExceptionIntegration, ) ; // eslint-disable-next-line deprecation/deprecation /** Exported only for tests */ function makeErrorHandler(client, options) { const timeout = 2000; let caughtFirstError = false; let caughtSecondError = false; let calledFatalError = false; let firstError; const clientOptions = client.getOptions(); return Object.assign( (error) => { let onFatalError = logAndExitProcess; if (options.onFatalError) { onFatalError = options.onFatalError; } else if (clientOptions.onFatalError) { onFatalError = clientOptions.onFatalError ; } // Attaching a listener to `uncaughtException` will prevent the node process from exiting. We generally do not // want to alter this behaviour so we check for other listeners that users may have attached themselves and adjust // exit behaviour of the SDK accordingly: // - If other listeners are attached, do not exit. // - If the only listener attached is ours, exit. const userProvidedListenersCount = ( global.process.listeners('uncaughtException') ).reduce((acc, listener) => { if ( // There are 3 listeners we ignore: listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself (listener.tag && listener.tag === 'sentry_tracingErrorCallback') || // the handler we register for tracing (listener )._errorHandler // the handler we register in this integration ) { return acc; } else { return acc + 1; } }, 0); const processWouldExit = userProvidedListenersCount === 0; const shouldApplyFatalHandlingLogic = options.exitEvenIfOtherHandlersAreRegistered || processWouldExit; if (!caughtFirstError) { // this is the first uncaught error and the ultimate reason for shutting down // we want to do absolutely everything possible to ensure it gets captured // also we want to make sure we don't go recursion crazy if more errors happen after this one firstError = error; caughtFirstError = true; if (getClient() === client) { captureException(error, { originalException: error, captureContext: { level: 'fatal', }, mechanism: { handled: false, type: 'onuncaughtexception', }, }); } if (!calledFatalError && shouldApplyFatalHandlingLogic) { calledFatalError = true; onFatalError(error); } } else { if (shouldApplyFatalHandlingLogic) { if (calledFatalError) { // we hit an error *after* calling onFatalError - pretty boned at this point, just shut it down DEBUG_BUILD && logger.warn( 'uncaught exception after calling fatal error shutdown callback - this is bad! forcing shutdown', ); logAndExitProcess(error); } else if (!caughtSecondError) { // two cases for how we can hit this branch: // - capturing of first error blew up and we just caught the exception from that // - quit trying to capture, proceed with shutdown // - a second independent error happened while waiting for first error to capture // - want to avoid causing premature shutdown before first error capture finishes // it's hard to immediately tell case 1 from case 2 without doing some fancy/questionable domain stuff // so let's instead just delay a bit before we proceed with our action here // in case 1, we just wait a bit unnecessarily but ultimately do the same thing // in case 2, the delay hopefully made us wait long enough for the capture to finish // two potential nonideal outcomes: // nonideal case 1: capturing fails fast, we sit around for a few seconds unnecessarily before proceeding correctly by calling onFatalError // nonideal case 2: case 2 happens, 1st error is captured but slowly, timeout completes before capture and we treat second error as the sendErr of (nonexistent) failure from trying to capture first error // note that after hitting this branch, we might catch more errors where (caughtSecondError && !calledFatalError) // we ignore them - they don't matter to us, we're just waiting for the second error timeout to finish caughtSecondError = true; setTimeout(() => { if (!calledFatalError) { // it was probably case 1, let's treat err as the sendErr and call onFatalError calledFatalError = true; onFatalError(firstError, error); } }, timeout); // capturing could take at least sendTimeout to fail, plus an arbitrary second for how long it takes to collect surrounding source etc } } } }, { _errorHandler: true }, ); } export { OnUncaughtException, makeErrorHandler, onUncaughtExceptionIntegration }; //# sourceMappingURL=onuncaughtexception.js.map