@flareapp/core 2.6.0 → 2.8.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.
@@ -0,0 +1,551 @@
1
+
2
+ //#region src/env/index.ts
3
+ const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.8.0" : "?";
4
+ const KEY = typeof FLARE_JS_KEY === "undefined" ? "" : FLARE_JS_KEY;
5
+ const SOURCEMAP_VERSION = typeof FLARE_SOURCEMAP_VERSION === "undefined" ? "" : FLARE_SOURCEMAP_VERSION;
6
+
7
+ //#endregion
8
+ //#region src/util/assert.ts
9
+ function assert(value, message, debug) {
10
+ if (debug && !value) console.error(`Flare JavaScript client v${CLIENT_VERSION}: ${message}`);
11
+ return !!value;
12
+ }
13
+
14
+ //#endregion
15
+ //#region src/util/assertKey.ts
16
+ function assertKey(key, debug) {
17
+ return assert(key, "The client was not yet initialised with an API key. Run client.light('<flare-project-key>') when you initialise your app. If you are running in dev mode and didn't run the light command on purpose, you can ignore this error.", debug);
18
+ }
19
+
20
+ //#endregion
21
+ //#region src/util/statelessRegExp.ts
22
+ function withoutStatefulFlags(pattern) {
23
+ if (!pattern) return;
24
+ return pattern.global || pattern.sticky ? new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, "")) : pattern;
25
+ }
26
+
27
+ //#endregion
28
+ //#region src/util/componentMatcher.ts
29
+ /**
30
+ * Built once so a mount costs one name resolution and one match. Strings match exactly, regexes by
31
+ * `test()`.
32
+ */
33
+ function createComponentMatcher(option) {
34
+ if (option === true) return () => true;
35
+ if (!option || option.length === 0) return () => false;
36
+ const names = new Set(option.filter((entry) => typeof entry === "string"));
37
+ const patterns = option.filter((entry) => entry instanceof RegExp).map((pattern) => withoutStatefulFlags(pattern));
38
+ return (name) => names.has(name) || patterns.some((pattern) => pattern.test(name));
39
+ }
40
+
41
+ //#endregion
42
+ //#region src/util/convertToError.ts
43
+ function convertToError(error) {
44
+ if (error instanceof Error) return error;
45
+ if (typeof error === "string") return new Error(error);
46
+ if (typeof error === "object" && error !== null) {
47
+ const obj = error;
48
+ const message = typeof obj.message === "string" ? obj.message : String(error);
49
+ const converted = new Error(message);
50
+ if (typeof obj.stack === "string") converted.stack = obj.stack;
51
+ if (typeof obj.name === "string") converted.name = obj.name;
52
+ return converted;
53
+ }
54
+ return new Error(String(error));
55
+ }
56
+
57
+ //#endregion
58
+ //#region src/util/createIdentityTagger.ts
59
+ /**
60
+ * A per-package SDK/framework identity tagger. Holds its own WeakSet guards, so each Flare instance
61
+ * (singleton or injected renderer) gets each of the two tags at most once.
62
+ *
63
+ * `frameworkName` is `FrameworkName` rather than `string` because those are the exact values the backend
64
+ * recognises, so a first-party package cannot invent one. A host app that needs its own name calls
65
+ * `setFramework` directly.
66
+ */
67
+ function createIdentityTagger(config) {
68
+ const sdkTagged = /* @__PURE__ */ new WeakSet();
69
+ const frameworkTagged = /* @__PURE__ */ new WeakSet();
70
+ return {
71
+ registerSdkIdentity(flare) {
72
+ if (sdkTagged.has(flare)) return;
73
+ sdkTagged.add(flare);
74
+ flare.setSdkInfo({
75
+ name: config.sdkName,
76
+ version: config.sdkVersion
77
+ });
78
+ },
79
+ tagFramework(flare, frameworkVersion) {
80
+ if (frameworkTagged.has(flare)) return;
81
+ frameworkTagged.add(flare);
82
+ flare.setFramework(frameworkVersion === void 0 ? { name: config.frameworkName } : {
83
+ name: config.frameworkName,
84
+ version: frameworkVersion
85
+ });
86
+ }
87
+ };
88
+ }
89
+
90
+ //#endregion
91
+ //#region src/util/extractCode.ts
92
+ const MAX_CODE_LENGTH = 64;
93
+ function extractCode(error) {
94
+ const code = error.code;
95
+ if (typeof code !== "string" || code.length === 0) return;
96
+ return code.slice(0, MAX_CODE_LENGTH);
97
+ }
98
+
99
+ //#endregion
100
+ //#region src/util/traversalBudget.ts
101
+ /**
102
+ * Cycle detection only tracks the ancestor path, so a value holding the same child under two keys is not
103
+ * a cycle but still costs 2^depth to walk. Host data (glows, addContext, span attributes) reaches that
104
+ * shape through any object graph shared by reference.
105
+ *
106
+ * The node cap only bounds that walk if EVERY visited node is charged, primitive leaves included. It used
107
+ * to charge containers only, so 15 shared objects over an array of 1000 strings walked ~17M uncharged
108
+ * strings before the cap fired: 1.2s and 1GB. Callers must spend before their leaf branches return, not
109
+ * after. We do not memoize instead: a value shared under two keys is not a cycle and must not read as one.
110
+ */
111
+ const MAX_TRAVERSAL_DEPTH = 24;
112
+ const MAX_TRAVERSAL_NODES = 5e4;
113
+ function createTraversalBudget(nodes = MAX_TRAVERSAL_NODES) {
114
+ return { remaining: nodes };
115
+ }
116
+ /** Consumes one node. False once the budget is spent: stop descending. */
117
+ function spendNode(budget) {
118
+ if (budget.remaining <= 0) return false;
119
+ budget.remaining--;
120
+ return true;
121
+ }
122
+ /** Marks where a walk stopped, so a truncated payload does not read as a complete one. */
123
+ const TRUNCATED = "[truncated: too large]";
124
+
125
+ //#endregion
126
+ //#region src/util/safeClone.ts
127
+ /**
128
+ * One JSON-safe recursive clone shared by flatJsonStringify (json mode) and vue serializeProps
129
+ * (display mode). Cycles become "[Circular]", a BigInt its decimal string, and a throwing getter
130
+ * "[Getter threw]" in both modes. json mode passes functions / symbols / non-plain objects through
131
+ * (so JSON.stringify still drops functions and calls Date.toJSON); display mode replaces them with
132
+ * placeholders and applies the depth / array / key / string caps and the key denylist.
133
+ */
134
+ function safeClone(value, options) {
135
+ const seen = /* @__PURE__ */ new WeakSet();
136
+ const budget = createTraversalBudget();
137
+ const depthCap = options.mode === "display" ? options.maxDepth : MAX_TRAVERSAL_DEPTH;
138
+ function walk(node, depth) {
139
+ if (!spendNode(budget)) return TRUNCATED;
140
+ if (node === null) return null;
141
+ const type = typeof node;
142
+ if (type === "bigint") return node.toString();
143
+ if (type === "function") return options.mode === "display" ? "[Function]" : node;
144
+ if (type === "symbol") return options.mode === "display" ? "[Symbol]" : node;
145
+ if (type === "string") return options.mode === "display" ? truncate(node, options.stringCap) : node;
146
+ if (type !== "object") return node;
147
+ if (seen.has(node)) return "[Circular]";
148
+ if (Array.isArray(node)) {
149
+ if (depth > depthCap) return options.mode === "display" ? "[Array]" : TRUNCATED;
150
+ seen.add(node);
151
+ const cap = options.mode === "display" ? options.arrayCap : Infinity;
152
+ const result = (node.length > cap ? node.slice(0, cap) : node).map((item) => walk(item, depth + 1));
153
+ if (node.length > cap) result.push(`[… ${node.length - cap} more items]`);
154
+ seen.delete(node);
155
+ return result;
156
+ }
157
+ if (!isPlainObject(node)) return options.mode === "display" ? "[Object]" : node;
158
+ if (depth > depthCap) return options.mode === "display" ? "[Object]" : TRUNCATED;
159
+ seen.add(node);
160
+ const result = {};
161
+ const keys = Object.keys(node);
162
+ const keyCap = options.mode === "display" ? options.objectKeyCap : Infinity;
163
+ const limitedKeys = keys.length > keyCap ? keys.slice(0, keyCap) : keys;
164
+ for (const key of limitedKeys) {
165
+ if (options.mode === "display" && options.denylist.test(key)) {
166
+ result[key] = "[redacted]";
167
+ continue;
168
+ }
169
+ try {
170
+ result[key] = walk(node[key], depth + 1);
171
+ } catch {
172
+ result[key] = "[Getter threw]";
173
+ }
174
+ }
175
+ if (keys.length > keyCap) result["…"] = `[${keys.length - keyCap} more keys]`;
176
+ seen.delete(node);
177
+ return result;
178
+ }
179
+ return walk(value, 0);
180
+ }
181
+ function truncate(value, max) {
182
+ if (value.length <= max) return value;
183
+ return `${value.slice(0, max)}…[truncated ${value.length - max} chars]`;
184
+ }
185
+ /**
186
+ * Literal object / null prototypes only. Class instances may have side-effecting getters or
187
+ * non-enumerable internals we should not traverse, so they are left to the caller's mode policy.
188
+ */
189
+ function isPlainObject(value) {
190
+ if (value === null || typeof value !== "object") return false;
191
+ const proto = Object.getPrototypeOf(value);
192
+ return proto === Object.prototype || proto === null;
193
+ }
194
+
195
+ //#endregion
196
+ //#region src/util/flatJsonStringify.ts
197
+ /**
198
+ * JSON.stringify hardened for untrusted glow / addContext data: cycles become "[Circular]", a BigInt
199
+ * its decimal string, and a throwing getter "[Getter threw]", each of which would otherwise throw and
200
+ * drop the whole report.
201
+ */
202
+ function flatJsonStringify(json) {
203
+ return JSON.stringify(safeClone(json, { mode: "json" }));
204
+ }
205
+
206
+ //#endregion
207
+ //#region src/util/glowsToEvents.ts
208
+ function glowsToEvents(glows) {
209
+ return glows.map((glow) => ({
210
+ type: "php_glow",
211
+ startTimeUnixNano: Math.round(glow.microtime * 1e9),
212
+ endTimeUnixNano: null,
213
+ attributes: {
214
+ "glow.name": String(glow.name),
215
+ "glow.level": glow.messageLevel,
216
+ "glow.context": glow.metaData ?? {}
217
+ }
218
+ }));
219
+ }
220
+
221
+ //#endregion
222
+ //#region src/util/now.ts
223
+ function now() {
224
+ return Math.round(Date.now() / 1e3);
225
+ }
226
+
227
+ //#endregion
228
+ //#region src/util/redactUrl.ts
229
+ const DEFAULT_URL_DENYLIST = /password|passwd|pwd|token|secret|authorization|\bauth\b|bearer|oauth|credentials?|cookie|api[-_]?key|private[-_]?key|session|csrf|xsrf|\bpin\b|\bssn\b|card[-_]?number|\bcvv\b/i;
230
+ function resolveDenylist(custom, replaceDefault = false, defaultDenylist = DEFAULT_URL_DENYLIST) {
231
+ if (!custom) return defaultDenylist;
232
+ if (replaceDefault) {
233
+ const safeFlags = custom.flags.replace(/[gy]/g, "");
234
+ return new RegExp(custom.source, safeFlags);
235
+ }
236
+ const flags = unionFlags(defaultDenylist.flags, custom.flags);
237
+ return new RegExp(`(?:${defaultDenylist.source})|(?:${custom.source})`, flags);
238
+ }
239
+ function unionFlags(a, b) {
240
+ const merged = /* @__PURE__ */ new Set();
241
+ for (const flag of a + b) {
242
+ if (flag === "g" || flag === "y") continue;
243
+ merged.add(flag);
244
+ }
245
+ return [...merged].join("");
246
+ }
247
+ /**
248
+ * Strips userinfo (`user:pass@`) from an absolute URL and replaces query-string values whose key
249
+ * matches `denylist` with `[redacted]`. Path segments are left untouched.
250
+ */
251
+ function redactUrlQuery(fullPath, denylist = DEFAULT_URL_DENYLIST) {
252
+ const url = stripUserinfo(fullPath);
253
+ const queryStart = url.indexOf("?");
254
+ if (queryStart === -1) return url;
255
+ const hashStart = url.indexOf("#", queryStart);
256
+ const queryEnd = hashStart === -1 ? url.length : hashStart;
257
+ const prefix = url.slice(0, queryStart + 1);
258
+ const queryString = url.slice(queryStart + 1, queryEnd);
259
+ const suffix = url.slice(queryEnd);
260
+ return `${prefix}${queryString.split("&").map((pair) => {
261
+ if (pair === "") return pair;
262
+ const eq = pair.indexOf("=");
263
+ const rawKey = eq === -1 ? pair : pair.slice(0, eq);
264
+ const decodedKey = safeDecode(rawKey);
265
+ if (!denylist.test(decodedKey)) return pair;
266
+ return eq === -1 ? rawKey : `${rawKey}=[redacted]`;
267
+ }).join("&")}${suffix}`;
268
+ }
269
+ /**
270
+ * Value-side mirror of `redactUrlQuery`: a new object where any value whose key matches `denylist`
271
+ * becomes `[redacted]`. Null-prototype result so a `__proto__` key is stored, not swallowed.
272
+ */
273
+ function redactObjectValues(obj, denylist = DEFAULT_URL_DENYLIST) {
274
+ const result = Object.create(null);
275
+ for (const key of Object.keys(obj)) result[key] = denylist.test(key) ? "[redacted]" : obj[key];
276
+ return result;
277
+ }
278
+ /**
279
+ * Removes userinfo (`user:pass@`) from an absolute URL's authority only. A path or query can legally
280
+ * contain `@`, so only the authority (after `scheme://`, up to the first `/`, `?`, `#`) is inspected.
281
+ */
282
+ function stripUserinfo(url) {
283
+ const schemeMatch = /^[a-z][a-z0-9+.-]*:\/\//i.exec(url);
284
+ if (!schemeMatch) return url;
285
+ const authorityStart = schemeMatch[0].length;
286
+ const rest = url.slice(authorityStart);
287
+ const delimiter = /[/?#]/.exec(rest);
288
+ const authorityEnd = delimiter ? authorityStart + delimiter.index : url.length;
289
+ const authority = url.slice(authorityStart, authorityEnd);
290
+ const at = authority.lastIndexOf("@");
291
+ if (at === -1) return url;
292
+ return url.slice(0, authorityStart) + authority.slice(at + 1) + url.slice(authorityEnd);
293
+ }
294
+ /**
295
+ * decodeURIComponent throws on malformed escape sequences (`%E0`, lone `%`, etc). Falls back to the
296
+ * raw key in that case rather than aborting the whole redaction pass.
297
+ */
298
+ function safeDecode(value) {
299
+ try {
300
+ return decodeURIComponent(value);
301
+ } catch {
302
+ return value;
303
+ }
304
+ }
305
+
306
+ //#endregion
307
+ //#region src/util/rejection.ts
308
+ /** Best-effort human-readable description of an arbitrary rejection reason. */
309
+ function describeRejectionReason(reason) {
310
+ if (typeof reason === "string") return reason;
311
+ if (reason && typeof reason === "object") {
312
+ const message = reason.message;
313
+ if (typeof message === "string" && message) return message;
314
+ try {
315
+ return JSON.stringify(reason);
316
+ } catch {
317
+ return "Unhandled promise rejection (non-serializable reason)";
318
+ }
319
+ }
320
+ return String(reason);
321
+ }
322
+ function hasStack(reason) {
323
+ return !!reason && typeof reason === "object" && typeof reason.stack === "string";
324
+ }
325
+ /**
326
+ * Routes by whether `reason` carries a stack: stack-bearing reasons go to `reportSilently`, stackless
327
+ * ones to `reportUnhandledRejection`.
328
+ * The `.catch` is what stops a transport failure from surfacing as a second unhandled rejection.
329
+ * `reportSilently` is assumed async and left unwrapped, so a synchronous throw there still propagates.
330
+ */
331
+ function routeRejection(reporter, reason) {
332
+ if (reason instanceof Error) {
333
+ reporter.reportSilently(reason);
334
+ return;
335
+ }
336
+ if (hasStack(reason)) {
337
+ const error = new Error(describeRejectionReason(reason));
338
+ error.stack = reason.stack;
339
+ reporter.reportSilently(error);
340
+ return;
341
+ }
342
+ Promise.resolve(reporter.reportUnhandledRejection(describeRejectionReason(reason))).catch(() => {});
343
+ }
344
+
345
+ //#endregion
346
+ //#region src/util/toCustomContext.ts
347
+ /** Wraps a framework payload as the `context.custom` attribute a report expects. */
348
+ function toCustomContext(framework, payload) {
349
+ return { "context.custom": { [framework]: payload } };
350
+ }
351
+
352
+ //#endregion
353
+ //#region src/util/urlAttributes.ts
354
+ /** Well past any routable URL, but short enough that an inline `data:` payload cannot ride along. */
355
+ const MAX_URL_LENGTH = 2048;
356
+ function truncateUrl(url) {
357
+ return url.length <= MAX_URL_LENGTH ? url : `${url.slice(0, MAX_URL_LENGTH)}…[truncated]`;
358
+ }
359
+ /**
360
+ * Builds the OTel `url.*` attributes for one absolute URL.
361
+ *
362
+ * Redacts the URL first and splits it after, so `url.full` and `url.query` always show the same
363
+ * redacted values.
364
+ *
365
+ * Leaves out `url.query` when there is no query string. Returns only `url.full` when the URL cannot
366
+ * be parsed, for example a relative one.
367
+ */
368
+ function urlAttributes(url, denylist = DEFAULT_URL_DENYLIST) {
369
+ const full = truncateUrl(redactUrlQuery(url, denylist));
370
+ const attributes = { "url.full": full };
371
+ let parsed;
372
+ try {
373
+ parsed = new URL(full);
374
+ } catch {
375
+ return attributes;
376
+ }
377
+ attributes["url.scheme"] = parsed.protocol.slice(0, -1);
378
+ attributes["url.path"] = parsed.pathname;
379
+ if (parsed.search) attributes["url.query"] = parsed.search.slice(1);
380
+ return attributes;
381
+ }
382
+
383
+ //#endregion
384
+ Object.defineProperty(exports, 'CLIENT_VERSION', {
385
+ enumerable: true,
386
+ get: function () {
387
+ return CLIENT_VERSION;
388
+ }
389
+ });
390
+ Object.defineProperty(exports, 'DEFAULT_URL_DENYLIST', {
391
+ enumerable: true,
392
+ get: function () {
393
+ return DEFAULT_URL_DENYLIST;
394
+ }
395
+ });
396
+ Object.defineProperty(exports, 'KEY', {
397
+ enumerable: true,
398
+ get: function () {
399
+ return KEY;
400
+ }
401
+ });
402
+ Object.defineProperty(exports, 'MAX_TRAVERSAL_DEPTH', {
403
+ enumerable: true,
404
+ get: function () {
405
+ return MAX_TRAVERSAL_DEPTH;
406
+ }
407
+ });
408
+ Object.defineProperty(exports, 'MAX_URL_LENGTH', {
409
+ enumerable: true,
410
+ get: function () {
411
+ return MAX_URL_LENGTH;
412
+ }
413
+ });
414
+ Object.defineProperty(exports, 'SOURCEMAP_VERSION', {
415
+ enumerable: true,
416
+ get: function () {
417
+ return SOURCEMAP_VERSION;
418
+ }
419
+ });
420
+ Object.defineProperty(exports, 'TRUNCATED', {
421
+ enumerable: true,
422
+ get: function () {
423
+ return TRUNCATED;
424
+ }
425
+ });
426
+ Object.defineProperty(exports, 'assert', {
427
+ enumerable: true,
428
+ get: function () {
429
+ return assert;
430
+ }
431
+ });
432
+ Object.defineProperty(exports, 'assertKey', {
433
+ enumerable: true,
434
+ get: function () {
435
+ return assertKey;
436
+ }
437
+ });
438
+ Object.defineProperty(exports, 'convertToError', {
439
+ enumerable: true,
440
+ get: function () {
441
+ return convertToError;
442
+ }
443
+ });
444
+ Object.defineProperty(exports, 'createComponentMatcher', {
445
+ enumerable: true,
446
+ get: function () {
447
+ return createComponentMatcher;
448
+ }
449
+ });
450
+ Object.defineProperty(exports, 'createIdentityTagger', {
451
+ enumerable: true,
452
+ get: function () {
453
+ return createIdentityTagger;
454
+ }
455
+ });
456
+ Object.defineProperty(exports, 'createTraversalBudget', {
457
+ enumerable: true,
458
+ get: function () {
459
+ return createTraversalBudget;
460
+ }
461
+ });
462
+ Object.defineProperty(exports, 'describeRejectionReason', {
463
+ enumerable: true,
464
+ get: function () {
465
+ return describeRejectionReason;
466
+ }
467
+ });
468
+ Object.defineProperty(exports, 'extractCode', {
469
+ enumerable: true,
470
+ get: function () {
471
+ return extractCode;
472
+ }
473
+ });
474
+ Object.defineProperty(exports, 'flatJsonStringify', {
475
+ enumerable: true,
476
+ get: function () {
477
+ return flatJsonStringify;
478
+ }
479
+ });
480
+ Object.defineProperty(exports, 'glowsToEvents', {
481
+ enumerable: true,
482
+ get: function () {
483
+ return glowsToEvents;
484
+ }
485
+ });
486
+ Object.defineProperty(exports, 'now', {
487
+ enumerable: true,
488
+ get: function () {
489
+ return now;
490
+ }
491
+ });
492
+ Object.defineProperty(exports, 'redactObjectValues', {
493
+ enumerable: true,
494
+ get: function () {
495
+ return redactObjectValues;
496
+ }
497
+ });
498
+ Object.defineProperty(exports, 'redactUrlQuery', {
499
+ enumerable: true,
500
+ get: function () {
501
+ return redactUrlQuery;
502
+ }
503
+ });
504
+ Object.defineProperty(exports, 'resolveDenylist', {
505
+ enumerable: true,
506
+ get: function () {
507
+ return resolveDenylist;
508
+ }
509
+ });
510
+ Object.defineProperty(exports, 'routeRejection', {
511
+ enumerable: true,
512
+ get: function () {
513
+ return routeRejection;
514
+ }
515
+ });
516
+ Object.defineProperty(exports, 'safeClone', {
517
+ enumerable: true,
518
+ get: function () {
519
+ return safeClone;
520
+ }
521
+ });
522
+ Object.defineProperty(exports, 'safeDecode', {
523
+ enumerable: true,
524
+ get: function () {
525
+ return safeDecode;
526
+ }
527
+ });
528
+ Object.defineProperty(exports, 'spendNode', {
529
+ enumerable: true,
530
+ get: function () {
531
+ return spendNode;
532
+ }
533
+ });
534
+ Object.defineProperty(exports, 'toCustomContext', {
535
+ enumerable: true,
536
+ get: function () {
537
+ return toCustomContext;
538
+ }
539
+ });
540
+ Object.defineProperty(exports, 'urlAttributes', {
541
+ enumerable: true,
542
+ get: function () {
543
+ return urlAttributes;
544
+ }
545
+ });
546
+ Object.defineProperty(exports, 'withoutStatefulFlags', {
547
+ enumerable: true,
548
+ get: function () {
549
+ return withoutStatefulFlags;
550
+ }
551
+ });