@fixback/sdk-core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +28 -9
  2. package/{src/annotation.ts → dist/annotation.d.ts} +36 -47
  3. package/dist/anonymous-id.d.ts +23 -0
  4. package/dist/anonymous-id.js +39 -0
  5. package/dist/anonymous-id.js.map +1 -0
  6. package/dist/auto-capture.d.ts +129 -0
  7. package/dist/auto-capture.js +210 -0
  8. package/dist/auto-capture.js.map +1 -0
  9. package/{src/backoff.ts → dist/backoff.d.ts} +17 -52
  10. package/dist/boot.d.ts +148 -0
  11. package/dist/boot.js +113 -0
  12. package/dist/boot.js.map +1 -0
  13. package/dist/breadcrumb.d.ts +133 -0
  14. package/dist/connect.d.ts +110 -0
  15. package/dist/connect.js +147 -0
  16. package/dist/connect.js.map +1 -0
  17. package/dist/env.d.ts +37 -0
  18. package/dist/env.js +83 -0
  19. package/dist/env.js.map +1 -0
  20. package/dist/fingerprint.d.ts +39 -0
  21. package/dist/fingerprint.js +10 -15
  22. package/dist/fingerprint.js.map +1 -1
  23. package/dist/http.d.ts +51 -0
  24. package/dist/http.js +42 -0
  25. package/dist/http.js.map +1 -0
  26. package/dist/index.d.ts +28 -0
  27. package/dist/index.js +115 -3
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +1057 -0
  30. package/dist/index.mjs.map +7 -0
  31. package/dist/options.d.ts +82 -0
  32. package/dist/options.js +22 -0
  33. package/dist/options.js.map +1 -0
  34. package/dist/release.d.ts +19 -0
  35. package/dist/release.js +36 -0
  36. package/dist/release.js.map +1 -0
  37. package/dist/scrub.d.ts +62 -0
  38. package/dist/stack.d.ts +51 -0
  39. package/dist/stack.js +97 -0
  40. package/dist/stack.js.map +1 -0
  41. package/dist/trace/buffer.d.ts +121 -0
  42. package/dist/trace/buffer.js +230 -0
  43. package/dist/trace/buffer.js.map +1 -0
  44. package/dist/trace/console-args.d.ts +60 -0
  45. package/dist/trace/console-args.js +189 -0
  46. package/dist/trace/console-args.js.map +1 -0
  47. package/dist/trace/console.d.ts +42 -0
  48. package/dist/trace/console.js +71 -0
  49. package/dist/trace/console.js.map +1 -0
  50. package/dist/trace/crumbs.d.ts +88 -0
  51. package/dist/trace/crumbs.js +164 -0
  52. package/dist/trace/crumbs.js.map +1 -0
  53. package/dist/trace/source.d.ts +30 -0
  54. package/dist/trace/source.js +59 -0
  55. package/dist/trace/source.js.map +1 -0
  56. package/dist/version.d.ts +13 -0
  57. package/dist/version.js +17 -0
  58. package/dist/version.js.map +1 -0
  59. package/dist/wire.d.ts +101 -0
  60. package/package.json +12 -8
  61. package/src/backoff.test.ts +0 -94
  62. package/src/breadcrumb.ts +0 -169
  63. package/src/fingerprint.test.ts +0 -96
  64. package/src/fingerprint.ts +0 -112
  65. package/src/index.ts +0 -63
  66. package/src/scrub.test.ts +0 -215
  67. package/src/scrub.ts +0 -226
  68. package/src/wire.ts +0 -116
package/dist/index.mjs ADDED
@@ -0,0 +1,1057 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/version.ts
6
+ var SDK_CORE_VERSION = "0.3.0";
7
+
8
+ // src/scrub.ts
9
+ var MIN_DIGIT_RUN = 7;
10
+ var EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
11
+ var DIGIT_RUN_RE = new RegExp(`\\d{${MIN_DIGIT_RUN},}`, "g");
12
+ var BEARER_RE = /\b(bearer|token)\s+[\w.\-~+/]+=*/gi;
13
+ var URL_IN_TEXT_RE = /\bhttps?:\/\/[^\s"'<>]+/gi;
14
+ function redactPii(text) {
15
+ if (typeof text !== "string" || text.length === 0) return text;
16
+ return text.replace(EMAIL_RE, "[redacted-email]").replace(BEARER_RE, (_match, keyword) => `${keyword} [redacted]`).replace(DIGIT_RUN_RE, "[redacted-number]");
17
+ }
18
+ function redactPathPii(pathAndBeyond) {
19
+ return pathAndBeyond.replace(EMAIL_RE, "[redacted-email]").replace(DIGIT_RUN_RE, "[redacted-number]");
20
+ }
21
+ function scrubUrl(url) {
22
+ if (typeof url !== "string" || url.length === 0) return url;
23
+ let out = url;
24
+ out = out.replace(/(^[a-z][a-z0-9+.-]*:\/\/)[^/@?#]*@/i, "$1");
25
+ out = out.replace(/\?[^#]*/, "");
26
+ out = out.replace(
27
+ /#.*$/,
28
+ (fragment) => fragment.includes("=") ? "" : fragment
29
+ );
30
+ const absolute = out.match(/^([a-z][a-z0-9+.-]*:\/\/[^/?#]*)([\s\S]*)$/i);
31
+ if (absolute) return absolute[1] + redactPathPii(absolute[2] ?? "");
32
+ const protocolRelative = out.match(/^(\/\/[^/?#]*)([\s\S]*)$/);
33
+ if (protocolRelative) {
34
+ return protocolRelative[1] + redactPathPii(protocolRelative[2] ?? "");
35
+ }
36
+ return redactPathPii(out);
37
+ }
38
+ function scrubCrumbData(data) {
39
+ const next = { ...data };
40
+ if (typeof next.url === "string") next.url = scrubUrl(next.url);
41
+ if (typeof next.from === "string") next.from = scrubUrl(next.from);
42
+ if (typeof next.to === "string") next.to = scrubUrl(next.to);
43
+ return next;
44
+ }
45
+ function scrubMessage(message) {
46
+ return redactPii(message.replace(URL_IN_TEXT_RE, (url) => scrubUrl(url)));
47
+ }
48
+ function scrubArgValue(value) {
49
+ if (typeof value === "string") return scrubMessage(value);
50
+ if (Array.isArray(value)) return value.map(scrubArgValue);
51
+ if (value && typeof value === "object") {
52
+ const out = {};
53
+ for (const [key, v] of Object.entries(value)) {
54
+ out[key] = scrubArgValue(v);
55
+ }
56
+ return out;
57
+ }
58
+ return value;
59
+ }
60
+ function scrubConsoleArg(arg) {
61
+ switch (arg.t) {
62
+ case "string":
63
+ return typeof arg.v === "string" ? { t: "string", v: scrubMessage(arg.v) } : arg;
64
+ case "json":
65
+ return { t: "json", v: scrubArgValue(arg.v) };
66
+ case "error": {
67
+ if (!arg.v || typeof arg.v !== "object") return arg;
68
+ const v = arg.v;
69
+ const next = { ...v };
70
+ if (typeof next.message === "string") next.message = scrubMessage(next.message);
71
+ if (typeof next.stack === "string") next.stack = scrubMessage(next.stack);
72
+ return { t: "error", v: next };
73
+ }
74
+ default:
75
+ return arg;
76
+ }
77
+ }
78
+ function scrubCrumb(crumb) {
79
+ const next = { ...crumb };
80
+ if (typeof next.message === "string") next.message = scrubMessage(next.message);
81
+ if (next.data) next.data = scrubCrumbData(next.data);
82
+ if (typeof next.url === "string") next.url = scrubUrl(next.url);
83
+ if (Array.isArray(next.args)) next.args = next.args.map(scrubConsoleArg);
84
+ return next;
85
+ }
86
+ function applyDefaultScrub(draft) {
87
+ const next = { ...draft };
88
+ if (typeof next.url === "string") next.url = scrubUrl(next.url);
89
+ if (next.trace && next.trace.length > 0) {
90
+ next.trace = next.trace.map(scrubCrumb);
91
+ }
92
+ if (next.errorFrames && next.errorFrames.length > 0) {
93
+ next.errorFrames = next.errorFrames.map((frame) => ({
94
+ ...frame,
95
+ file: scrubUrl(frame.file)
96
+ }));
97
+ }
98
+ return next;
99
+ }
100
+ function runBeforeSend(draft, options = {}) {
101
+ const current = options.scrub === false ? draft : applyDefaultScrub(draft);
102
+ const hook = options.hook;
103
+ if (!hook) return current;
104
+ try {
105
+ const result = hook(current);
106
+ return result ?? null;
107
+ } catch {
108
+ return current;
109
+ }
110
+ }
111
+
112
+ // src/stack.ts
113
+ var STRUCTURED_FRAME_LIMIT = 30;
114
+ function parseStackLine(line) {
115
+ const v8Named = line.match(/^at\s+(.+?)\s+\((.+)\)$/);
116
+ if (v8Named) return { fn: v8Named[1] ?? null, location: v8Named[2] ?? "" };
117
+ const v8Bare = line.match(/^at\s+(.+)$/);
118
+ if (v8Bare) return { fn: null, location: v8Bare[1] ?? "" };
119
+ const at = line.indexOf("@");
120
+ if (at >= 0) {
121
+ return { fn: at > 0 ? line.slice(0, at) : null, location: line.slice(at + 1) };
122
+ }
123
+ return null;
124
+ }
125
+ function parseFrameLocation(location) {
126
+ const match = location.match(/^(.*?):(\d+)(?::(\d+))?$/);
127
+ if (!match) return null;
128
+ const file = match[1] ?? "";
129
+ if (file.length === 0 || file === "native" || file.includes("<anonymous>")) {
130
+ return null;
131
+ }
132
+ const line = Number(match[2]);
133
+ if (!Number.isFinite(line)) return null;
134
+ const column = match[3] !== void 0 ? Number(match[3]) : null;
135
+ return { file, line, column };
136
+ }
137
+ function extractStructuredFrames(stack, limit = STRUCTURED_FRAME_LIMIT) {
138
+ if (typeof stack !== "string" || stack.length === 0) return [];
139
+ const frames = [];
140
+ for (const raw of stack.split("\n")) {
141
+ if (frames.length >= limit) break;
142
+ const tokens = parseStackLine(raw.trim());
143
+ if (!tokens || !tokens.location) continue;
144
+ const parsed = parseFrameLocation(tokens.location);
145
+ if (!parsed) continue;
146
+ frames.push({
147
+ file: scrubUrl(parsed.file),
148
+ line: parsed.line,
149
+ column: parsed.column,
150
+ function: tokens.fn && tokens.fn.length > 0 ? tokens.fn : null
151
+ });
152
+ }
153
+ return frames;
154
+ }
155
+
156
+ // src/fingerprint.ts
157
+ var UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
158
+ var URL_RE = /\bhttps?:\/\/[^\s"')]+/gi;
159
+ var HEX_0X_RE = /\b0x[0-9a-f]+\b/gi;
160
+ var HEX_RUN_RE = /\b[0-9a-f]{8,}\b/gi;
161
+ var DIGIT_RUN_RE2 = /\d{4,}/g;
162
+ var FINGERPRINT_FRAME_LIMIT = 5;
163
+ function normalize(value) {
164
+ if (typeof value !== "string" || value.length === 0) return "";
165
+ return value.replace(UUID_RE, "<uuid>").replace(URL_RE, "<url>").replace(HEX_0X_RE, "<hex>").replace(HEX_RUN_RE, "<hex>").replace(DIGIT_RUN_RE2, "<n>").trim();
166
+ }
167
+ function hashString(input) {
168
+ let h = 2166136261;
169
+ for (let i = 0; i < input.length; i++) {
170
+ h ^= input.charCodeAt(i);
171
+ h = Math.imul(h, 16777619);
172
+ }
173
+ return (h >>> 0).toString(36);
174
+ }
175
+ function compactLocation(location) {
176
+ const noQuery = location.replace(/\?[^:]*/, "");
177
+ const lastSlash = noQuery.lastIndexOf("/");
178
+ return lastSlash >= 0 ? noQuery.slice(lastSlash + 1) : noQuery;
179
+ }
180
+ function parseFrame(line) {
181
+ const tokens = parseStackLine(line);
182
+ if (!tokens) return null;
183
+ return `${tokens.fn ?? ""}@${compactLocation(tokens.location)}`;
184
+ }
185
+ function extractTopFrames(stack, limit = FINGERPRINT_FRAME_LIMIT) {
186
+ if (typeof stack !== "string" || stack.length === 0) return "";
187
+ const frames = [];
188
+ for (const raw of stack.split("\n")) {
189
+ const frame = parseFrame(raw.trim());
190
+ if (frame) {
191
+ frames.push(frame);
192
+ if (frames.length >= limit) break;
193
+ }
194
+ }
195
+ return frames.join(" < ");
196
+ }
197
+ function computeFingerprint(type, value, stack) {
198
+ return hashString(`${type}|${normalize(value)}|${extractTopFrames(stack)}`);
199
+ }
200
+
201
+ // src/backoff.ts
202
+ var DEFAULT_RETRY_AFTER_SECONDS = 60;
203
+ function parseRetryAfter(header, now) {
204
+ if (header == null) return DEFAULT_RETRY_AFTER_SECONDS;
205
+ const value = header.trim();
206
+ if (value === "") return DEFAULT_RETRY_AFTER_SECONDS;
207
+ if (/^\d+$/.test(value)) {
208
+ return Number(value);
209
+ }
210
+ const when = Date.parse(value);
211
+ if (!Number.isNaN(when)) {
212
+ return Math.max(0, Math.ceil((when - now) / 1e3));
213
+ }
214
+ return DEFAULT_RETRY_AFTER_SECONDS;
215
+ }
216
+ var AutoReportBackoff = class {
217
+ constructor(now = Date.now) {
218
+ this.now = now;
219
+ /** Epoch ms until which automatic reports are held; `0` when clear. */
220
+ __publicField(this, "pausedUntil", 0);
221
+ }
222
+ /** Is the automatic-report pause window currently open? */
223
+ isPaused() {
224
+ return this.now() < this.pausedUntil;
225
+ }
226
+ /** Whole seconds remaining in the pause window (`0` when clear). */
227
+ retryAfterSeconds() {
228
+ return Math.max(0, Math.ceil((this.pausedUntil - this.now()) / 1e3));
229
+ }
230
+ /**
231
+ * Open (or extend) the window from a `429`'s `Retry-After` value, returning the
232
+ * seconds it will hold for. The window only ever grows — a shorter later hold
233
+ * never clips a longer one already in effect.
234
+ */
235
+ hold(retryAfterHeader) {
236
+ const now = this.now();
237
+ const seconds = parseRetryAfter(retryAfterHeader, now);
238
+ const until = now + seconds * 1e3;
239
+ if (until > this.pausedUntil) this.pausedUntil = until;
240
+ return seconds;
241
+ }
242
+ };
243
+
244
+ // src/env.ts
245
+ function nowMono() {
246
+ const perf = globalThis.performance;
247
+ return perf && typeof perf.now === "function" ? perf.now() : Date.now();
248
+ }
249
+ function textEncoder() {
250
+ return globalThis.TextEncoder;
251
+ }
252
+ function utf8ByteLength(value) {
253
+ try {
254
+ const Encoder = textEncoder();
255
+ return Encoder ? new Encoder().encode(value).length : value.length;
256
+ } catch {
257
+ return value.length;
258
+ }
259
+ }
260
+ function serializedBytes(value) {
261
+ try {
262
+ const json = JSON.stringify(value) ?? "";
263
+ return utf8ByteLength(json);
264
+ } catch {
265
+ return Number.POSITIVE_INFINITY;
266
+ }
267
+ }
268
+ function trivialBodySize(body) {
269
+ if (typeof body === "string") return utf8ByteLength(body);
270
+ const BlobCtor = globalThis.Blob;
271
+ if (BlobCtor && body instanceof BlobCtor) return body.size;
272
+ if (typeof ArrayBuffer !== "undefined") {
273
+ if (body instanceof ArrayBuffer) return body.byteLength;
274
+ if (ArrayBuffer.isView(body)) return body.byteLength;
275
+ }
276
+ return void 0;
277
+ }
278
+
279
+ // src/http.ts
280
+ function resolveFetch() {
281
+ const g = globalThis;
282
+ if (typeof g.fetch !== "function") return void 0;
283
+ const bound = g.fetch.bind(globalThis);
284
+ return (url, init) => bound(url, init);
285
+ }
286
+ function safeHeader(response, name) {
287
+ try {
288
+ return response.headers?.get?.(name) ?? null;
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
293
+
294
+ // src/boot.ts
295
+ function bootEndpoint(apiUrl) {
296
+ return `${apiUrl.replace(/\/+$/, "")}/api/ingest/boot`;
297
+ }
298
+ function feedbackEndpoint(apiUrl) {
299
+ return `${apiUrl.replace(/\/+$/, "")}/api/ingest/feedback`;
300
+ }
301
+ function isBootAnswer(value) {
302
+ if (typeof value !== "object" || value === null) return false;
303
+ const v = value;
304
+ return typeof v.originAllowed === "boolean" && typeof v.canSubmit === "boolean" && typeof v.gate === "string" && (v.tier === null || typeof v.tier === "string");
305
+ }
306
+ function compactIdentity(identity) {
307
+ if (!identity) return {};
308
+ const out = {};
309
+ if (identity.hostIdentity) out.hostIdentity = identity.hostIdentity;
310
+ if (identity.reporterSession) out.reporterSession = identity.reporterSession;
311
+ if (identity.anonymousId) out.anonymousId = identity.anonymousId;
312
+ return out;
313
+ }
314
+ function isFixbackApiRequest(apiUrl, url) {
315
+ const apiBase = apiUrl.replace(/\/+$/, "");
316
+ return url.startsWith(`${apiBase}/api/ingest/`) || url.startsWith(`${apiBase}/api/connect/`);
317
+ }
318
+ async function requestBoot(apiUrl, request, deps = {}) {
319
+ const doFetch = deps.fetch ?? resolveFetch();
320
+ if (!doFetch) return null;
321
+ let response;
322
+ try {
323
+ response = await doFetch(bootEndpoint(apiUrl), {
324
+ method: "POST",
325
+ headers: {
326
+ "content-type": "application/json",
327
+ ...deps.origin ? { origin: deps.origin } : {}
328
+ },
329
+ body: JSON.stringify(request)
330
+ });
331
+ } catch {
332
+ return null;
333
+ }
334
+ if (!response.ok) return null;
335
+ let body;
336
+ try {
337
+ body = await response.json?.();
338
+ } catch {
339
+ return null;
340
+ }
341
+ return isBootAnswer(body) ? body : null;
342
+ }
343
+
344
+ // src/connect.ts
345
+ var CONNECT_MESSAGE_SOURCE = "fixback-connect";
346
+ var CONNECT_RETURN_PARAM = "fixback";
347
+ function connectExchangeEndpoint(apiUrl) {
348
+ return `${apiUrl.replace(/\/+$/, "")}/api/connect/exchange`;
349
+ }
350
+ function connectSignOutEndpoint(apiUrl) {
351
+ return `${apiUrl.replace(/\/+$/, "")}/api/connect/signout`;
352
+ }
353
+ function buildConnectUrl(connectUrl, params) {
354
+ const base = connectUrl.split("#")[0] ?? connectUrl;
355
+ const sep = base.includes("?") ? "&" : "?";
356
+ const query = `key=${encodeURIComponent(params.key)}&return=${encodeURIComponent(params.returnUrl)}`;
357
+ return `${base}${sep}${query}`;
358
+ }
359
+ function reporterSessionStorageKey(publishableKey) {
360
+ return `fixback.reporterSession.${publishableKey}`;
361
+ }
362
+ function parseConnectMessage(data) {
363
+ if (typeof data !== "object" || data === null) return null;
364
+ const v = data;
365
+ if (v.source !== CONNECT_MESSAGE_SOURCE) return null;
366
+ return typeof v.code === "string" && v.code.length > 0 ? v.code : null;
367
+ }
368
+ function isConnectExchangeResponse(value) {
369
+ if (typeof value !== "object" || value === null) return false;
370
+ const v = value;
371
+ if (typeof v.reporterSession !== "string" || v.reporterSession.length === 0) {
372
+ return false;
373
+ }
374
+ const account = v.account;
375
+ return typeof account === "object" && account !== null && typeof account.name === "string" && typeof account.email === "string";
376
+ }
377
+ async function exchangeConnectCode(apiUrl, request, deps = {}) {
378
+ const doFetch = deps.fetch ?? resolveFetch();
379
+ if (!doFetch) return null;
380
+ let response;
381
+ try {
382
+ response = await doFetch(connectExchangeEndpoint(apiUrl), {
383
+ method: "POST",
384
+ headers: { "content-type": "application/json" },
385
+ body: JSON.stringify(request)
386
+ });
387
+ } catch {
388
+ return null;
389
+ }
390
+ if (!response.ok) return null;
391
+ let body;
392
+ try {
393
+ body = await response.json?.();
394
+ } catch {
395
+ return null;
396
+ }
397
+ return isConnectExchangeResponse(body) ? body : null;
398
+ }
399
+ async function revokeReporterSession(apiUrl, token, deps = {}) {
400
+ const doFetch = deps.fetch ?? resolveFetch();
401
+ if (!doFetch) return;
402
+ try {
403
+ await doFetch(connectSignOutEndpoint(apiUrl), {
404
+ method: "POST",
405
+ headers: { "content-type": "application/json" },
406
+ body: JSON.stringify({ reporterSession: token })
407
+ });
408
+ } catch {
409
+ }
410
+ }
411
+
412
+ // src/anonymous-id.ts
413
+ var ANONYMOUS_ID_STORAGE_KEY = "fixback.anonymousId";
414
+ function generateAnonymousId() {
415
+ const c = globalThis.crypto;
416
+ if (c && typeof c.randomUUID === "function") {
417
+ try {
418
+ return c.randomUUID();
419
+ } catch {
420
+ }
421
+ }
422
+ return `fb-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
423
+ }
424
+
425
+ // src/release.ts
426
+ var RELEASE_MAX_LENGTH = 100;
427
+ var RESERVED_RELEASES = /* @__PURE__ */ new Set([".", "..", "latest"]);
428
+ function normaliseRelease(value) {
429
+ const trimmed = value.trim();
430
+ if (trimmed.length === 0 || trimmed.length > RELEASE_MAX_LENGTH) return void 0;
431
+ if (RESERVED_RELEASES.has(trimmed)) return void 0;
432
+ if (/[\s/\\\u0000-\u001f\u007f]/.test(trimmed)) return void 0;
433
+ return trimmed;
434
+ }
435
+
436
+ // src/options.ts
437
+ var DEFAULT_API_URL = "https://api.fixback.dev";
438
+
439
+ // src/trace/console-args.ts
440
+ var MAX_MESSAGE_LENGTH = 300;
441
+ var MAX_ARG_DEPTH = 4;
442
+ var MAX_ARG_ITEMS = 100;
443
+ var MAX_ARG_STRING_LENGTH = 1024;
444
+ var MAX_CONSOLE_ARGS_BYTES = 4096;
445
+ var MAX_ERROR_STACK_LENGTH = 2048;
446
+ function capLength(value, max) {
447
+ return value.length > max ? `${value.slice(0, max)}\u2026` : value;
448
+ }
449
+ function stringifyArg(arg) {
450
+ if (typeof arg === "string") return arg;
451
+ if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
452
+ if (arg === null || arg === void 0) return String(arg);
453
+ if (typeof arg === "number" || typeof arg === "boolean") return String(arg);
454
+ try {
455
+ return JSON.stringify(arg) ?? String(arg);
456
+ } catch {
457
+ return "[object]";
458
+ }
459
+ }
460
+ function joinArgs(args) {
461
+ const text = args.map(stringifyArg).join(" ");
462
+ return text.length > MAX_MESSAGE_LENGTH ? `${text.slice(0, MAX_MESSAGE_LENGTH)}\u2026` : text;
463
+ }
464
+ function describeErrorValue(error) {
465
+ const out = {
466
+ name: error.name || "Error",
467
+ message: capLength(String(error.message ?? ""), MAX_ARG_STRING_LENGTH)
468
+ };
469
+ if (typeof error.stack === "string" && error.stack.length > 0) {
470
+ out.stack = capLength(error.stack, MAX_ERROR_STACK_LENGTH);
471
+ }
472
+ return out;
473
+ }
474
+ function safeCloneValue(value, depth, seen) {
475
+ if (value === null) return null;
476
+ const type = typeof value;
477
+ if (type === "string") return capLength(value, MAX_ARG_STRING_LENGTH);
478
+ if (type === "number") return Number.isFinite(value) ? value : String(value);
479
+ if (type === "boolean") return value;
480
+ if (type === "bigint") return `${value.toString()}n`;
481
+ if (type === "symbol") return value.toString();
482
+ if (type === "function") return "[Function]";
483
+ if (type === "undefined") return null;
484
+ const obj = value;
485
+ if (value instanceof Error) {
486
+ const { name, message } = describeErrorValue(value);
487
+ return { name, message };
488
+ }
489
+ if (seen.has(obj)) return "[Circular]";
490
+ if (depth <= 0) return Array.isArray(value) ? "[Array]" : "[Object]";
491
+ seen.add(obj);
492
+ try {
493
+ if (Array.isArray(value)) {
494
+ const items = value.slice(0, MAX_ARG_ITEMS).map((item) => safeCloneValue(item, depth - 1, seen));
495
+ if (value.length > MAX_ARG_ITEMS) {
496
+ items.push(`\u2026 ${value.length - MAX_ARG_ITEMS} more`);
497
+ }
498
+ return items;
499
+ }
500
+ const source = value;
501
+ const keys = Object.keys(source);
502
+ const out = {};
503
+ for (const key of keys.slice(0, MAX_ARG_ITEMS)) {
504
+ const child = source[key];
505
+ if (typeof child === "undefined" || typeof child === "function") continue;
506
+ out[key] = safeCloneValue(child, depth - 1, seen);
507
+ }
508
+ if (keys.length > MAX_ARG_ITEMS) out["\u2026"] = `${keys.length - MAX_ARG_ITEMS} more`;
509
+ return out;
510
+ } finally {
511
+ seen.delete(obj);
512
+ }
513
+ }
514
+ function toConsoleArg(value) {
515
+ if (value === null || value === void 0) return { t: "null", v: null };
516
+ const type = typeof value;
517
+ if (type === "string") {
518
+ return { t: "string", v: capLength(value, MAX_ARG_STRING_LENGTH) };
519
+ }
520
+ if (type === "number") {
521
+ return Number.isFinite(value) ? { t: "number", v: value } : { t: "string", v: String(value) };
522
+ }
523
+ if (type === "boolean") return { t: "bool", v: value };
524
+ if (type === "bigint") return { t: "string", v: `${value.toString()}n` };
525
+ if (type === "symbol") return { t: "string", v: value.toString() };
526
+ if (type === "function") return { t: "string", v: "[Function]" };
527
+ if (value instanceof Error) return { t: "error", v: describeErrorValue(value) };
528
+ return { t: "json", v: safeCloneValue(value, MAX_ARG_DEPTH, /* @__PURE__ */ new Set()) };
529
+ }
530
+ function capConsoleArgs(args) {
531
+ if (serializedBytes(args) <= MAX_CONSOLE_ARGS_BYTES) return args;
532
+ let out = args.slice();
533
+ while (out.length > 1 && serializedBytes(out) > MAX_CONSOLE_ARGS_BYTES) {
534
+ out = out.slice(0, -1);
535
+ }
536
+ if (out.length === 1 && serializedBytes(out) > MAX_CONSOLE_ARGS_BYTES) {
537
+ return [{ t: "string", v: "[trace: console argument omitted (too large)]" }];
538
+ }
539
+ return out;
540
+ }
541
+
542
+ // src/trace/crumbs.ts
543
+ var MAX_URL_LENGTH = 2048;
544
+ function consoleCrumb(level, args, timestamp, source) {
545
+ const structured = capConsoleArgs(args.map(toConsoleArg));
546
+ return {
547
+ category: "console",
548
+ level,
549
+ message: joinArgs(args),
550
+ timestamp,
551
+ ...structured.length > 0 ? { args: structured } : {},
552
+ ...source ? { source } : {}
553
+ };
554
+ }
555
+ function navigationCrumb(from, to, timestamp) {
556
+ const fromUrl = capLength(scrubUrl(from), MAX_URL_LENGTH);
557
+ const toUrl = capLength(scrubUrl(to), MAX_URL_LENGTH);
558
+ return {
559
+ category: "navigation",
560
+ message: `${fromUrl} \u2192 ${toUrl}`,
561
+ timestamp,
562
+ data: { from: fromUrl, to: toUrl }
563
+ };
564
+ }
565
+ function outcomeFromStatus(status) {
566
+ if (typeof status !== "number" || status <= 0) return "network-error";
567
+ if (status >= 500) return "http-5xx";
568
+ if (status >= 400) return "http-4xx";
569
+ return "ok";
570
+ }
571
+ function parseContentLength(value) {
572
+ if (typeof value !== "string") return void 0;
573
+ const bytes = Number.parseInt(value, 10);
574
+ return Number.isFinite(bytes) && bytes >= 0 ? bytes : void 0;
575
+ }
576
+ function contentTypeOf(value) {
577
+ if (typeof value !== "string" || value.length === 0) return void 0;
578
+ const media = value.split(";")[0]?.trim();
579
+ return media && media.length > 0 ? media : void 0;
580
+ }
581
+ function networkCrumb(input, timestamp) {
582
+ const url = capLength(scrubUrl(input.url), MAX_URL_LENGTH);
583
+ const hasStatus = typeof input.status === "number" && input.status > 0;
584
+ const statusPart = hasStatus ? ` \u2192 ${input.status}` : "";
585
+ const outcomePart = input.outcome !== "ok" ? ` (${input.outcome})` : "";
586
+ return {
587
+ category: input.api,
588
+ api: input.api,
589
+ method: input.method,
590
+ url,
591
+ message: `${input.method} ${url}${statusPart}${outcomePart}`,
592
+ timestamp,
593
+ ...hasStatus ? { status: input.status } : {},
594
+ ...input.statusText ? { statusText: input.statusText } : {},
595
+ ...typeof input.durationMs === "number" ? { durationMs: input.durationMs } : {},
596
+ ...typeof input.reqSize === "number" ? { reqSize: input.reqSize } : {},
597
+ ...typeof input.respSize === "number" ? { respSize: input.respSize } : {},
598
+ ...input.contentType ? { contentType: input.contentType } : {},
599
+ outcome: input.outcome
600
+ };
601
+ }
602
+ function fetchCrumb(method, url, status, timestamp) {
603
+ return networkCrumb(
604
+ { api: "fetch", method, url, status, outcome: outcomeFromStatus(status) },
605
+ timestamp
606
+ );
607
+ }
608
+ function xhrCrumb(method, url, status, timestamp) {
609
+ return networkCrumb(
610
+ { api: "xhr", method, url, status, outcome: outcomeFromStatus(status) },
611
+ timestamp
612
+ );
613
+ }
614
+ function describeError(error) {
615
+ if (error instanceof Error) {
616
+ return { name: error.name || "Error", message: error.message };
617
+ }
618
+ if (typeof error === "string") return { name: "Error", message: error };
619
+ return { name: "Error", message: stringifyArg(error) };
620
+ }
621
+ function errorCrumb(error, timestamp, causedBy) {
622
+ const { name, message } = describeError(error);
623
+ const crumb = {
624
+ category: "error",
625
+ level: "error",
626
+ message: message ? `${name}: ${message}` : name,
627
+ timestamp,
628
+ data: { errorType: name }
629
+ };
630
+ return causedBy && causedBy.length > 0 ? { ...crumb, causedBy } : crumb;
631
+ }
632
+
633
+ // src/auto-capture.ts
634
+ var DEFAULT_BURST_CAPACITY = 5;
635
+ var DEFAULT_BURST_REFILL_MS = 2e3;
636
+ var DEFAULT_MAX_DISTINCT_AUTO = 20;
637
+ var CAUSED_BY_LIMIT = 5;
638
+ function asString(value) {
639
+ if (typeof value === "string") return value;
640
+ if (value == null) return "";
641
+ try {
642
+ return String(value);
643
+ } catch {
644
+ return "";
645
+ }
646
+ }
647
+ function precedingIds(entries) {
648
+ if (!entries || entries.length === 0) return [];
649
+ const ids = [];
650
+ for (let i = entries.length - 1; i >= 0 && ids.length < CAUSED_BY_LIMIT; i -= 1) {
651
+ const id = entries[i]?.id;
652
+ if (typeof id === "string" && id.length > 0) ids.unshift(id);
653
+ }
654
+ return ids;
655
+ }
656
+ var TokenBucket = class {
657
+ constructor(options) {
658
+ this.options = options;
659
+ __publicField(this, "tokens");
660
+ __publicField(this, "last");
661
+ __publicField(this, "now");
662
+ this.now = options.now ?? Date.now;
663
+ this.tokens = Math.max(0, options.capacity);
664
+ this.last = this.now();
665
+ }
666
+ /** Consume a token if one is available (refilling first), else refuse. */
667
+ take() {
668
+ const now = this.now();
669
+ const { capacity, refillIntervalMs } = this.options;
670
+ if (refillIntervalMs > 0 && now > this.last) {
671
+ const refill = Math.floor((now - this.last) / refillIntervalMs);
672
+ if (refill > 0) {
673
+ this.tokens = Math.min(capacity, this.tokens + refill);
674
+ this.last += refill * refillIntervalMs;
675
+ }
676
+ }
677
+ if (this.tokens >= 1) {
678
+ this.tokens -= 1;
679
+ return true;
680
+ }
681
+ return false;
682
+ }
683
+ };
684
+ function createAutoCapture(config) {
685
+ const now = config.now ?? Date.now;
686
+ const maxDistinct = config.maxDistinct ?? DEFAULT_MAX_DISTINCT_AUTO;
687
+ const bucket = new TokenBucket({
688
+ capacity: config.burstCapacity ?? DEFAULT_BURST_CAPACITY,
689
+ refillIntervalMs: config.burstRefillMs ?? DEFAULT_BURST_REFILL_MS,
690
+ now
691
+ });
692
+ const seen = /* @__PURE__ */ new Map();
693
+ let dropped = 0;
694
+ async function file(fingerprint, occurrences, rich) {
695
+ const entry = seen.get(fingerprint);
696
+ if (!entry) return;
697
+ const previouslySent = entry.sentCount;
698
+ entry.sentCount = occurrences;
699
+ try {
700
+ const accepted = await config.submit({
701
+ fingerprint,
702
+ occurrences,
703
+ rich,
704
+ frames: entry.frames
705
+ });
706
+ if (!accepted && entry.sentCount === occurrences) {
707
+ entry.sentCount = previouslySent;
708
+ }
709
+ } catch {
710
+ if (entry.sentCount === occurrences) entry.sentCount = previouslySent;
711
+ }
712
+ }
713
+ function handle(extracted) {
714
+ const fingerprint = computeFingerprint(
715
+ extracted.type,
716
+ extracted.value,
717
+ extracted.stack
718
+ );
719
+ const existing = seen.get(fingerprint);
720
+ if (existing) {
721
+ existing.count += 1;
722
+ existing.lastAt = now();
723
+ return;
724
+ }
725
+ if (!bucket.take()) {
726
+ dropped += 1;
727
+ return;
728
+ }
729
+ if (seen.size >= maxDistinct) {
730
+ dropped += 1;
731
+ return;
732
+ }
733
+ const at = now();
734
+ seen.set(fingerprint, {
735
+ count: 1,
736
+ firstAt: at,
737
+ lastAt: at,
738
+ sentCount: 0,
739
+ // Parsed once per fingerprint, from the first firing's stack (#117) — the
740
+ // wire shape symbolication consumes, scrubbed like every captured URL.
741
+ frames: extractStructuredFrames(extracted.stack)
742
+ });
743
+ try {
744
+ const causedBy = precedingIds(config.buffer?.snapshot());
745
+ config.buffer?.add(errorCrumb(extracted.original, at, causedBy));
746
+ } catch {
747
+ }
748
+ void file(fingerprint, 1, true);
749
+ }
750
+ return {
751
+ capture(raw) {
752
+ try {
753
+ const extracted = config.extract(raw);
754
+ if (extracted) handle(extracted);
755
+ } catch {
756
+ }
757
+ },
758
+ flush() {
759
+ try {
760
+ for (const [fingerprint, entry] of seen) {
761
+ if (entry.count > entry.sentCount) {
762
+ void file(fingerprint, entry.count, false);
763
+ }
764
+ }
765
+ } catch {
766
+ }
767
+ },
768
+ droppedCount: () => dropped
769
+ };
770
+ }
771
+
772
+ // src/trace/buffer.ts
773
+ var DEFAULT_STREAM_BUDGETS = {
774
+ network: 100,
775
+ console: 80,
776
+ breadcrumbs: 40
777
+ };
778
+ var DEFAULT_MAX_AGE_MS = 3 * 60 * 1e3;
779
+ var DEFAULT_CONSOLE_LEVELS = [
780
+ "log",
781
+ "info",
782
+ "warn",
783
+ "error",
784
+ "assert",
785
+ "debug"
786
+ ];
787
+ var PINNED_CONSOLE_LEVELS = [
788
+ "warn",
789
+ "error",
790
+ "assert"
791
+ ];
792
+ function isPinnedConsoleLevel(level) {
793
+ return level !== void 0 && PINNED_CONSOLE_LEVELS.includes(level);
794
+ }
795
+ var SOURCE_CAPTURE_LEVELS = PINNED_CONSOLE_LEVELS;
796
+ function capturesSource(level) {
797
+ return SOURCE_CAPTURE_LEVELS.includes(level);
798
+ }
799
+ function streamOf(category) {
800
+ if (category === "console") return "console";
801
+ if (category === "fetch" || category === "xhr" || category === "beacon") {
802
+ return "network";
803
+ }
804
+ return "breadcrumbs";
805
+ }
806
+ function normalizeBudget(value, fallback) {
807
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
808
+ return fallback;
809
+ }
810
+ return Math.floor(value);
811
+ }
812
+ function createIdFactory() {
813
+ const salt = Math.random().toString(36).slice(2, 8);
814
+ let seq = 0;
815
+ return () => `${salt}${(seq++).toString(36)}`;
816
+ }
817
+ var STREAMS = ["network", "console", "breadcrumbs"];
818
+ function trimConsoleStream(held, budget) {
819
+ const over = held.length - budget;
820
+ if (over <= 0) return held;
821
+ const drop = /* @__PURE__ */ new Set();
822
+ for (let i = 0; i < held.length && drop.size < over; i += 1) {
823
+ if (!isPinnedConsoleLevel(held[i].crumb.level)) drop.add(i);
824
+ }
825
+ for (let i = 0; i < held.length && drop.size < over; i += 1) {
826
+ if (!drop.has(i)) drop.add(i);
827
+ }
828
+ return held.filter((_, i) => !drop.has(i));
829
+ }
830
+ function createBreadcrumbBuffer(config = {}) {
831
+ const budgets = {
832
+ network: normalizeBudget(config.budgets?.network, DEFAULT_STREAM_BUDGETS.network),
833
+ console: normalizeBudget(config.budgets?.console, DEFAULT_STREAM_BUDGETS.console),
834
+ breadcrumbs: normalizeBudget(
835
+ config.budgets?.breadcrumbs,
836
+ DEFAULT_STREAM_BUDGETS.breadcrumbs
837
+ )
838
+ };
839
+ const maxAgeMs = config.maxAgeMs === void 0 ? DEFAULT_MAX_AGE_MS : config.maxAgeMs;
840
+ const beforeBreadcrumb = config.beforeBreadcrumb;
841
+ const now = config.now ?? Date.now;
842
+ const mono = config.mono ?? nowMono;
843
+ const nextId = config.nextId ?? createIdFactory();
844
+ const streams = {
845
+ network: [],
846
+ console: [],
847
+ breadcrumbs: []
848
+ };
849
+ let seq = 0;
850
+ function pruneByAge() {
851
+ if (typeof maxAgeMs !== "number" || maxAgeMs <= 0) return;
852
+ const cutoff = now() - maxAgeMs;
853
+ for (const stream of STREAMS) {
854
+ const held = streams[stream];
855
+ if (held.length > 0) {
856
+ streams[stream] = held.filter((h) => h.crumb.timestamp >= cutoff);
857
+ }
858
+ }
859
+ }
860
+ return {
861
+ add(crumb) {
862
+ let entry = crumb;
863
+ if (beforeBreadcrumb) {
864
+ try {
865
+ entry = beforeBreadcrumb(crumb);
866
+ } catch {
867
+ entry = crumb;
868
+ }
869
+ }
870
+ if (!entry) return;
871
+ const stamped = {
872
+ ...entry,
873
+ id: entry.id ?? nextId(),
874
+ timestamp: typeof entry.timestamp === "number" ? entry.timestamp : now(),
875
+ mono: typeof entry.mono === "number" ? entry.mono : mono()
876
+ };
877
+ const stream = streamOf(stamped.category);
878
+ streams[stream].push({ crumb: stamped, seq: seq++ });
879
+ pruneByAge();
880
+ const held = streams[stream];
881
+ const budget = budgets[stream];
882
+ if (held.length > budget) {
883
+ streams[stream] = stream === "console" ? trimConsoleStream(held, budget) : held.slice(-budget);
884
+ }
885
+ },
886
+ snapshot() {
887
+ pruneByAge();
888
+ const merged = [];
889
+ for (const stream of STREAMS) merged.push(...streams[stream]);
890
+ merged.sort((a, b) => {
891
+ const am = typeof a.crumb.mono === "number" ? a.crumb.mono : a.crumb.timestamp;
892
+ const bm = typeof b.crumb.mono === "number" ? b.crumb.mono : b.crumb.timestamp;
893
+ return am - bm || a.seq - b.seq;
894
+ });
895
+ return merged.map((h) => h.crumb);
896
+ },
897
+ clear() {
898
+ streams.network = [];
899
+ streams.console = [];
900
+ streams.breadcrumbs = [];
901
+ seq = 0;
902
+ }
903
+ };
904
+ }
905
+
906
+ // src/trace/source.ts
907
+ function parseStackFrame(line) {
908
+ const tokens = parseStackLine(line);
909
+ if (!tokens || !tokens.location) return void 0;
910
+ const loc = tokens.location.replace(/\?[^:]*/, "");
911
+ const m = loc.match(/^(.*):(\d+):\d+$/) ?? loc.match(/^(.*):(\d+)$/);
912
+ if (!m) return void 0;
913
+ const file = m[1] ?? "";
914
+ const lineNo = Number(m[2]);
915
+ if (!file || !Number.isFinite(lineNo)) return void 0;
916
+ return { file, line: lineNo };
917
+ }
918
+ function sourceFromStack(stack, skipFrames = 0) {
919
+ if (typeof stack !== "string" || stack.length === 0) return void 0;
920
+ const frames = [];
921
+ for (const raw of stack.split("\n")) {
922
+ const frame = parseStackFrame(raw.trim());
923
+ if (frame) frames.push(frame);
924
+ }
925
+ return frames[skipFrames];
926
+ }
927
+ var CONSOLE_SOURCE_SKIP_FRAMES = 1;
928
+
929
+ // src/trace/console.ts
930
+ function instrumentConsole(buffer, consoleObj, levels, now) {
931
+ const restores = [];
932
+ for (const level of levels) {
933
+ const original = consoleObj[level];
934
+ if (typeof original !== "function") continue;
935
+ const withSource = capturesSource(level);
936
+ const wrapper = (...args) => {
937
+ let source;
938
+ if (withSource) {
939
+ try {
940
+ source = sourceFromStack(new Error().stack, CONSOLE_SOURCE_SKIP_FRAMES);
941
+ } catch {
942
+ source = void 0;
943
+ }
944
+ }
945
+ try {
946
+ if (level === "assert") {
947
+ if (!args[0]) {
948
+ buffer.add(consoleCrumb("assert", args.slice(1), now(), source));
949
+ }
950
+ } else {
951
+ buffer.add(consoleCrumb(level, args, now(), source));
952
+ }
953
+ } catch {
954
+ }
955
+ return original.apply(consoleObj, args);
956
+ };
957
+ consoleObj[level] = wrapper;
958
+ restores.push(() => {
959
+ consoleObj[level] = original;
960
+ });
961
+ }
962
+ return () => {
963
+ for (const restore of restores) restore();
964
+ };
965
+ }
966
+ export {
967
+ ANONYMOUS_ID_STORAGE_KEY,
968
+ AutoReportBackoff,
969
+ CAUSED_BY_LIMIT,
970
+ CONNECT_MESSAGE_SOURCE,
971
+ CONNECT_RETURN_PARAM,
972
+ CONSOLE_SOURCE_SKIP_FRAMES,
973
+ DEFAULT_API_URL,
974
+ DEFAULT_BURST_CAPACITY,
975
+ DEFAULT_BURST_REFILL_MS,
976
+ DEFAULT_CONSOLE_LEVELS,
977
+ DEFAULT_MAX_AGE_MS,
978
+ DEFAULT_MAX_DISTINCT_AUTO,
979
+ DEFAULT_RETRY_AFTER_SECONDS,
980
+ DEFAULT_STREAM_BUDGETS,
981
+ MAX_ARG_DEPTH,
982
+ MAX_ARG_ITEMS,
983
+ MAX_ARG_STRING_LENGTH,
984
+ MAX_CONSOLE_ARGS_BYTES,
985
+ MAX_MESSAGE_LENGTH,
986
+ MAX_URL_LENGTH,
987
+ PINNED_CONSOLE_LEVELS,
988
+ RELEASE_MAX_LENGTH,
989
+ SDK_CORE_VERSION,
990
+ SOURCE_CAPTURE_LEVELS,
991
+ TokenBucket,
992
+ applyDefaultScrub,
993
+ asString,
994
+ bootEndpoint,
995
+ buildConnectUrl,
996
+ capConsoleArgs,
997
+ capLength,
998
+ capturesSource,
999
+ compactIdentity,
1000
+ computeFingerprint,
1001
+ connectExchangeEndpoint,
1002
+ connectSignOutEndpoint,
1003
+ consoleCrumb,
1004
+ contentTypeOf,
1005
+ createAutoCapture,
1006
+ createBreadcrumbBuffer,
1007
+ createIdFactory,
1008
+ describeError,
1009
+ describeErrorValue,
1010
+ errorCrumb,
1011
+ exchangeConnectCode,
1012
+ extractStructuredFrames,
1013
+ extractTopFrames,
1014
+ feedbackEndpoint,
1015
+ fetchCrumb,
1016
+ generateAnonymousId,
1017
+ hashString,
1018
+ instrumentConsole,
1019
+ isBootAnswer,
1020
+ isConnectExchangeResponse,
1021
+ isFixbackApiRequest,
1022
+ isPinnedConsoleLevel,
1023
+ joinArgs,
1024
+ navigationCrumb,
1025
+ networkCrumb,
1026
+ normaliseRelease,
1027
+ normalize,
1028
+ normalizeBudget,
1029
+ nowMono,
1030
+ outcomeFromStatus,
1031
+ parseConnectMessage,
1032
+ parseContentLength,
1033
+ parseFrameLocation,
1034
+ parseRetryAfter,
1035
+ parseStackFrame,
1036
+ parseStackLine,
1037
+ precedingIds,
1038
+ redactPii,
1039
+ reporterSessionStorageKey,
1040
+ requestBoot,
1041
+ resolveFetch,
1042
+ revokeReporterSession,
1043
+ runBeforeSend,
1044
+ safeCloneValue,
1045
+ safeHeader,
1046
+ scrubUrl,
1047
+ serializedBytes,
1048
+ sourceFromStack,
1049
+ streamOf,
1050
+ stringifyArg,
1051
+ toConsoleArg,
1052
+ trimConsoleStream,
1053
+ trivialBodySize,
1054
+ utf8ByteLength,
1055
+ xhrCrumb
1056
+ };
1057
+ //# sourceMappingURL=index.mjs.map