@flareapp/node 0.11.0 → 0.12.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/index.cjs +6 -90
- package/dist/index.d.cts +5 -11
- package/dist/index.d.mts +5 -11
- package/dist/index.mjs +6 -90
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -34,24 +34,8 @@ let node_fs_promises = require("node:fs/promises");
|
|
|
34
34
|
let node_url = require("node:url");
|
|
35
35
|
|
|
36
36
|
//#region src/context/body.ts
|
|
37
|
-
/** `^` plus `\b` accepts a `; charset=utf-8` suffix while still rejecting `...-urlencoded-foo`. */
|
|
38
37
|
const DEFAULT_BODY_CONTENT_TYPES = /^application\/(json|x-www-form-urlencoded)\b/i;
|
|
39
|
-
/** Reuses core's URL denylist, so credentials are caught by the same regex everywhere. */
|
|
40
38
|
const DEFAULT_BODY_KEY_DENYLIST = _flareapp_core.DEFAULT_URL_DENYLIST;
|
|
41
|
-
/**
|
|
42
|
-
* Normalize, redact, serialize, and size-cap a request body for a Flare report. Returns the JSON string,
|
|
43
|
-
* or `null` when the body should not be reported (unknown shape, content-type miss, serialization fail).
|
|
44
|
-
*
|
|
45
|
-
* Accepts four runtime shapes:
|
|
46
|
-
* - `string`: must match `contentType` per `bodyAllowedContentTypes`, else dropped.
|
|
47
|
-
* - `Buffer`: decoded UTF-8 then treated as a string.
|
|
48
|
-
* - `URLSearchParams`: flattened to a plain record. No content-type gate (shape is unambiguous).
|
|
49
|
-
* - Other `object`/array: used as-is, no gate. The common middleware path (Express/Fastify `req.body`).
|
|
50
|
-
* Anything else returns `null`.
|
|
51
|
-
*
|
|
52
|
-
* Then: redact (denylisted keys become `'[redacted]'`, cycles become `'[Circular]'`), `JSON.stringify`
|
|
53
|
-
* (drop body if it throws on BigInt/Symbol/etc), and truncate to `bodyMaxBytes` including the suffix.
|
|
54
|
-
*/
|
|
55
39
|
function captureBody(body, contentType, opts) {
|
|
56
40
|
if (body === void 0 || body === null) return null;
|
|
57
41
|
let parsed;
|
|
@@ -77,7 +61,6 @@ function captureBody(body, contentType, opts) {
|
|
|
77
61
|
}
|
|
78
62
|
const TRUNCATION_SUFFIX = "…[truncated]";
|
|
79
63
|
const TRUNCATION_SUFFIX_BYTES = Buffer.byteLength(TRUNCATION_SUFFIX, "utf8");
|
|
80
|
-
/** Walks back over continuation bytes (`10xxxxxx`) to a codepoint boundary, so the result still decodes. */
|
|
81
64
|
function truncateToByteLimit(serialized, maxBytes) {
|
|
82
65
|
const buf = Buffer.from(serialized, "utf8");
|
|
83
66
|
if (buf.length <= maxBytes) return serialized;
|
|
@@ -91,15 +74,12 @@ function truncateToByteLimit(serialized, maxBytes) {
|
|
|
91
74
|
while (cut > 0 && (buf[cut] & 192) === 128) cut--;
|
|
92
75
|
return buf.subarray(0, cut).toString("utf8") + TRUNCATION_SUFFIX;
|
|
93
76
|
}
|
|
94
|
-
/** Normalizes to the bare media type first, so a strict custom regex like `/^application\/json$/` still
|
|
95
|
-
* matches `application/json; charset=utf-8`. */
|
|
96
77
|
function matchesContentType(ct, allowed) {
|
|
97
78
|
if (!ct) return false;
|
|
98
79
|
const mediaType = ct.split(";")[0].trim().toLowerCase();
|
|
99
80
|
if (!mediaType) return false;
|
|
100
81
|
return allowed.test(mediaType);
|
|
101
82
|
}
|
|
102
|
-
/** Returns `undefined` rather than `null` on failure, since `null` is itself a valid JSON value. */
|
|
103
83
|
function parseString(text, contentType) {
|
|
104
84
|
if (contentType && /x-www-form-urlencoded/i.test(contentType)) return Object.fromEntries(new URLSearchParams(text).entries());
|
|
105
85
|
try {
|
|
@@ -108,13 +88,11 @@ function parseString(text, contentType) {
|
|
|
108
88
|
return;
|
|
109
89
|
}
|
|
110
90
|
}
|
|
111
|
-
/** Excludes class instances, streams, FormData, ArrayBuffer views, Buffer and URLSearchParams. */
|
|
112
91
|
function isPlainObject(value) {
|
|
113
92
|
if (value === null || typeof value !== "object") return false;
|
|
114
93
|
const proto = Object.getPrototypeOf(value);
|
|
115
94
|
return proto === null || proto === Object.prototype;
|
|
116
95
|
}
|
|
117
|
-
/** `seen` is a parameter rather than a closure to avoid allocating a WeakSet per recursion. */
|
|
118
96
|
function redact(value, denylist, seen = /* @__PURE__ */ new WeakSet()) {
|
|
119
97
|
if (value === null || typeof value !== "object") return value;
|
|
120
98
|
if (seen.has(value)) return "[Circular]";
|
|
@@ -127,7 +105,6 @@ function redact(value, denylist, seen = /* @__PURE__ */ new WeakSet()) {
|
|
|
127
105
|
|
|
128
106
|
//#endregion
|
|
129
107
|
//#region src/context/deviceInfo.ts
|
|
130
|
-
/** Node os + runtime as normalised device info. */
|
|
131
108
|
var NodeDeviceInfoProvider = class {
|
|
132
109
|
collect() {
|
|
133
110
|
return {
|
|
@@ -146,7 +123,6 @@ const nodeDeviceInfoProvider = new NodeDeviceInfoProvider();
|
|
|
146
123
|
|
|
147
124
|
//#endregion
|
|
148
125
|
//#region src/context/headers.ts
|
|
149
|
-
/** Case-insensitive. An array value collapses to its first element; callers here want a single value. */
|
|
150
126
|
function findHeader(headers, name) {
|
|
151
127
|
if (!headers) return;
|
|
152
128
|
const target = name.toLowerCase();
|
|
@@ -156,20 +132,12 @@ function findHeader(headers, name) {
|
|
|
156
132
|
return Array.isArray(value) ? value[0] : value;
|
|
157
133
|
}
|
|
158
134
|
}
|
|
159
|
-
/** The `^` and `$` matter: without them, `cookie` would also match a header like `X-Some-Cookie-Hint`. */
|
|
160
135
|
const DEFAULT_HEADER_DENYLIST = /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key|x-csrf-token|x-xsrf-token|x-auth-token|forwarded|x-forwarded-(?:for|user))$/i;
|
|
161
|
-
/** `g`/`y` are stripped from a custom pattern: those carry lastIndex, which makes `.test()` stateful. */
|
|
162
136
|
function resolveHeaderDenylist(custom, replaceDefault = false) {
|
|
163
137
|
if (!custom) return DEFAULT_HEADER_DENYLIST;
|
|
164
138
|
if (replaceDefault) return new RegExp(custom.source, custom.flags.replace(/[gy]/g, ""));
|
|
165
139
|
return new RegExp(`(?:${DEFAULT_HEADER_DENYLIST.source})|(?:${custom.source})`, "i");
|
|
166
140
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Turns headers into `http.request.header.<name>` attributes. The two lists differ on purpose: an
|
|
169
|
-
* allowlist drops a header entirely, for apps that may only send named headers, while the denylist
|
|
170
|
-
* keeps the name and replaces the value, so you can still see the header was there. `undefined` is
|
|
171
|
-
* how `node:http` says "not sent", so those are dropped.
|
|
172
|
-
*/
|
|
173
141
|
function projectHeaders(headers, options) {
|
|
174
142
|
const out = {};
|
|
175
143
|
if (!headers) return out;
|
|
@@ -185,10 +153,6 @@ function projectHeaders(headers, options) {
|
|
|
185
153
|
|
|
186
154
|
//#endregion
|
|
187
155
|
//#region src/context/process.ts
|
|
188
|
-
/**
|
|
189
|
-
* Host + process attributes, read per report so `process.uptime()` and `os.hostname()` stay current.
|
|
190
|
-
* os and runtime come from `NodeDeviceInfoProvider`.
|
|
191
|
-
*/
|
|
192
156
|
function collectProcessAttributes() {
|
|
193
157
|
return {
|
|
194
158
|
"process.pid": process.pid,
|
|
@@ -200,13 +164,6 @@ function collectProcessAttributes() {
|
|
|
200
164
|
|
|
201
165
|
//#endregion
|
|
202
166
|
//#region src/context/collectNode.ts
|
|
203
|
-
/**
|
|
204
|
-
* Turns process info (always) and the active request scope (only inside `runWithContext`) into
|
|
205
|
-
* OTel-style attributes. User identity is not handled here: `Flare.setUser` writes straight to
|
|
206
|
-
* `pendingAttributes`.
|
|
207
|
-
*
|
|
208
|
-
* `getOptions` is a getter so `configureNode(...)` shows up on later reports without rebuilding this.
|
|
209
|
-
*/
|
|
210
167
|
function makeNodeContextCollector(provider, getOptions) {
|
|
211
168
|
return (config) => {
|
|
212
169
|
const attrs = {
|
|
@@ -283,23 +240,12 @@ function buildFatalCallbacks(flare, getOpts, exit = process.exit.bind(process))
|
|
|
283
240
|
|
|
284
241
|
//#endregion
|
|
285
242
|
//#region src/process/handlers.ts
|
|
286
|
-
/**
|
|
287
|
-
* Owns the `uncaughtException` and `unhandledRejection` listeners that feed fatal failures to Flare.
|
|
288
|
-
* Separate from `NodeFlare` because it is pure `process`-event plumbing with no Flare semantics, which
|
|
289
|
-
* keeps it trivially testable.
|
|
290
|
-
*
|
|
291
|
-
* Deliberately kept separate from electron's ProcessHandlerManager, not a shared module. @flareapp/electron
|
|
292
|
-
* does not depend on @flareapp/node, and the only package both import is @flareapp/core, which ships in
|
|
293
|
-
* every browser bundle and touches `process` only behind a typeof guard. A shared manager belongs in a new
|
|
294
|
-
* package, not in core, and one method does not pay for one.
|
|
295
|
-
*/
|
|
296
243
|
var ProcessHandlerManager = class {
|
|
297
244
|
uncaughtHandler = null;
|
|
298
245
|
rejectionHandler = null;
|
|
299
246
|
constructor(cbs) {
|
|
300
247
|
this.cbs = cbs;
|
|
301
248
|
}
|
|
302
|
-
/** Idempotent: a no-op when the attached listeners already match the supplied modes. */
|
|
303
249
|
reconcile(opts) {
|
|
304
250
|
this.reconcileOne("uncaughtException", opts.uncaughtExceptionMode, () => this.uncaughtHandler, (h) => {
|
|
305
251
|
this.uncaughtHandler = h;
|
|
@@ -308,7 +254,6 @@ var ProcessHandlerManager = class {
|
|
|
308
254
|
this.rejectionHandler = h;
|
|
309
255
|
}, (reason) => this.cbs.onRejection(reason));
|
|
310
256
|
}
|
|
311
|
-
/** Remove both listeners regardless of intent. Safe when nothing is attached. */
|
|
312
257
|
detach() {
|
|
313
258
|
if (this.uncaughtHandler) {
|
|
314
259
|
process.off("uncaughtException", this.uncaughtHandler);
|
|
@@ -319,11 +264,6 @@ var ProcessHandlerManager = class {
|
|
|
319
264
|
this.rejectionHandler = null;
|
|
320
265
|
}
|
|
321
266
|
}
|
|
322
|
-
/**
|
|
323
|
-
* Generic attach/detach for one event. The `get`/`set` closures share this body across both events
|
|
324
|
-
* while mutating distinct fields (`uncaughtHandler` vs `rejectionHandler`). Attaches when wanted and
|
|
325
|
-
* absent, detaches when unwanted and present, else no-op.
|
|
326
|
-
*/
|
|
327
267
|
reconcileOne(event, mode, get, set, impl) {
|
|
328
268
|
const current = get();
|
|
329
269
|
const wants = mode !== "off";
|
|
@@ -346,25 +286,15 @@ var NodeScope = class extends _flareapp_core.Scope {
|
|
|
346
286
|
|
|
347
287
|
//#endregion
|
|
348
288
|
//#region src/scope/AsyncLocalStorageScopeProvider.ts
|
|
349
|
-
/**
|
|
350
|
-
* Gives every in-flight request its own `NodeScope`, isolated from concurrent requests.
|
|
351
|
-
*
|
|
352
|
-
* The `fallback` scope catches work outside any request (process-level reports, startup errors,
|
|
353
|
-
* scheduled jobs). It is per-instance rather than fresh per read, so outside-scope writes persist for a
|
|
354
|
-
* later outside report.
|
|
355
|
-
*/
|
|
356
289
|
var AsyncLocalStorageScopeProvider = class {
|
|
357
290
|
als = new node_async_hooks.AsyncLocalStorage();
|
|
358
291
|
fallback = new NodeScope();
|
|
359
|
-
/** Never null: falls back to the shared scope outside `runWithContext`. */
|
|
360
292
|
active() {
|
|
361
293
|
return this.als.getStore() ?? this.fallback;
|
|
362
294
|
}
|
|
363
|
-
/** Null outside `runWithContext`, so callers can tell "inside a request" from "not". */
|
|
364
295
|
getContext() {
|
|
365
296
|
return this.als.getStore() ?? null;
|
|
366
297
|
}
|
|
367
|
-
/** `request` is shallow-cloned so later edits to the caller's object do not leak into the scope. */
|
|
368
298
|
runWithContext(request, fn) {
|
|
369
299
|
const scope = new NodeScope();
|
|
370
300
|
scope.request = { ...request };
|
|
@@ -381,11 +311,6 @@ var AsyncLocalStorageScopeProvider = class {
|
|
|
381
311
|
|
|
382
312
|
//#endregion
|
|
383
313
|
//#region src/stacktrace/DiskFileReader.ts
|
|
384
|
-
/**
|
|
385
|
-
* Reads snippet sources off disk: on the server a frame's "URL" is usually a local path or a `file://`
|
|
386
|
-
* URL from `import.meta.url`. Only unambiguously local paths are read, which rules out traversal,
|
|
387
|
-
* following an http frame in a server build, and cwd-relative ambiguity. Never throws.
|
|
388
|
-
*/
|
|
389
314
|
var DiskFileReader = class {
|
|
390
315
|
async read(url) {
|
|
391
316
|
if (!isLocalFileUrl(url)) return null;
|
|
@@ -396,7 +321,6 @@ var DiskFileReader = class {
|
|
|
396
321
|
}
|
|
397
322
|
}
|
|
398
323
|
};
|
|
399
|
-
/** `file://` (any casing), POSIX absolute, Windows drive-letter (`C:\foo`), Windows UNC (`\\`). */
|
|
400
324
|
function isLocalFileUrl(url) {
|
|
401
325
|
return /^file:\/\//i.test(url) || url.startsWith("/") || /^[a-z]:[\\/]/i.test(url) || url.startsWith("\\\\");
|
|
402
326
|
}
|
|
@@ -404,9 +328,7 @@ function isLocalFileUrl(url) {
|
|
|
404
328
|
//#endregion
|
|
405
329
|
//#region src/Flare.ts
|
|
406
330
|
const NODE_SDK_NAME = "@flareapp/node";
|
|
407
|
-
const NODE_SDK_VERSION = typeof process !== "undefined" && true ? "0.
|
|
408
|
-
/** `g`/`y` make `.test()` keep `lastIndex` state, so reusing the regex across keys skips every other
|
|
409
|
-
* match. All other flags are preserved. */
|
|
331
|
+
const NODE_SDK_VERSION = typeof process !== "undefined" && true ? "0.12.0" : "?";
|
|
410
332
|
function sanitizeRegex(re) {
|
|
411
333
|
const safeFlags = re.flags.replace(/[gy]/g, "");
|
|
412
334
|
return new RegExp(re.source, safeFlags);
|
|
@@ -424,18 +346,12 @@ const DEFAULT_NODE_OPTIONS = {
|
|
|
424
346
|
bodyKeyDenylist: DEFAULT_BODY_KEY_DENYLIST
|
|
425
347
|
};
|
|
426
348
|
/**
|
|
427
|
-
* Node.js
|
|
428
|
-
*
|
|
429
|
-
* Subclasses core's `Flare` and wires the Node-only seams in its constructor:
|
|
430
|
-
* - `AsyncLocalStorageScopeProvider` so each `runWithContext(...)` callback gets its own `NodeScope`,
|
|
431
|
-
* isolated from concurrent requests.
|
|
432
|
-
* - `makeNodeContextCollector(...)` turns the current `NodeScope` + process info into report attributes.
|
|
433
|
-
* - `DiskFileReader` reads source for stack-trace snippets via `node:fs/promises`, not `fetch`.
|
|
434
|
-
* - `ProcessHandlerManager` attaches/detaches the fatal process listeners per `NodeOptions`.
|
|
349
|
+
* Node.js `Flare` singleton, exported from `@flareapp/node` as `flare`.
|
|
435
350
|
*
|
|
436
|
-
*
|
|
437
|
-
* `
|
|
438
|
-
*
|
|
351
|
+
* Subclasses core's `Flare` and wires the Node-only seams: per-request scope via
|
|
352
|
+
* `AsyncLocalStorageScopeProvider`, a Node context collector, `DiskFileReader` for stack snippets,
|
|
353
|
+
* and `ProcessHandlerManager` for the fatal listeners. Adds `configureNode`, `runWithContext`,
|
|
354
|
+
* `mergeContext`, `getContext`, and `removeProcessListeners` on top of the core API.
|
|
439
355
|
*/
|
|
440
356
|
var NodeFlare = class extends _flareapp_core.Flare {
|
|
441
357
|
nodeOptions = { ...DEFAULT_NODE_OPTIONS };
|
package/dist/index.d.cts
CHANGED
|
@@ -29,18 +29,12 @@ declare class NodeScope extends Scope$1 {
|
|
|
29
29
|
//#endregion
|
|
30
30
|
//#region src/Flare.d.ts
|
|
31
31
|
/**
|
|
32
|
-
* Node.js
|
|
32
|
+
* Node.js `Flare` singleton, exported from `@flareapp/node` as `flare`.
|
|
33
33
|
*
|
|
34
|
-
* Subclasses core's `Flare` and wires the Node-only seams
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* - `DiskFileReader` reads source for stack-trace snippets via `node:fs/promises`, not `fetch`.
|
|
39
|
-
* - `ProcessHandlerManager` attaches/detaches the fatal process listeners per `NodeOptions`.
|
|
40
|
-
*
|
|
41
|
-
* Adds Node-only API on top of core: `configureNode`, `runWithContext`, `mergeContext`, `getContext`,
|
|
42
|
-
* `removeProcessListeners`. Inherited core methods return `this`, so chaining keeps the `NodeFlare`
|
|
43
|
-
* type and `configureNode(...)` stays callable mid-chain.
|
|
34
|
+
* Subclasses core's `Flare` and wires the Node-only seams: per-request scope via
|
|
35
|
+
* `AsyncLocalStorageScopeProvider`, a Node context collector, `DiskFileReader` for stack snippets,
|
|
36
|
+
* and `ProcessHandlerManager` for the fatal listeners. Adds `configureNode`, `runWithContext`,
|
|
37
|
+
* `mergeContext`, `getContext`, and `removeProcessListeners` on top of the core API.
|
|
44
38
|
*/
|
|
45
39
|
declare class NodeFlare extends Flare$1 {
|
|
46
40
|
private nodeOptions;
|
package/dist/index.d.mts
CHANGED
|
@@ -29,18 +29,12 @@ declare class NodeScope extends Scope$1 {
|
|
|
29
29
|
//#endregion
|
|
30
30
|
//#region src/Flare.d.ts
|
|
31
31
|
/**
|
|
32
|
-
* Node.js
|
|
32
|
+
* Node.js `Flare` singleton, exported from `@flareapp/node` as `flare`.
|
|
33
33
|
*
|
|
34
|
-
* Subclasses core's `Flare` and wires the Node-only seams
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* - `DiskFileReader` reads source for stack-trace snippets via `node:fs/promises`, not `fetch`.
|
|
39
|
-
* - `ProcessHandlerManager` attaches/detaches the fatal process listeners per `NodeOptions`.
|
|
40
|
-
*
|
|
41
|
-
* Adds Node-only API on top of core: `configureNode`, `runWithContext`, `mergeContext`, `getContext`,
|
|
42
|
-
* `removeProcessListeners`. Inherited core methods return `this`, so chaining keeps the `NodeFlare`
|
|
43
|
-
* type and `configureNode(...)` stays callable mid-chain.
|
|
34
|
+
* Subclasses core's `Flare` and wires the Node-only seams: per-request scope via
|
|
35
|
+
* `AsyncLocalStorageScopeProvider`, a Node context collector, `DiskFileReader` for stack snippets,
|
|
36
|
+
* and `ProcessHandlerManager` for the fatal listeners. Adds `configureNode`, `runWithContext`,
|
|
37
|
+
* `mergeContext`, `getContext`, and `removeProcessListeners` on top of the core API.
|
|
44
38
|
*/
|
|
45
39
|
declare class NodeFlare extends Flare$1 {
|
|
46
40
|
private nodeOptions;
|
package/dist/index.mjs
CHANGED
|
@@ -5,24 +5,8 @@ import { readFile } from "node:fs/promises";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
//#region src/context/body.ts
|
|
8
|
-
/** `^` plus `\b` accepts a `; charset=utf-8` suffix while still rejecting `...-urlencoded-foo`. */
|
|
9
8
|
const DEFAULT_BODY_CONTENT_TYPES = /^application\/(json|x-www-form-urlencoded)\b/i;
|
|
10
|
-
/** Reuses core's URL denylist, so credentials are caught by the same regex everywhere. */
|
|
11
9
|
const DEFAULT_BODY_KEY_DENYLIST = DEFAULT_URL_DENYLIST$1;
|
|
12
|
-
/**
|
|
13
|
-
* Normalize, redact, serialize, and size-cap a request body for a Flare report. Returns the JSON string,
|
|
14
|
-
* or `null` when the body should not be reported (unknown shape, content-type miss, serialization fail).
|
|
15
|
-
*
|
|
16
|
-
* Accepts four runtime shapes:
|
|
17
|
-
* - `string`: must match `contentType` per `bodyAllowedContentTypes`, else dropped.
|
|
18
|
-
* - `Buffer`: decoded UTF-8 then treated as a string.
|
|
19
|
-
* - `URLSearchParams`: flattened to a plain record. No content-type gate (shape is unambiguous).
|
|
20
|
-
* - Other `object`/array: used as-is, no gate. The common middleware path (Express/Fastify `req.body`).
|
|
21
|
-
* Anything else returns `null`.
|
|
22
|
-
*
|
|
23
|
-
* Then: redact (denylisted keys become `'[redacted]'`, cycles become `'[Circular]'`), `JSON.stringify`
|
|
24
|
-
* (drop body if it throws on BigInt/Symbol/etc), and truncate to `bodyMaxBytes` including the suffix.
|
|
25
|
-
*/
|
|
26
10
|
function captureBody(body, contentType, opts) {
|
|
27
11
|
if (body === void 0 || body === null) return null;
|
|
28
12
|
let parsed;
|
|
@@ -48,7 +32,6 @@ function captureBody(body, contentType, opts) {
|
|
|
48
32
|
}
|
|
49
33
|
const TRUNCATION_SUFFIX = "…[truncated]";
|
|
50
34
|
const TRUNCATION_SUFFIX_BYTES = Buffer.byteLength(TRUNCATION_SUFFIX, "utf8");
|
|
51
|
-
/** Walks back over continuation bytes (`10xxxxxx`) to a codepoint boundary, so the result still decodes. */
|
|
52
35
|
function truncateToByteLimit(serialized, maxBytes) {
|
|
53
36
|
const buf = Buffer.from(serialized, "utf8");
|
|
54
37
|
if (buf.length <= maxBytes) return serialized;
|
|
@@ -62,15 +45,12 @@ function truncateToByteLimit(serialized, maxBytes) {
|
|
|
62
45
|
while (cut > 0 && (buf[cut] & 192) === 128) cut--;
|
|
63
46
|
return buf.subarray(0, cut).toString("utf8") + TRUNCATION_SUFFIX;
|
|
64
47
|
}
|
|
65
|
-
/** Normalizes to the bare media type first, so a strict custom regex like `/^application\/json$/` still
|
|
66
|
-
* matches `application/json; charset=utf-8`. */
|
|
67
48
|
function matchesContentType(ct, allowed) {
|
|
68
49
|
if (!ct) return false;
|
|
69
50
|
const mediaType = ct.split(";")[0].trim().toLowerCase();
|
|
70
51
|
if (!mediaType) return false;
|
|
71
52
|
return allowed.test(mediaType);
|
|
72
53
|
}
|
|
73
|
-
/** Returns `undefined` rather than `null` on failure, since `null` is itself a valid JSON value. */
|
|
74
54
|
function parseString(text, contentType) {
|
|
75
55
|
if (contentType && /x-www-form-urlencoded/i.test(contentType)) return Object.fromEntries(new URLSearchParams(text).entries());
|
|
76
56
|
try {
|
|
@@ -79,13 +59,11 @@ function parseString(text, contentType) {
|
|
|
79
59
|
return;
|
|
80
60
|
}
|
|
81
61
|
}
|
|
82
|
-
/** Excludes class instances, streams, FormData, ArrayBuffer views, Buffer and URLSearchParams. */
|
|
83
62
|
function isPlainObject(value) {
|
|
84
63
|
if (value === null || typeof value !== "object") return false;
|
|
85
64
|
const proto = Object.getPrototypeOf(value);
|
|
86
65
|
return proto === null || proto === Object.prototype;
|
|
87
66
|
}
|
|
88
|
-
/** `seen` is a parameter rather than a closure to avoid allocating a WeakSet per recursion. */
|
|
89
67
|
function redact(value, denylist, seen = /* @__PURE__ */ new WeakSet()) {
|
|
90
68
|
if (value === null || typeof value !== "object") return value;
|
|
91
69
|
if (seen.has(value)) return "[Circular]";
|
|
@@ -98,7 +76,6 @@ function redact(value, denylist, seen = /* @__PURE__ */ new WeakSet()) {
|
|
|
98
76
|
|
|
99
77
|
//#endregion
|
|
100
78
|
//#region src/context/deviceInfo.ts
|
|
101
|
-
/** Node os + runtime as normalised device info. */
|
|
102
79
|
var NodeDeviceInfoProvider = class {
|
|
103
80
|
collect() {
|
|
104
81
|
return {
|
|
@@ -117,7 +94,6 @@ const nodeDeviceInfoProvider = new NodeDeviceInfoProvider();
|
|
|
117
94
|
|
|
118
95
|
//#endregion
|
|
119
96
|
//#region src/context/headers.ts
|
|
120
|
-
/** Case-insensitive. An array value collapses to its first element; callers here want a single value. */
|
|
121
97
|
function findHeader(headers, name) {
|
|
122
98
|
if (!headers) return;
|
|
123
99
|
const target = name.toLowerCase();
|
|
@@ -127,20 +103,12 @@ function findHeader(headers, name) {
|
|
|
127
103
|
return Array.isArray(value) ? value[0] : value;
|
|
128
104
|
}
|
|
129
105
|
}
|
|
130
|
-
/** The `^` and `$` matter: without them, `cookie` would also match a header like `X-Some-Cookie-Hint`. */
|
|
131
106
|
const DEFAULT_HEADER_DENYLIST = /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key|x-csrf-token|x-xsrf-token|x-auth-token|forwarded|x-forwarded-(?:for|user))$/i;
|
|
132
|
-
/** `g`/`y` are stripped from a custom pattern: those carry lastIndex, which makes `.test()` stateful. */
|
|
133
107
|
function resolveHeaderDenylist(custom, replaceDefault = false) {
|
|
134
108
|
if (!custom) return DEFAULT_HEADER_DENYLIST;
|
|
135
109
|
if (replaceDefault) return new RegExp(custom.source, custom.flags.replace(/[gy]/g, ""));
|
|
136
110
|
return new RegExp(`(?:${DEFAULT_HEADER_DENYLIST.source})|(?:${custom.source})`, "i");
|
|
137
111
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Turns headers into `http.request.header.<name>` attributes. The two lists differ on purpose: an
|
|
140
|
-
* allowlist drops a header entirely, for apps that may only send named headers, while the denylist
|
|
141
|
-
* keeps the name and replaces the value, so you can still see the header was there. `undefined` is
|
|
142
|
-
* how `node:http` says "not sent", so those are dropped.
|
|
143
|
-
*/
|
|
144
112
|
function projectHeaders(headers, options) {
|
|
145
113
|
const out = {};
|
|
146
114
|
if (!headers) return out;
|
|
@@ -156,10 +124,6 @@ function projectHeaders(headers, options) {
|
|
|
156
124
|
|
|
157
125
|
//#endregion
|
|
158
126
|
//#region src/context/process.ts
|
|
159
|
-
/**
|
|
160
|
-
* Host + process attributes, read per report so `process.uptime()` and `os.hostname()` stay current.
|
|
161
|
-
* os and runtime come from `NodeDeviceInfoProvider`.
|
|
162
|
-
*/
|
|
163
127
|
function collectProcessAttributes() {
|
|
164
128
|
return {
|
|
165
129
|
"process.pid": process.pid,
|
|
@@ -171,13 +135,6 @@ function collectProcessAttributes() {
|
|
|
171
135
|
|
|
172
136
|
//#endregion
|
|
173
137
|
//#region src/context/collectNode.ts
|
|
174
|
-
/**
|
|
175
|
-
* Turns process info (always) and the active request scope (only inside `runWithContext`) into
|
|
176
|
-
* OTel-style attributes. User identity is not handled here: `Flare.setUser` writes straight to
|
|
177
|
-
* `pendingAttributes`.
|
|
178
|
-
*
|
|
179
|
-
* `getOptions` is a getter so `configureNode(...)` shows up on later reports without rebuilding this.
|
|
180
|
-
*/
|
|
181
138
|
function makeNodeContextCollector(provider, getOptions) {
|
|
182
139
|
return (config) => {
|
|
183
140
|
const attrs = {
|
|
@@ -254,23 +211,12 @@ function buildFatalCallbacks(flare, getOpts, exit = process.exit.bind(process))
|
|
|
254
211
|
|
|
255
212
|
//#endregion
|
|
256
213
|
//#region src/process/handlers.ts
|
|
257
|
-
/**
|
|
258
|
-
* Owns the `uncaughtException` and `unhandledRejection` listeners that feed fatal failures to Flare.
|
|
259
|
-
* Separate from `NodeFlare` because it is pure `process`-event plumbing with no Flare semantics, which
|
|
260
|
-
* keeps it trivially testable.
|
|
261
|
-
*
|
|
262
|
-
* Deliberately kept separate from electron's ProcessHandlerManager, not a shared module. @flareapp/electron
|
|
263
|
-
* does not depend on @flareapp/node, and the only package both import is @flareapp/core, which ships in
|
|
264
|
-
* every browser bundle and touches `process` only behind a typeof guard. A shared manager belongs in a new
|
|
265
|
-
* package, not in core, and one method does not pay for one.
|
|
266
|
-
*/
|
|
267
214
|
var ProcessHandlerManager = class {
|
|
268
215
|
uncaughtHandler = null;
|
|
269
216
|
rejectionHandler = null;
|
|
270
217
|
constructor(cbs) {
|
|
271
218
|
this.cbs = cbs;
|
|
272
219
|
}
|
|
273
|
-
/** Idempotent: a no-op when the attached listeners already match the supplied modes. */
|
|
274
220
|
reconcile(opts) {
|
|
275
221
|
this.reconcileOne("uncaughtException", opts.uncaughtExceptionMode, () => this.uncaughtHandler, (h) => {
|
|
276
222
|
this.uncaughtHandler = h;
|
|
@@ -279,7 +225,6 @@ var ProcessHandlerManager = class {
|
|
|
279
225
|
this.rejectionHandler = h;
|
|
280
226
|
}, (reason) => this.cbs.onRejection(reason));
|
|
281
227
|
}
|
|
282
|
-
/** Remove both listeners regardless of intent. Safe when nothing is attached. */
|
|
283
228
|
detach() {
|
|
284
229
|
if (this.uncaughtHandler) {
|
|
285
230
|
process.off("uncaughtException", this.uncaughtHandler);
|
|
@@ -290,11 +235,6 @@ var ProcessHandlerManager = class {
|
|
|
290
235
|
this.rejectionHandler = null;
|
|
291
236
|
}
|
|
292
237
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Generic attach/detach for one event. The `get`/`set` closures share this body across both events
|
|
295
|
-
* while mutating distinct fields (`uncaughtHandler` vs `rejectionHandler`). Attaches when wanted and
|
|
296
|
-
* absent, detaches when unwanted and present, else no-op.
|
|
297
|
-
*/
|
|
298
238
|
reconcileOne(event, mode, get, set, impl) {
|
|
299
239
|
const current = get();
|
|
300
240
|
const wants = mode !== "off";
|
|
@@ -317,25 +257,15 @@ var NodeScope = class extends Scope$1 {
|
|
|
317
257
|
|
|
318
258
|
//#endregion
|
|
319
259
|
//#region src/scope/AsyncLocalStorageScopeProvider.ts
|
|
320
|
-
/**
|
|
321
|
-
* Gives every in-flight request its own `NodeScope`, isolated from concurrent requests.
|
|
322
|
-
*
|
|
323
|
-
* The `fallback` scope catches work outside any request (process-level reports, startup errors,
|
|
324
|
-
* scheduled jobs). It is per-instance rather than fresh per read, so outside-scope writes persist for a
|
|
325
|
-
* later outside report.
|
|
326
|
-
*/
|
|
327
260
|
var AsyncLocalStorageScopeProvider = class {
|
|
328
261
|
als = new AsyncLocalStorage();
|
|
329
262
|
fallback = new NodeScope();
|
|
330
|
-
/** Never null: falls back to the shared scope outside `runWithContext`. */
|
|
331
263
|
active() {
|
|
332
264
|
return this.als.getStore() ?? this.fallback;
|
|
333
265
|
}
|
|
334
|
-
/** Null outside `runWithContext`, so callers can tell "inside a request" from "not". */
|
|
335
266
|
getContext() {
|
|
336
267
|
return this.als.getStore() ?? null;
|
|
337
268
|
}
|
|
338
|
-
/** `request` is shallow-cloned so later edits to the caller's object do not leak into the scope. */
|
|
339
269
|
runWithContext(request, fn) {
|
|
340
270
|
const scope = new NodeScope();
|
|
341
271
|
scope.request = { ...request };
|
|
@@ -352,11 +282,6 @@ var AsyncLocalStorageScopeProvider = class {
|
|
|
352
282
|
|
|
353
283
|
//#endregion
|
|
354
284
|
//#region src/stacktrace/DiskFileReader.ts
|
|
355
|
-
/**
|
|
356
|
-
* Reads snippet sources off disk: on the server a frame's "URL" is usually a local path or a `file://`
|
|
357
|
-
* URL from `import.meta.url`. Only unambiguously local paths are read, which rules out traversal,
|
|
358
|
-
* following an http frame in a server build, and cwd-relative ambiguity. Never throws.
|
|
359
|
-
*/
|
|
360
285
|
var DiskFileReader = class {
|
|
361
286
|
async read(url) {
|
|
362
287
|
if (!isLocalFileUrl(url)) return null;
|
|
@@ -367,7 +292,6 @@ var DiskFileReader = class {
|
|
|
367
292
|
}
|
|
368
293
|
}
|
|
369
294
|
};
|
|
370
|
-
/** `file://` (any casing), POSIX absolute, Windows drive-letter (`C:\foo`), Windows UNC (`\\`). */
|
|
371
295
|
function isLocalFileUrl(url) {
|
|
372
296
|
return /^file:\/\//i.test(url) || url.startsWith("/") || /^[a-z]:[\\/]/i.test(url) || url.startsWith("\\\\");
|
|
373
297
|
}
|
|
@@ -375,9 +299,7 @@ function isLocalFileUrl(url) {
|
|
|
375
299
|
//#endregion
|
|
376
300
|
//#region src/Flare.ts
|
|
377
301
|
const NODE_SDK_NAME = "@flareapp/node";
|
|
378
|
-
const NODE_SDK_VERSION = typeof process !== "undefined" && true ? "0.
|
|
379
|
-
/** `g`/`y` make `.test()` keep `lastIndex` state, so reusing the regex across keys skips every other
|
|
380
|
-
* match. All other flags are preserved. */
|
|
302
|
+
const NODE_SDK_VERSION = typeof process !== "undefined" && true ? "0.12.0" : "?";
|
|
381
303
|
function sanitizeRegex(re) {
|
|
382
304
|
const safeFlags = re.flags.replace(/[gy]/g, "");
|
|
383
305
|
return new RegExp(re.source, safeFlags);
|
|
@@ -395,18 +317,12 @@ const DEFAULT_NODE_OPTIONS = {
|
|
|
395
317
|
bodyKeyDenylist: DEFAULT_BODY_KEY_DENYLIST
|
|
396
318
|
};
|
|
397
319
|
/**
|
|
398
|
-
* Node.js
|
|
399
|
-
*
|
|
400
|
-
* Subclasses core's `Flare` and wires the Node-only seams in its constructor:
|
|
401
|
-
* - `AsyncLocalStorageScopeProvider` so each `runWithContext(...)` callback gets its own `NodeScope`,
|
|
402
|
-
* isolated from concurrent requests.
|
|
403
|
-
* - `makeNodeContextCollector(...)` turns the current `NodeScope` + process info into report attributes.
|
|
404
|
-
* - `DiskFileReader` reads source for stack-trace snippets via `node:fs/promises`, not `fetch`.
|
|
405
|
-
* - `ProcessHandlerManager` attaches/detaches the fatal process listeners per `NodeOptions`.
|
|
320
|
+
* Node.js `Flare` singleton, exported from `@flareapp/node` as `flare`.
|
|
406
321
|
*
|
|
407
|
-
*
|
|
408
|
-
* `
|
|
409
|
-
*
|
|
322
|
+
* Subclasses core's `Flare` and wires the Node-only seams: per-request scope via
|
|
323
|
+
* `AsyncLocalStorageScopeProvider`, a Node context collector, `DiskFileReader` for stack snippets,
|
|
324
|
+
* and `ProcessHandlerManager` for the fatal listeners. Adds `configureNode`, `runWithContext`,
|
|
325
|
+
* `mergeContext`, `getContext`, and `removeProcessListeners` on top of the core API.
|
|
410
326
|
*/
|
|
411
327
|
var NodeFlare = class extends Flare$1 {
|
|
412
328
|
nodeOptions = { ...DEFAULT_NODE_OPTIONS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flareapp/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Node.js SDK for flareapp.io",
|
|
5
5
|
"homepage": "https://flareapp.io",
|
|
6
6
|
"bugs": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"release": "release-it"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@flareapp/core": "2.
|
|
47
|
+
"@flareapp/core": "2.12.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@flareapp/test-helpers": "*",
|