@flareapp/node 0.1.0 → 0.1.1
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 +834 -0
- package/dist/index.d.cts +138 -0
- package/dist/index.d.mts +138 -0
- package/dist/index.mjs +755 -0
- package/package.json +5 -2
- package/.oxlintrc.json +0 -7
- package/.release-it.json +0 -13
- package/CHANGELOG.md +0 -22
- package/src/Flare.ts +0 -224
- package/src/context/body.ts +0 -185
- package/src/context/collectNode.ts +0 -116
- package/src/context/headers.ts +0 -90
- package/src/context/process.ts +0 -25
- package/src/index.ts +0 -27
- package/src/process/fatal.ts +0 -54
- package/src/process/handlers.ts +0 -109
- package/src/scope/AsyncLocalStorageScopeProvider.ts +0 -86
- package/src/scope/NodeScope.ts +0 -8
- package/src/stacktrace/DiskFileReader.ts +0 -57
- package/src/types.ts +0 -37
- package/tests/asyncScopeProvider.test.ts +0 -43
- package/tests/body.test.ts +0 -129
- package/tests/diskFileReader.test.ts +0 -36
- package/tests/fatalHandlers.test.ts +0 -140
- package/tests/flush.test.ts +0 -11
- package/tests/headers.test.ts +0 -86
- package/tests/integration.test.ts +0 -63
- package/tests/lifecycle.test.ts +0 -71
- package/tests/nodeContextCollector.test.ts +0 -106
- package/tests/nodeExports.test.ts +0 -11
- package/tests/nodeScope.test.ts +0 -19
- package/tests/processAttributes.test.ts +0 -15
- package/tests/processHandlers.test.ts +0 -47
- package/tests/regexFlagSanitization.test.ts +0 -88
- package/tests/scopeIsolation.test.ts +0 -47
- package/tests/setFrameworkScope.test.ts +0 -86
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -18
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
24
|
+
value: mod,
|
|
25
|
+
enumerable: true
|
|
26
|
+
}) : target, mod));
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
let _flareapp_core = require("@flareapp/core");
|
|
30
|
+
let node_os = require("node:os");
|
|
31
|
+
node_os = __toESM(node_os);
|
|
32
|
+
let node_async_hooks = require("node:async_hooks");
|
|
33
|
+
let node_fs_promises = require("node:fs/promises");
|
|
34
|
+
let node_url = require("node:url");
|
|
35
|
+
|
|
36
|
+
//#region src/context/body.ts
|
|
37
|
+
/**
|
|
38
|
+
* Content types accepted by default for body capture. JSON and
|
|
39
|
+
* URL-encoded forms cover the vast majority of API payloads while keeping
|
|
40
|
+
* the parser surface tiny. `\b` after the second alternative prevents
|
|
41
|
+
* accidental matches like `application/x-www-form-urlencoded-foo` (artificial
|
|
42
|
+
* but cheap to defend against). The leading `^` plus `\b` lets us match
|
|
43
|
+
* either bare types or types with `; charset=utf-8` style suffixes.
|
|
44
|
+
*/
|
|
45
|
+
const DEFAULT_BODY_CONTENT_TYPES = /^application\/(json|x-www-form-urlencoded)\b/i;
|
|
46
|
+
/**
|
|
47
|
+
* Keys whose values get replaced with `[redacted]` during body redaction.
|
|
48
|
+
* Reuses core's URL denylist so credentials, tokens, etc are caught with the
|
|
49
|
+
* same regex everywhere (less surface for users to keep in sync).
|
|
50
|
+
*/
|
|
51
|
+
const DEFAULT_BODY_KEY_DENYLIST = _flareapp_core.DEFAULT_URL_DENYLIST;
|
|
52
|
+
/**
|
|
53
|
+
* Normalize, redact, serialize, and size-cap a request body for inclusion in
|
|
54
|
+
* a Flare report.
|
|
55
|
+
*
|
|
56
|
+
* Accepts four runtime shapes (whatever the user hands us via
|
|
57
|
+
* `runWithContext({ body, ... })`):
|
|
58
|
+
*
|
|
59
|
+
* - `string` — assumed to match the declared `contentType`. Must be JSON or
|
|
60
|
+
* form-encoded text per `bodyAllowedContentTypes`; otherwise dropped.
|
|
61
|
+
* - `Buffer` — decoded as UTF-8 then treated like a string.
|
|
62
|
+
* - `URLSearchParams` — flattened to a plain `Record<string, string>`. No
|
|
63
|
+
* content-type gate (the type is unambiguous from the shape).
|
|
64
|
+
* - Other `object` (POJO, array) — used as-is, no content-type gate. This is
|
|
65
|
+
* the common middleware path (Express's `req.body`, Fastify's, etc).
|
|
66
|
+
*
|
|
67
|
+
* Anything else (`number`, `boolean`, class instance, stream) returns `null`
|
|
68
|
+
* and the body is not reported.
|
|
69
|
+
*
|
|
70
|
+
* After parsing:
|
|
71
|
+
*
|
|
72
|
+
* 1. **Redact.** Walk the value, replacing any property whose key matches
|
|
73
|
+
* `bodyKeyDenylist` with `'[redacted]'`. Handles arrays, nested objects,
|
|
74
|
+
* and circular references (`WeakSet`-tracked, emits `'[Circular]'` on
|
|
75
|
+
* repeat sight).
|
|
76
|
+
* 2. **Stringify.** `JSON.stringify`; if it throws (BigInt, Symbol, etc),
|
|
77
|
+
* drop the body entirely.
|
|
78
|
+
* 3. **Truncate.** Cap at `bodyMaxBytes` UTF-8 bytes (the option's named
|
|
79
|
+
* semantic) INCLUDING the suffix. Truncation respects codepoint boundaries
|
|
80
|
+
* so the result decodes cleanly with no replacement characters.
|
|
81
|
+
*
|
|
82
|
+
* Returns the final JSON string, or `null` when the body should not be
|
|
83
|
+
* reported (unknown shape, content-type miss, serialization failure).
|
|
84
|
+
*/
|
|
85
|
+
function captureBody(body, contentType, opts) {
|
|
86
|
+
if (body === void 0 || body === null) return null;
|
|
87
|
+
let parsed;
|
|
88
|
+
if (typeof body === "string") {
|
|
89
|
+
if (!matchesContentType(contentType, opts.bodyAllowedContentTypes)) return null;
|
|
90
|
+
parsed = parseString(body, contentType);
|
|
91
|
+
if (parsed === void 0) return null;
|
|
92
|
+
} else if (Buffer.isBuffer(body)) {
|
|
93
|
+
if (!matchesContentType(contentType, opts.bodyAllowedContentTypes)) return null;
|
|
94
|
+
parsed = parseString(body.toString("utf8"), contentType);
|
|
95
|
+
if (parsed === void 0) return null;
|
|
96
|
+
} else if (body instanceof URLSearchParams) parsed = Object.fromEntries(body.entries());
|
|
97
|
+
else if (Array.isArray(body) || isPlainObject(body)) parsed = body;
|
|
98
|
+
else return null;
|
|
99
|
+
const redacted = redact(parsed, opts.bodyKeyDenylist);
|
|
100
|
+
let serialized;
|
|
101
|
+
try {
|
|
102
|
+
serialized = JSON.stringify(redacted);
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return truncateToByteLimit(serialized, opts.bodyMaxBytes);
|
|
107
|
+
}
|
|
108
|
+
const TRUNCATION_SUFFIX = "…[truncated]";
|
|
109
|
+
const TRUNCATION_SUFFIX_BYTES = Buffer.byteLength(TRUNCATION_SUFFIX, "utf8");
|
|
110
|
+
/**
|
|
111
|
+
* Truncate a serialized string so the resulting UTF-8 byte length never
|
|
112
|
+
* exceeds `maxBytes`, including the appended truncation suffix.
|
|
113
|
+
*
|
|
114
|
+
* Walks backwards from the budget index while the byte at that position is a
|
|
115
|
+
* UTF-8 continuation byte (`10xxxxxx`), stopping at the first byte that
|
|
116
|
+
* starts a new codepoint. Slicing at that index leaves a buffer that decodes
|
|
117
|
+
* cleanly with no replacement characters.
|
|
118
|
+
*/
|
|
119
|
+
function truncateToByteLimit(serialized, maxBytes) {
|
|
120
|
+
const buf = Buffer.from(serialized, "utf8");
|
|
121
|
+
if (buf.length <= maxBytes) return serialized;
|
|
122
|
+
if (maxBytes <= TRUNCATION_SUFFIX_BYTES) {
|
|
123
|
+
const suffixBuf = Buffer.from(TRUNCATION_SUFFIX, "utf8");
|
|
124
|
+
let cut = maxBytes;
|
|
125
|
+
while (cut > 0 && (suffixBuf[cut] & 192) === 128) cut--;
|
|
126
|
+
return suffixBuf.subarray(0, cut).toString("utf8");
|
|
127
|
+
}
|
|
128
|
+
let cut = maxBytes - TRUNCATION_SUFFIX_BYTES;
|
|
129
|
+
while (cut > 0 && (buf[cut] & 192) === 128) cut--;
|
|
130
|
+
return buf.subarray(0, cut).toString("utf8") + TRUNCATION_SUFFIX;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check whether a `content-type` header is on the allowlist. Normalizes to the
|
|
134
|
+
* bare media type first: strips any parameters (`; charset=utf-8`), trims, and
|
|
135
|
+
* lowercases, so the regex is tested against `application/json` rather than the
|
|
136
|
+
* full header. This lets a strict custom regex like `/^application\/json$/`
|
|
137
|
+
* still match `application/json; charset=utf-8`. Empty/missing is a hard miss.
|
|
138
|
+
*/
|
|
139
|
+
function matchesContentType(ct, allowed) {
|
|
140
|
+
if (!ct) return false;
|
|
141
|
+
const mediaType = ct.split(";")[0].trim().toLowerCase();
|
|
142
|
+
if (!mediaType) return false;
|
|
143
|
+
return allowed.test(mediaType);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Parse a serialized body string into a JS value, branching on the declared
|
|
147
|
+
* content type.
|
|
148
|
+
*
|
|
149
|
+
* - URL-encoded forms become a flat object so the same `redact` walker works.
|
|
150
|
+
* - Otherwise treat as JSON. Returns `undefined` (NOT `null`, which is a
|
|
151
|
+
* legitimate JSON value) when parsing fails, so the caller can distinguish
|
|
152
|
+
* "couldn't parse" from "parsed to literal null".
|
|
153
|
+
*/
|
|
154
|
+
function parseString(text, contentType) {
|
|
155
|
+
if (contentType && /x-www-form-urlencoded/i.test(contentType)) return Object.fromEntries(new URLSearchParams(text).entries());
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(text);
|
|
158
|
+
} catch {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* True only for `Object.create(null)` or `{}`-shaped values. Excludes class
|
|
164
|
+
* instances (their prototype chain points somewhere other than Object.prototype
|
|
165
|
+
* or null), streams, FormData, ArrayBuffer views, Buffer, URLSearchParams,
|
|
166
|
+
* and other built-ins that happen to be `typeof === 'object'`.
|
|
167
|
+
*/
|
|
168
|
+
function isPlainObject(value) {
|
|
169
|
+
if (value === null || typeof value !== "object") return false;
|
|
170
|
+
const proto = Object.getPrototypeOf(value);
|
|
171
|
+
return proto === null || proto === Object.prototype;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Recursively walk `value`, replacing values for denylisted keys with
|
|
175
|
+
* `'[redacted]'` and substituting `'[Circular]'` for any object visited more
|
|
176
|
+
* than once.
|
|
177
|
+
*
|
|
178
|
+
* `seen` is a `WeakSet` of already-visited objects. Carried as a parameter
|
|
179
|
+
* (rather than a closure variable) so the same recursive call can pass it
|
|
180
|
+
* down without per-call allocation.
|
|
181
|
+
*
|
|
182
|
+
* Primitives and `null` pass through unchanged. Arrays preserve order;
|
|
183
|
+
* objects preserve keys.
|
|
184
|
+
*/
|
|
185
|
+
function redact(value, denylist, seen = /* @__PURE__ */ new WeakSet()) {
|
|
186
|
+
if (value === null || typeof value !== "object") return value;
|
|
187
|
+
if (seen.has(value)) return "[Circular]";
|
|
188
|
+
seen.add(value);
|
|
189
|
+
if (Array.isArray(value)) return value.map((v) => redact(v, denylist, seen));
|
|
190
|
+
const out = {};
|
|
191
|
+
for (const [k, v] of Object.entries(value)) out[k] = denylist.test(k) ? "[redacted]" : redact(v, denylist, seen);
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/context/headers.ts
|
|
197
|
+
/**
|
|
198
|
+
* Case-insensitively look up a header value. Returns the first defined value
|
|
199
|
+
* for the lowercased name, or undefined. Array values (rare but valid for
|
|
200
|
+
* some headers) are coalesced to the first element since the consumers in
|
|
201
|
+
* this package treat the value as scalar.
|
|
202
|
+
*/
|
|
203
|
+
function findHeader(headers, name) {
|
|
204
|
+
if (!headers) return void 0;
|
|
205
|
+
const target = name.toLowerCase();
|
|
206
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
207
|
+
if (key.toLowerCase() !== target) continue;
|
|
208
|
+
if (value === void 0) continue;
|
|
209
|
+
return Array.isArray(value) ? value[0] : value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Default-redacted header names. The pattern is anchored to the FULL header
|
|
214
|
+
* name (`^...$`) and case-insensitive so it catches `Authorization`,
|
|
215
|
+
* `AUTHORIZATION`, `authorization`, etc. Anchoring matters: an unanchored
|
|
216
|
+
* `cookie` would match `X-Some-Cookie-Hint` too, which we do NOT want — only
|
|
217
|
+
* the exact header by name should be redacted by default.
|
|
218
|
+
*
|
|
219
|
+
* Covers the usual credential carriers (`authorization`, `cookie`, etc) plus
|
|
220
|
+
* common proxy-set headers that often expose client IPs (`forwarded`,
|
|
221
|
+
* `x-forwarded-for`, `x-forwarded-user`). Users add domain-specific entries
|
|
222
|
+
* via `configureNode({ headerDenylist: ... })`.
|
|
223
|
+
*/
|
|
224
|
+
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;
|
|
225
|
+
/**
|
|
226
|
+
* Combine the built-in denylist with an optional user-supplied one.
|
|
227
|
+
*
|
|
228
|
+
* - No custom regex -> use the default as-is.
|
|
229
|
+
* - Custom + replace = true -> use only the custom pattern (with `g`/`y`
|
|
230
|
+
* flags stripped so `.test()` stays stateless).
|
|
231
|
+
* - Custom + replace = false -> union: `(?:default)|(?:custom)`, forcing case
|
|
232
|
+
* insensitivity since header names are
|
|
233
|
+
* case-insensitive over the wire.
|
|
234
|
+
*/
|
|
235
|
+
function resolveHeaderDenylist(custom, replaceDefault = false) {
|
|
236
|
+
if (!custom) return DEFAULT_HEADER_DENYLIST;
|
|
237
|
+
if (replaceDefault) return new RegExp(custom.source, custom.flags.replace(/[gy]/g, ""));
|
|
238
|
+
return new RegExp(`(?:${DEFAULT_HEADER_DENYLIST.source})|(?:${custom.source})`, "i");
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Project an HTTP request `headers` object into report attributes.
|
|
242
|
+
*
|
|
243
|
+
* Behavior per header:
|
|
244
|
+
*
|
|
245
|
+
* - **Unset values** (entry exists but the value is `undefined`) are dropped
|
|
246
|
+
* entirely — `node:http` represents "header was not sent" this way.
|
|
247
|
+
* - **Names are lowercased.** OTel's attribute convention uses lowercase
|
|
248
|
+
* header keys, and HTTP header names are case-insensitive anyway.
|
|
249
|
+
* - **Allowlist gate.** If `headerAllowlist` is set, only headers whose
|
|
250
|
+
* lowercased name matches are emitted; everything else is silently dropped
|
|
251
|
+
* (NOT redacted, dropped). This is the strongest filter — useful for
|
|
252
|
+
* compliance scenarios where you must opt into headers explicitly.
|
|
253
|
+
* - **Array values** (`set-cookie` can be `string[]`) are joined with `, ` so
|
|
254
|
+
* the emitted value is a flat string, matching the on-the-wire shape that
|
|
255
|
+
* most HTTP clients render.
|
|
256
|
+
* - **Denylist redaction.** If the name matches `headerDenylist`, the value
|
|
257
|
+
* is replaced with `'[redacted]'` (the key still appears so consumers can
|
|
258
|
+
* tell the header was present).
|
|
259
|
+
*
|
|
260
|
+
* Output keys are `http.request.header.<lowercased-name>`, per OTel.
|
|
261
|
+
*/
|
|
262
|
+
function projectHeaders(headers, options) {
|
|
263
|
+
const out = {};
|
|
264
|
+
if (!headers) return out;
|
|
265
|
+
for (const [rawName, rawValue] of Object.entries(headers)) {
|
|
266
|
+
if (rawValue === void 0) continue;
|
|
267
|
+
const name = rawName.toLowerCase();
|
|
268
|
+
if (options.headerAllowlist && !options.headerAllowlist.test(name)) continue;
|
|
269
|
+
const value = Array.isArray(rawValue) ? rawValue.join(", ") : rawValue;
|
|
270
|
+
out[`http.request.header.${name}`] = options.headerDenylist.test(name) ? "[redacted]" : value;
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/context/process.ts
|
|
277
|
+
/**
|
|
278
|
+
* Snapshot the Node runtime + host environment at report time and project
|
|
279
|
+
* into OTel-style attribute keys. Cheap (just property reads + a couple of
|
|
280
|
+
* syscalls via `os`), so called per-report rather than cached; this keeps
|
|
281
|
+
* `process.uptime` honest and follows the value of `os.hostname()` if it
|
|
282
|
+
* changes mid-run (unlikely but free correctness).
|
|
283
|
+
*
|
|
284
|
+
* Keys are stable OTel resource attributes; the Flare backend recognizes them.
|
|
285
|
+
*/
|
|
286
|
+
function collectProcessAttributes() {
|
|
287
|
+
return {
|
|
288
|
+
"process.runtime.name": "nodejs",
|
|
289
|
+
"process.runtime.version": process.version,
|
|
290
|
+
"process.pid": process.pid,
|
|
291
|
+
"process.uptime": process.uptime(),
|
|
292
|
+
"host.name": node_os.default.hostname(),
|
|
293
|
+
"host.arch": process.arch,
|
|
294
|
+
"os.type": node_os.default.type(),
|
|
295
|
+
"os.version": node_os.default.release()
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/context/collectNode.ts
|
|
301
|
+
/**
|
|
302
|
+
* Build the Node-side `ContextCollector` that core's `Flare` calls on every
|
|
303
|
+
* report. The returned function projects two sources into OTel-style report
|
|
304
|
+
* attributes:
|
|
305
|
+
*
|
|
306
|
+
* 1. **Process info** — runtime version, pid, hostname, etc. Always present.
|
|
307
|
+
* 2. **Active request scope** — method, path/url (with query-string keys
|
|
308
|
+
* redacted), headers (with the denylist applied), optional body, and
|
|
309
|
+
* authenticated user. Present when `runWithContext(...)` is active;
|
|
310
|
+
* falls back to the shared scope otherwise (no request attrs emitted then).
|
|
311
|
+
*
|
|
312
|
+
* Both `provider` and `getOptions` are passed in (not captured by reference to
|
|
313
|
+
* concrete instances) so the closure stays decoupled from `NodeFlare`'s
|
|
314
|
+
* internals. `getOptions` is a getter (not a value) so that `configureNode(...)`
|
|
315
|
+
* changes are visible on subsequent reports without rebuilding the collector.
|
|
316
|
+
*
|
|
317
|
+
* The function returned matches `ContextCollector = (config) => Attributes`,
|
|
318
|
+
* which is core's interface for `Flare`'s third constructor parameter.
|
|
319
|
+
*/
|
|
320
|
+
function makeNodeContextCollector(provider, getOptions) {
|
|
321
|
+
return (config) => {
|
|
322
|
+
const attrs = {
|
|
323
|
+
"flare.entry_point.type": "server",
|
|
324
|
+
...collectProcessAttributes()
|
|
325
|
+
};
|
|
326
|
+
const scope = provider.active();
|
|
327
|
+
const { request } = scope;
|
|
328
|
+
if (request.method) attrs["http.request.method"] = request.method;
|
|
329
|
+
if (request.path) {
|
|
330
|
+
const queryStart = request.path.indexOf("?");
|
|
331
|
+
if (queryStart === -1) attrs["url.path"] = request.path;
|
|
332
|
+
else {
|
|
333
|
+
attrs["url.path"] = request.path.slice(0, queryStart);
|
|
334
|
+
const redactedQuery = (0, _flareapp_core.redactUrlQuery)(request.path, config.urlDenylist);
|
|
335
|
+
const redactedQueryStart = redactedQuery.indexOf("?");
|
|
336
|
+
attrs["url.query"] = redactedQuery.slice(redactedQueryStart + 1);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (request.url) attrs["url.full"] = (0, _flareapp_core.redactUrlQuery)(request.url, config.urlDenylist);
|
|
340
|
+
const opts = getOptions();
|
|
341
|
+
Object.assign(attrs, projectHeaders(request.headers, opts));
|
|
342
|
+
if (opts.captureRequestBody) {
|
|
343
|
+
const contentType = findHeader(request.headers, "content-type");
|
|
344
|
+
const body = captureBody(request.body, contentType, opts);
|
|
345
|
+
if (body !== null) attrs["http.request.body"] = body;
|
|
346
|
+
}
|
|
347
|
+
if (scope.user) {
|
|
348
|
+
if (scope.user.id !== void 0) attrs["enduser.id"] = String(scope.user.id);
|
|
349
|
+
if (scope.user.email !== void 0) attrs["enduser.email"] = scope.user.email;
|
|
350
|
+
if (scope.user.username !== void 0) attrs["enduser.username"] = scope.user.username;
|
|
351
|
+
if (scope.user.ipAddress !== void 0) attrs["client.address"] = scope.user.ipAddress;
|
|
352
|
+
}
|
|
353
|
+
return attrs;
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/process/fatal.ts
|
|
359
|
+
function buildFatalCallbacks(flare, getOpts, exit = process.exit.bind(process)) {
|
|
360
|
+
return {
|
|
361
|
+
async onUncaught(err, origin) {
|
|
362
|
+
const opts = getOpts();
|
|
363
|
+
if (opts.uncaughtExceptionMode === "report-and-exit") process.exitCode = 1;
|
|
364
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
365
|
+
try {
|
|
366
|
+
await flare.report(error, { "process.uncaught_exception.origin": origin });
|
|
367
|
+
} catch {}
|
|
368
|
+
if (opts.uncaughtExceptionMode === "report-and-exit") {
|
|
369
|
+
await flare.flush(opts.shutdownTimeoutMs);
|
|
370
|
+
exit(1);
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
async onRejection(reason) {
|
|
374
|
+
const opts = getOpts();
|
|
375
|
+
if (opts.unhandledRejectionMode === "report-and-exit") process.exitCode = 1;
|
|
376
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
377
|
+
try {
|
|
378
|
+
await flare.report(error);
|
|
379
|
+
} catch {}
|
|
380
|
+
if (opts.unhandledRejectionMode === "report-and-exit") {
|
|
381
|
+
await flare.flush(opts.shutdownTimeoutMs);
|
|
382
|
+
exit(1);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/process/handlers.ts
|
|
390
|
+
/**
|
|
391
|
+
* Owns the lifecycle of the two process-level error listeners that capture
|
|
392
|
+
* fatal failures and feed them to Flare:
|
|
393
|
+
*
|
|
394
|
+
* - `process.on('uncaughtException', ...)`
|
|
395
|
+
* - `process.on('unhandledRejection', ...)`
|
|
396
|
+
*
|
|
397
|
+
* The manager has two responsibilities:
|
|
398
|
+
*
|
|
399
|
+
* 1. **Reconcile listener state with intent.** Given the current `FatalMode`
|
|
400
|
+
* for each event (`'off' | 'report' | 'report-and-exit'`), make the actual
|
|
401
|
+
* listener attachment match: attach when it should be attached but isn't,
|
|
402
|
+
* detach when it shouldn't be attached but is, no-op when already in the
|
|
403
|
+
* desired state. This is idempotent — calling `reconcile(...)` repeatedly
|
|
404
|
+
* with the same options is safe.
|
|
405
|
+
* 2. **Tear down on demand.** `detach()` removes both listeners regardless of
|
|
406
|
+
* intent, for tests and graceful shutdown.
|
|
407
|
+
*
|
|
408
|
+
* Why keep this separate from `NodeFlare`: the attach/detach logic is purely
|
|
409
|
+
* about Node `process` events and contains no Flare semantics. Isolating it
|
|
410
|
+
* makes it trivial to test (the test suite drives `reconcile()` directly with
|
|
411
|
+
* stub callbacks and asserts on `process.listeners(...)`) and keeps
|
|
412
|
+
* `NodeFlare` focused on report assembly + user-facing API.
|
|
413
|
+
*/
|
|
414
|
+
var ProcessHandlerManager = class {
|
|
415
|
+
/** The currently-attached listener for `uncaughtException`, or `null`. */
|
|
416
|
+
uncaughtHandler = null;
|
|
417
|
+
/** The currently-attached listener for `unhandledRejection`, or `null`. */
|
|
418
|
+
rejectionHandler = null;
|
|
419
|
+
constructor(cbs) {
|
|
420
|
+
this.cbs = cbs;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Bring the attached listeners into agreement with the supplied modes.
|
|
424
|
+
* Idempotent: when current state already matches intent, this is a no-op.
|
|
425
|
+
*/
|
|
426
|
+
reconcile(opts) {
|
|
427
|
+
this.reconcileOne("uncaughtException", opts.uncaughtExceptionMode, () => this.uncaughtHandler, (h) => {
|
|
428
|
+
this.uncaughtHandler = h;
|
|
429
|
+
}, (err, origin) => this.cbs.onUncaught(err, origin));
|
|
430
|
+
this.reconcileOne("unhandledRejection", opts.unhandledRejectionMode, () => this.rejectionHandler, (h) => {
|
|
431
|
+
this.rejectionHandler = h;
|
|
432
|
+
}, (reason) => this.cbs.onRejection(reason));
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Remove both listeners regardless of current intent. Used by tests and by
|
|
436
|
+
* `NodeFlare.removeProcessListeners()`. Safe to call when nothing is
|
|
437
|
+
* attached.
|
|
438
|
+
*/
|
|
439
|
+
detach() {
|
|
440
|
+
if (this.uncaughtHandler) {
|
|
441
|
+
process.off("uncaughtException", this.uncaughtHandler);
|
|
442
|
+
this.uncaughtHandler = null;
|
|
443
|
+
}
|
|
444
|
+
if (this.rejectionHandler) {
|
|
445
|
+
process.off("unhandledRejection", this.rejectionHandler);
|
|
446
|
+
this.rejectionHandler = null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Generic attach/detach for one event. The `get`/`set` closures let us
|
|
451
|
+
* share this body between the two events while still mutating distinct
|
|
452
|
+
* fields (`uncaughtHandler` vs `rejectionHandler`).
|
|
453
|
+
*
|
|
454
|
+
* Truth table:
|
|
455
|
+
* - intent off, currently attached -> detach
|
|
456
|
+
* - intent off, not attached -> no-op
|
|
457
|
+
* - intent on, currently attached -> no-op (already correct)
|
|
458
|
+
* - intent on, not attached -> attach
|
|
459
|
+
*/
|
|
460
|
+
reconcileOne(event, mode, get, set, impl) {
|
|
461
|
+
const current = get();
|
|
462
|
+
const wants = mode !== "off";
|
|
463
|
+
if (wants && !current) {
|
|
464
|
+
set(impl);
|
|
465
|
+
process.on(event, impl);
|
|
466
|
+
} else if (!wants && current) {
|
|
467
|
+
process.off(event, current);
|
|
468
|
+
set(null);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/scope/NodeScope.ts
|
|
475
|
+
var NodeScope = class extends _flareapp_core.Scope {
|
|
476
|
+
request = {};
|
|
477
|
+
user = null;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/scope/AsyncLocalStorageScopeProvider.ts
|
|
482
|
+
/**
|
|
483
|
+
* `ScopeProvider` implementation that gives every in-flight request its own
|
|
484
|
+
* `NodeScope`, isolated from concurrent requests.
|
|
485
|
+
*
|
|
486
|
+
* Built on Node's `node:async_hooks#AsyncLocalStorage`: when code runs inside
|
|
487
|
+
* `als.run(scope, fn)`, every `als.getStore()` call from within `fn` (and any
|
|
488
|
+
* async work `fn` awaits, including timers, promises, `process.nextTick`, etc)
|
|
489
|
+
* returns that `scope`. Outside any `als.run` call, `getStore()` returns
|
|
490
|
+
* `undefined`. This is the same primitive that lets observability libraries
|
|
491
|
+
* propagate trace context across async boundaries without manual plumbing.
|
|
492
|
+
*
|
|
493
|
+
* Two "kinds of read" surfaced separately:
|
|
494
|
+
*
|
|
495
|
+
* - `active()` — never returns null. The internal read used by `Flare` for
|
|
496
|
+
* every glow, attribute set, and report. When called inside `runWithContext`,
|
|
497
|
+
* returns the per-request `NodeScope`. Outside, returns a shared `fallback`
|
|
498
|
+
* scope so glows/attributes/reports issued outside any request still have
|
|
499
|
+
* somewhere to land (process-level reports, startup errors, scheduled jobs).
|
|
500
|
+
* - `getContext()` — public debug helper. Returns `null` outside any
|
|
501
|
+
* `runWithContext`, so consumers can distinguish "I am inside a request" from
|
|
502
|
+
* "I am not". The fallback is intentionally NOT exposed here.
|
|
503
|
+
*
|
|
504
|
+
* The fallback is also a per-instance `NodeScope` so that writes from outside
|
|
505
|
+
* a request scope persist for subsequent outside-scope reports.
|
|
506
|
+
*/
|
|
507
|
+
var AsyncLocalStorageScopeProvider = class {
|
|
508
|
+
als = new node_async_hooks.AsyncLocalStorage();
|
|
509
|
+
fallback = new NodeScope();
|
|
510
|
+
/**
|
|
511
|
+
* Internal: returns the per-request scope when inside `runWithContext`,
|
|
512
|
+
* or the shared fallback otherwise. Always returns a real `NodeScope`.
|
|
513
|
+
*/
|
|
514
|
+
active() {
|
|
515
|
+
return this.als.getStore() ?? this.fallback;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Public: returns the per-request scope when inside `runWithContext`, or
|
|
519
|
+
* `null` otherwise. Useful for assertions like "am I in a request?".
|
|
520
|
+
*/
|
|
521
|
+
getContext() {
|
|
522
|
+
return this.als.getStore() ?? null;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Open a fresh request scope around `fn` and run it. Every async hop
|
|
526
|
+
* inside `fn` (awaits, timers, promise chains) sees the same scope via
|
|
527
|
+
* `active()`/`getContext()`; concurrent calls each get their own.
|
|
528
|
+
*
|
|
529
|
+
* `request` is shallow-cloned so later edits to the caller's object do not
|
|
530
|
+
* leak into the stored scope.
|
|
531
|
+
*/
|
|
532
|
+
runWithContext(request, fn) {
|
|
533
|
+
const scope = new NodeScope();
|
|
534
|
+
scope.request = { ...request };
|
|
535
|
+
return this.als.run(scope, fn);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Patch the current scope's `request` shape. When called inside
|
|
539
|
+
* `runWithContext`, the patch is visible to all subsequent reads from
|
|
540
|
+
* within the same request chain. When called outside, the patch lands on
|
|
541
|
+
* the fallback scope.
|
|
542
|
+
*/
|
|
543
|
+
mergeContext(partial) {
|
|
544
|
+
const scope = this.als.getStore() ?? this.fallback;
|
|
545
|
+
scope.request = {
|
|
546
|
+
...scope.request,
|
|
547
|
+
...partial
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Set the authenticated user on the current scope. Same in-scope vs
|
|
552
|
+
* fallback semantics as `mergeContext`.
|
|
553
|
+
*/
|
|
554
|
+
setUser(user) {
|
|
555
|
+
const scope = this.als.getStore() ?? this.fallback;
|
|
556
|
+
scope.user = user;
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/stacktrace/DiskFileReader.ts
|
|
562
|
+
/**
|
|
563
|
+
* Node `FileReader` implementation that reads source files from disk.
|
|
564
|
+
*
|
|
565
|
+
* Wired into `@flareapp/node`'s singleton so the stack-trace builder can pull
|
|
566
|
+
* source for each frame and render a snippet. On the server the frame's "URL"
|
|
567
|
+
* is usually a local path (e.g. `/app/dist/server.js`) or a `file://` URL
|
|
568
|
+
* (from `import.meta.url`), so we resolve straight off disk instead of going
|
|
569
|
+
* over the network.
|
|
570
|
+
*
|
|
571
|
+
* Safety gates:
|
|
572
|
+
*
|
|
573
|
+
* 1. **Local-path allowlist.** Only `file://` URLs and absolute filesystem
|
|
574
|
+
* paths (POSIX `/foo`, Windows `C:\foo` or `\\server\share\foo`) are
|
|
575
|
+
* accepted. HTTP URLs and relative paths return `null` immediately. We
|
|
576
|
+
* refuse to read anything that does not unambiguously identify a local
|
|
577
|
+
* file — no surprise traversal, no following http stack frames in a
|
|
578
|
+
* server build, no relative-path ambiguity around the current working
|
|
579
|
+
* directory.
|
|
580
|
+
* 2. **Catch-all.** Missing files, permission errors, and any other failure
|
|
581
|
+
* return `null`. The `read()` contract returns `null` on every failure
|
|
582
|
+
* path and never throws.
|
|
583
|
+
*
|
|
584
|
+
* `fileURLToPath` is used when the input is a `file://` URL so we hand
|
|
585
|
+
* `readFile` a real OS path. Otherwise the URL IS already a path and is
|
|
586
|
+
* passed through unchanged.
|
|
587
|
+
*/
|
|
588
|
+
var DiskFileReader = class {
|
|
589
|
+
async read(url) {
|
|
590
|
+
if (!isLocalFileUrl(url)) return null;
|
|
591
|
+
try {
|
|
592
|
+
return await (0, node_fs_promises.readFile)(/^file:\/\//i.test(url) ? (0, node_url.fileURLToPath)(url) : url, "utf-8");
|
|
593
|
+
} catch {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
/**
|
|
599
|
+
* Return true when `url` is something we are willing to treat as a local
|
|
600
|
+
* file. Matches four shapes:
|
|
601
|
+
*
|
|
602
|
+
* - `file://...` URLs (any casing of the scheme)
|
|
603
|
+
* - POSIX absolute paths starting with `/`
|
|
604
|
+
* - Windows drive-letter paths like `C:\foo` or `c:/foo`
|
|
605
|
+
* - Windows UNC paths starting with `\\`
|
|
606
|
+
*
|
|
607
|
+
* Anything else (relative paths, http, data, blob, etc) is rejected.
|
|
608
|
+
*/
|
|
609
|
+
function isLocalFileUrl(url) {
|
|
610
|
+
return /^file:\/\//i.test(url) || url.startsWith("/") || /^[a-z]:[\\/]/i.test(url) || url.startsWith("\\\\");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/Flare.ts
|
|
615
|
+
const NODE_SDK_NAME = "@flareapp/node";
|
|
616
|
+
const NODE_SDK_VERSION = typeof process !== "undefined" && true ? "0.1.1" : "?";
|
|
617
|
+
/**
|
|
618
|
+
* Strip the `g` and `y` flags from a user-supplied regex.
|
|
619
|
+
*
|
|
620
|
+
* `RegExp.prototype.test()` and `.exec()` keep `lastIndex` state when either of
|
|
621
|
+
* these flags is set, which means reusing the same regex across many keys (as
|
|
622
|
+
* the header denylist and body redaction do) silently skips matches after the
|
|
623
|
+
* first hit. Reconstructing the regex without those flags gives stateless
|
|
624
|
+
* matching while preserving everything else (`i`, `m`, `s`, `u`, source).
|
|
625
|
+
*/
|
|
626
|
+
function sanitizeRegex(re) {
|
|
627
|
+
const safeFlags = re.flags.replace(/[gy]/g, "");
|
|
628
|
+
return new RegExp(re.source, safeFlags);
|
|
629
|
+
}
|
|
630
|
+
const DEFAULT_NODE_OPTIONS = {
|
|
631
|
+
uncaughtExceptionMode: "report-and-exit",
|
|
632
|
+
unhandledRejectionMode: "report-and-exit",
|
|
633
|
+
shutdownTimeoutMs: 2e3,
|
|
634
|
+
headerDenylist: DEFAULT_HEADER_DENYLIST,
|
|
635
|
+
headerAllowlist: null,
|
|
636
|
+
replaceDefaultHeaderDenylist: false,
|
|
637
|
+
captureRequestBody: false,
|
|
638
|
+
bodyMaxBytes: 16384,
|
|
639
|
+
bodyAllowedContentTypes: DEFAULT_BODY_CONTENT_TYPES,
|
|
640
|
+
bodyKeyDenylist: DEFAULT_BODY_KEY_DENYLIST
|
|
641
|
+
};
|
|
642
|
+
/**
|
|
643
|
+
* Node.js-specific `Flare` singleton, exposed from `@flareapp/node` as `flare`.
|
|
644
|
+
*
|
|
645
|
+
* Subclasses core's `Flare` and wires the Node-only seams in its constructor:
|
|
646
|
+
*
|
|
647
|
+
* - `AsyncLocalStorageScopeProvider` so each `runWithContext(...)` callback
|
|
648
|
+
* gets its own `NodeScope` (glows, attributes, user, entry-point, request),
|
|
649
|
+
* isolated from concurrent requests.
|
|
650
|
+
* - `makeNodeContextCollector(...)` to project the current `NodeScope` and
|
|
651
|
+
* process info into report attributes (http.request.*, url.path, etc).
|
|
652
|
+
* - `DiskFileReader` to read source files for stack-trace snippets via
|
|
653
|
+
* `node:fs/promises` instead of the browser's `fetch`.
|
|
654
|
+
* - `ProcessHandlerManager` to attach/detach `uncaughtException` and
|
|
655
|
+
* `unhandledRejection` listeners based on the current `NodeOptions`.
|
|
656
|
+
*
|
|
657
|
+
* Also adds Node-only API surface on top of core: `configureNode(...)`,
|
|
658
|
+
* `runWithContext(...)`, `mergeContext(...)`, `setUser(...)`, `getContext()`,
|
|
659
|
+
* `removeProcessListeners()`. Inherited core methods (`light`, `configure`,
|
|
660
|
+
* `addContext`, `glow`, etc.) return `this`, so chaining keeps the
|
|
661
|
+
* `NodeFlare` type and `configureNode(...)` stays callable mid-chain.
|
|
662
|
+
*/
|
|
663
|
+
var NodeFlare = class extends _flareapp_core.Flare {
|
|
664
|
+
nodeOptions = { ...DEFAULT_NODE_OPTIONS };
|
|
665
|
+
isLit = false;
|
|
666
|
+
nodeScopeProvider;
|
|
667
|
+
handlerManager;
|
|
668
|
+
constructor() {
|
|
669
|
+
const scopeProvider = new AsyncLocalStorageScopeProvider();
|
|
670
|
+
const collector = makeNodeContextCollector(scopeProvider, () => this.nodeOptions);
|
|
671
|
+
super(new _flareapp_core.Api(), collector, new DiskFileReader(), scopeProvider);
|
|
672
|
+
this.nodeScopeProvider = scopeProvider;
|
|
673
|
+
this.setSdkInfo({
|
|
674
|
+
name: NODE_SDK_NAME,
|
|
675
|
+
version: NODE_SDK_VERSION
|
|
676
|
+
});
|
|
677
|
+
this.handlerManager = new ProcessHandlerManager(buildFatalCallbacks(this, () => this.nodeOptions));
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Set the API key (and optional debug flag), then reconcile process
|
|
681
|
+
* listeners with the current `nodeOptions`. Reconcile runs on EVERY call,
|
|
682
|
+
* not just the first, so `light()` is the right escape hatch to re-attach
|
|
683
|
+
* after `removeProcessListeners()`.
|
|
684
|
+
*/
|
|
685
|
+
light(key, debug) {
|
|
686
|
+
super.light(key, debug);
|
|
687
|
+
this.isLit = true;
|
|
688
|
+
this.handlerManager.reconcile(this.nodeOptions);
|
|
689
|
+
return this;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Merge Node-only options (fatal-handler modes, header/body redaction
|
|
693
|
+
* config, shutdown timeout) into the active configuration. Safe to call
|
|
694
|
+
* before or after `light()`:
|
|
695
|
+
*
|
|
696
|
+
* - Before `light()`: options are stored; listeners are attached when
|
|
697
|
+
* `light()` runs.
|
|
698
|
+
* - After `light()`: options are stored AND listeners are reconciled
|
|
699
|
+
* immediately, so flipping a mode to `'off'` detaches the handler and
|
|
700
|
+
* flipping it back to `'report'`/`'report-and-exit'` re-attaches.
|
|
701
|
+
*
|
|
702
|
+
* Regex options (`headerAllowlist`, `bodyAllowedContentTypes`,
|
|
703
|
+
* `bodyKeyDenylist`) are passed through `sanitizeRegex` to strip stateful
|
|
704
|
+
* `g`/`y` flags; without that, `RegExp.prototype.test` would skip matches
|
|
705
|
+
* across keys.
|
|
706
|
+
*/
|
|
707
|
+
configureNode(partial) {
|
|
708
|
+
if (partial.headerDenylist !== void 0 || partial.replaceDefaultHeaderDenylist !== void 0) {
|
|
709
|
+
this.nodeOptions.headerDenylist = resolveHeaderDenylist(partial.headerDenylist ?? void 0, partial.replaceDefaultHeaderDenylist ?? this.nodeOptions.replaceDefaultHeaderDenylist);
|
|
710
|
+
this.nodeOptions.replaceDefaultHeaderDenylist = partial.replaceDefaultHeaderDenylist ?? this.nodeOptions.replaceDefaultHeaderDenylist;
|
|
711
|
+
}
|
|
712
|
+
if (partial.headerAllowlist !== void 0) this.nodeOptions.headerAllowlist = partial.headerAllowlist === null ? null : sanitizeRegex(partial.headerAllowlist);
|
|
713
|
+
if (partial.uncaughtExceptionMode !== void 0) this.nodeOptions.uncaughtExceptionMode = partial.uncaughtExceptionMode;
|
|
714
|
+
if (partial.unhandledRejectionMode !== void 0) this.nodeOptions.unhandledRejectionMode = partial.unhandledRejectionMode;
|
|
715
|
+
if (partial.shutdownTimeoutMs !== void 0) this.nodeOptions.shutdownTimeoutMs = partial.shutdownTimeoutMs;
|
|
716
|
+
if (partial.captureRequestBody !== void 0) this.nodeOptions.captureRequestBody = partial.captureRequestBody;
|
|
717
|
+
if (partial.bodyMaxBytes !== void 0) this.nodeOptions.bodyMaxBytes = partial.bodyMaxBytes;
|
|
718
|
+
if (partial.bodyAllowedContentTypes !== void 0) this.nodeOptions.bodyAllowedContentTypes = sanitizeRegex(partial.bodyAllowedContentTypes);
|
|
719
|
+
if (partial.bodyKeyDenylist !== void 0) this.nodeOptions.bodyKeyDenylist = sanitizeRegex(partial.bodyKeyDenylist);
|
|
720
|
+
if (this.isLit) this.handlerManager.reconcile(this.nodeOptions);
|
|
721
|
+
return this;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Run `fn` inside a fresh `NodeScope` carrying the supplied request
|
|
725
|
+
* metadata. Inside `fn` (and any async work it awaits), `flare.glow(...)`,
|
|
726
|
+
* `flare.addContext(...)`, `flare.setUser(...)`, and `flare.report(...)`
|
|
727
|
+
* see a scope that is isolated from other concurrent requests.
|
|
728
|
+
*
|
|
729
|
+
* Mirrors a typical web-framework middleware: call once per request,
|
|
730
|
+
* wrapping the request handler, and the SDK will attribute any error
|
|
731
|
+
* reported inside the chain to the right request.
|
|
732
|
+
*/
|
|
733
|
+
runWithContext(request, fn) {
|
|
734
|
+
return this.nodeScopeProvider.runWithContext(request, fn);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Patch the request metadata on the active scope after `runWithContext(...)`
|
|
738
|
+
* has already started. Useful when fields become known partway through a
|
|
739
|
+
* request (e.g., the resolved absolute URL after proxy headers are parsed).
|
|
740
|
+
*
|
|
741
|
+
* Outside any `runWithContext(...)` callback, this writes to the fallback
|
|
742
|
+
* scope; the patch is visible to subsequent reports issued from outside a
|
|
743
|
+
* request scope but is NOT inherited by future `runWithContext(...)` calls.
|
|
744
|
+
*/
|
|
745
|
+
mergeContext(partial) {
|
|
746
|
+
this.nodeScopeProvider.mergeContext(partial);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Attach an authenticated user to the active scope. Inside a request scope
|
|
750
|
+
* this is per-request; outside it lands on the fallback scope. The fields
|
|
751
|
+
* are projected to OTel-style keys (`enduser.id`, `enduser.email`,
|
|
752
|
+
* `enduser.username`, `client.address`) by the Node context collector.
|
|
753
|
+
*/
|
|
754
|
+
setUser(user) {
|
|
755
|
+
this.nodeScopeProvider.setUser(user);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Returns the request scope when called inside `runWithContext(...)`, or
|
|
759
|
+
* `null` outside. Intentionally returns `null` (not the fallback scope)
|
|
760
|
+
* when no request is active, so callers can distinguish "we are inside a
|
|
761
|
+
* request" from "we are not". Primarily useful for debugging.
|
|
762
|
+
*/
|
|
763
|
+
getContext() {
|
|
764
|
+
return this.nodeScopeProvider.getContext();
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Detach the `uncaughtException` and `unhandledRejection` listeners
|
|
768
|
+
* without changing `nodeOptions`. Intended for tests and for graceful
|
|
769
|
+
* shutdown paths where you want to take ownership of process exit
|
|
770
|
+
* yourself.
|
|
771
|
+
*
|
|
772
|
+
* Calling `light()` afterwards re-attaches based on the current options.
|
|
773
|
+
*/
|
|
774
|
+
removeProcessListeners() {
|
|
775
|
+
this.handlerManager.detach();
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/index.ts
|
|
781
|
+
const flare = new NodeFlare();
|
|
782
|
+
|
|
783
|
+
//#endregion
|
|
784
|
+
Object.defineProperty(exports, 'DEFAULT_URL_DENYLIST', {
|
|
785
|
+
enumerable: true,
|
|
786
|
+
get: function () {
|
|
787
|
+
return _flareapp_core.DEFAULT_URL_DENYLIST;
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
Object.defineProperty(exports, 'Flare', {
|
|
791
|
+
enumerable: true,
|
|
792
|
+
get: function () {
|
|
793
|
+
return _flareapp_core.Flare;
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
Object.defineProperty(exports, 'GlobalScopeProvider', {
|
|
797
|
+
enumerable: true,
|
|
798
|
+
get: function () {
|
|
799
|
+
return _flareapp_core.GlobalScopeProvider;
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
exports.NodeFlare = NodeFlare;
|
|
803
|
+
exports.NodeScope = NodeScope;
|
|
804
|
+
Object.defineProperty(exports, 'NullFileReader', {
|
|
805
|
+
enumerable: true,
|
|
806
|
+
get: function () {
|
|
807
|
+
return _flareapp_core.NullFileReader;
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
Object.defineProperty(exports, 'Scope', {
|
|
811
|
+
enumerable: true,
|
|
812
|
+
get: function () {
|
|
813
|
+
return _flareapp_core.Scope;
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
Object.defineProperty(exports, 'convertToError', {
|
|
817
|
+
enumerable: true,
|
|
818
|
+
get: function () {
|
|
819
|
+
return _flareapp_core.convertToError;
|
|
820
|
+
}
|
|
821
|
+
});
|
|
822
|
+
exports.flare = flare;
|
|
823
|
+
Object.defineProperty(exports, 'redactUrlQuery', {
|
|
824
|
+
enumerable: true,
|
|
825
|
+
get: function () {
|
|
826
|
+
return _flareapp_core.redactUrlQuery;
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
Object.defineProperty(exports, 'resolveDenylist', {
|
|
830
|
+
enumerable: true,
|
|
831
|
+
get: function () {
|
|
832
|
+
return _flareapp_core.resolveDenylist;
|
|
833
|
+
}
|
|
834
|
+
});
|