@sentry/node
Version:
Official Sentry SDK for Node.js
153 lines (137 loc) • 6.39 kB
JavaScript
import { getClient, getCurrentHub } from '@sentry/core';
import { logger } from '@sentry/utils';
import { DEBUG_BUILD } from '../debug-build.js';
import { logAndExitProcess } from './utils/errorhandling.js';
/** Global Exception handler */
class OnUncaughtException {
/**
* @inheritDoc
*/
static __initStatic() {this.id = 'OnUncaughtException';}
/**
* @inheritDoc
*/
__init() {this.name = OnUncaughtException.id;}
/**
* @inheritDoc
*/
__init2() {this.handler = this._makeErrorHandler();}
// CAREFUL: Please think twice before updating the way _options looks because the Next.js SDK depends on it in `index.server.ts`
/**
* @inheritDoc
*/
constructor(options = {}) {OnUncaughtException.prototype.__init.call(this);OnUncaughtException.prototype.__init2.call(this);
this._options = {
exitEvenIfOtherHandlersAreRegistered: true,
...options,
};
}
/**
* @inheritDoc
*/
setupOnce() {
global.process.on('uncaughtException', this.handler);
}
/**
* @hidden
*/
_makeErrorHandler() {
const timeout = 2000;
let caughtFirstError = false;
let caughtSecondError = false;
let calledFatalError = false;
let firstError;
return (error) => {
let onFatalError = logAndExitProcess;
const client = getClient();
if (this._options.onFatalError) {
onFatalError = this._options.onFatalError;
} else if (client && client.getOptions().onFatalError) {
onFatalError = client.getOptions().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 === this.handler // the handler we register in this integration
) {
return acc;
} else {
return acc + 1;
}
}, 0);
const processWouldExit = userProvidedListenersCount === 0;
const shouldApplyFatalHandlingLogic = this._options.exitEvenIfOtherHandlersAreRegistered || processWouldExit;
if (!caughtFirstError) {
const hub = getCurrentHub();
// 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 (hub.getIntegration(OnUncaughtException)) {
hub.withScope((scope) => {
scope.setLevel('fatal');
hub.captureException(error, {
originalException: error,
data: { mechanism: { handled: false, type: 'onuncaughtexception' } },
});
if (!calledFatalError && shouldApplyFatalHandlingLogic) {
calledFatalError = true;
onFatalError(error);
}
});
} else {
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
}
}
}
};
}
} OnUncaughtException.__initStatic();
export { OnUncaughtException };
//# sourceMappingURL=onuncaughtexception.js.map