149 lines
6.2 KiB
JavaScript
149 lines
6.2 KiB
JavaScript
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
|