@flareapp/core 2.7.0 → 2.9.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 CHANGED
@@ -26,200 +26,88 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  }) : target, mod));
27
27
 
28
28
  //#endregion
29
+ const require_urlAttributes = require('./urlAttributes-CEP6bOBn.cjs');
29
30
  let error_stack_parser = require("error-stack-parser");
30
31
  error_stack_parser = __toESM(error_stack_parser);
31
32
 
32
- //#region src/env/index.ts
33
- const CLIENT_VERSION = typeof process !== "undefined" && true ? "2.7.0" : "?";
34
- const KEY = typeof FLARE_JS_KEY === "undefined" ? "" : FLARE_JS_KEY;
35
- const SOURCEMAP_VERSION = typeof FLARE_SOURCEMAP_VERSION === "undefined" ? "" : FLARE_SOURCEMAP_VERSION;
36
-
37
- //#endregion
38
- //#region src/util/assert.ts
39
- function assert(value, message, debug) {
40
- if (debug && !value) console.error(`Flare JavaScript client v${CLIENT_VERSION}: ${message}`);
41
- return !!value;
42
- }
43
-
44
- //#endregion
45
- //#region src/util/assertKey.ts
46
- function assertKey(key, debug) {
47
- 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);
48
- }
49
-
50
- //#endregion
51
- //#region src/util/convertToError.ts
52
- function convertToError(error) {
53
- if (error instanceof Error) return error;
54
- if (typeof error === "string") return new Error(error);
55
- if (typeof error === "object" && error !== null) {
56
- const obj = error;
57
- const message = typeof obj.message === "string" ? obj.message : String(error);
58
- const converted = new Error(message);
59
- if (typeof obj.stack === "string") converted.stack = obj.stack;
60
- if (typeof obj.name === "string") converted.name = obj.name;
61
- return converted;
62
- }
63
- return new Error(String(error));
64
- }
65
-
66
- //#endregion
67
- //#region src/util/extractCode.ts
68
- const MAX_CODE_LENGTH = 64;
69
- function extractCode(error) {
70
- const code = error.code;
71
- if (typeof code !== "string" || code.length === 0) return;
72
- return code.slice(0, MAX_CODE_LENGTH);
73
- }
74
-
75
- //#endregion
76
- //#region src/util/flatJsonStringify.ts
77
- function flatJsonStringify(json) {
78
- return JSON.stringify(decycle(json));
79
- }
80
- function isPlainObject(value) {
81
- if (typeof value !== "object" || value === null) return false;
82
- const proto = Object.getPrototypeOf(value);
83
- return proto === Object.prototype || proto === null;
84
- }
85
- function decycle(root) {
86
- const inPath = /* @__PURE__ */ new WeakSet();
87
- function clone(node) {
88
- if (Array.isArray(node)) {
89
- if (inPath.has(node)) return "[Circular]";
90
- inPath.add(node);
91
- const result = node.map(clone);
92
- inPath.delete(node);
93
- return result;
94
- }
95
- if (isPlainObject(node)) {
96
- if (inPath.has(node)) return "[Circular]";
97
- inPath.add(node);
98
- const result = {};
99
- for (const [k, v] of Object.entries(node)) result[k] = clone(v);
100
- inPath.delete(node);
101
- return result;
102
- }
103
- return node;
104
- }
105
- return clone(root);
106
- }
107
-
108
- //#endregion
109
- //#region src/util/glowsToEvents.ts
110
- function glowsToEvents(glows) {
111
- return glows.map((glow) => ({
112
- type: "php_glow",
113
- startTimeUnixNano: Math.round(glow.microtime * 1e9),
114
- endTimeUnixNano: null,
115
- attributes: {
116
- "glow.name": String(glow.name),
117
- "glow.level": glow.messageLevel,
118
- "glow.context": glow.metaData ?? {}
119
- }
120
- }));
121
- }
33
+ //#region src/framework.ts
34
+ /**
35
+ * Framework names the Flare backend recognises. Wire format, so the values never change: they ship as
36
+ * `flare.framework.name` and (lowercased) as `context.custom.framework`.
37
+ *
38
+ * `Js` and `Node` are the base SDKs' fallback claim, overwritten when a framework package tags its
39
+ * own name. `NodeElectron` is an Electron main process; its renderers report their own.
40
+ */
41
+ const FrameworkName = {
42
+ Js: "js",
43
+ Node: "node",
44
+ NodeElectron: "node-electron",
45
+ React: "react",
46
+ Vue: "vue",
47
+ Svelte: "svelte",
48
+ SvelteKit: "sveltekit",
49
+ ReactNative: "react-native"
50
+ };
122
51
 
123
52
  //#endregion
124
- //#region src/util/now.ts
125
- function now() {
126
- return Math.round(Date.now() / 1e3);
127
- }
53
+ //#region src/spanTypes.ts
54
+ /**
55
+ * Span types the Flare backend recognises. Wire format, so the values never change: they ship as the
56
+ * `flare.span_type` attribute and the backend groups performance data by them.
57
+ *
58
+ * These are the browser client's set. They live in core because core's `SpanOptions.spanType` needs
59
+ * to name them and core cannot import from `@flareapp/js`.
60
+ */
61
+ const BrowserSpanType = {
62
+ Pageload: "browser_pageload",
63
+ Navigation: "browser_navigation",
64
+ Fetch: "browser_fetch",
65
+ Xhr: "browser_xhr",
66
+ Component: "browser_component",
67
+ WebVital: "browser_web_vital"
68
+ };
69
+ const BrowserSpanEventType = {
70
+ Click: "browser_click",
71
+ Input: "browser_input",
72
+ RouteChange: "browser_route_change"
73
+ };
128
74
 
129
75
  //#endregion
130
- //#region src/util/redactUrl.ts
131
- 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;
132
- function resolveDenylist(custom, replaceDefault = false, defaultDenylist = DEFAULT_URL_DENYLIST) {
133
- if (!custom) return defaultDenylist;
134
- if (replaceDefault) {
135
- const safeFlags = custom.flags.replace(/[gy]/g, "");
136
- return new RegExp(custom.source, safeFlags);
137
- }
138
- const flags = unionFlags(defaultDenylist.flags, custom.flags);
139
- return new RegExp(`(?:${defaultDenylist.source})|(?:${custom.source})`, flags);
140
- }
141
- function unionFlags(a, b) {
142
- const merged = /* @__PURE__ */ new Set();
143
- for (const flag of a + b) {
144
- if (flag === "g" || flag === "y") continue;
145
- merged.add(flag);
146
- }
147
- return [...merged].join("");
148
- }
149
- function redactUrlQuery(fullPath, denylist = DEFAULT_URL_DENYLIST) {
150
- const queryStart = fullPath.indexOf("?");
151
- if (queryStart === -1) return fullPath;
152
- const hashStart = fullPath.indexOf("#", queryStart);
153
- const queryEnd = hashStart === -1 ? fullPath.length : hashStart;
154
- const prefix = fullPath.slice(0, queryStart + 1);
155
- const queryString = fullPath.slice(queryStart + 1, queryEnd);
156
- const suffix = fullPath.slice(queryEnd);
157
- return `${prefix}${queryString.split("&").map((pair) => {
158
- if (pair === "") return pair;
159
- const eq = pair.indexOf("=");
160
- const rawKey = eq === -1 ? pair : pair.slice(0, eq);
161
- const decodedKey = safeDecode(rawKey);
162
- if (!denylist.test(decodedKey)) return pair;
163
- return eq === -1 ? rawKey : `${rawKey}=[redacted]`;
164
- }).join("&")}${suffix}`;
165
- }
166
- function safeDecode(value) {
167
- try {
168
- return decodeURIComponent(value);
169
- } catch {
170
- return value;
171
- }
172
- }
76
+ //#region src/types.ts
77
+ /** OTel status codes. Wire format: these numbers ship in the span envelope, so the values never change. */
78
+ const SpanStatusCode = {
79
+ Unset: 0,
80
+ Ok: 1,
81
+ Error: 2
82
+ };
173
83
 
174
84
  //#endregion
175
- //#region src/util/rejection.ts
176
- /** Best-effort human-readable description of an arbitrary rejection reason. */
177
- function describeRejectionReason(reason) {
178
- if (typeof reason === "string") return reason;
179
- if (reason && typeof reason === "object") {
180
- const message = reason.message;
181
- if (typeof message === "string" && message) return message;
182
- try {
183
- return JSON.stringify(reason);
184
- } catch {
185
- return "Unhandled promise rejection (non-serializable reason)";
186
- }
187
- }
188
- return String(reason);
189
- }
190
- function hasStack$1(reason) {
191
- return !!reason && typeof reason === "object" && typeof reason.stack === "string";
192
- }
193
- /**
194
- * Route a rejection reason to the reporter: an Error (or any stack-bearing
195
- * object) goes to `reportSilently` so the STACK survives; only a stackless
196
- * reason falls back to `reportUnhandledRejection` (string message, empty-stack
197
- * `UnhandledRejection` class). Any rejection from `reportUnhandledRejection`'s
198
- * returned promise is swallowed so a transport failure cannot itself surface as
199
- * an unhandled rejection. `reportSilently` is assumed not to throw synchronously
200
- * (core's does its work asynchronously); it is intentionally not wrapped, so a
201
- * synchronous throw there would propagate.
202
- */
203
- function routeRejection(reporter, reason) {
204
- if (reason instanceof Error) {
205
- reporter.reportSilently(reason);
206
- return;
207
- }
208
- if (hasStack$1(reason)) {
209
- const error = new Error(describeRejectionReason(reason));
210
- error.stack = reason.stack;
211
- reporter.reportSilently(error);
212
- return;
213
- }
214
- Promise.resolve(reporter.reportUnhandledRejection(describeRejectionReason(reason))).catch(() => {});
85
+ //#region src/util/utf8Bytes.ts
86
+ const textEncoder = new TextEncoder();
87
+ /** UTF-8 byte length of `value`. */
88
+ function utf8Bytes(value) {
89
+ return textEncoder.encode(value).length;
215
90
  }
216
91
 
217
92
  //#endregion
218
93
  //#region src/api/Api.ts
94
+ const MAX_PENDING_KEEPALIVE_BYTES = 6e4;
95
+ const MAX_PENDING_KEEPALIVE_REQUESTS = 15;
219
96
  var Api = class {
97
+ pendingKeepaliveBytes = 0;
98
+ pendingKeepaliveRequests = 0;
99
+ /**
100
+ * How many keepalive bytes are still available. Logs and traces share one browser allowance and both
101
+ * flush on page hide, so whichever goes second has to pack against what is left rather than assume the
102
+ * whole budget, or it exceeds the gate in send() and silently degrades to a cancellable fetch.
103
+ */
104
+ keepaliveBudgetRemaining() {
105
+ if (this.pendingKeepaliveRequests >= MAX_PENDING_KEEPALIVE_REQUESTS) return 0;
106
+ return Math.max(0, MAX_PENDING_KEEPALIVE_BYTES - this.pendingKeepaliveBytes);
107
+ }
220
108
  report(report, url, key, reportBrowserExtensionErrors, debug = false) {
221
- return fetch(url, {
222
- method: "POST",
109
+ return this.send({
110
+ url,
223
111
  headers: {
224
112
  "Accept": "application/json",
225
113
  "Content-Type": "application/json",
@@ -227,34 +115,274 @@ var Api = class {
227
115
  "X-Report-Browser-Extension-Errors": JSON.stringify(reportBrowserExtensionErrors),
228
116
  "X-Flare-Client-Version": "2"
229
117
  },
230
- body: flatJsonStringify(report)
231
- }).then((response) => {
232
- if (debug && response.status !== 201) console.error(`Received response with status ${response.status} from Flare`);
233
- }, (error) => {
234
- if (debug) console.error(error);
118
+ body: require_urlAttributes.flatJsonStringify(report),
119
+ label: "Flare",
120
+ debug,
121
+ keepalive: false
235
122
  });
236
123
  }
237
124
  logs(envelope, url, key, debug = false, keepalive = false) {
125
+ return this.send({
126
+ url,
127
+ headers: this.ingestHeaders(key),
128
+ body: require_urlAttributes.flatJsonStringify(envelope),
129
+ label: "Flare logs",
130
+ debug,
131
+ keepalive
132
+ });
133
+ }
134
+ traces(envelope, url, key, debug = false, keepalive = false) {
135
+ let body;
136
+ try {
137
+ body = JSON.stringify(envelope);
138
+ } catch {
139
+ body = require_urlAttributes.flatJsonStringify(envelope);
140
+ }
141
+ return this.send({
142
+ url,
143
+ headers: this.ingestHeaders(key),
144
+ body,
145
+ label: "Flare traces",
146
+ debug,
147
+ keepalive
148
+ });
149
+ }
150
+ ingestHeaders(key) {
151
+ return {
152
+ "Accept": "application/json",
153
+ "Content-Type": "application/json",
154
+ "x-api-token": key ?? ""
155
+ };
156
+ }
157
+ send(request) {
158
+ const { url, headers, body, label, debug, keepalive: keepaliveRequested } = request;
159
+ const bytes = keepaliveRequested ? utf8Bytes(body) : 0;
160
+ const keepalive = keepaliveRequested && this.pendingKeepaliveRequests < MAX_PENDING_KEEPALIVE_REQUESTS && this.pendingKeepaliveBytes + bytes <= MAX_PENDING_KEEPALIVE_BYTES;
161
+ if (keepalive) {
162
+ this.pendingKeepaliveBytes += bytes;
163
+ this.pendingKeepaliveRequests += 1;
164
+ }
238
165
  return fetch(url, {
239
166
  method: "POST",
240
167
  keepalive,
241
- headers: {
242
- "Accept": "application/json",
243
- "Content-Type": "application/json",
244
- "x-api-token": key ?? ""
245
- },
246
- body: flatJsonStringify(envelope)
168
+ headers,
169
+ body
247
170
  }).then((response) => {
248
- if (debug && response.status !== 201) console.error(`Received response with status ${response.status} from Flare logs`);
171
+ if (debug && response.status !== 201) console.error(`Received response with status ${response.status} from ${label}`);
249
172
  }, (error) => {
250
173
  if (debug) console.error(error);
174
+ }).finally(() => {
175
+ if (keepalive) {
176
+ this.pendingKeepaliveBytes -= bytes;
177
+ this.pendingKeepaliveRequests -= 1;
178
+ }
179
+ });
180
+ }
181
+ };
182
+
183
+ //#endregion
184
+ //#region src/breadcrumbs/recordBreadcrumb.ts
185
+ const MAX_BREADCRUMB_URL_LENGTH = 256;
186
+ function breadcrumbUrl(href, denylist) {
187
+ const redacted = require_urlAttributes.redactUrlQuery(href, denylist);
188
+ return redacted.length > MAX_BREADCRUMB_URL_LENGTH ? redacted.slice(0, MAX_BREADCRUMB_URL_LENGTH) : redacted;
189
+ }
190
+ function recordBreadcrumb(scopeProvider, config, type, attributes, startTimeUnixNano) {
191
+ if (!config.enableBreadcrumbs) return;
192
+ scopeProvider.active().addBreadcrumb({
193
+ type,
194
+ startTimeUnixNano,
195
+ endTimeUnixNano: null,
196
+ attributes
197
+ }, config.maxBreadcrumbs);
198
+ }
199
+
200
+ //#endregion
201
+ //#region src/telemetry/TelemetryBuffer.ts
202
+ /**
203
+ * The batching machine behind both telemetry signals: hold records, ship them when a size, weight or time
204
+ * trigger fires, and shed the oldest when nothing can drain. One instance owns one signal; what that signal
205
+ * is comes entirely from the policy.
206
+ */
207
+ var TelemetryBuffer = class {
208
+ entries = [];
209
+ bufferedBytes = 0;
210
+ timer;
211
+ timerActive = false;
212
+ constructor(deps, policy) {
213
+ this.deps = deps;
214
+ this.policy = policy;
215
+ const flush = (opts) => this.flush(opts);
216
+ this.deps.scheduler.register(flush);
217
+ }
218
+ length() {
219
+ return this.entries.length;
220
+ }
221
+ add(record) {
222
+ const config = this.deps.getConfig();
223
+ const limits = this.policy.limits(config);
224
+ const bytes = this.policy.estimateBytes(record);
225
+ if (bytes > limits.maxBytes) {
226
+ if (config.debug) console.error(this.policy.oversizedMessage);
227
+ return;
228
+ }
229
+ this.entries.push({
230
+ record,
231
+ bytes
251
232
  });
233
+ this.bufferedBytes += bytes;
234
+ this.policy.onRecordBuffered?.(record);
235
+ this.evaluateTriggers(config, limits);
236
+ this.trim(limits);
237
+ }
238
+ flush(opts) {
239
+ const config = this.deps.getConfig();
240
+ if (!this.policy.enabled(config)) return;
241
+ if (this.entries.length === 0) return;
242
+ if (!require_urlAttributes.assertKey(config.key, config.debug)) {
243
+ this.clearTimer();
244
+ return;
245
+ }
246
+ this.clearTimer();
247
+ const resource = this.policy.resourceForFlush();
248
+ let selected;
249
+ let sendKeepalive = !!opts?.keepalive;
250
+ if (opts?.keepalive) {
251
+ selected = this.packForKeepalive(config, resource);
252
+ if (selected.length === 0) {
253
+ selected = this.entries;
254
+ this.entries = [];
255
+ this.bufferedBytes = 0;
256
+ sendKeepalive = false;
257
+ } else {
258
+ this.entries = this.entries.filter((entry) => !selected.includes(entry));
259
+ this.bufferedBytes = this.entries.reduce((sum, entry) => sum + entry.bytes, 0);
260
+ if (this.entries.length > 0) this.armTimer(this.policy.limits(config));
261
+ }
262
+ } else {
263
+ selected = this.entries;
264
+ this.entries = [];
265
+ this.bufferedBytes = 0;
266
+ }
267
+ if (selected.length === 0) return;
268
+ try {
269
+ this.policy.send(this.policy.buildEnvelope(selected.map((entry) => entry.record), resource), config, sendKeepalive);
270
+ } catch (error) {
271
+ if (config.debug) console.error(this.policy.sendFailureMessage, error);
272
+ }
273
+ }
274
+ clear() {
275
+ this.entries = [];
276
+ this.bufferedBytes = 0;
277
+ this.clearTimer();
278
+ }
279
+ evaluateTriggers(config, limits) {
280
+ if (this.entries.length >= limits.maxSize) {
281
+ this.flush();
282
+ return;
283
+ }
284
+ if (this.bufferedBytes >= limits.maxBytes) {
285
+ this.flush();
286
+ return;
287
+ }
288
+ this.armTimer(limits);
289
+ }
290
+ armTimer(limits) {
291
+ if (this.timerActive) return;
292
+ this.timerActive = true;
293
+ this.timer = setTimeout(() => {
294
+ this.timerActive = false;
295
+ this.timer = void 0;
296
+ this.flush();
297
+ }, limits.flushIntervalMs);
298
+ this.timer.unref?.();
299
+ }
300
+ trim(limits) {
301
+ if (this.entries.length > limits.maxSize) {
302
+ const excess = this.entries.length - limits.maxSize;
303
+ for (let i = 0; i < excess; i++) this.bufferedBytes -= this.entries[i].bytes;
304
+ this.entries = this.entries.slice(excess);
305
+ }
306
+ while (this.entries.length > 1 && this.bufferedBytes > limits.maxBytes) {
307
+ const dropped = this.entries.shift();
308
+ if (dropped) this.bufferedBytes -= dropped.bytes;
309
+ }
310
+ }
311
+ /**
312
+ * Newest-wins. An over-budget record is skipped, not a stop signal, so a smaller older record behind a fat
313
+ * one still ships. Runs on visibilitychange:hidden, which fires on plain backgrounding too, so the tail this
314
+ * leaves behind is retained and re-armed rather than dropped (see flush).
315
+ */
316
+ packForKeepalive(config, resource) {
317
+ const fixedBytes = this.policy.emptyEnvelopeBytes(resource);
318
+ const budget = Math.min(config.keepaliveMaxBytes, this.policy.keepaliveBudget?.(config) ?? config.keepaliveMaxBytes);
319
+ const selected = [];
320
+ let selectedBytes = 0;
321
+ let droppedCount = 0;
322
+ for (let i = this.entries.length - 1; i >= 0; i--) {
323
+ const entry = this.entries[i];
324
+ const candidateBytes = this.policy.recordBytes(entry.record);
325
+ if (fixedBytes + selectedBytes + candidateBytes + selected.length <= budget) {
326
+ selected.unshift(entry);
327
+ selectedBytes += candidateBytes;
328
+ } else if (config.debug) droppedCount++;
329
+ }
330
+ if (config.debug && droppedCount > 0) console.error(this.policy.keepaliveDropMessage(droppedCount));
331
+ return selected;
332
+ }
333
+ clearTimer() {
334
+ if (this.timer) {
335
+ clearTimeout(this.timer);
336
+ this.timer = void 0;
337
+ }
338
+ this.timerActive = false;
252
339
  }
253
340
  };
254
341
 
342
+ //#endregion
343
+ //#region src/telemetry/resourceIdentity.ts
344
+ /**
345
+ * Builds the attributes that go on every logs or traces envelope: the caller's own attributes in `base`, with
346
+ * our SDK, service and framework identity on top. Our keys win, so a user value cannot overwrite something
347
+ * like `telemetry.sdk.name`.
348
+ */
349
+ function buildResourceIdentity(base, config, sdk, framework) {
350
+ const identity = {
351
+ "telemetry.sdk.language": "javascript",
352
+ "telemetry.sdk.name": sdk.name,
353
+ "telemetry.sdk.version": sdk.version,
354
+ "flare.language.name": "javascript"
355
+ };
356
+ if (config.serviceName) identity["service.name"] = config.serviceName;
357
+ if (config.version) identity["service.version"] = config.version;
358
+ if (config.stage) identity["service.stage"] = config.stage;
359
+ if (framework?.name) identity["flare.framework.name"] = framework.name;
360
+ if (framework?.version) identity["flare.framework.version"] = framework.version;
361
+ return {
362
+ ...base,
363
+ ...identity
364
+ };
365
+ }
366
+
255
367
  //#endregion
256
368
  //#region src/logging/otel.ts
257
- function valueToOpenTelemetry(value, inPath = /* @__PURE__ */ new WeakSet()) {
369
+ /**
370
+ * Converts one attribute value into the OpenTelemetry `AnyValue` shape. Strings, numbers and booleans become
371
+ * leaves, arrays and objects are walked recursively. Anything OpenTelemetry cannot carry (null, undefined,
372
+ * NaN, functions) returns null and the caller drops that key.
373
+ *
374
+ * A value that contains itself becomes the string `[Circular]`. `inPath` only holds the parents of the value
375
+ * being converted right now, so the same object used twice side by side is converted twice instead of being
376
+ * wrongly called circular.
377
+ *
378
+ * The walk also stops at a maximum depth and a maximum number of nodes, see traversalBudget.ts. Pass `budget`
379
+ * to let several calls share one allowance, otherwise every call gets its own.
380
+ */
381
+ function valueToOpenTelemetry(value, inPath = /* @__PURE__ */ new WeakSet(), budget = require_urlAttributes.createTraversalBudget()) {
382
+ return convert(value, inPath, 0, budget);
383
+ }
384
+ function convert(value, inPath, depth, budget) {
385
+ if (!require_urlAttributes.spendNode(budget)) return { stringValue: require_urlAttributes.TRUNCATED };
258
386
  if (typeof value === "string") return { stringValue: value };
259
387
  if (typeof value === "boolean") return { boolValue: value };
260
388
  if (typeof value === "number") {
@@ -262,12 +390,13 @@ function valueToOpenTelemetry(value, inPath = /* @__PURE__ */ new WeakSet()) {
262
390
  return Number.isInteger(value) ? { intValue: value } : { doubleValue: value };
263
391
  }
264
392
  if (value === null || value === void 0) return null;
393
+ if (depth >= require_urlAttributes.MAX_TRAVERSAL_DEPTH) return { stringValue: require_urlAttributes.TRUNCATED };
265
394
  if (Array.isArray(value)) {
266
395
  if (inPath.has(value)) return { stringValue: "[Circular]" };
267
396
  inPath.add(value);
268
397
  const values = [];
269
398
  for (const item of value) {
270
- const mapped = valueToOpenTelemetry(item, inPath);
399
+ const mapped = convert(item, inPath, depth + 1, budget);
271
400
  if (mapped !== null) values.push(mapped);
272
401
  }
273
402
  inPath.delete(value);
@@ -278,7 +407,7 @@ function valueToOpenTelemetry(value, inPath = /* @__PURE__ */ new WeakSet()) {
278
407
  inPath.add(value);
279
408
  const values = [];
280
409
  for (const [key, item] of Object.entries(value)) {
281
- const mapped = valueToOpenTelemetry(item, inPath);
410
+ const mapped = convert(item, inPath, depth + 1, budget);
282
411
  if (mapped !== null) values.push({
283
412
  key,
284
413
  value: mapped
@@ -290,9 +419,10 @@ function valueToOpenTelemetry(value, inPath = /* @__PURE__ */ new WeakSet()) {
290
419
  return null;
291
420
  }
292
421
  function attributesToOpenTelemetry(attributes) {
422
+ const budget = require_urlAttributes.createTraversalBudget();
293
423
  const out = [];
294
424
  for (const [key, value] of Object.entries(attributes)) {
295
- const mapped = valueToOpenTelemetry(value);
425
+ const mapped = valueToOpenTelemetry(value, /* @__PURE__ */ new WeakSet(), budget);
296
426
  if (mapped !== null) out.push({
297
427
  key,
298
428
  value: mapped
@@ -303,6 +433,18 @@ function attributesToOpenTelemetry(attributes) {
303
433
 
304
434
  //#endregion
305
435
  //#region src/logging/envelope.ts
436
+ function toOtelLogRecord(record) {
437
+ return {
438
+ timeUnixNano: record.timeUnixNano,
439
+ observedTimeUnixNano: record.timeUnixNano,
440
+ severityNumber: record.severityNumber,
441
+ severityText: record.severityText,
442
+ body: { stringValue: record.message },
443
+ attributes: record.recordAttributes,
444
+ flags: 0,
445
+ droppedAttributesCount: 0
446
+ };
447
+ }
306
448
  function buildLogsEnvelope(records, resourceAttributes, scopeName, scopeVersion) {
307
449
  return { resourceLogs: [{
308
450
  resource: {
@@ -316,19 +458,24 @@ function buildLogsEnvelope(records, resourceAttributes, scopeName, scopeVersion)
316
458
  attributes: [],
317
459
  droppedAttributesCount: 0
318
460
  },
319
- logRecords: records.map((record) => ({
320
- timeUnixNano: record.timeUnixNano,
321
- observedTimeUnixNano: record.timeUnixNano,
322
- severityNumber: record.severityNumber,
323
- severityText: record.severityText,
324
- body: { stringValue: record.message },
325
- attributes: record.recordAttributes,
326
- flags: 0,
327
- droppedAttributesCount: 0
328
- }))
461
+ logRecords: records.map(toOtelLogRecord)
329
462
  }]
330
463
  }] };
331
464
  }
465
+ /**
466
+ * How many UTF-8 bytes one record adds to an envelope. We measure the real toOtelLogRecord output instead of
467
+ * reusing the cached BufferedLog estimate, because keepaliveMaxBytes is a hard browser limit and an estimate
468
+ * is not good enough.
469
+ *
470
+ * Uses flatJsonStringify to match Api.logs, which sends the envelope through the same encoder.
471
+ */
472
+ function otelLogRecordBytes(record) {
473
+ return utf8Bytes(require_urlAttributes.flatJsonStringify(toOtelLogRecord(record)));
474
+ }
475
+ /** UTF-8 bytes of an empty envelope: the fixed overhead every batch has, before any records are added. */
476
+ function emptyLogsEnvelopeBytes(resourceAttributes, scopeName, scopeVersion) {
477
+ return utf8Bytes(require_urlAttributes.flatJsonStringify(buildLogsEnvelope([], resourceAttributes, scopeName, scopeVersion)));
478
+ }
332
479
 
333
480
  //#endregion
334
481
  //#region src/logging/severity.ts
@@ -355,14 +502,42 @@ function isAtOrAboveMinimum(level, minimum) {
355
502
  //#endregion
356
503
  //#region src/logging/Logger.ts
357
504
  var Logger = class {
358
- buffer = [];
505
+ inner;
359
506
  resourceAttributes = {};
360
- timer;
361
- timerActive = false;
362
507
  constructor(deps) {
363
508
  this.deps = deps;
364
- const flush = (opts) => this.flush(opts);
365
- this.deps.scheduler.register(flush);
509
+ this.inner = new TelemetryBuffer({
510
+ getConfig: deps.getConfig,
511
+ scheduler: deps.scheduler
512
+ }, {
513
+ limits: (config) => ({
514
+ maxSize: config.maxLogBufferSize,
515
+ maxBytes: config.logFlushMaxBytes,
516
+ flushIntervalMs: config.logFlushIntervalMs
517
+ }),
518
+ enabled: (config) => config.enableLogs,
519
+ keepaliveBudget: () => deps.api.keepaliveBudgetRemaining(),
520
+ estimateBytes: (record) => this.estimateBytes(record),
521
+ emptyEnvelopeBytes: (resource) => {
522
+ const sdk = deps.getSdkInfo();
523
+ return emptyLogsEnvelopeBytes(resource, sdk.name, sdk.version);
524
+ },
525
+ recordBytes: (record) => otelLogRecordBytes(record),
526
+ oversizedMessage: "Flare: dropping oversized log record",
527
+ keepaliveDropMessage: (count) => `Flare: dropped ${count} log record(s) from keepalive envelope (over budget)`,
528
+ sendFailureMessage: "Flare: failed to send buffered log records",
529
+ resourceForFlush: () => this.resourceForFlush(),
530
+ buildEnvelope: (records, resource) => {
531
+ const sdk = deps.getSdkInfo();
532
+ return buildLogsEnvelope(records, resource, sdk.name, sdk.version);
533
+ },
534
+ send: (envelope, config, keepalive) => {
535
+ deps.track(deps.api.logs(envelope, config.logsIngestUrl, config.key, config.debug, keepalive));
536
+ },
537
+ onRecordBuffered: (record) => {
538
+ this.resourceAttributes = record.resourceAttributes;
539
+ }
540
+ });
366
541
  }
367
542
  debug(message, context = {}, attributes = {}) {
368
543
  this.record("debug", message, context, attributes);
@@ -389,7 +564,13 @@ var Logger = class {
389
564
  this.record("emergency", message, context, attributes);
390
565
  }
391
566
  bufferLength() {
392
- return this.buffer.length;
567
+ return this.inner.length();
568
+ }
569
+ flush(opts) {
570
+ this.inner.flush(opts);
571
+ }
572
+ clear() {
573
+ this.inner.clear();
393
574
  }
394
575
  record(level, message, context, attributes) {
395
576
  const config = this.deps.getConfig();
@@ -400,114 +581,20 @@ var Logger = class {
400
581
  ...attributes
401
582
  };
402
583
  const { record, resource } = this.deps.buildLogAttributes(userAttributes);
403
- const buffered = {
584
+ this.inner.add({
404
585
  timeUnixNano: String(Date.now()) + "000000",
405
586
  severityNumber: severityNumber(level),
406
587
  severityText: severityText(level),
407
588
  message,
408
589
  recordAttributes: attributesToOpenTelemetry(record),
409
590
  resourceAttributes: resource
410
- };
411
- if (this.estimateBytes(buffered) > config.logFlushMaxBytes) {
412
- if (config.debug) console.error("Flare: dropping oversized log record");
413
- return;
414
- }
415
- this.buffer.push(buffered);
416
- this.resourceAttributes = resource;
417
- this.evaluateTriggers(config);
418
- this.trim(config);
419
- }
420
- evaluateTriggers(config) {
421
- if (this.buffer.length >= config.maxLogBufferSize) {
422
- this.flush();
423
- return;
424
- }
425
- if (this.bufferBytes() >= config.logFlushMaxBytes) {
426
- this.flush();
427
- return;
428
- }
429
- this.armTimer(config);
430
- }
431
- armTimer(config) {
432
- if (this.timerActive) return;
433
- this.timerActive = true;
434
- this.timer = setTimeout(() => this.flush(), config.logFlushIntervalMs);
435
- this.timer.unref?.();
436
- }
437
- trim(config) {
438
- if (this.buffer.length > config.maxLogBufferSize) this.buffer = this.buffer.slice(this.buffer.length - config.maxLogBufferSize);
439
- while (this.buffer.length > 1 && this.bufferBytes() > config.logFlushMaxBytes) this.buffer.shift();
440
- }
441
- flush(opts) {
442
- const config = this.deps.getConfig();
443
- if (!config.enableLogs) return;
444
- if (this.buffer.length === 0) return;
445
- if (!assertKey(config.key, config.debug)) {
446
- this.clearTimer();
447
- return;
448
- }
449
- this.clearTimer();
450
- let records;
451
- if (opts?.keepalive) {
452
- records = this.packForKeepalive(config);
453
- this.buffer = this.buffer.filter((log) => !records.includes(log));
454
- if (this.buffer.length > 0) this.armTimer(config);
455
- } else {
456
- records = this.buffer;
457
- this.buffer = [];
458
- }
459
- if (records.length === 0) return;
460
- this.deps.track(this.deps.api.logs(this.buildEnvelope(records), config.logsIngestUrl, config.key, config.debug, !!opts?.keepalive));
461
- }
462
- clear() {
463
- this.buffer = [];
464
- this.clearTimer();
465
- }
466
- packForKeepalive(config) {
467
- let selected = [];
468
- for (let i = this.buffer.length - 1; i >= 0; i--) {
469
- const trial = [this.buffer[i], ...selected];
470
- if (new TextEncoder().encode(flatJsonStringify(this.buildEnvelope(trial))).length <= config.keepaliveMaxBytes) selected = trial;
471
- else if (config.debug) console.error("Flare: dropping log record from keepalive envelope (over budget)");
472
- }
473
- return selected;
474
- }
475
- buildEnvelope(records) {
476
- const sdk = this.deps.getSdkInfo();
477
- return buildLogsEnvelope(records, this.resourceForFlush(), sdk.name, sdk.version);
591
+ });
478
592
  }
479
593
  resourceForFlush() {
480
- const config = this.deps.getConfig();
481
- const sdk = this.deps.getSdkInfo();
482
- const framework = this.deps.getFramework();
483
- const identity = {
484
- "telemetry.sdk.language": "javascript",
485
- "telemetry.sdk.name": sdk.name,
486
- "telemetry.sdk.version": sdk.version,
487
- "flare.language.name": "javascript"
488
- };
489
- if (config.serviceName) identity["service.name"] = config.serviceName;
490
- if (config.version) identity["service.version"] = config.version;
491
- if (config.stage) identity["service.stage"] = config.stage;
492
- if (framework?.name) identity["flare.framework.name"] = framework.name;
493
- if (framework?.version) identity["flare.framework.version"] = framework.version;
494
- return {
495
- ...this.resourceAttributes,
496
- ...identity
497
- };
498
- }
499
- clearTimer() {
500
- if (this.timer) {
501
- clearTimeout(this.timer);
502
- this.timer = void 0;
503
- }
504
- this.timerActive = false;
594
+ return buildResourceIdentity(this.resourceAttributes, this.deps.getConfig(), this.deps.getSdkInfo(), this.deps.getFramework());
505
595
  }
506
596
  estimateBytes(log) {
507
- return flatJsonStringify(log).length;
508
- }
509
- bufferBytes() {
510
- return this.buffer.reduce((sum, log) => sum + this.estimateBytes(log), 0);
597
+ return require_urlAttributes.flatJsonStringify(log).length;
511
598
  }
512
599
  };
513
600
 
@@ -542,31 +629,17 @@ function partitionAttributes(attributes) {
542
629
 
543
630
  //#endregion
544
631
  //#region src/Scope.ts
545
- /**
546
- * Maps each `User` identity field to the flat report attribute key it projects to.
547
- * `Flare.setUser`'s set pass writes through these so the literal key strings live in
548
- * exactly one place; `USER_IDENTITY_KEYS` (the clear pass) derives from them, so adding
549
- * a field here can never silently leave the clear pass out of date.
550
- */
632
+ /** `USER_IDENTITY_KEYS` derives from this, so adding a field here can never leave the clear pass stale. */
551
633
  const USER_FIELD_KEYS = {
552
634
  id: "user.id",
553
635
  email: "user.email",
554
636
  fullName: "user.full_name",
555
637
  ipAddress: "client.address"
556
638
  };
557
- /**
558
- * The report attribute keys that `Flare.setUser` owns: the four projected identity
559
- * fields plus the `user.attributes` bag for extras. Single source of truth so the
560
- * clear pass and the set pass in `setUser` cannot drift, and so consumers that must
561
- * stamp identity outside core's report pipeline (Electron's forwarded-renderer path)
562
- * pick up the exact same set instead of re-hardcoding it.
563
- */
639
+ /** Every key `Flare.setUser` owns. Consumers stamping identity outside core's report pipeline (Electron's
640
+ * forwarded-renderer path) reuse this exact set. */
564
641
  const USER_IDENTITY_KEYS = [...Object.values(USER_FIELD_KEYS), "user.attributes"];
565
- /**
566
- * Pick the user-identity attributes currently set on a scope. Used where identity must
567
- * be copied onto a report that does not flow through `Flare.report()` (which would
568
- * otherwise spread `pendingAttributes` automatically).
569
- */
642
+ /** For reports that do not flow through `Flare.report()`, which would spread `pendingAttributes` itself. */
570
643
  function userIdentityAttributes(scope) {
571
644
  const attrs = {};
572
645
  for (const key of USER_IDENTITY_KEYS) {
@@ -576,39 +649,16 @@ function userIdentityAttributes(scope) {
576
649
  return attrs;
577
650
  }
578
651
  /**
579
- * Holds the per-call mutable state that used to live on the `Flare` instance:
580
- * breadcrumbs (`glows`), custom attributes (`pendingAttributes`), and the
581
- * current entry-point handler.
582
- *
583
- * Why this exists as its own class: in the browser there is one `Flare` per
584
- * page and one user at a time, so a single shared bag of state is fine. In
585
- * Node, a single `Flare` instance serves many concurrent requests, and each
586
- * request wants its own breadcrumbs and its own custom context that do NOT
587
- * leak into other requests. Splitting this state out of `Flare` lets the
588
- * consumer choose: one global `Scope` (browser) or one `Scope` per request
589
- * via AsyncLocalStorage (Node).
590
- *
591
- * `Flare` reads and writes this through `scopeProvider.active()` instead of
592
- * holding the state directly, so the per-request behavior comes from the
593
- * provider, not from the class itself.
594
- *
595
- * `NodeScope` (in `@flareapp/node`) extends this with a `request` bucket
596
- * (HTTP method, path, headers). User identity is written to `pendingAttributes`
597
- * by `Flare.setUser`, so it needs no dedicated field. Browser does not need `request`.
652
+ * Per-call mutable state, split out of `Flare` so the consumer can choose one global `Scope` (browser, one
653
+ * user at a time) or one per request via AsyncLocalStorage (Node, where concurrent requests must not leak
654
+ * into each other). `@flareapp/node`'s `NodeScope` extends this with a `request` bucket.
598
655
  */
599
656
  var Scope = class {
600
657
  glows = [];
658
+ breadcrumbs = [];
601
659
  pendingAttributes = {};
602
660
  entryPoint = null;
603
- /**
604
- * Append a breadcrumb. Caps the list at `maxGlowsPerReport` by dropping the
605
- * OLDEST entries when the limit is exceeded; this keeps reports below a
606
- * payload-size threshold while preserving the most recent events leading
607
- * up to an error.
608
- *
609
- * `slice(length - max)` returns the trailing `max` items, which is the
610
- * shortest way to drop from the front and keep insertion order.
611
- */
661
+ /** Caps at `maxGlowsPerReport` by dropping the oldest, so the payload stays bounded. */
612
662
  addGlow(glow, maxGlowsPerReport) {
613
663
  this.glows.push(glow);
614
664
  if (this.glows.length > maxGlowsPerReport) this.glows = this.glows.slice(this.glows.length - maxGlowsPerReport);
@@ -616,29 +666,24 @@ var Scope = class {
616
666
  clearGlows() {
617
667
  this.glows = [];
618
668
  }
619
- /**
620
- * Set a single attribute on this scope. Called from `Flare.addContext` and
621
- * `Flare.addContextGroup`. Last write wins.
622
- */
669
+ /** Drops the oldest when full. */
670
+ addBreadcrumb(breadcrumb, maxBreadcrumbs) {
671
+ if (maxBreadcrumbs <= 0) return;
672
+ this.breadcrumbs.push(breadcrumb);
673
+ if (this.breadcrumbs.length > maxBreadcrumbs) this.breadcrumbs = this.breadcrumbs.slice(this.breadcrumbs.length - maxBreadcrumbs);
674
+ }
675
+ clearBreadcrumbs() {
676
+ this.breadcrumbs = [];
677
+ }
623
678
  setAttribute(key, value) {
624
679
  this.pendingAttributes[key] = value;
625
680
  }
626
- /**
627
- * Shallow-merge a bag of attributes into this scope. Used by Node's
628
- * AsyncLocalStorage provider when patching the live request context via
629
- * `flare.mergeContext({ ... })`. Last write wins per key; nested objects
630
- * are NOT deep-merged.
631
- */
681
+ /** Shallow: last write wins per key, nested objects are not deep-merged. */
632
682
  mergeAttributes(partial) {
633
683
  Object.assign(this.pendingAttributes, partial);
634
684
  }
635
685
  };
636
- /**
637
- * The simplest provider: one `Scope` for the lifetime of the provider, shared
638
- * by every caller. This is the right default for environments with a single
639
- * logical context (browser tab, CLI script, etc.) and is the default that
640
- * `Flare`'s constructor falls back to when no provider is supplied.
641
- */
686
+ /** One `Scope` for the provider's lifetime. The right default for a browser tab or a CLI script. */
642
687
  var GlobalScopeProvider = class {
643
688
  scope = new Scope();
644
689
  active() {
@@ -665,11 +710,14 @@ function getCodeSnippet(fileReader, url, lineNumber, columnNumber) {
665
710
  });
666
711
  }
667
712
  function readFile(fileReader, url) {
668
- if (cachedFiles[url] !== void 0) return Promise.resolve(cachedFiles[url]);
669
- return fileReader.read(url).then((text) => {
670
- if (text !== null) cachedFiles[url] = text;
713
+ const cached = cachedFiles[url];
714
+ if (cached !== void 0) return cached;
715
+ const pending = fileReader.read(url).then((text) => {
716
+ if (text === null) delete cachedFiles[url];
671
717
  return text;
672
718
  });
719
+ cachedFiles[url] = pending;
720
+ return pending;
673
721
  }
674
722
  function readLinesFromFile(fileText, lineNumber, columnNumber, maxSnippetLineLength = 1e3, maxSnippetLines = 40) {
675
723
  const codeSnippet = {};
@@ -709,7 +757,7 @@ function createStackTrace(error, debug, fileReader) {
709
757
  try {
710
758
  parsedFrames = error_stack_parser.default.parse(error);
711
759
  } catch (parseError) {
712
- assert(false, "Couldn't parse stacktrace of below error:", debug);
760
+ require_urlAttributes.assert(false, "Couldn't parse stacktrace of below error:", debug);
713
761
  if (debug) {
714
762
  console.error(parseError);
715
763
  console.error(error);
@@ -761,25 +809,11 @@ function isApplicationFrame(fileName) {
761
809
  //#endregion
762
810
  //#region src/stacktrace/NullFileReader.ts
763
811
  /**
764
- * No-op `FileReader` that returns `null` for every URL it is asked to read.
765
- *
766
- * Used as the default for `Flare`'s `fileReader` constructor parameter so the
767
- * class is usable without picking a side: instantiated bare (`new Flare()`),
768
- * reports still build, but stack frames omit source-code snippets — which is
769
- * the correct, safe behavior in an environment we know nothing about.
770
- *
771
- * The two real implementations live in the consumer packages and take their
772
- * place once the right environment is established:
773
- *
774
- * - `@flareapp/js` injects `FetchFileReader`, which `fetch()`s source maps
775
- * and original files over HTTP for browser stack frames.
776
- * - `@flareapp/node` injects `DiskFileReader`, which reads files from disk
777
- * via `node:fs/promises` for server stack frames.
778
- *
779
- * The interface (`read(url) -> Promise<string | null>`) lets the stack-trace
780
- * builder treat all three the same way: ask for a URL, render the snippet
781
- * when text comes back, gracefully skip it when `null` does. No environment
782
- * checks anywhere in core.
812
+ * No-op `FileReader` returning `null` for every URL. Default for `Flare`'s `fileReader` param so `new Flare()` builds
813
+ * reports without picking an environment; stack frames just omit source snippets. Consumer packages inject the real
814
+ * ones: `@flareapp/js` a fetch-based reader, `@flareapp/node` a disk reader. The `read(url) -> Promise<string | null>`
815
+ * interface lets the stack-trace builder treat all three the same (render on text, skip on null), so core needs no
816
+ * environment checks.
783
817
  */
784
818
  var NullFileReader = class {
785
819
  read(_url) {
@@ -787,22 +821,636 @@ var NullFileReader = class {
787
821
  }
788
822
  };
789
823
 
824
+ //#endregion
825
+ //#region src/tracing/context.ts
826
+ var InMemoryActiveSpanHolder = class {
827
+ active;
828
+ root;
829
+ getActive() {
830
+ return this.active ?? this.root;
831
+ }
832
+ withActive(span, fn) {
833
+ const previous = this.active;
834
+ this.active = span;
835
+ try {
836
+ return fn();
837
+ } finally {
838
+ this.active = previous;
839
+ }
840
+ }
841
+ setActiveRoot(span) {
842
+ this.root = span;
843
+ }
844
+ };
845
+
846
+ //#endregion
847
+ //#region src/tracing/ids.ts
848
+ function randomHex(bytes) {
849
+ const randomBytes = new Uint8Array(bytes);
850
+ const cryptoApi = globalThis.crypto;
851
+ if (cryptoApi && typeof cryptoApi.getRandomValues === "function") cryptoApi.getRandomValues(randomBytes);
852
+ else for (let i = 0; i < bytes; i++) randomBytes[i] = Math.floor(Math.random() * 256);
853
+ if (randomBytes.every((b) => b === 0)) randomBytes[bytes - 1] = 1;
854
+ let out = "";
855
+ for (let i = 0; i < bytes; i++) out += randomBytes[i].toString(16).padStart(2, "0");
856
+ return out;
857
+ }
858
+ function traceId() {
859
+ return randomHex(16);
860
+ }
861
+ function spanId() {
862
+ return randomHex(8);
863
+ }
864
+
865
+ //#endregion
866
+ //#region src/tracing/sampler.ts
867
+ function resolveSampling(samplingContext, config, rng = Math.random) {
868
+ if (samplingContext.parentSampled !== void 0) return samplingContext.parentSampled;
869
+ let rate;
870
+ if (config.tracesSampler) {
871
+ let result;
872
+ try {
873
+ result = config.tracesSampler(samplingContext);
874
+ } catch (error) {
875
+ if (config.debug) console.error("Flare: tracesSampler threw, treating span as not sampled", error);
876
+ return false;
877
+ }
878
+ if (typeof result === "boolean") return result;
879
+ rate = result;
880
+ } else rate = config.tracesSampleRate;
881
+ rate = Math.max(0, Math.min(1, rate));
882
+ if (rate <= 0) return false;
883
+ if (rate >= 1) return true;
884
+ return rng() < rate;
885
+ }
886
+
887
+ //#endregion
888
+ //#region src/tracing/Span.ts
889
+ var SpanImpl = class {
890
+ traceId;
891
+ spanId;
892
+ parentSpanId;
893
+ name;
894
+ isRecording;
895
+ epoch;
896
+ stateGeneration;
897
+ startTimeUnixNano;
898
+ scopeAttributes;
899
+ endTimeUnixNano = 0;
900
+ status = { code: SpanStatusCode.Unset };
901
+ attributes = {};
902
+ droppedAttributesCount = 0;
903
+ events = [];
904
+ droppedEventsCount = 0;
905
+ ended = false;
906
+ constructor(init, deps) {
907
+ this.deps = deps;
908
+ this.traceId = init.traceId;
909
+ this.spanId = init.spanId;
910
+ this.parentSpanId = init.parentSpanId;
911
+ this.name = init.name;
912
+ this.isRecording = init.recording;
913
+ this.epoch = init.epoch;
914
+ this.stateGeneration = init.stateGeneration;
915
+ this.startTimeUnixNano = init.startTimeUnixNano;
916
+ this.scopeAttributes = init.scopeAttributes;
917
+ }
918
+ setAttribute(key, value) {
919
+ if (this.ended) return this;
920
+ if (!(key in this.attributes) && Object.keys(this.attributes).length >= this.deps.maxAttributesPerSpan) {
921
+ this.droppedAttributesCount++;
922
+ return this;
923
+ }
924
+ this.attributes[key] = value;
925
+ return this;
926
+ }
927
+ setStatus(status) {
928
+ if (!this.ended) this.status = status;
929
+ return this;
930
+ }
931
+ addEvent(name, attributes = {}) {
932
+ if (this.ended) return this;
933
+ if (this.events.length >= this.deps.maxEventsPerSpan) {
934
+ this.droppedEventsCount++;
935
+ return this;
936
+ }
937
+ const capped = {};
938
+ let dropped = 0;
939
+ for (const [key, value] of Object.entries(attributes)) {
940
+ if (Object.keys(capped).length >= this.deps.maxAttributesPerSpanEvent) {
941
+ dropped++;
942
+ continue;
943
+ }
944
+ capped[key] = value;
945
+ }
946
+ this.events.push({
947
+ name,
948
+ timeUnixNano: this.deps.now(),
949
+ attributes: capped,
950
+ droppedAttributesCount: dropped
951
+ });
952
+ return this;
953
+ }
954
+ end(endTimeUnixNano) {
955
+ if (this.ended) return;
956
+ this.ended = true;
957
+ this.endTimeUnixNano = endTimeUnixNano ?? this.deps.now();
958
+ this.deps.onEnd(this);
959
+ }
960
+ };
961
+
962
+ //#endregion
963
+ //#region src/tracing/envelope.ts
964
+ function toOtelSpan(span) {
965
+ const status = span.status.message !== void 0 ? {
966
+ code: span.status.code,
967
+ message: span.status.message
968
+ } : { code: span.status.code };
969
+ return {
970
+ traceId: span.traceId,
971
+ spanId: span.spanId,
972
+ parentSpanId: span.parentSpanId,
973
+ name: span.name,
974
+ startTimeUnixNano: span.startTimeUnixNano,
975
+ endTimeUnixNano: span.endTimeUnixNano,
976
+ status,
977
+ attributes: span.recordAttributes,
978
+ events: span.events,
979
+ droppedAttributesCount: span.droppedAttributesCount,
980
+ droppedEventsCount: span.droppedEventsCount,
981
+ links: [],
982
+ droppedLinksCount: 0
983
+ };
984
+ }
985
+ function buildTracesEnvelope(spans, resourceAttributes, scopeName, scopeVersion) {
986
+ return { resourceSpans: [{
987
+ resource: {
988
+ attributes: attributesToOpenTelemetry(resourceAttributes),
989
+ droppedAttributesCount: 0
990
+ },
991
+ scopeSpans: [{
992
+ scope: {
993
+ name: scopeName,
994
+ version: scopeVersion,
995
+ attributes: [],
996
+ droppedAttributesCount: 0
997
+ },
998
+ spans: spans.map(toOtelSpan)
999
+ }]
1000
+ }] };
1001
+ }
1002
+ /**
1003
+ * How many UTF-8 bytes one span adds to an envelope. We measure the real toOtelSpan output instead of reusing
1004
+ * the cached BufferedSpan estimate, because keepaliveMaxBytes is a hard browser limit and an estimate is not
1005
+ * good enough.
1006
+ *
1007
+ * We use flatJsonStringify instead of JSON.stringify because a span keeps values the caller still owns, like
1008
+ * status.message, and those can turn unserializable after the span ended. This runs from a visibilitychange
1009
+ * listener with no try/catch around it, so a throw here loses the flush. flatJsonStringify handles the usual
1010
+ * suspects (circular references, BigInt, a getter that throws on a plain object) but is not bulletproof: a
1011
+ * class instance with a throwing getter goes through untouched and can still throw.
1012
+ */
1013
+ function otelSpanBytes(span) {
1014
+ return utf8Bytes(require_urlAttributes.flatJsonStringify(toOtelSpan(span)));
1015
+ }
1016
+ /** UTF-8 bytes of an empty envelope: the fixed overhead every batch has, before any spans are added. */
1017
+ function emptyTracesEnvelopeBytes(resourceAttributes, scopeName, scopeVersion) {
1018
+ return utf8Bytes(JSON.stringify(buildTracesEnvelope([], resourceAttributes, scopeName, scopeVersion)));
1019
+ }
1020
+
1021
+ //#endregion
1022
+ //#region src/tracing/SpanBuffer.ts
1023
+ /** The span half of the shared telemetry buffer: names the config keys, the envelope and the ingest call. */
1024
+ var SpanBuffer = class {
1025
+ inner;
1026
+ constructor(deps) {
1027
+ this.deps = deps;
1028
+ this.inner = new TelemetryBuffer({
1029
+ getConfig: deps.getConfig,
1030
+ scheduler: deps.scheduler
1031
+ }, {
1032
+ limits: (config) => ({
1033
+ maxSize: config.maxSpanBufferSize,
1034
+ maxBytes: config.spanFlushMaxBytes,
1035
+ flushIntervalMs: config.spanFlushIntervalMs
1036
+ }),
1037
+ enabled: (config) => config.enableTracing,
1038
+ keepaliveBudget: () => deps.api.keepaliveBudgetRemaining(),
1039
+ estimateBytes: (span) => this.estimateBytes(span),
1040
+ emptyEnvelopeBytes: (resource) => {
1041
+ const sdk = deps.getSdkInfo();
1042
+ return emptyTracesEnvelopeBytes(resource, sdk.name, sdk.version);
1043
+ },
1044
+ recordBytes: (span) => otelSpanBytes(span),
1045
+ oversizedMessage: "Flare: dropping oversized span",
1046
+ keepaliveDropMessage: (count) => `Flare: dropped ${count} span(s) from keepalive envelope (over budget)`,
1047
+ sendFailureMessage: "Flare: failed to send buffered spans",
1048
+ resourceForFlush: () => this.resourceForFlush(),
1049
+ buildEnvelope: (spans, resource) => {
1050
+ const sdk = deps.getSdkInfo();
1051
+ return buildTracesEnvelope(spans, resource, sdk.name, sdk.version);
1052
+ },
1053
+ send: (envelope, config, keepalive) => {
1054
+ deps.track(deps.api.traces(envelope, config.tracesIngestUrl, config.key, config.debug, keepalive));
1055
+ }
1056
+ });
1057
+ }
1058
+ length() {
1059
+ return this.inner.length();
1060
+ }
1061
+ add(span) {
1062
+ this.inner.add(span);
1063
+ }
1064
+ flush(opts) {
1065
+ this.inner.flush(opts);
1066
+ }
1067
+ clear() {
1068
+ this.inner.clear();
1069
+ }
1070
+ resourceForFlush() {
1071
+ return buildResourceIdentity(this.deps.getResourceAttributes(), this.deps.getConfig(), this.deps.getSdkInfo(), this.deps.getFramework());
1072
+ }
1073
+ estimateBytes(span) {
1074
+ return JSON.stringify(span).length;
1075
+ }
1076
+ };
1077
+
1078
+ //#endregion
1079
+ //#region src/tracing/traceparent.ts
1080
+ const HEX32 = /^[0-9a-f]{32}$/;
1081
+ const HEX16 = /^[0-9a-f]{16}$/;
1082
+ const HEX8 = /^[0-9a-f]{2}$/;
1083
+ const ZERO32 = "0".repeat(32);
1084
+ const ZERO16 = "0".repeat(16);
1085
+ function buildTraceparent(traceId, spanId, sampled) {
1086
+ return `00-${traceId}-${spanId}-${sampled ? "01" : "00"}`;
1087
+ }
1088
+ function parseTraceparent(header) {
1089
+ const parts = header.trim().split("-");
1090
+ if (parts.length !== 4) return null;
1091
+ const [version, traceId, spanId, flags] = parts;
1092
+ if (version !== "00") return null;
1093
+ if (!HEX32.test(traceId) || traceId === ZERO32) return null;
1094
+ if (!HEX16.test(spanId) || spanId === ZERO16) return null;
1095
+ if (!HEX8.test(flags)) return null;
1096
+ return {
1097
+ traceId,
1098
+ parentSpanId: spanId,
1099
+ sampled: (parseInt(flags, 16) & 1) === 1
1100
+ };
1101
+ }
1102
+
1103
+ //#endregion
1104
+ //#region src/tracing/Tracer.ts
1105
+ function isPromiseLike(value) {
1106
+ return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
1107
+ }
1108
+ /** `SpanOptions.parent` is a structurally overlapping union; `isRecording` is what tells a real Span apart. */
1109
+ function isSpan(parent) {
1110
+ return "isRecording" in parent;
1111
+ }
1112
+ /** A SpanImpl carries the epoch it was created under; a hand-stitched `{traceId, spanId}` parent does not. */
1113
+ function hasEpoch(parent) {
1114
+ return "epoch" in parent && typeof parent.epoch === "number";
1115
+ }
1116
+ function defaultNowNano() {
1117
+ const performanceApi = globalThis.performance;
1118
+ const ms = performanceApi && typeof performanceApi.now === "function" && typeof performanceApi.timeOrigin === "number" ? performanceApi.timeOrigin + performanceApi.now() : Date.now();
1119
+ return Math.round(ms * 1e6);
1120
+ }
1121
+ /**
1122
+ * Both trace maps cap their size the same way: insertion order is LRU, so the first key is the one to drop.
1123
+ * Only evicts when `key` is not already in the map. A set() that overwrites an existing key does not grow the
1124
+ * map, so it must not evict an unrelated entry to make room for it.
1125
+ */
1126
+ function evictLruIfNew(map, key, cap) {
1127
+ if (map.has(key) || map.size < cap) return;
1128
+ const lru = map.keys().next().value;
1129
+ if (lru !== void 0) map.delete(lru);
1130
+ }
1131
+ const MAX_CLOSED_TRACES = 100;
1132
+ /** Bounded backstop for the live TraceState map: an app that never ends spans must not grow it forever. */
1133
+ const DEFAULT_MAX_LIVE_TRACES = 1e3;
1134
+ var Tracer = class {
1135
+ buffer;
1136
+ holder;
1137
+ traceStates = /* @__PURE__ */ new Map();
1138
+ closedTraces = /* @__PURE__ */ new Map();
1139
+ stateGeneration = 0;
1140
+ now;
1141
+ rng;
1142
+ maxLiveTraces;
1143
+ epoch = 0;
1144
+ pendingContinuation = null;
1145
+ spanListeners = /* @__PURE__ */ new Set();
1146
+ constructor(deps) {
1147
+ this.deps = deps;
1148
+ this.buffer = new SpanBuffer({
1149
+ api: deps.api,
1150
+ getConfig: deps.getConfig,
1151
+ getSdkInfo: deps.getSdkInfo,
1152
+ getFramework: deps.getFramework,
1153
+ getResourceAttributes: deps.getResourceAttributes,
1154
+ track: deps.track,
1155
+ scheduler: deps.scheduler
1156
+ });
1157
+ this.holder = deps.activeSpanHolder ?? new InMemoryActiveSpanHolder();
1158
+ this.now = deps.now ?? defaultNowNano;
1159
+ this.rng = deps.rng ?? Math.random;
1160
+ this.maxLiveTraces = deps.maxLiveTraces ?? DEFAULT_MAX_LIVE_TRACES;
1161
+ }
1162
+ getActiveSpan() {
1163
+ return this.holder.getActive();
1164
+ }
1165
+ setActiveRoot(span) {
1166
+ this.holder.setActiveRoot?.(span);
1167
+ }
1168
+ /**
1169
+ * Take one span against `traceId`'s cap up front, for a caller that publishes a span id before the
1170
+ * span exists (the component profilers do; their descendants record first). False means the trace is
1171
+ * full and the caller should stay transparent instead of handing out an id the cap will refuse.
1172
+ * Consumed by the matching `startSpan({ claimed: true })`.
1173
+ */
1174
+ claimSpanSlot(traceId) {
1175
+ const config = this.deps.getConfig();
1176
+ if (!config.enableTracing) return false;
1177
+ const state = this.traceStates.get(traceId);
1178
+ if (!state || !state.recording || state.startedSpanCount >= config.maxSpansPerTrace) return false;
1179
+ state.startedSpanCount++;
1180
+ return true;
1181
+ }
1182
+ addSpanListener(fn) {
1183
+ this.spanListeners.add(fn);
1184
+ return () => {
1185
+ this.spanListeners.delete(fn);
1186
+ };
1187
+ }
1188
+ emitSpanEvent(phase, span) {
1189
+ for (const fn of this.spanListeners) try {
1190
+ fn({
1191
+ phase,
1192
+ span
1193
+ });
1194
+ } catch {}
1195
+ }
1196
+ flush(opts) {
1197
+ this.buffer.flush(opts);
1198
+ }
1199
+ clear() {
1200
+ this.buffer.clear();
1201
+ this.traceStates.clear();
1202
+ this.closedTraces.clear();
1203
+ this.setActiveRoot(void 0);
1204
+ this.pendingContinuation = null;
1205
+ this.epoch++;
1206
+ }
1207
+ continueFromTraceparent(header) {
1208
+ this.pendingContinuation = parseTraceparent(header);
1209
+ }
1210
+ /**
1211
+ * Runs `fn` with the span active, so spans started inside auto-parent to it, then ends it.
1212
+ * Records an error status first if `fn` throws or its returned promise rejects.
1213
+ */
1214
+ withSpan(name, fn, opts = {}) {
1215
+ const span = this.startSpan(name, opts);
1216
+ const finishError = (error) => {
1217
+ span.setStatus({
1218
+ code: SpanStatusCode.Error,
1219
+ message: error instanceof Error ? error.message : String(error)
1220
+ });
1221
+ span.end();
1222
+ };
1223
+ return this.holder.withActive(span, () => {
1224
+ try {
1225
+ const result = fn(span);
1226
+ if (isPromiseLike(result)) return result.then((value) => {
1227
+ span.end();
1228
+ return value;
1229
+ }, (error) => {
1230
+ finishError(error);
1231
+ throw error;
1232
+ });
1233
+ span.end();
1234
+ return result;
1235
+ } catch (error) {
1236
+ finishError(error);
1237
+ throw error;
1238
+ }
1239
+ });
1240
+ }
1241
+ /** Starts a span the caller must end. Unlike `withSpan`, it does not become the active span,
1242
+ * so spans started after it do not auto-parent to it. */
1243
+ startSpan(name, opts = {}) {
1244
+ const config = this.deps.getConfig();
1245
+ const spanId$1 = opts.spanId ?? spanId();
1246
+ const continuation = this.pendingContinuation;
1247
+ this.pendingContinuation = null;
1248
+ if (!config.enableTracing) return this.startInertSpan(name, spanId$1, opts, config);
1249
+ const { traceId, parentSpanId, state } = this.resolveTrace(spanId$1, name, opts, config, continuation);
1250
+ let recording = state.recording;
1251
+ if (opts.claimed) {} else if (state.startedSpanCount >= config.maxSpansPerTrace) {
1252
+ recording = false;
1253
+ if (config.debug && !state.loggedCap) {
1254
+ state.loggedCap = true;
1255
+ console.error("Flare: maxSpansPerTrace reached, dropping span");
1256
+ }
1257
+ } else state.startedSpanCount++;
1258
+ state.openSpanCount++;
1259
+ const isLocalRoot = state.localRootSpanId === spanId$1;
1260
+ const span = this.makeSpan({
1261
+ traceId,
1262
+ spanId: spanId$1,
1263
+ parentSpanId,
1264
+ name,
1265
+ recording,
1266
+ isLocalRoot,
1267
+ stateGeneration: state.generation
1268
+ }, opts, config);
1269
+ this.emitSpanEvent("start", span);
1270
+ return span;
1271
+ }
1272
+ /** A real Span handle that records nothing, so callers never have to branch on whether tracing is on. */
1273
+ startInertSpan(name, spanId, opts, config) {
1274
+ const span = this.makeSpan({
1275
+ traceId: traceId(),
1276
+ spanId,
1277
+ parentSpanId: null,
1278
+ name,
1279
+ recording: false,
1280
+ isLocalRoot: true,
1281
+ stateGeneration: 0
1282
+ }, opts, config);
1283
+ this.emitSpanEvent("start", span);
1284
+ return span;
1285
+ }
1286
+ resolveTrace(spanId, name, opts, config, continuation) {
1287
+ let parent = opts.forceRoot ? opts.parent : opts.parent ?? this.holder.getActive();
1288
+ if (parent && hasEpoch(parent) && parent.epoch !== this.epoch) parent = void 0;
1289
+ if (parent && "spanId" in parent && "traceId" in parent) {
1290
+ const traceId = parent.traceId;
1291
+ const fallbackRecording = () => isSpan(parent) ? parent.isRecording : resolveSampling({
1292
+ name,
1293
+ attributes: opts.attributes ?? {},
1294
+ spanType: opts.spanType
1295
+ }, config, this.rng);
1296
+ const state = this.getOrSeedState(traceId, spanId, fallbackRecording);
1297
+ return {
1298
+ traceId,
1299
+ parentSpanId: parent.spanId,
1300
+ state
1301
+ };
1302
+ }
1303
+ if (continuation) {
1304
+ const recording = resolveSampling({
1305
+ name,
1306
+ parentSampled: continuation.sampled,
1307
+ attributes: opts.attributes ?? {},
1308
+ spanType: opts.spanType
1309
+ }, config, this.rng);
1310
+ const state = this.createState(continuation.traceId, spanId, recording);
1311
+ return {
1312
+ traceId: continuation.traceId,
1313
+ parentSpanId: continuation.parentSpanId,
1314
+ state
1315
+ };
1316
+ }
1317
+ const traceId$1 = traceId();
1318
+ const recording = resolveSampling({
1319
+ name,
1320
+ attributes: opts.attributes ?? {},
1321
+ spanType: opts.spanType
1322
+ }, config, this.rng);
1323
+ return {
1324
+ traceId: traceId$1,
1325
+ parentSpanId: null,
1326
+ state: this.createState(traceId$1, spanId, recording)
1327
+ };
1328
+ }
1329
+ getOrSeedState(traceId, localRootSpanId, fallbackRecording) {
1330
+ const existing = this.traceStates.get(traceId);
1331
+ if (existing) {
1332
+ this.traceStates.delete(traceId);
1333
+ this.traceStates.set(traceId, existing);
1334
+ return existing;
1335
+ }
1336
+ const closed = this.closedTraces.get(traceId);
1337
+ if (closed) {
1338
+ this.closedTraces.delete(traceId);
1339
+ const state = this.createState(traceId, closed.localRootSpanId, closed.recording);
1340
+ state.startedSpanCount = closed.startedSpanCount;
1341
+ state.rootEnded = true;
1342
+ return state;
1343
+ }
1344
+ return this.createState(traceId, localRootSpanId, fallbackRecording());
1345
+ }
1346
+ createState(traceId, localRootSpanId, recording) {
1347
+ evictLruIfNew(this.traceStates, traceId, this.maxLiveTraces);
1348
+ const state = {
1349
+ traceId,
1350
+ recording,
1351
+ localRootSpanId,
1352
+ rootEnded: false,
1353
+ startedSpanCount: 0,
1354
+ openSpanCount: 0,
1355
+ generation: ++this.stateGeneration,
1356
+ loggedCap: false
1357
+ };
1358
+ this.traceStates.set(traceId, state);
1359
+ return state;
1360
+ }
1361
+ makeSpan(init, opts, config) {
1362
+ const scopeAttributes = init.recording && init.isLocalRoot ? this.deps.getScopeAttributes() : {};
1363
+ const span = new SpanImpl({
1364
+ ...init,
1365
+ startTimeUnixNano: opts.startTimeUnixNano ?? this.now(),
1366
+ epoch: this.epoch,
1367
+ scopeAttributes
1368
+ }, {
1369
+ maxAttributesPerSpan: config.maxAttributesPerSpan,
1370
+ maxEventsPerSpan: config.maxEventsPerSpan,
1371
+ maxAttributesPerSpanEvent: config.maxAttributesPerSpanEvent,
1372
+ now: this.now,
1373
+ onEnd: (s) => this.onSpanEnd(s)
1374
+ });
1375
+ if (opts.spanType) span.setAttribute("flare.span_type", opts.spanType);
1376
+ if (opts.attributes) for (const [key, value] of Object.entries(opts.attributes)) span.setAttribute(key, value);
1377
+ return span;
1378
+ }
1379
+ /** Bounded, LRU by insertion order, like traceStates. Holds primitives only, never a span. */
1380
+ rememberClosed(state) {
1381
+ evictLruIfNew(this.closedTraces, state.traceId, MAX_CLOSED_TRACES);
1382
+ this.closedTraces.set(state.traceId, {
1383
+ localRootSpanId: state.localRootSpanId,
1384
+ recording: state.recording,
1385
+ startedSpanCount: state.startedSpanCount
1386
+ });
1387
+ }
1388
+ onSpanEnd(span) {
1389
+ this.emitSpanEvent("end", span);
1390
+ if (span.epoch !== this.epoch) return;
1391
+ const state = this.traceStates.get(span.traceId);
1392
+ if (state && state.generation === span.stateGeneration) {
1393
+ state.openSpanCount--;
1394
+ if (span.spanId === state.localRootSpanId) state.rootEnded = true;
1395
+ if (state.rootEnded && state.openSpanCount <= 0) {
1396
+ this.traceStates.delete(span.traceId);
1397
+ this.rememberClosed(state);
1398
+ }
1399
+ }
1400
+ if (!span.isRecording) return;
1401
+ if (!this.deps.getConfig().enableTracing) return;
1402
+ try {
1403
+ const record = {
1404
+ ...span.scopeAttributes,
1405
+ ...span.attributes
1406
+ };
1407
+ const buffered = {
1408
+ traceId: span.traceId,
1409
+ spanId: span.spanId,
1410
+ parentSpanId: span.parentSpanId,
1411
+ name: span.name,
1412
+ startTimeUnixNano: span.startTimeUnixNano,
1413
+ endTimeUnixNano: span.endTimeUnixNano,
1414
+ status: span.status,
1415
+ recordAttributes: attributesToOpenTelemetry(record),
1416
+ droppedAttributesCount: span.droppedAttributesCount,
1417
+ droppedEventsCount: span.droppedEventsCount,
1418
+ events: span.events.map((event) => ({
1419
+ name: event.name,
1420
+ timeUnixNano: event.timeUnixNano,
1421
+ attributes: attributesToOpenTelemetry(event.attributes),
1422
+ droppedAttributesCount: event.droppedAttributesCount
1423
+ }))
1424
+ };
1425
+ this.buffer.add(buffered);
1426
+ } catch (error) {
1427
+ if (this.deps.getConfig().debug) console.error("Flare: failed to buffer span", error);
1428
+ }
1429
+ }
1430
+ };
1431
+
790
1432
  //#endregion
791
1433
  //#region src/Flare.ts
1434
+ /** Scope attributes a span never inherits. Derived from `USER_IDENTITY_KEYS` so a future user field is
1435
+ * excluded automatically, without anyone needing to remember to list it here. See `getScopeAttributes`. */
1436
+ const SPAN_SCOPE_EXCLUDED_KEYS = USER_IDENTITY_KEYS.filter((key) => key !== USER_FIELD_KEYS.id);
792
1437
  const DEFAULT_SDK_NAME = "@flareapp/core";
793
1438
  var Flare = class {
794
1439
  inflight = /* @__PURE__ */ new Set();
795
1440
  _logger;
1441
+ _tracer;
796
1442
  _config = {
797
1443
  key: null,
798
1444
  version: "",
799
- sourcemapVersionId: SOURCEMAP_VERSION,
1445
+ sourcemapVersionId: require_urlAttributes.SOURCEMAP_VERSION,
800
1446
  stage: "",
801
1447
  maxGlowsPerReport: 30,
1448
+ enableBreadcrumbs: false,
1449
+ maxBreadcrumbs: 100,
802
1450
  ingestUrl: "https://ingress.flareapp.io/v1/errors",
803
1451
  reportBrowserExtensionErrors: false,
804
1452
  debug: false,
805
- urlDenylist: DEFAULT_URL_DENYLIST,
1453
+ urlDenylist: require_urlAttributes.DEFAULT_URL_DENYLIST,
806
1454
  replaceDefaultUrlDenylist: false,
807
1455
  sampleRate: 1,
808
1456
  beforeEvaluate: (error) => error,
@@ -812,26 +1460,37 @@ var Flare = class {
812
1460
  maxLogBufferSize: 100,
813
1461
  logFlushIntervalMs: 5e3,
814
1462
  logFlushMaxBytes: 8e5,
815
- keepaliveMaxBytes: 6e4
1463
+ keepaliveMaxBytes: 6e4,
1464
+ enableTracing: false,
1465
+ tracesIngestUrl: "https://ingress.flareapp.io/v1/traces",
1466
+ tracesSampleRate: 1,
1467
+ maxSpanBufferSize: 100,
1468
+ spanFlushIntervalMs: 5e3,
1469
+ spanFlushMaxBytes: 8e5,
1470
+ maxSpansPerTrace: 1024,
1471
+ maxAttributesPerSpan: 128,
1472
+ maxEventsPerSpan: 128,
1473
+ maxAttributesPerSpanEvent: 128
816
1474
  };
817
1475
  sdkInfo = {
818
1476
  name: DEFAULT_SDK_NAME,
819
- version: CLIENT_VERSION
1477
+ version: require_urlAttributes.CLIENT_VERSION
820
1478
  };
821
1479
  framework = null;
822
1480
  /**
823
- * @param api sends the report over HTTP.
824
- * @param contextCollector returns per-report attributes (browser DOM info, Node
825
- * process info, etc). Default is a no-op.
826
- * @param fileReader reads source files for stack-trace snippets. Default
827
- * returns null (no snippets); `@flareapp/js` injects a
828
- * fetch-based reader, `@flareapp/node` injects a disk reader.
829
- * @param scopeProvider returns the current `Scope` (per-call mutable state:
830
- * glows, pendingAttributes, entryPoint). Browser uses a
831
- * single global scope; Node uses an AsyncLocalStorage-
832
- * backed provider so each request gets its own.
1481
+ * @param api fetch transport for reports, logs and traces. Stateless: ingest url and
1482
+ * key are passed per call, so tests swap in a fake.
1483
+ * @param contextCollector per-report attributes (browser DOM, Node process). No-op by default.
1484
+ * @param fileReader source files for stack-trace snippets. Defaults to no snippets;
1485
+ * `@flareapp/js` injects a fetch reader, `@flareapp/node` a disk reader.
1486
+ * @param scopeProvider the current `Scope`. Browser uses one global scope; Node an
1487
+ * AsyncLocalStorage-backed provider so each request gets its own.
1488
+ * @param scheduler drains the log and span buffers when the host's lifecycle ends (browser
1489
+ * unload, process exit). No-op by default, leaving only size/timer flushes.
1490
+ * @param activeSpanHolder tracks the active span so new spans auto-parent to it. In-memory by
1491
+ * default; a platform can back it with AsyncLocalStorage instead.
833
1492
  */
834
- constructor(api = new Api(), contextCollector = () => ({}), fileReader = new NullFileReader(), scopeProvider = new GlobalScopeProvider(), scheduler = new NoopFlushScheduler()) {
1493
+ constructor(api = new Api(), contextCollector = () => ({}), fileReader = new NullFileReader(), scopeProvider = new GlobalScopeProvider(), scheduler = new NoopFlushScheduler(), activeSpanHolder = new InMemoryActiveSpanHolder()) {
835
1494
  this.api = api;
836
1495
  this.contextCollector = contextCollector;
837
1496
  this.fileReader = fileReader;
@@ -845,57 +1504,25 @@ var Flare = class {
845
1504
  track: (p) => this.track(p),
846
1505
  scheduler
847
1506
  });
1507
+ this._tracer = new Tracer({
1508
+ api: this.api,
1509
+ getConfig: () => this._config,
1510
+ getSdkInfo: () => this.sdkInfo,
1511
+ getFramework: () => this.framework,
1512
+ getScopeAttributes: () => this.getScopeAttributes(),
1513
+ getResourceAttributes: () => this.spanResourceAttributes(),
1514
+ track: (p) => this.track(p),
1515
+ scheduler,
1516
+ activeSpanHolder
1517
+ });
848
1518
  }
849
1519
  /**
850
- * Register an in-flight report so `flush()` can wait for it. Called by
851
- * every public report entry point (`report`, `reportSilently`,
852
- * `reportMessage`, `reportUnhandledRejection`, `test`); each wraps its
853
- * full async pipeline (beforeEvaluate -> stack trace + source snippets ->
854
- * beforeSubmit -> `api.report()`) so the entire roundtrip is what's
855
- * tracked, not just the HTTP send at the end.
856
- *
857
- * Two problems this method solves at once.
858
- *
859
- * Problem 1: hold a reference to the work without leaking rejections.
860
- *
861
- * `p` is the real report pipeline; it can reject (network failure,
862
- * `beforeSubmit` throws, etc). If we stored `p` directly in `inflight`
863
- * and no caller attached a `.catch` (the global error listeners use
864
- * `reportSilently` which DOES catch, but the path is still subtle), an
865
- * eventual rejection would surface as an unhandled-rejection warning
866
- * on Node and a console error in the browser. Bad citizen.
867
- *
868
- * So we build a SHADOW promise that mirrors `p`'s timing but cannot
869
- * reject:
870
- *
871
- * p.then(
872
- * () => undefined, // on fulfilment, value is undefined
873
- * () => undefined, // on rejection, ALSO resolve with undefined
874
- * )
875
- *
876
- * Providing the second argument means we have "handled" any rejection
877
- * from `p`. The shadow always resolves with `undefined`, and `p`'s
878
- * rejection is consumed at the boundary. From the runtime's point of
879
- * view, the shadow is well-behaved.
880
- *
881
- * Problem 2: self-cleaning entry.
882
- *
883
- * `tracked.finally(() => this.inflight.delete(tracked))`. `finally`
884
- * fires whether the shadow resolves or rejects, but the shadow can no
885
- * longer reject (problem 1 normalized it), so this is effectively
886
- * "when the underlying report has settled, remove me from the Set."
887
- * No GC magic, no external cleanup, no race window.
888
- *
889
- * Note that `.finally` itself returns a new promise that we drop on
890
- * the floor. If the cleanup callback ever throws, that would surface
891
- * as an unhandled rejection on the dropped promise; `delete` does not
892
- * throw so we are safe today, but anything more elaborate added here
893
- * should be wrapped in try/catch.
1520
+ * Register an in-flight report so `flush()` can wait for it. Every entry point wraps its whole async
1521
+ * pipeline, from beforeEvaluate through api.report, so flush() waits on all of it.
894
1522
  *
895
- * The return value is the ORIGINAL `p`. The caller awaits real success
896
- * or failure; the tracking is completely invisible to them. This is why
897
- * `await flare.report(err)` inside a fatal handler observes network
898
- * errors the same as before tracking was added.
1523
+ * What goes in the Set is a shadow promise that mirrors `p`'s timing but cannot reject, so a failed
1524
+ * report never surfaces as an unhandled rejection warning. `p` itself is returned untouched, so the
1525
+ * caller still observes real success or failure.
899
1526
  */
900
1527
  track(p) {
901
1528
  const tracked = p.then(() => void 0, () => void 0);
@@ -904,86 +1531,16 @@ var Flare = class {
904
1531
  return p;
905
1532
  }
906
1533
  /**
907
- * Wait until every in-flight report settles, or until `timeoutMs`
908
- * elapses, whichever comes first. Always resolves; never rejects.
1534
+ * Wait until every in-flight report settles or `timeoutMs` elapses. Always resolves, never rejects.
1535
+ * Written for `@flareapp/node`'s fatal handler, which awaits the fatal report itself then flushes to
1536
+ * drain any other concurrent reports before `process.exit`.
909
1537
  *
910
- * The main consumer is `@flareapp/node`'s fatal handler:
911
- *
912
- * process.on('uncaughtException', async (err) => {
913
- * process.exitCode = 1;
914
- * try { await flare.report(err); } catch {}
915
- * await flare.flush(shutdownTimeoutMs);
916
- * process.exit(1);
917
- * });
918
- *
919
- * The fatal `report` is awaited explicitly; `flush` then drains any
920
- * OTHER reports that were already in flight (a request handler that
921
- * fired `flare.report(...)` concurrently with the crash). The timeout
922
- * caps the wait so a hung HTTP request cannot indefinitely block
923
- * shutdown.
924
- *
925
- * Walking the implementation:
926
- *
927
- * const pending = [...this.inflight];
928
- *
929
- * Spread takes a SNAPSHOT of the Set at this instant. Reports that
930
- * start AFTER this line are not included in `pending`, so they are
931
- * not awaited by THIS flush call. This is intentional: it bounds
932
- * the wait. Without the snapshot, a handler that kept emitting
933
- * reports during shutdown could keep flush alive forever and block
934
- * the process from exiting.
935
- *
936
- * if (pending.length === 0) return Promise.resolve();
937
- *
938
- * Fast path. No timer scheduled, no promise constructor needed.
939
- * Resolves on the microtask queue. Cheap.
940
- *
941
- * return new Promise<void>((resolve) => {
942
- * const timer = setTimeout(resolve, timeoutMs);
943
- * Promise.allSettled(pending).then(() => {
944
- * clearTimeout(timer);
945
- * resolve();
946
- * });
947
- * });
948
- *
949
- * The race between two outcomes, both calling the same `resolve`:
950
- *
951
- * 1. `setTimeout(resolve, timeoutMs)` schedules a "give up" call.
952
- * After `timeoutMs` it fires, calling `resolve()` from the
953
- * timer-queue side. The outer promise resolves immediately,
954
- * even if reports are still pending. Those reports are abandoned
955
- * (they continue running but the process is about to die).
956
- *
957
- * 2. `Promise.allSettled(pending)` returns a promise that resolves
958
- * when every promise in `pending` has either fulfilled or
959
- * rejected. It NEVER rejects on its own. We use `allSettled`
960
- * rather than `Promise.all` because `all` short-circuits on the
961
- * first rejection -- we want to wait for everyone regardless of
962
- * whether their HTTP calls succeed or fail. (Our shadows cannot
963
- * reject anyway because `track` normalized them, but using
964
- * `allSettled` documents the intent and survives future changes
965
- * to shadow construction.) When it resolves, we call
966
- * `clearTimeout(timer)` to cancel the pending timer (so it does
967
- * not fire later and call `resolve` a second time -- a no-op,
968
- * but wasted work) and then `resolve()` ourselves.
969
- *
970
- * Resolve can only meaningfully fire once. Subsequent calls to the
971
- * same `resolve` are silently ignored by the Promise spec, so the
972
- * race is safe even if for some reason both branches fired together.
973
- *
974
- * Things flush() deliberately does NOT do:
975
- *
976
- * - It does not reject. Even if every report failed, allSettled
977
- * resolves. Callers do not need a `.catch`.
978
- * - It does not retry. One pipeline attempt per report, then move on.
979
- * - It does not stop new reports from starting. The Flare instance
980
- * is still usable after flush resolves. flush is "wait for what is
981
- * in flight," not "freeze the SDK."
982
- * - It does not drain reports started after the snapshot. Call flush
983
- * again if you need to wait for those too.
1538
+ * Snapshotting the Set bounds the wait: reports started after this line are not awaited, so a handler
1539
+ * that keeps emitting during shutdown cannot block the process forever. Call flush again for those.
984
1540
  */
985
1541
  flush(timeoutMs = 2e3) {
986
1542
  this._logger.flush();
1543
+ this._tracer.flush();
987
1544
  const pending = [...this.inflight];
988
1545
  if (pending.length === 0) return Promise.resolve();
989
1546
  return new Promise((resolve) => {
@@ -1003,22 +1560,44 @@ var Flare = class {
1003
1560
  get logger() {
1004
1561
  return this._logger;
1005
1562
  }
1006
- light(key = KEY, debug) {
1563
+ get tracer() {
1564
+ return this._tracer;
1565
+ }
1566
+ /** Starts a span the caller must end. Unlike `withSpan`, it does not become the active span,
1567
+ * so spans started after it do not auto-parent to it. */
1568
+ startSpan(name, opts) {
1569
+ return this._tracer.startSpan(name, opts);
1570
+ }
1571
+ /**
1572
+ * Runs `fn` with the span active, so spans started inside auto-parent to it, then ends it.
1573
+ * Records an error status first if `fn` throws or its returned promise rejects.
1574
+ */
1575
+ withSpan(name, fn, opts) {
1576
+ return this._tracer.withSpan(name, fn, opts);
1577
+ }
1578
+ light(key = require_urlAttributes.KEY, debug) {
1007
1579
  this._config.key = key;
1008
1580
  if (debug !== void 0) this._config.debug = debug;
1009
1581
  this._logger.flush();
1582
+ this._tracer.flush();
1010
1583
  return this;
1011
1584
  }
1012
1585
  configure(config) {
1013
1586
  const wasLogsEnabled = this._config.enableLogs;
1587
+ const wasTracingEnabled = this._config.enableTracing;
1588
+ const wasBreadcrumbsEnabled = this._config.enableBreadcrumbs;
1014
1589
  this._config = {
1015
1590
  ...this._config,
1016
1591
  ...config
1017
1592
  };
1018
1593
  if (config.sampleRate !== void 0) this._config.sampleRate = Math.max(0, Math.min(1, config.sampleRate));
1019
- this._config.urlDenylist = resolveDenylist(config.urlDenylist, config.replaceDefaultUrlDenylist ?? this._config.replaceDefaultUrlDenylist);
1594
+ if (config.tracesSampleRate !== void 0) this._config.tracesSampleRate = Math.max(0, Math.min(1, config.tracesSampleRate));
1595
+ if (config.urlDenylist !== void 0 || config.replaceDefaultUrlDenylist !== void 0) this._config.urlDenylist = require_urlAttributes.resolveDenylist(config.urlDenylist, config.replaceDefaultUrlDenylist ?? this._config.replaceDefaultUrlDenylist);
1020
1596
  if (wasLogsEnabled && this._config.enableLogs === false) this._logger.clear();
1021
1597
  if (config.key !== void 0) this._logger.flush();
1598
+ if (wasBreadcrumbsEnabled && this._config.enableBreadcrumbs === false) this.scopeProvider.active().clearBreadcrumbs();
1599
+ if (wasTracingEnabled && this._config.enableTracing === false) this._tracer.clear();
1600
+ if (config.key !== void 0) this._tracer.flush();
1022
1601
  return this;
1023
1602
  }
1024
1603
  test() {
@@ -1030,7 +1609,7 @@ var Flare = class {
1030
1609
  return this.sendReport(report);
1031
1610
  }
1032
1611
  glow(name, level = "info", data = []) {
1033
- const time = now();
1612
+ const time = require_urlAttributes.now();
1034
1613
  this.scopeProvider.active().addGlow({
1035
1614
  name,
1036
1615
  messageLevel: level,
@@ -1040,6 +1619,9 @@ var Flare = class {
1040
1619
  }, this._config.maxGlowsPerReport);
1041
1620
  return this;
1042
1621
  }
1622
+ addBreadcrumb(type, attributes, startTimeUnixNano) {
1623
+ recordBreadcrumb(this.scopeProvider, this._config, type, attributes, startTimeUnixNano);
1624
+ }
1043
1625
  clearGlows() {
1044
1626
  this.scopeProvider.active().clearGlows();
1045
1627
  return this;
@@ -1058,11 +1640,8 @@ var Flare = class {
1058
1640
  return this;
1059
1641
  }
1060
1642
  /**
1061
- * Attach an identified user to the active scope. Fields are projected to the
1062
- * keys the Flare backend reads: `user.id`, `user.email`, `user.full_name`,
1063
- * and `client.address`. Any extra keys are bundled into `user.attributes`.
1064
- * Pass `null` to clear the user. Scope-aware: in Node this targets the
1065
- * per-request scope via the scope provider.
1643
+ * Maps the known fields onto the keys the Flare backend reads (see `USER_FIELD_KEYS`) and bundles
1644
+ * anything else into `user.attributes`. Pass `null` to clear. In Node this targets the per-request scope.
1066
1645
  */
1067
1646
  setUser(user) {
1068
1647
  const scope = this.scopeProvider.active();
@@ -1095,7 +1674,7 @@ var Flare = class {
1095
1674
  async reportInternal(error, attributes = {}) {
1096
1675
  if (this._config.sampleRate < 1 && Math.random() >= this._config.sampleRate) return;
1097
1676
  const seenAtUnixNano = Date.now() * 1e6;
1098
- const coerced = error instanceof Error ? error : new Error(typeof error === "string" ? error : String(error));
1677
+ const coerced = error instanceof Error ? error : new Error(String(error));
1099
1678
  const errorToReport = await this._config.beforeEvaluate(coerced);
1100
1679
  if (!errorToReport) return;
1101
1680
  const report = await this.createReportFromError(errorToReport, attributes, seenAtUnixNano);
@@ -1144,9 +1723,9 @@ var Flare = class {
1144
1723
  return this.sendReport(report);
1145
1724
  }
1146
1725
  async createReportFromError(error, attributes = {}, seenAtUnixNano = Date.now() * 1e6) {
1147
- if (!assert(error, "No error provided.", this._config.debug)) return false;
1726
+ if (!require_urlAttributes.assert(error, "No error provided.", this._config.debug)) return false;
1148
1727
  const stacktrace = await createStackTrace(error, this._config.debug, this.fileReader);
1149
- assert(stacktrace.length, "Couldn't generate stacktrace of this error: " + error, this._config.debug);
1728
+ require_urlAttributes.assert(stacktrace.length, "Couldn't generate stacktrace of this error: " + error, this._config.debug);
1150
1729
  const exceptionClass = error.constructor && error.constructor.name ? error.constructor.name : "undefined";
1151
1730
  return this.buildReport({
1152
1731
  exceptionClass,
@@ -1155,7 +1734,7 @@ var Flare = class {
1155
1734
  isLog: false,
1156
1735
  level: void 0,
1157
1736
  extraAttributes: attributes,
1158
- code: extractCode(error),
1737
+ code: require_urlAttributes.extractCode(error),
1159
1738
  seenAtUnixNano
1160
1739
  });
1161
1740
  }
@@ -1177,7 +1756,10 @@ var Flare = class {
1177
1756
  const baseAttributes = includeBase ? this.buildBaseAttributes() : {};
1178
1757
  const entryPoint = activeScope.entryPoint;
1179
1758
  const entryPointOverrides = {};
1180
- if (entryPoint?.identifier !== void 0) entryPointOverrides["flare.entry_point.handler.identifier"] = entryPoint.identifier;
1759
+ if (entryPoint?.identifier !== void 0) {
1760
+ entryPointOverrides["flare.entry_point.handler.identifier"] = entryPoint.identifier;
1761
+ entryPointOverrides["http.route"] = entryPoint.identifier;
1762
+ }
1181
1763
  if (entryPoint?.type !== void 0) entryPointOverrides["flare.entry_point.handler.type"] = entryPoint.type;
1182
1764
  if (entryPoint?.name !== void 0) entryPointOverrides["flare.entry_point.handler.name"] = entryPoint.name;
1183
1765
  const attributes = {
@@ -1206,6 +1788,23 @@ var Flare = class {
1206
1788
  record: this.assembleAttributes(collectorRecord, userAttributes, false)
1207
1789
  };
1208
1790
  }
1791
+ /**
1792
+ * Local roots only, snapshotted by the Tracer at span START so a long-lived root does not drift into
1793
+ * the next page's scope. Children get none, and no span ever runs the DOM collector.
1794
+ *
1795
+ * Everything assembled is inherited except user identity (excluding the opaque `user.id`): a root span
1796
+ * goes out for every page view, so email, full name, IP and `user.attributes` would turn normal
1797
+ * browsing into PII traffic. The rest — `context.custom`, `addContextGroup` bags — stays in, because
1798
+ * the trace viewer renders any span attribute whose key does not start with `flare.`.
1799
+ */
1800
+ getScopeAttributes() {
1801
+ const scoped = { ...this.assembleAttributes({}, {}, false) };
1802
+ for (const key of SPAN_SCOPE_EXCLUDED_KEYS) delete scoped[key];
1803
+ return scoped;
1804
+ }
1805
+ spanResourceAttributes() {
1806
+ return partitionAttributes(this.contextCollector(this._config)).resource;
1807
+ }
1209
1808
  buildReport(input) {
1210
1809
  const activeScope = this.scopeProvider.active();
1211
1810
  const attributes = this.assembleAttributes(this.contextCollector(this._config), input.extraAttributes, true);
@@ -1214,7 +1813,7 @@ var Flare = class {
1214
1813
  message: input.message,
1215
1814
  seenAtUnixNano: input.seenAtUnixNano,
1216
1815
  stacktrace: input.stacktrace,
1217
- events: glowsToEvents(activeScope.glows),
1816
+ events: require_urlAttributes.timelineEvents(activeScope.glows, activeScope.breadcrumbs),
1218
1817
  attributes
1219
1818
  };
1220
1819
  if (input.isLog) report.isLog = true;
@@ -1224,7 +1823,7 @@ var Flare = class {
1224
1823
  return report;
1225
1824
  }
1226
1825
  async sendReport(report) {
1227
- if (!assertKey(this._config.key, this._config.debug)) return;
1826
+ if (!require_urlAttributes.assertKey(this._config.key, this._config.debug)) return;
1228
1827
  const reportToSubmit = await this._config.beforeSubmit(report);
1229
1828
  if (!reportToSubmit) return;
1230
1829
  return this.api.report(reportToSubmit, this._config.ingestUrl, this._config.key, this._config.reportBrowserExtensionErrors, this._config.debug);
@@ -1233,26 +1832,47 @@ var Flare = class {
1233
1832
 
1234
1833
  //#endregion
1235
1834
  exports.Api = Api;
1236
- exports.DEFAULT_URL_DENYLIST = DEFAULT_URL_DENYLIST;
1835
+ exports.BrowserSpanEventType = BrowserSpanEventType;
1836
+ exports.BrowserSpanType = BrowserSpanType;
1837
+ exports.DEFAULT_MAX_LIVE_TRACES = DEFAULT_MAX_LIVE_TRACES;
1838
+ exports.DEFAULT_URL_DENYLIST = require_urlAttributes.DEFAULT_URL_DENYLIST;
1237
1839
  exports.Flare = Flare;
1840
+ exports.FrameworkName = FrameworkName;
1238
1841
  exports.GlobalScopeProvider = GlobalScopeProvider;
1842
+ exports.InMemoryActiveSpanHolder = InMemoryActiveSpanHolder;
1239
1843
  exports.Logger = Logger;
1844
+ exports.MAX_BREADCRUMB_URL_LENGTH = MAX_BREADCRUMB_URL_LENGTH;
1240
1845
  exports.NoopFlushScheduler = NoopFlushScheduler;
1241
1846
  exports.NullFileReader = NullFileReader;
1242
1847
  exports.Scope = Scope;
1848
+ exports.SpanStatusCode = SpanStatusCode;
1849
+ exports.Tracer = Tracer;
1243
1850
  exports.USER_IDENTITY_KEYS = USER_IDENTITY_KEYS;
1244
- exports.assert = assert;
1245
- exports.assertKey = assertKey;
1246
- exports.convertToError = convertToError;
1851
+ exports.assert = require_urlAttributes.assert;
1852
+ exports.assertKey = require_urlAttributes.assertKey;
1853
+ exports.breadcrumbUrl = breadcrumbUrl;
1854
+ exports.buildTraceparent = buildTraceparent;
1855
+ exports.buildTracesEnvelope = buildTracesEnvelope;
1856
+ exports.convertToError = require_urlAttributes.convertToError;
1857
+ exports.createIdentityTagger = require_urlAttributes.createIdentityTagger;
1247
1858
  exports.createStackTrace = createStackTrace;
1248
- exports.describeRejectionReason = describeRejectionReason;
1249
- exports.extractCode = extractCode;
1250
- exports.flatJsonStringify = flatJsonStringify;
1859
+ exports.defaultNowNano = defaultNowNano;
1860
+ exports.describeRejectionReason = require_urlAttributes.describeRejectionReason;
1861
+ exports.extractCode = require_urlAttributes.extractCode;
1862
+ exports.flatJsonStringify = require_urlAttributes.flatJsonStringify;
1251
1863
  exports.getCodeSnippet = getCodeSnippet;
1252
- exports.glowsToEvents = glowsToEvents;
1253
- exports.now = now;
1864
+ exports.glowsToEvents = require_urlAttributes.glowsToEvents;
1865
+ exports.now = require_urlAttributes.now;
1866
+ exports.parseTraceparent = parseTraceparent;
1254
1867
  exports.readLinesFromFile = readLinesFromFile;
1255
- exports.redactUrlQuery = redactUrlQuery;
1256
- exports.resolveDenylist = resolveDenylist;
1257
- exports.routeRejection = routeRejection;
1868
+ exports.recordBreadcrumb = recordBreadcrumb;
1869
+ exports.redactObjectValues = require_urlAttributes.redactObjectValues;
1870
+ exports.redactUrlQuery = require_urlAttributes.redactUrlQuery;
1871
+ exports.resolveDenylist = require_urlAttributes.resolveDenylist;
1872
+ exports.routeRejection = require_urlAttributes.routeRejection;
1873
+ exports.safeClone = require_urlAttributes.safeClone;
1874
+ exports.safeDecode = require_urlAttributes.safeDecode;
1875
+ exports.spanId = spanId;
1876
+ exports.toCustomContext = require_urlAttributes.toCustomContext;
1877
+ exports.urlAttributes = require_urlAttributes.urlAttributes;
1258
1878
  exports.userIdentityAttributes = userIdentityAttributes;