@telemetry-tracker/node 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fatal.d.ts +17 -0
- package/dist/fatal.js +74 -0
- package/dist/index.d.ts +37 -1
- package/dist/index.js +65 -17
- package/package.json +7 -4
package/dist/fatal.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Bound how long a crashing process waits for the error ingest request. */
|
|
2
|
+
export declare const FATAL_FLUSH_TIMEOUT_MS = 2000;
|
|
3
|
+
export type FatalFlushDeps = {
|
|
4
|
+
ingest: (error: Error, context: Record<string, unknown>) => Promise<void>;
|
|
5
|
+
exit: (code: number) => void;
|
|
6
|
+
/** Override the default {@link FATAL_FLUSH_TIMEOUT_MS}. */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Report a fatal error, wait up to `timeoutMs` for ingest, then exit(1).
|
|
11
|
+
* Accepts any thrown value (null/undefined/primitives/objects).
|
|
12
|
+
* Clears the timeout when ingest settles so the timer does not keep the process alive.
|
|
13
|
+
* Telemetry-internal failures must not escape (would become Node exit code 7).
|
|
14
|
+
*/
|
|
15
|
+
export declare function flushFatalError(thrown: unknown, source: string, deps: FatalFlushDeps): void;
|
|
16
|
+
/** @internal test helper */
|
|
17
|
+
export declare function resetFatalFlushStateForTests(): void;
|
package/dist/fatal.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { toReportableError } from "@telemetry-tracker/core";
|
|
2
|
+
/** Bound how long a crashing process waits for the error ingest request. */
|
|
3
|
+
export const FATAL_FLUSH_TIMEOUT_MS = 2000;
|
|
4
|
+
/** Prevents double exit when strict mode raises rejection as uncaughtException too. */
|
|
5
|
+
let fatalFlushInProgress = false;
|
|
6
|
+
function safeExit(deps) {
|
|
7
|
+
try {
|
|
8
|
+
deps.exit(1);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// ignore — never throw from the crash handler
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Report a fatal error, wait up to `timeoutMs` for ingest, then exit(1).
|
|
16
|
+
* Accepts any thrown value (null/undefined/primitives/objects).
|
|
17
|
+
* Clears the timeout when ingest settles so the timer does not keep the process alive.
|
|
18
|
+
* Telemetry-internal failures must not escape (would become Node exit code 7).
|
|
19
|
+
*/
|
|
20
|
+
export function flushFatalError(thrown, source, deps) {
|
|
21
|
+
try {
|
|
22
|
+
if (fatalFlushInProgress)
|
|
23
|
+
return;
|
|
24
|
+
fatalFlushInProgress = true;
|
|
25
|
+
const timeoutMs = deps.timeoutMs ?? FATAL_FLUSH_TIMEOUT_MS;
|
|
26
|
+
let settled = false;
|
|
27
|
+
let timeoutId;
|
|
28
|
+
let flush;
|
|
29
|
+
try {
|
|
30
|
+
const err = toReportableError(thrown);
|
|
31
|
+
let pending;
|
|
32
|
+
try {
|
|
33
|
+
pending = Promise.resolve(deps.ingest(err, { source }));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
pending = Promise.resolve();
|
|
37
|
+
}
|
|
38
|
+
flush = pending.catch(() => undefined);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
flush = Promise.resolve();
|
|
42
|
+
}
|
|
43
|
+
const timer = new Promise((resolve) => {
|
|
44
|
+
timeoutId = setTimeout(resolve, timeoutMs);
|
|
45
|
+
const handle = timeoutId;
|
|
46
|
+
if (typeof handle.unref === "function")
|
|
47
|
+
handle.unref();
|
|
48
|
+
});
|
|
49
|
+
const finish = () => {
|
|
50
|
+
if (settled)
|
|
51
|
+
return;
|
|
52
|
+
settled = true;
|
|
53
|
+
fatalFlushInProgress = false;
|
|
54
|
+
if (timeoutId !== undefined) {
|
|
55
|
+
try {
|
|
56
|
+
clearTimeout(timeoutId);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// ignore
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
safeExit(deps);
|
|
63
|
+
};
|
|
64
|
+
void Promise.race([flush, timer]).then(finish, finish);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
fatalFlushInProgress = false;
|
|
68
|
+
safeExit(deps);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** @internal test helper */
|
|
72
|
+
export function resetFatalFlushStateForTests() {
|
|
73
|
+
fatalFlushInProgress = false;
|
|
74
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,14 +2,50 @@ import { identify, trackEvent, trackError as coreTrackError, type TelemetryConfi
|
|
|
2
2
|
export type TelemetryNodeConfig = TelemetryConfig & {
|
|
3
3
|
app: string;
|
|
4
4
|
platform?: string;
|
|
5
|
+
/**
|
|
6
|
+
* When true (default), report an unhandled rejection, flush ingest, then
|
|
7
|
+
* `process.exit(1)` — matching Node’s default crash since v15. Set `false`
|
|
8
|
+
* to only report and keep the process running (legacy 1.3.x behaviour).
|
|
9
|
+
*
|
|
10
|
+
* With `node --unhandled-rejections=strict`, rejections are also raised as
|
|
11
|
+
* uncaught exceptions; the SDK de-dupes a single fatal flush per error.
|
|
12
|
+
*/
|
|
13
|
+
exitOnUnhandledRejection?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Max ms to wait for fatal error ingest before exiting. Default 2000.
|
|
16
|
+
* The timeout is cleared when ingest settles and does not keep the process alive.
|
|
17
|
+
*/
|
|
18
|
+
fatalFlushTimeoutMs?: number;
|
|
5
19
|
};
|
|
20
|
+
export { FATAL_FLUSH_TIMEOUT_MS, flushFatalError } from "./fatal.js";
|
|
21
|
+
/** @internal Exported for unit tests. */
|
|
22
|
+
export declare function createUncaughtExceptionHandler(deps: {
|
|
23
|
+
ingest: (error: Error, context: Record<string, unknown>) => Promise<void>;
|
|
24
|
+
exit: (code: number) => void;
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
}): (err: unknown) => void;
|
|
27
|
+
/** @internal Exported for unit tests. */
|
|
28
|
+
export declare function createUnhandledRejectionHandler(deps: {
|
|
29
|
+
exitOnUnhandledRejection: boolean;
|
|
30
|
+
ingest: (error: Error, context: Record<string, unknown>) => Promise<void>;
|
|
31
|
+
trackError: (error: Error, context: Record<string, unknown>) => void;
|
|
32
|
+
exit: (code: number) => void;
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
}): (reason: unknown) => void;
|
|
6
35
|
export declare function init(config: TelemetryNodeConfig): void;
|
|
7
36
|
export { identify, trackEvent, coreTrackError as trackError };
|
|
8
37
|
export declare function getConfig(): TelemetryConfig | null;
|
|
38
|
+
type MaybeEmitter = {
|
|
39
|
+
on?(event: string, listener: () => void): void;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Generic (req, res, next) middleware. Times duration until the **response**
|
|
43
|
+
* finishes (`finish`/`close`), not the request body stream.
|
|
44
|
+
*/
|
|
9
45
|
export declare function middleware(opts?: {
|
|
10
46
|
trackRequestBody?: boolean;
|
|
11
47
|
}): (req: {
|
|
12
48
|
method?: string;
|
|
13
49
|
url?: string;
|
|
14
50
|
body?: unknown;
|
|
15
|
-
},
|
|
51
|
+
}, res: MaybeEmitter | unknown, next: () => void) => void;
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,61 @@
|
|
|
1
|
-
import { init as coreInit, identify, trackEvent, trackError as coreTrackError, getConfigOrNull, } from "@telemetry-tracker/core";
|
|
1
|
+
import { init as coreInit, identify, trackEvent, trackError as coreTrackError, ingestError, getConfigOrNull, toReportableError, } from "@telemetry-tracker/core";
|
|
2
|
+
import { FATAL_FLUSH_TIMEOUT_MS, flushFatalError } from "./fatal.js";
|
|
3
|
+
export { FATAL_FLUSH_TIMEOUT_MS, flushFatalError } from "./fatal.js";
|
|
2
4
|
let installed = false;
|
|
5
|
+
let exitOnUnhandledRejection = true;
|
|
6
|
+
let fatalFlushTimeoutMs = FATAL_FLUSH_TIMEOUT_MS;
|
|
7
|
+
/** @internal Exported for unit tests. */
|
|
8
|
+
export function createUncaughtExceptionHandler(deps) {
|
|
9
|
+
return (err) => {
|
|
10
|
+
flushFatalError(err, "uncaughtException", deps);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** @internal Exported for unit tests. */
|
|
14
|
+
export function createUnhandledRejectionHandler(deps) {
|
|
15
|
+
return (reason) => {
|
|
16
|
+
const err = toReportableError(reason);
|
|
17
|
+
if (deps.exitOnUnhandledRejection) {
|
|
18
|
+
flushFatalError(err, "unhandledRejection", {
|
|
19
|
+
ingest: deps.ingest,
|
|
20
|
+
exit: deps.exit,
|
|
21
|
+
timeoutMs: deps.timeoutMs,
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
deps.trackError(err, { source: "unhandledRejection" });
|
|
26
|
+
};
|
|
27
|
+
}
|
|
3
28
|
function installGlobalHandlers() {
|
|
4
29
|
if (installed)
|
|
5
30
|
return;
|
|
6
31
|
installed = true;
|
|
7
|
-
process.on("uncaughtException", (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
process.on("uncaughtException", createUncaughtExceptionHandler({
|
|
33
|
+
ingest: ingestError,
|
|
34
|
+
exit: (code) => process.exit(code),
|
|
35
|
+
get timeoutMs() {
|
|
36
|
+
return fatalFlushTimeoutMs;
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
process.on("unhandledRejection", createUnhandledRejectionHandler({
|
|
40
|
+
get exitOnUnhandledRejection() {
|
|
41
|
+
return exitOnUnhandledRejection;
|
|
42
|
+
},
|
|
43
|
+
ingest: ingestError,
|
|
44
|
+
trackError: coreTrackError,
|
|
45
|
+
exit: (code) => process.exit(code),
|
|
46
|
+
get timeoutMs() {
|
|
47
|
+
return fatalFlushTimeoutMs;
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
15
50
|
}
|
|
16
51
|
export function init(config) {
|
|
52
|
+
exitOnUnhandledRejection = config.exitOnUnhandledRejection !== false;
|
|
53
|
+
fatalFlushTimeoutMs =
|
|
54
|
+
typeof config.fatalFlushTimeoutMs === "number" &&
|
|
55
|
+
Number.isFinite(config.fatalFlushTimeoutMs) &&
|
|
56
|
+
config.fatalFlushTimeoutMs >= 0
|
|
57
|
+
? config.fatalFlushTimeoutMs
|
|
58
|
+
: FATAL_FLUSH_TIMEOUT_MS;
|
|
17
59
|
coreInit({ ...config, platform: config.platform ?? "node" });
|
|
18
60
|
installGlobalHandlers();
|
|
19
61
|
}
|
|
@@ -21,9 +63,13 @@ export { identify, trackEvent, coreTrackError as trackError };
|
|
|
21
63
|
export function getConfig() {
|
|
22
64
|
return getConfigOrNull();
|
|
23
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Generic (req, res, next) middleware. Times duration until the **response**
|
|
68
|
+
* finishes (`finish`/`close`), not the request body stream.
|
|
69
|
+
*/
|
|
24
70
|
export function middleware(opts) {
|
|
25
71
|
const trackRequestBody = opts?.trackRequestBody ?? false;
|
|
26
|
-
return function telemetryMiddleware(req,
|
|
72
|
+
return function telemetryMiddleware(req, res, next) {
|
|
27
73
|
const cfg = getConfigOrNull();
|
|
28
74
|
if (!cfg) {
|
|
29
75
|
next();
|
|
@@ -45,15 +91,17 @@ export function middleware(opts) {
|
|
|
45
91
|
...(trackRequestBody && req.body ? { body: req.body } : {}),
|
|
46
92
|
});
|
|
47
93
|
};
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
94
|
+
const resWithOn = res;
|
|
95
|
+
const canWatchResponse = typeof resWithOn?.on === "function";
|
|
96
|
+
if (canWatchResponse) {
|
|
97
|
+
resWithOn.on("finish", done);
|
|
98
|
+
resWithOn.on("close", done);
|
|
52
99
|
}
|
|
53
|
-
|
|
54
|
-
|
|
100
|
+
next();
|
|
101
|
+
// Frameworks without a Node response emitter (e.g. plain objects / some
|
|
102
|
+
// Fastify adapters): record once after next(), never call next twice.
|
|
103
|
+
if (!canWatchResponse) {
|
|
55
104
|
done();
|
|
56
105
|
}
|
|
57
|
-
next();
|
|
58
106
|
};
|
|
59
107
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetry-tracker/node",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Telemetry Tracker SDK for Node.js: global error handlers, request middleware.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,14 +28,17 @@
|
|
|
28
28
|
"middleware"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@telemetry-tracker/core": "^1.
|
|
31
|
+
"@telemetry-tracker/core": "^1.5.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^22.9.0",
|
|
35
|
-
"typescript": "^5.6.3"
|
|
35
|
+
"typescript": "^5.6.3",
|
|
36
|
+
"vitest": "^3.2.6"
|
|
36
37
|
},
|
|
38
|
+
"gitHead": "886d9f8e0b92a78471f27b87365fc40ba7043ce1",
|
|
37
39
|
"scripts": {
|
|
38
40
|
"build": "tsc",
|
|
39
|
-
"dev": "tsc --watch"
|
|
41
|
+
"dev": "tsc --watch",
|
|
42
|
+
"test": "vitest run"
|
|
40
43
|
}
|
|
41
44
|
}
|