-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
onuncaughtexception.ts
129 lines (117 loc) · 5.22 KB
/
onuncaughtexception.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { getCurrentHub, Scope } from '@sentry/core';
import { Integration } from '@sentry/types';
import { logger } from '@sentry/utils';
import { NodeClient } from '../client';
import { logAndExitProcess } from './utils/errorhandling';
type OnFatalErrorHandler = (firstError: Error, secondError?: Error) => void;
/** Global Exception handler */
export class OnUncaughtException implements Integration {
/**
* @inheritDoc
*/
public static id: string = 'OnUncaughtException';
/**
* @inheritDoc
*/
public name: string = OnUncaughtException.id;
/**
* @inheritDoc
*/
public readonly handler: (error: Error) => void = this._makeErrorHandler();
/**
* @inheritDoc
*/
public constructor(
private readonly _options: {
/**
* Default onFatalError handler
* @param firstError Error that has been thrown
* @param secondError If this was called multiple times this will be set
*/
onFatalError?(firstError: Error, secondError?: Error): void;
} = {},
) {}
/**
* @inheritDoc
*/
public setupOnce(): void {
global.process.on('uncaughtException', this.handler.bind(this));
}
/**
* @hidden
*/
private _makeErrorHandler(): (error: Error) => void {
const timeout = 2000;
let caughtFirstError: boolean = false;
let caughtSecondError: boolean = false;
let calledFatalError: boolean = false;
let firstError: Error;
return (error: Error): void => {
let onFatalError: OnFatalErrorHandler = logAndExitProcess;
const client = getCurrentHub().getClient<NodeClient>();
if (this._options.onFatalError) {
// eslint-disable-next-line @typescript-eslint/unbound-method
onFatalError = this._options.onFatalError;
} else if (client && client.getOptions().onFatalError) {
// eslint-disable-next-line @typescript-eslint/unbound-method
onFatalError = client.getOptions().onFatalError as OnFatalErrorHandler;
}
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) => {
scope.setLevel('fatal');
hub.captureException(error, {
originalException: error,
data: { mechanism: { handled: false, type: 'onuncaughtexception' } },
});
if (!calledFatalError) {
calledFatalError = true;
onFatalError(error);
}
});
} else {
if (!calledFatalError) {
calledFatalError = true;
onFatalError(error);
}
}
} else 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);
} else {
// it was probably case 2, our first error finished capturing while we waited, cool, do nothing
}
}, timeout); // capturing could take at least sendTimeout to fail, plus an arbitrary second for how long it takes to collect surrounding source etc
}
};
}
}