@fixback/node 0.2.0 → 0.3.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/README.md +4 -1
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +25 -19
- package/dist/config.js +11 -6
- package/dist/config.js.map +1 -1
- package/dist/host-identity.d.ts +60 -0
- package/dist/host-identity.js +72 -0
- package/dist/host-identity.js.map +1 -0
- package/dist/http.d.ts +2 -2
- package/dist/http.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/stack.d.ts +8 -18
- package/dist/stack.js +8 -77
- package/dist/stack.js.map +1 -1
- package/dist/transport.d.ts +1 -17
- package/dist/transport.js +4 -20
- package/dist/transport.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wire.d.ts +10 -16
- package/package.json +8 -6
- package/src/client.test.ts +0 -272
- package/src/client.ts +0 -262
- package/src/config.test.ts +0 -79
- package/src/config.ts +0 -131
- package/src/context.test.ts +0 -99
- package/src/context.ts +0 -124
- package/src/express.test.ts +0 -112
- package/src/express.ts +0 -85
- package/src/fingerprint-parity.test.ts +0 -86
- package/src/http.ts +0 -83
- package/src/index.ts +0 -51
- package/src/nestjs.test.ts +0 -146
- package/src/nestjs.ts +0 -123
- package/src/process.test.ts +0 -154
- package/src/process.ts +0 -153
- package/src/stack.test.ts +0 -89
- package/src/stack.ts +0 -153
- package/src/transport.test.ts +0 -187
- package/src/transport.ts +0 -227
- package/src/version.test.ts +0 -10
- package/src/version.ts +0 -13
- package/src/wire.ts +0 -116
package/dist/transport.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAEH,
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/wire.d.ts
CHANGED
|
@@ -87,22 +87,16 @@ export interface ServerErrorPayload {
|
|
|
87
87
|
readonly errorFrames?: readonly CapturedFrame[];
|
|
88
88
|
readonly server?: ServerContext;
|
|
89
89
|
}
|
|
90
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* The JSON batch envelope `POST`ed to `/api/errors` — a non-empty array of errors.
|
|
92
|
+
* The transport types its request body with this, so the envelope shape is checked
|
|
93
|
+
* against the endpoint's contract rather than assembled inline.
|
|
94
|
+
*
|
|
95
|
+
* The endpoint's *response* is deliberately not modelled: the transport reads only
|
|
96
|
+
* the status and `Retry-After` (ADR-0026's partial-failure results are for the
|
|
97
|
+
* dashboard, not the SDK), and an unread shape would be a contract to keep in
|
|
98
|
+
* lock-step for nothing.
|
|
99
|
+
*/
|
|
91
100
|
export interface ServerErrorBatch {
|
|
92
101
|
readonly errors: readonly ServerErrorPayload[];
|
|
93
102
|
}
|
|
94
|
-
/** The per-item accept/reject result the endpoint returns (ADR-0026 partial-failure). */
|
|
95
|
-
export type ServerErrorItemResult = {
|
|
96
|
-
readonly accepted: true;
|
|
97
|
-
readonly issueId: string;
|
|
98
|
-
readonly folded: boolean;
|
|
99
|
-
readonly feedbackId?: string;
|
|
100
|
-
} | {
|
|
101
|
-
readonly accepted: false;
|
|
102
|
-
readonly reason: string;
|
|
103
|
-
};
|
|
104
|
-
/** The endpoint's response body — `results` on `201`, or a `message` on a shed `429`. */
|
|
105
|
-
export interface ServerErrorResponse {
|
|
106
|
-
readonly results?: readonly ServerErrorItemResult[];
|
|
107
|
-
readonly message?: string;
|
|
108
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fixback/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The Fixback backend error SDK for Node servers — one-call init, batched secret-key capture, process handlers, and Express + NestJS adapters.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
30
|
-
"src",
|
|
31
30
|
"README.md",
|
|
32
31
|
"LICENSE"
|
|
33
32
|
],
|
|
@@ -45,13 +44,13 @@
|
|
|
45
44
|
"url": "git+https://github.com/wemuda/fixback.git",
|
|
46
45
|
"directory": "packages/node"
|
|
47
46
|
},
|
|
48
|
-
"homepage": "https://
|
|
49
|
-
"bugs": "https://
|
|
47
|
+
"homepage": "https://docs.fixback.dev/sdk/node",
|
|
48
|
+
"bugs": "https://docs.fixback.dev",
|
|
50
49
|
"publishConfig": {
|
|
51
50
|
"access": "public"
|
|
52
51
|
},
|
|
53
52
|
"dependencies": {
|
|
54
|
-
"@fixback/sdk-core": "0.
|
|
53
|
+
"@fixback/sdk-core": "0.3.0"
|
|
55
54
|
},
|
|
56
55
|
"peerDependencies": {
|
|
57
56
|
"@nestjs/common": ">=10.0.0",
|
|
@@ -82,8 +81,11 @@
|
|
|
82
81
|
"typescript": "5.9.3",
|
|
83
82
|
"vitest": "^4.1.10"
|
|
84
83
|
},
|
|
84
|
+
"engines": {
|
|
85
|
+
"node": ">=20.12"
|
|
86
|
+
},
|
|
85
87
|
"scripts": {
|
|
86
|
-
"build": "tsc -p tsconfig.build.json && node scripts/finalize-cjs.mjs",
|
|
88
|
+
"build": "tsc -p tsconfig.build.json && node ../../scripts/finalize-cjs.mjs dist",
|
|
87
89
|
"lint": "eslint .",
|
|
88
90
|
"typecheck": "tsc --noEmit",
|
|
89
91
|
"test": "vitest run"
|
package/src/client.test.ts
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { computeFingerprint } from "@fixback/sdk-core";
|
|
2
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
captureException as moduleCaptureException,
|
|
6
|
-
captureMessage as moduleCaptureMessage,
|
|
7
|
-
close as moduleClose,
|
|
8
|
-
FixbackClient,
|
|
9
|
-
getClient,
|
|
10
|
-
init,
|
|
11
|
-
} from "./client";
|
|
12
|
-
import { resolveConfig } from "./config";
|
|
13
|
-
import { runWithRequestStore } from "./context";
|
|
14
|
-
import type { CaptureTransport } from "./transport";
|
|
15
|
-
import type { ServerErrorPayload } from "./wire";
|
|
16
|
-
|
|
17
|
-
function recorder(): { enqueued: ServerErrorPayload[]; transport: CaptureTransport } {
|
|
18
|
-
const enqueued: ServerErrorPayload[] = [];
|
|
19
|
-
return {
|
|
20
|
-
enqueued,
|
|
21
|
-
transport: {
|
|
22
|
-
enqueue: (payload) => enqueued.push(payload),
|
|
23
|
-
flush: async () => {},
|
|
24
|
-
close: async () => {},
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function client(options: Parameters<typeof resolveConfig>[0], transport: CaptureTransport) {
|
|
30
|
-
return new FixbackClient(resolveConfig(options), { transport });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** A tiny process stand-in so module tests never attach handlers to the real process. */
|
|
34
|
-
function fakeProcess() {
|
|
35
|
-
const map = new Map<string, Array<(...args: unknown[]) => void>>();
|
|
36
|
-
return {
|
|
37
|
-
on(event: string, listener: (...args: unknown[]) => void) {
|
|
38
|
-
map.set(event, [...(map.get(event) ?? []), listener]);
|
|
39
|
-
return this;
|
|
40
|
-
},
|
|
41
|
-
removeListener(event: string, listener: (...args: unknown[]) => void) {
|
|
42
|
-
map.set(event, (map.get(event) ?? []).filter((l) => l !== listener));
|
|
43
|
-
return this;
|
|
44
|
-
},
|
|
45
|
-
listeners(event: string) {
|
|
46
|
-
return [...(map.get(event) ?? [])];
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe("FixbackClient.captureException", () => {
|
|
52
|
-
it("enqueues a payload with a fingerprint, handled:true, environment, release and frames", () => {
|
|
53
|
-
const { enqueued, transport } = recorder();
|
|
54
|
-
const c = client({ secretKey: "sk", environment: "production", release: "1.2.3" }, transport);
|
|
55
|
-
|
|
56
|
-
c.captureException(new TypeError("boom"));
|
|
57
|
-
|
|
58
|
-
expect(enqueued).toHaveLength(1);
|
|
59
|
-
const payload = enqueued[0]!;
|
|
60
|
-
expect(typeof payload.errorSignature).toBe("string");
|
|
61
|
-
expect(payload.errorSignature.length).toBeGreaterThan(0);
|
|
62
|
-
expect(payload.handled).toBe(true);
|
|
63
|
-
expect(payload.environment).toBe("production");
|
|
64
|
-
expect(payload.release).toBe("1.2.3");
|
|
65
|
-
expect(payload.errorFrames?.length ?? 0).toBeGreaterThan(0);
|
|
66
|
-
// The error class / message never travel on the wire (no server slot; untrusted).
|
|
67
|
-
const asRecord = payload as unknown as Record<string, unknown>;
|
|
68
|
-
expect(asRecord.type).toBeUndefined();
|
|
69
|
-
expect(asRecord.value).toBeUndefined();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("fingerprints via the shared core — the same error yields a stable key", () => {
|
|
73
|
-
const { enqueued, transport } = recorder();
|
|
74
|
-
const c = client({ secretKey: "sk" }, transport);
|
|
75
|
-
const err = new Error("same");
|
|
76
|
-
err.stack = "Error: same\n at fn (/srv/app/a.js:1:1)";
|
|
77
|
-
c.captureException(err);
|
|
78
|
-
c.captureException(err);
|
|
79
|
-
expect(enqueued[0]!.errorSignature).toBe(enqueued[1]!.errorSignature);
|
|
80
|
-
expect(enqueued[0]!.errorSignature).toBe(computeFingerprint("Error", "same", err.stack));
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("attaches the in-flight request's server context from AsyncLocalStorage", () => {
|
|
84
|
-
const { enqueued, transport } = recorder();
|
|
85
|
-
const c = client({ secretKey: "sk" }, transport);
|
|
86
|
-
runWithRequestStore(
|
|
87
|
-
{
|
|
88
|
-
req: { method: "POST", route: { path: "/pay/:id" }, path: "/pay/42" },
|
|
89
|
-
res: { statusCode: 500 },
|
|
90
|
-
requestId: "r-1",
|
|
91
|
-
user: "u-9",
|
|
92
|
-
},
|
|
93
|
-
() => c.captureException(new Error("x")),
|
|
94
|
-
);
|
|
95
|
-
expect(enqueued[0]!.server).toEqual({
|
|
96
|
-
method: "POST",
|
|
97
|
-
route: "/pay/:id",
|
|
98
|
-
statusCode: 500,
|
|
99
|
-
requestId: "r-1",
|
|
100
|
-
user: "u-9",
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("lets an explicit context override merge over the request context (Nest filter status)", () => {
|
|
105
|
-
const { enqueued, transport } = recorder();
|
|
106
|
-
const c = client({ secretKey: "sk" }, transport);
|
|
107
|
-
runWithRequestStore(
|
|
108
|
-
{ req: { method: "GET", route: { path: "/x" } }, res: { statusCode: 200 } },
|
|
109
|
-
() => c.captureException(new Error("x"), { handled: true, server: { statusCode: 503 } }),
|
|
110
|
-
);
|
|
111
|
-
expect(enqueued[0]!.server?.statusCode).toBe(503);
|
|
112
|
-
expect(enqueued[0]!.server?.method).toBe("GET");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("captures a non-Error throwable without throwing", () => {
|
|
116
|
-
const { enqueued, transport } = recorder();
|
|
117
|
-
const c = client({ secretKey: "sk" }, transport);
|
|
118
|
-
expect(() => c.captureException("string failure")).not.toThrow();
|
|
119
|
-
expect(enqueued[0]!.errorSignature.length).toBeGreaterThan(0);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("scrubs credentials and query strings out of frame urls before transport", () => {
|
|
123
|
-
const { enqueued, transport } = recorder();
|
|
124
|
-
const c = client({ secretKey: "sk" }, transport);
|
|
125
|
-
const err = new Error("x");
|
|
126
|
-
err.stack = "Error: x\n at h (https://user:pass@cdn.ex.com/a.js?token=abc123:1:1)";
|
|
127
|
-
c.captureException(err);
|
|
128
|
-
const file = enqueued[0]!.errorFrames?.[0]?.file ?? "";
|
|
129
|
-
expect(file).not.toContain("token=abc123");
|
|
130
|
-
expect(file).not.toContain("user:pass@");
|
|
131
|
-
expect(file).toContain("cdn.ex.com/a.js");
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("is private-by-default — never carries cookies, auth headers or the concrete path", () => {
|
|
135
|
-
const { enqueued, transport } = recorder();
|
|
136
|
-
const c = client({ secretKey: "sk" }, transport);
|
|
137
|
-
runWithRequestStore(
|
|
138
|
-
{
|
|
139
|
-
req: {
|
|
140
|
-
method: "GET",
|
|
141
|
-
route: { path: "/u/:id" },
|
|
142
|
-
path: "/u/42",
|
|
143
|
-
headers: { cookie: "session=secret-cookie", authorization: "Bearer sk_leak" },
|
|
144
|
-
},
|
|
145
|
-
res: { statusCode: 500 },
|
|
146
|
-
},
|
|
147
|
-
() => c.captureException(new Error("x")),
|
|
148
|
-
);
|
|
149
|
-
const json = JSON.stringify(enqueued[0]);
|
|
150
|
-
expect(json).not.toContain("secret-cookie");
|
|
151
|
-
expect(json).not.toContain("sk_leak");
|
|
152
|
-
expect(json).not.toContain("/u/42");
|
|
153
|
-
expect(json).toContain("/u/:id");
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("does nothing when the client is disabled (fail-quiet gate)", () => {
|
|
157
|
-
const { enqueued, transport } = recorder();
|
|
158
|
-
const c = client({ secretKey: "" }, transport); // no key → inert
|
|
159
|
-
c.captureException(new Error("x"));
|
|
160
|
-
expect(enqueued).toHaveLength(0);
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe("FixbackClient beforeSend", () => {
|
|
165
|
-
it("runs the hook so it can redact the event", () => {
|
|
166
|
-
const { enqueued, transport } = recorder();
|
|
167
|
-
const c = new FixbackClient(
|
|
168
|
-
resolveConfig({
|
|
169
|
-
secretKey: "sk",
|
|
170
|
-
beforeSend: (event) => {
|
|
171
|
-
event.server = { ...event.server, user: "[redacted]" };
|
|
172
|
-
return event;
|
|
173
|
-
},
|
|
174
|
-
}),
|
|
175
|
-
{ transport },
|
|
176
|
-
);
|
|
177
|
-
runWithRequestStore(
|
|
178
|
-
{ req: { method: "GET", route: { path: "/x" } }, user: "real-user" },
|
|
179
|
-
() => c.captureException(new Error("x")),
|
|
180
|
-
);
|
|
181
|
-
expect(enqueued[0]!.server?.user).toBe("[redacted]");
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("drops the event entirely when the hook returns null", () => {
|
|
185
|
-
const { enqueued, transport } = recorder();
|
|
186
|
-
const c = new FixbackClient(resolveConfig({ secretKey: "sk", beforeSend: () => null }), {
|
|
187
|
-
transport,
|
|
188
|
-
});
|
|
189
|
-
c.captureException(new Error("dropped"));
|
|
190
|
-
expect(enqueued).toHaveLength(0);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("keeps the scrubbed event when the hook throws (a buggy hook never breaks capture)", () => {
|
|
194
|
-
const { enqueued, transport } = recorder();
|
|
195
|
-
const c = new FixbackClient(
|
|
196
|
-
resolveConfig({
|
|
197
|
-
secretKey: "sk",
|
|
198
|
-
beforeSend: () => {
|
|
199
|
-
throw new Error("hook bug");
|
|
200
|
-
},
|
|
201
|
-
}),
|
|
202
|
-
{ transport },
|
|
203
|
-
);
|
|
204
|
-
expect(() => c.captureException(new Error("survives"))).not.toThrow();
|
|
205
|
-
expect(enqueued).toHaveLength(1);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
describe("FixbackClient.captureMessage", () => {
|
|
210
|
-
it("files a handled message with no frames; groups by message text", () => {
|
|
211
|
-
const { enqueued, transport } = recorder();
|
|
212
|
-
const c = client({ secretKey: "sk" }, transport);
|
|
213
|
-
c.captureMessage("cache miss storm", "warning");
|
|
214
|
-
c.captureMessage("cache miss storm");
|
|
215
|
-
c.captureMessage("something else");
|
|
216
|
-
expect(enqueued[0]!.handled).toBe(true);
|
|
217
|
-
expect(enqueued[0]!.errorFrames).toBeUndefined();
|
|
218
|
-
expect(enqueued[1]!.errorSignature).toBe(enqueued[0]!.errorSignature);
|
|
219
|
-
expect(enqueued[2]!.errorSignature).not.toBe(enqueued[0]!.errorSignature);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
describe("module singleton API", () => {
|
|
224
|
-
afterEach(async () => {
|
|
225
|
-
await moduleClose();
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("init sets the active client, installs process handlers, and delegates captures", () => {
|
|
229
|
-
const { enqueued, transport } = recorder();
|
|
230
|
-
const proc = fakeProcess();
|
|
231
|
-
const active = init({ secretKey: "sk" }, { transport, process: proc, onFatalError: () => {} });
|
|
232
|
-
expect(getClient()).toBe(active);
|
|
233
|
-
expect(proc.listeners("uncaughtException")).toHaveLength(1);
|
|
234
|
-
expect(proc.listeners("unhandledRejection")).toHaveLength(1);
|
|
235
|
-
moduleCaptureException(new Error("via module"));
|
|
236
|
-
moduleCaptureMessage("hello");
|
|
237
|
-
expect(enqueued).toHaveLength(2);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("removes the process handlers and clears the client on close", async () => {
|
|
241
|
-
const proc = fakeProcess();
|
|
242
|
-
init({ secretKey: "sk" }, { transport: recorder().transport, process: proc, onFatalError: () => {} });
|
|
243
|
-
expect(proc.listeners("uncaughtException")).toHaveLength(1);
|
|
244
|
-
await moduleClose();
|
|
245
|
-
expect(proc.listeners("uncaughtException")).toHaveLength(0);
|
|
246
|
-
expect(getClient()).toBeNull();
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("closes the previous client and removes its handlers when init is called again", () => {
|
|
250
|
-
let closed = 0;
|
|
251
|
-
const first: CaptureTransport = {
|
|
252
|
-
enqueue: () => {},
|
|
253
|
-
flush: async () => {},
|
|
254
|
-
close: async () => {
|
|
255
|
-
closed += 1;
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
const proc1 = fakeProcess();
|
|
259
|
-
init({ secretKey: "sk" }, { transport: first, process: proc1, onFatalError: () => {} });
|
|
260
|
-
init({ secretKey: "sk" }, { transport: recorder().transport, process: fakeProcess(), onFatalError: () => {} });
|
|
261
|
-
expect(closed).toBe(1);
|
|
262
|
-
expect(proc1.listeners("uncaughtException")).toHaveLength(0);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("no-ops (never throws) when disabled — no handlers, no capture", () => {
|
|
266
|
-
const proc = fakeProcess();
|
|
267
|
-
init({ secretKey: "" }, { transport: recorder().transport, process: proc, onFatalError: () => {} });
|
|
268
|
-
expect(proc.listeners("uncaughtException")).toHaveLength(0);
|
|
269
|
-
expect(() => moduleCaptureException(new Error("x"))).not.toThrow();
|
|
270
|
-
expect(() => moduleCaptureMessage("x")).not.toThrow();
|
|
271
|
-
});
|
|
272
|
-
});
|
package/src/client.ts
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `FixbackClient` and the module-level singleton API (`init`, `captureException`,
|
|
3
|
-
* `captureMessage`, `flush`, `close`) — the SDK's capture surface.
|
|
4
|
-
*
|
|
5
|
-
* A capture distils the error, fingerprints it with the **shared core**
|
|
6
|
-
* (`@fixback/sdk-core`, so the key matches the browser SDK — ADR-0028), attaches the
|
|
7
|
-
* in-flight request's {@link ServerContext} from AsyncLocalStorage, runs the scrub
|
|
8
|
-
* choke point and the project's `beforeSend`, then hands a {@link ServerErrorPayload}
|
|
9
|
-
* to the batched transport. Every step is wrapped so a capture **never throws into
|
|
10
|
-
* the host** (story 34): a misbehaving hook, an unserialisable error, or a transport
|
|
11
|
-
* hiccup is swallowed, and the server keeps running.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { computeFingerprint, scrubUrl } from "@fixback/sdk-core";
|
|
15
|
-
|
|
16
|
-
import { type InitOptions, resolveConfig, type ResolvedConfig } from "./config";
|
|
17
|
-
import { currentServerContext } from "./context";
|
|
18
|
-
import { installProcessHandlers, type ProcessLike } from "./process";
|
|
19
|
-
import { extractError, extractStructuredFrames } from "./stack";
|
|
20
|
-
import { type CaptureTransport, Transport } from "./transport";
|
|
21
|
-
import type { FixbackErrorEvent, ServerContext, ServerErrorPayload, Severity } from "./wire";
|
|
22
|
-
|
|
23
|
-
export type { BeforeSend, FixbackErrorEvent, ServerContext, Severity } from "./wire";
|
|
24
|
-
export type { InitOptions } from "./config";
|
|
25
|
-
export type { CaptureTransport } from "./transport";
|
|
26
|
-
|
|
27
|
-
/** Optional per-capture context — override `handled` or add/override server context. */
|
|
28
|
-
export interface CaptureContext {
|
|
29
|
-
/** Override the handled flag (defaults to `true` for a manual/caught capture). */
|
|
30
|
-
readonly handled?: boolean;
|
|
31
|
-
/** Server context to merge over what the request scope provides (e.g. a filter's status). */
|
|
32
|
-
readonly server?: ServerContext;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** The minimal capture surface the framework adapters file through (satisfied by {@link FixbackClient}). */
|
|
36
|
-
export interface CaptureClient {
|
|
37
|
-
captureException(error: unknown, context?: CaptureContext): void;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Advanced/testing seam: supply a custom {@link CaptureTransport} or process. */
|
|
41
|
-
export interface ClientDeps {
|
|
42
|
-
readonly transport?: CaptureTransport;
|
|
43
|
-
/** The process to attach handlers to (defaults to the real `process`). */
|
|
44
|
-
readonly process?: ProcessLike;
|
|
45
|
-
/** Override Node's default fatal behaviour after a sole-listener uncaught exception. */
|
|
46
|
-
readonly onFatalError?: (error: unknown) => void;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
type MutablePayload = { -readonly [K in keyof ServerErrorPayload]: ServerErrorPayload[K] };
|
|
50
|
-
|
|
51
|
-
/** Merge two server contexts, the override winning; `undefined` when both are empty. */
|
|
52
|
-
function mergeServerContext(
|
|
53
|
-
base: ServerContext | undefined,
|
|
54
|
-
override: ServerContext | undefined,
|
|
55
|
-
): ServerContext | undefined {
|
|
56
|
-
if (!base && !override) return undefined;
|
|
57
|
-
const merged: Record<string, unknown> = { ...(base ?? {}), ...(override ?? {}) };
|
|
58
|
-
for (const key of Object.keys(merged)) {
|
|
59
|
-
if (merged[key] === undefined) delete merged[key];
|
|
60
|
-
}
|
|
61
|
-
return Object.keys(merged).length > 0 ? (merged as ServerContext) : undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export class FixbackClient {
|
|
65
|
-
private readonly config: ResolvedConfig;
|
|
66
|
-
private readonly transport: CaptureTransport | null;
|
|
67
|
-
|
|
68
|
-
constructor(config: ResolvedConfig, deps: ClientDeps = {}) {
|
|
69
|
-
this.config = config;
|
|
70
|
-
this.transport =
|
|
71
|
-
deps.transport ??
|
|
72
|
-
(config.enabled
|
|
73
|
-
? new Transport({
|
|
74
|
-
endpoint: config.errorsEndpoint,
|
|
75
|
-
secretKey: config.secretKey,
|
|
76
|
-
maxBatchSize: config.maxBatchSize,
|
|
77
|
-
maxQueueSize: config.maxQueueSize,
|
|
78
|
-
flushIntervalMs: config.flushIntervalMs,
|
|
79
|
-
timeoutMs: config.timeoutMs,
|
|
80
|
-
})
|
|
81
|
-
: null);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Whether this client will capture (its secret key resolved and the gate is on). */
|
|
85
|
-
get enabled(): boolean {
|
|
86
|
-
return this.config.enabled;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** The header the request-context middleware reads a correlation id from. */
|
|
90
|
-
get requestIdHeader(): string {
|
|
91
|
-
return this.config.requestIdHeader;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Report a caught or manually-observed error. `handled` defaults to `true`; the
|
|
96
|
-
* process handlers pass `handled: false` for an uncaught crash.
|
|
97
|
-
*/
|
|
98
|
-
captureException(error: unknown, context: CaptureContext = {}): void {
|
|
99
|
-
const { type, value, stack } = extractError(error);
|
|
100
|
-
const frames = extractStructuredFrames(stack);
|
|
101
|
-
this.dispatch(
|
|
102
|
-
{
|
|
103
|
-
errorSignature: computeFingerprint(type, value, stack),
|
|
104
|
-
type,
|
|
105
|
-
value,
|
|
106
|
-
handled: context.handled ?? true,
|
|
107
|
-
errorFrames: frames.length > 0 ? frames : undefined,
|
|
108
|
-
},
|
|
109
|
-
context,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Report a noteworthy non-error condition, grouped by its message text. */
|
|
114
|
-
captureMessage(message: string, level: Severity = "info", context: CaptureContext = {}): void {
|
|
115
|
-
const value = typeof message === "string" ? message : String(message);
|
|
116
|
-
this.dispatch(
|
|
117
|
-
{
|
|
118
|
-
errorSignature: computeFingerprint("Message", value),
|
|
119
|
-
type: "Message",
|
|
120
|
-
value,
|
|
121
|
-
level,
|
|
122
|
-
handled: context.handled ?? true,
|
|
123
|
-
},
|
|
124
|
-
context,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Flush the transport's buffer now. Resolves even when the SDK is disabled. */
|
|
129
|
-
flush(): Promise<void> {
|
|
130
|
-
return this.transport?.flush() ?? Promise.resolve();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Stop the transport (its timer) and drain what remains. */
|
|
134
|
-
close(): Promise<void> {
|
|
135
|
-
return this.transport?.close() ?? Promise.resolve();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Assemble, scrub, run `beforeSend`, and enqueue — swallowing any failure. */
|
|
139
|
-
private dispatch(
|
|
140
|
-
partial: Pick<FixbackErrorEvent, "errorSignature" | "type" | "value" | "handled"> &
|
|
141
|
-
Partial<Pick<FixbackErrorEvent, "level" | "errorFrames">>,
|
|
142
|
-
context: CaptureContext,
|
|
143
|
-
): void {
|
|
144
|
-
if (!this.config.enabled || !this.transport) return;
|
|
145
|
-
try {
|
|
146
|
-
const event: FixbackErrorEvent = {
|
|
147
|
-
errorSignature: partial.errorSignature,
|
|
148
|
-
type: partial.type,
|
|
149
|
-
value: partial.value,
|
|
150
|
-
handled: partial.handled,
|
|
151
|
-
level: partial.level,
|
|
152
|
-
environment: this.config.environment,
|
|
153
|
-
release: this.config.release,
|
|
154
|
-
errorFrames: partial.errorFrames,
|
|
155
|
-
server: mergeServerContext(currentServerContext(), context.server),
|
|
156
|
-
};
|
|
157
|
-
if (this.config.scrub) applyDefaultScrub(event);
|
|
158
|
-
const finalised = this.runBeforeSend(event);
|
|
159
|
-
if (!finalised) return; // dropped at the choke point — no transport
|
|
160
|
-
this.transport.enqueue(toPayload(finalised));
|
|
161
|
-
} catch {
|
|
162
|
-
// Capture must never throw into the host (story 34).
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Run the project hook after the default scrubbers; a throwing hook is a no-op. */
|
|
167
|
-
private runBeforeSend(event: FixbackErrorEvent): FixbackErrorEvent | null {
|
|
168
|
-
const hook = this.config.beforeSend;
|
|
169
|
-
if (!hook) return event;
|
|
170
|
-
try {
|
|
171
|
-
return hook(event) ?? null;
|
|
172
|
-
} catch {
|
|
173
|
-
return event; // a buggy hook never breaks the report path nor leaks unscrubbed data
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** The built-in default scrub: re-sweep frame URLs through the shared-core scrubber. */
|
|
179
|
-
function applyDefaultScrub(event: FixbackErrorEvent): void {
|
|
180
|
-
if (event.errorFrames && event.errorFrames.length > 0) {
|
|
181
|
-
event.errorFrames = event.errorFrames.map((frame) => ({
|
|
182
|
-
...frame,
|
|
183
|
-
file: scrubUrl(frame.file),
|
|
184
|
-
}));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Project the event onto the wire payload, dropping the event-only fields. */
|
|
189
|
-
function toPayload(event: FixbackErrorEvent): ServerErrorPayload {
|
|
190
|
-
const payload: MutablePayload = { errorSignature: event.errorSignature };
|
|
191
|
-
if (event.handled !== undefined) payload.handled = event.handled;
|
|
192
|
-
if (event.environment !== undefined) payload.environment = event.environment;
|
|
193
|
-
if (event.release !== undefined) payload.release = event.release;
|
|
194
|
-
if (event.occurrences !== undefined) payload.occurrences = event.occurrences;
|
|
195
|
-
if (event.errorFrames && event.errorFrames.length > 0) payload.errorFrames = event.errorFrames;
|
|
196
|
-
if (event.server) payload.server = event.server;
|
|
197
|
-
return payload;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// --- Module-level singleton API ----------------------------------------------
|
|
201
|
-
|
|
202
|
-
let activeClient: FixbackClient | null = null;
|
|
203
|
-
let activeUninstall: (() => void) | null = null;
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Initialise the SDK: resolve the config, replace any previous client (closing it and
|
|
207
|
-
* removing its process handlers), install the polite process handlers, and return the
|
|
208
|
-
* active {@link FixbackClient}. Fail-quiet — a missing secret key yields an inert
|
|
209
|
-
* client (no handlers, no transport) rather than throwing.
|
|
210
|
-
*/
|
|
211
|
-
export function init(options: InitOptions, deps?: ClientDeps): FixbackClient {
|
|
212
|
-
const previousClient = activeClient;
|
|
213
|
-
const previousUninstall = activeUninstall;
|
|
214
|
-
if (previousUninstall) previousUninstall();
|
|
215
|
-
if (previousClient) void previousClient.close();
|
|
216
|
-
|
|
217
|
-
const config = resolveConfig(options);
|
|
218
|
-
const client = new FixbackClient(config, deps);
|
|
219
|
-
activeClient = client;
|
|
220
|
-
activeUninstall =
|
|
221
|
-
config.enabled && (config.captureUncaughtException || config.captureUnhandledRejection)
|
|
222
|
-
? installProcessHandlers(
|
|
223
|
-
client,
|
|
224
|
-
{
|
|
225
|
-
captureUncaughtException: config.captureUncaughtException,
|
|
226
|
-
captureUnhandledRejection: config.captureUnhandledRejection,
|
|
227
|
-
},
|
|
228
|
-
{ process: deps?.process, onFatalError: deps?.onFatalError },
|
|
229
|
-
)
|
|
230
|
-
: null;
|
|
231
|
-
return client;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** The active client set by {@link init}, or `null` before the first `init`. */
|
|
235
|
-
export function getClient(): FixbackClient | null {
|
|
236
|
-
return activeClient;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** Report a caught/manual error through the active client (no-op before `init`). */
|
|
240
|
-
export function captureException(error: unknown, context?: CaptureContext): void {
|
|
241
|
-
activeClient?.captureException(error, context);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** Report a message through the active client (no-op before `init`). */
|
|
245
|
-
export function captureMessage(message: string, level?: Severity, context?: CaptureContext): void {
|
|
246
|
-
activeClient?.captureMessage(message, level, context);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** Flush the active client's buffer (no-op before `init`). */
|
|
250
|
-
export function flush(): Promise<void> {
|
|
251
|
-
return activeClient?.flush() ?? Promise.resolve();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** Close and clear the active client, removing its process handlers (no-op before `init`). */
|
|
255
|
-
export function close(): Promise<void> {
|
|
256
|
-
const current = activeClient;
|
|
257
|
-
const uninstall = activeUninstall;
|
|
258
|
-
activeClient = null;
|
|
259
|
-
activeUninstall = null;
|
|
260
|
-
if (uninstall) uninstall();
|
|
261
|
-
return current?.close() ?? Promise.resolve();
|
|
262
|
-
}
|