@bugban/js 1.0.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 ADDED
@@ -0,0 +1,1700 @@
1
+ 'use strict';
2
+
3
+ // src/env.ts
4
+ var SDK_NAME = "bugban-js";
5
+ var SDK_VERSION = "1.0.0";
6
+ var isBrowser = (() => {
7
+ try {
8
+ return typeof window !== "undefined" && typeof window.document !== "undefined";
9
+ } catch (e) {
10
+ return false;
11
+ }
12
+ })();
13
+ var isNode = (() => {
14
+ try {
15
+ return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
16
+ } catch (e) {
17
+ return false;
18
+ }
19
+ })();
20
+ function runtimeName() {
21
+ if (isBrowser) return "browser";
22
+ if (isNode) return "node";
23
+ return "unknown";
24
+ }
25
+ function runtimeVersion() {
26
+ try {
27
+ if (isNode) return process.versions.node;
28
+ if (isBrowser) return navigator.userAgent;
29
+ } catch (e) {
30
+ }
31
+ return void 0;
32
+ }
33
+ async function nodeModule2(name) {
34
+ if (!isNode) return void 0;
35
+ const prefixed = "node:".concat(name);
36
+ for (const spec of [prefixed, String(name)]) {
37
+ try {
38
+ const ns = await import(
39
+ /* @vite-ignore */
40
+ /* webpackIgnore: true */
41
+ spec
42
+ );
43
+ if (ns) return ns.default && !ns.hostname && !ns.existsSync ? ns.default : ns;
44
+ } catch (e) {
45
+ }
46
+ }
47
+ return tryRequire(name);
48
+ }
49
+ async function hostname() {
50
+ try {
51
+ if (isBrowser) return window.location.hostname;
52
+ if (isNode) {
53
+ const os = await nodeModule2("os");
54
+ if (os && typeof os.hostname === "function") return String(os.hostname());
55
+ }
56
+ } catch (e) {
57
+ }
58
+ return void 0;
59
+ }
60
+ async function appPackageInfo() {
61
+ if (!isNode) return {};
62
+ try {
63
+ const fs = await nodeModule2("fs");
64
+ const path = await nodeModule2("path");
65
+ if (!fs || !path || typeof fs.existsSync !== "function") return {};
66
+ let dir = process.cwd();
67
+ for (let depth = 0; depth < 6; depth++) {
68
+ const file = path.join(dir, "package.json");
69
+ if (fs.existsSync(file)) {
70
+ const json = JSON.parse(fs.readFileSync(file, "utf8"));
71
+ return { name: json.name, version: json.version };
72
+ }
73
+ const parent = path.dirname(dir);
74
+ if (parent === dir) break;
75
+ dir = parent;
76
+ }
77
+ } catch (e) {
78
+ }
79
+ return {};
80
+ }
81
+ async function dependencyVersion(pkg) {
82
+ if (!isNode) return void 0;
83
+ try {
84
+ const mod = await nodeModule2("module");
85
+ if (mod && typeof mod.createRequire === "function") {
86
+ const r = mod.createRequire(process.cwd() + "/package.json");
87
+ const json = r(`${pkg}/package.json`);
88
+ return json && json.version ? String(json.version) : void 0;
89
+ }
90
+ } catch (e) {
91
+ }
92
+ return void 0;
93
+ }
94
+ function env(key) {
95
+ try {
96
+ if (typeof process !== "undefined" && process.env) {
97
+ const v = process.env[key];
98
+ return v == null || v === "" ? void 0 : String(v);
99
+ }
100
+ } catch (e) {
101
+ }
102
+ return void 0;
103
+ }
104
+ function tryRequire(name) {
105
+ try {
106
+ const req = typeof module !== "undefined" && typeof module.require === "function" ? module.require : typeof globalThis.require === "function" ? globalThis.require : void 0;
107
+ return req ? req(name) : void 0;
108
+ } catch (e) {
109
+ return void 0;
110
+ }
111
+ }
112
+ function deviceInfo() {
113
+ var _a, _b;
114
+ const out = {
115
+ sdk: SDK_NAME,
116
+ sdk_version: SDK_VERSION,
117
+ runtime: runtimeName(),
118
+ runtime_version: runtimeVersion()
119
+ };
120
+ try {
121
+ if (isBrowser) {
122
+ out.user_agent = navigator.userAgent;
123
+ out.language = navigator.language;
124
+ out.screen = { width: (_a = window.screen) == null ? void 0 : _a.width, height: (_b = window.screen) == null ? void 0 : _b.height };
125
+ out.viewport = { width: window.innerWidth, height: window.innerHeight };
126
+ out.online = navigator.onLine;
127
+ }
128
+ if (isNode) {
129
+ out.platform = process.platform;
130
+ out.arch = process.arch;
131
+ out.pid = process.pid;
132
+ }
133
+ } catch (e) {
134
+ }
135
+ return out;
136
+ }
137
+
138
+ // src/config.ts
139
+ function section(value, defaults, defaultOn) {
140
+ if (value === false) return false;
141
+ if (value === true || value === void 0) return defaultOn || value === true ? defaults : false;
142
+ return { ...defaults, ...value };
143
+ }
144
+ var CONSOLE_DEFAULTS = {
145
+ levels: ["log", "info", "warn", "error", "debug"],
146
+ // Only console.error becomes an event. console.warn is far too noisy in a
147
+ // typical app — React alone emits several per render in development.
148
+ captureAsEvent: ["error"]
149
+ };
150
+ var NETWORK_DEFAULTS = {
151
+ enabled: true,
152
+ captureFailures: true,
153
+ ignoreUrls: []
154
+ };
155
+ var SNAPSHOT_DEFAULTS = {
156
+ enabled: true,
157
+ // Never flipped to false by a preset. Turning this off ships whatever the
158
+ // end user typed — passwords included — to the Bugban server, so it has to
159
+ // be an explicit, deliberate line in the app's own config.
160
+ maskInputs: true,
161
+ maskSelectors: [],
162
+ maxBytes: 5e5
163
+ };
164
+ var VITALS_DEFAULTS = {
165
+ enabled: true,
166
+ // Vitals describe the whole audience, not one error, so a sample is enough
167
+ // and keeps ingest volume proportional to traffic.
168
+ sampleRate: 0.2,
169
+ longTaskMs: 100,
170
+ slowResourceMs: 1e3
171
+ };
172
+ function resolveOptions(input = {}) {
173
+ var _a, _b;
174
+ const apiKey = input.apiKey || env("BUGBAN_API_KEY") || env("NEXT_PUBLIC_BUGBAN_API_KEY") || "";
175
+ const host = input.host || env("BUGBAN_HOST") || env("NEXT_PUBLIC_BUGBAN_HOST") || "https://bugban.online";
176
+ return {
177
+ ...input,
178
+ apiKey,
179
+ host: host.replace(/\/+$/, ""),
180
+ release: input.release || env("BUGBAN_RELEASE"),
181
+ environment: input.environment || env("BUGBAN_ENV") || env("NODE_ENV") || "production",
182
+ // No key means no-op, exactly like the PHP SDK: an app that ships
183
+ // without configuration must not error, it must simply do nothing.
184
+ enabled: input.enabled !== false && apiKey !== "",
185
+ debug: input.debug === true,
186
+ maxBreadcrumbs: clamp((_a = input.maxBreadcrumbs) != null ? _a : 50, 1, 200),
187
+ sampleRate: clamp((_b = input.sampleRate) != null ? _b : 1, 0, 1),
188
+ console: section(input.console, CONSOLE_DEFAULTS, true),
189
+ network: section(input.network, NETWORK_DEFAULTS, true),
190
+ snapshot: section(input.snapshot, SNAPSHOT_DEFAULTS, true),
191
+ vitals: section(input.vitals, VITALS_DEFAULTS, true),
192
+ globalHandlers: input.globalHandlers !== false,
193
+ domBreadcrumbs: input.domBreadcrumbs !== false,
194
+ installPing: input.installPing !== false,
195
+ redactKeys: input.redactKeys || [],
196
+ ignoreErrors: input.ignoreErrors || []
197
+ };
198
+ }
199
+ function clamp(n, min, max) {
200
+ if (typeof n !== "number" || isNaN(n)) return min;
201
+ return Math.min(max, Math.max(min, n));
202
+ }
203
+
204
+ // src/utils.ts
205
+ function safe(fn, fallback) {
206
+ try {
207
+ return fn();
208
+ } catch (e) {
209
+ return fallback;
210
+ }
211
+ }
212
+ function nowIso() {
213
+ try {
214
+ return (/* @__PURE__ */ new Date()).toISOString();
215
+ } catch (e) {
216
+ return "";
217
+ }
218
+ }
219
+ function truncate(value, max) {
220
+ const s = typeof value === "string" ? value : stringify(value);
221
+ return s.length > max ? s.slice(0, max) + "\u2026" : s;
222
+ }
223
+ function stringify(value, max = 2e3) {
224
+ try {
225
+ const seen = /* @__PURE__ */ new WeakSet();
226
+ const out = JSON.stringify(value, (_k, v) => {
227
+ if (typeof v === "bigint") return v.toString();
228
+ if (typeof v === "function") return "[Function]";
229
+ if (v instanceof Error) return { name: v.name, message: v.message };
230
+ if (typeof v === "object" && v !== null) {
231
+ if (seen.has(v)) return "[Circular]";
232
+ seen.add(v);
233
+ }
234
+ return v;
235
+ });
236
+ if (out == null) return String(value);
237
+ return out.length > max ? out.slice(0, max) + "\u2026" : out;
238
+ } catch (e) {
239
+ try {
240
+ return String(value);
241
+ } catch (e2) {
242
+ return "[unserializable]";
243
+ }
244
+ }
245
+ }
246
+ function matchesAny(value, patterns) {
247
+ for (const p of patterns) {
248
+ try {
249
+ if (typeof p === "string") {
250
+ if (value.indexOf(p) !== -1) return true;
251
+ } else if (p.test(value)) {
252
+ return true;
253
+ }
254
+ } catch (e) {
255
+ }
256
+ }
257
+ return false;
258
+ }
259
+ function randomId() {
260
+ try {
261
+ const c = globalThis.crypto;
262
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
263
+ if (c && typeof c.getRandomValues === "function") {
264
+ const b = new Uint8Array(16);
265
+ c.getRandomValues(b);
266
+ return Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
267
+ }
268
+ } catch (e) {
269
+ }
270
+ return Math.random().toString(36).slice(2) + Date.now().toString(36);
271
+ }
272
+ function sampled(rate2) {
273
+ if (rate2 >= 1) return true;
274
+ if (rate2 <= 0) return false;
275
+ return Math.random() < rate2;
276
+ }
277
+ function selectorFor(el) {
278
+ try {
279
+ const node = el;
280
+ if (!node || typeof node.tagName !== "string") return void 0;
281
+ let s = node.tagName.toLowerCase();
282
+ if (node.id) return `${s}#${node.id}`;
283
+ if (typeof node.className === "string" && node.className.trim()) {
284
+ const cls = node.className.trim().split(/\s+/).slice(0, 2).join(".");
285
+ if (cls) s += `.${cls}`;
286
+ }
287
+ return s;
288
+ } catch (e) {
289
+ return void 0;
290
+ }
291
+ }
292
+
293
+ // src/transport.ts
294
+ function byteLength(s) {
295
+ try {
296
+ if (typeof TextEncoder === "function") return new TextEncoder().encode(s).length;
297
+ } catch (e) {
298
+ }
299
+ try {
300
+ const B = globalThis.Buffer;
301
+ if (B && typeof B.byteLength === "function") return B.byteLength(s);
302
+ } catch (e) {
303
+ }
304
+ let n = 0;
305
+ for (let i = 0; i < s.length; i++) {
306
+ const c = s.charCodeAt(i);
307
+ if (c < 128) n += 1;
308
+ else if (c < 2048) n += 2;
309
+ else if (c >= 55296 && c <= 56319) {
310
+ n += 4;
311
+ i++;
312
+ } else n += 3;
313
+ }
314
+ return n;
315
+ }
316
+ var Transport = class {
317
+ constructor(host, apiKey, debug = false) {
318
+ this.host = host;
319
+ this.apiKey = apiKey;
320
+ this.debug = debug;
321
+ }
322
+ /** Fire-and-forget POST. Resolves to true when the server accepted it. */
323
+ async send(path, body, opts = {}) {
324
+ const url = this.host.replace(/\/+$/, "") + path;
325
+ let json;
326
+ try {
327
+ json = JSON.stringify(body);
328
+ } catch (e) {
329
+ return false;
330
+ }
331
+ if (opts.beacon && isBrowser) {
332
+ const sent = safe(() => {
333
+ const nav = navigator;
334
+ if (typeof nav.sendBeacon !== "function") return false;
335
+ const blob = new Blob([JSON.stringify({ ...body, _key: this.apiKey })], {
336
+ type: "application/json"
337
+ });
338
+ return nav.sendBeacon(url, blob);
339
+ }, false);
340
+ if (sent) return true;
341
+ }
342
+ if (typeof fetch === "function") {
343
+ return this.viaFetch(url, json, opts);
344
+ }
345
+ if (isNode) {
346
+ return this.viaNodeHttp(url, json, opts);
347
+ }
348
+ this.log("no transport available");
349
+ return false;
350
+ }
351
+ async viaFetch(url, json, opts) {
352
+ var _a;
353
+ try {
354
+ const controller = typeof AbortController === "function" ? new AbortController() : void 0;
355
+ const timer = controller ? setTimeout(() => safe(() => controller.abort()), (_a = opts.timeoutMs) != null ? _a : 8e3) : void 0;
356
+ const res = await fetch(url, {
357
+ method: "POST",
358
+ headers: {
359
+ "Content-Type": "application/json",
360
+ "X-Bugban-Key": this.apiKey
361
+ },
362
+ body: json,
363
+ // keepalive lets the request outlive the page in browsers that
364
+ // support it; it is ignored elsewhere.
365
+ keepalive: opts.beacon === true,
366
+ signal: controller ? controller.signal : void 0,
367
+ // Never send the site's cookies to Bugban.
368
+ credentials: "omit",
369
+ mode: "cors"
370
+ });
371
+ if (timer) clearTimeout(timer);
372
+ if (!res.ok) this.log("ingest rejected", res.status);
373
+ return res.ok;
374
+ } catch (e) {
375
+ this.log("fetch failed", e);
376
+ return false;
377
+ }
378
+ }
379
+ /**
380
+ * Node without global fetch — that is Node 17 and older, which a fair
381
+ * number of customers still run in production. Uses the http/https module
382
+ * loaded the bundler-opaque way, because `globalThis.require` does not
383
+ * exist under ESM and this path would otherwise silently deliver nothing.
384
+ */
385
+ async viaNodeHttp(url, json, opts) {
386
+ const isHttps = url.indexOf("https://") === 0;
387
+ const mod = await nodeModule2(isHttps ? "https" : "http");
388
+ if (!mod || typeof mod.request !== "function") return false;
389
+ return new Promise((resolve) => {
390
+ var _a;
391
+ try {
392
+ const u = new URL(url);
393
+ const req = mod.request(
394
+ {
395
+ method: "POST",
396
+ hostname: u.hostname,
397
+ port: u.port || (isHttps ? 443 : 80),
398
+ path: u.pathname + u.search,
399
+ timeout: (_a = opts.timeoutMs) != null ? _a : 8e3,
400
+ headers: {
401
+ "Content-Type": "application/json",
402
+ // Byte length, not string length — a multi-byte
403
+ // message would otherwise truncate the body.
404
+ "Content-Length": byteLength(json),
405
+ "X-Bugban-Key": this.apiKey
406
+ }
407
+ },
408
+ (res) => {
409
+ res.resume();
410
+ resolve(res.statusCode >= 200 && res.statusCode < 300);
411
+ }
412
+ );
413
+ req.on("error", () => resolve(false));
414
+ req.on("timeout", () => {
415
+ safe(() => req.destroy());
416
+ resolve(false);
417
+ });
418
+ req.write(json);
419
+ req.end();
420
+ } catch (e) {
421
+ resolve(false);
422
+ }
423
+ });
424
+ }
425
+ log(...args) {
426
+ if (!this.debug) return;
427
+ safe(() => console.warn("[bugban]", ...args));
428
+ }
429
+ };
430
+
431
+ // src/breadcrumbs.ts
432
+ var BreadcrumbBuffer = class {
433
+ constructor(max) {
434
+ this.max = max;
435
+ this.items = [];
436
+ }
437
+ add(crumb) {
438
+ try {
439
+ const full = {
440
+ type: crumb.type,
441
+ message: crumb.message,
442
+ level: crumb.level,
443
+ data: crumb.data,
444
+ timestamp: crumb.timestamp || nowIso()
445
+ };
446
+ this.items.push(full);
447
+ if (this.items.length > this.max) {
448
+ this.items.splice(0, this.items.length - this.max);
449
+ }
450
+ } catch (e) {
451
+ }
452
+ }
453
+ /** Snapshot for sending. Copied so a later push cannot mutate a sent event. */
454
+ all() {
455
+ return this.items.slice();
456
+ }
457
+ clear() {
458
+ this.items = [];
459
+ }
460
+ setMax(max) {
461
+ this.max = max;
462
+ if (this.items.length > max) {
463
+ this.items.splice(0, this.items.length - max);
464
+ }
465
+ }
466
+ };
467
+
468
+ // src/stacktrace.ts
469
+ var V8_LINE = /^\s*at (?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+)|([^)]+))\)?\s*$/;
470
+ var MOZ_LINE = /^\s*(?:(.*?)@)?(.+?):(\d+):(\d+)\s*$/;
471
+ var VENDOR_PATTERNS = [
472
+ "node_modules",
473
+ "webpack-internal",
474
+ "webpack://",
475
+ "/.next/",
476
+ "/.nuxt/",
477
+ "node:internal",
478
+ "internal/process"
479
+ ];
480
+ function isInApp(file) {
481
+ if (!file) return false;
482
+ for (const p of VENDOR_PATTERNS) {
483
+ if (file.indexOf(p) !== -1) return false;
484
+ }
485
+ return true;
486
+ }
487
+ function toFrame(fn, file, line, col) {
488
+ const f = {
489
+ file: file || void 0,
490
+ line: line ? parseInt(line, 10) : void 0,
491
+ column: col ? parseInt(col, 10) : void 0,
492
+ in_app: isInApp(file)
493
+ };
494
+ if (fn && fn !== "<anonymous>") f.function = fn.trim();
495
+ return f;
496
+ }
497
+ function parseStack(stack, limit = 50) {
498
+ if (typeof stack !== "string" || stack === "") return [];
499
+ const frames = [];
500
+ const lines = stack.split("\n");
501
+ for (const raw of lines) {
502
+ if (frames.length >= limit) break;
503
+ const line = raw.replace(/\r$/, "");
504
+ if (!line.trim()) continue;
505
+ if (!/^\s*at\s/.test(line) && line.indexOf("@") === -1) continue;
506
+ let m = V8_LINE.exec(line);
507
+ if (m) {
508
+ if (m[5] && !m[2]) {
509
+ const loc = /^(.*?):(\d+):(\d+)$/.exec(m[5]);
510
+ frames.push(
511
+ loc ? toFrame(m[1], loc[1], loc[2], loc[3]) : { function: m[1], file: m[5], in_app: isInApp(m[5]) }
512
+ );
513
+ } else {
514
+ frames.push(toFrame(m[1], m[2] || "", m[3] || "", m[4] || ""));
515
+ }
516
+ continue;
517
+ }
518
+ m = MOZ_LINE.exec(line);
519
+ if (m) {
520
+ frames.push(toFrame(m[1], m[2] || "", m[3] || "", m[4] || ""));
521
+ continue;
522
+ }
523
+ frames.push({ function: line.trim(), in_app: false });
524
+ }
525
+ return frames;
526
+ }
527
+ function normalizeError(input) {
528
+ if (input instanceof Error) {
529
+ return {
530
+ name: input.name || "Error",
531
+ message: input.message || String(input),
532
+ stack: input.stack
533
+ };
534
+ }
535
+ if (typeof input === "string") return { name: "Error", message: input };
536
+ if (input && typeof input === "object") {
537
+ const o = input;
538
+ if (typeof o.message === "string") {
539
+ return {
540
+ name: typeof o.name === "string" ? o.name : "Error",
541
+ message: o.message,
542
+ stack: typeof o.stack === "string" ? o.stack : void 0
543
+ };
544
+ }
545
+ try {
546
+ return { name: "Error", message: JSON.stringify(o) };
547
+ } catch (e) {
548
+ return { name: "Error", message: "[object]" };
549
+ }
550
+ }
551
+ return { name: "Error", message: String(input) };
552
+ }
553
+
554
+ // src/snapshot.ts
555
+ var MASK_CHAR = "\u2022";
556
+ var DROP_TAGS = ["SCRIPT", "NOSCRIPT", "TEMPLATE"];
557
+ function maskValue(value) {
558
+ const len = Math.min(value.length, 12);
559
+ return len > 0 ? MASK_CHAR.repeat(len) : "";
560
+ }
561
+ function maskFormFields(root) {
562
+ const fields = root.querySelectorAll("input, textarea, select");
563
+ for (let i = 0; i < fields.length; i++) {
564
+ const el = fields[i];
565
+ try {
566
+ const type = (el.getAttribute("type") || "").toLowerCase();
567
+ if (type === "password") {
568
+ el.setAttribute("value", MASK_CHAR.repeat(8));
569
+ continue;
570
+ }
571
+ if (type === "checkbox" || type === "radio" || type === "submit" || type === "button") {
572
+ continue;
573
+ }
574
+ const current = el.getAttribute("value") || "";
575
+ if (current) el.setAttribute("value", maskValue(current));
576
+ if (el.tagName === "TEXTAREA" && el.textContent) {
577
+ el.textContent = maskValue(el.textContent);
578
+ }
579
+ } catch (e) {
580
+ }
581
+ }
582
+ }
583
+ function maskSelectors(root, selectors) {
584
+ for (const sel of selectors) {
585
+ try {
586
+ const nodes = root.querySelectorAll(sel);
587
+ for (let i = 0; i < nodes.length; i++) {
588
+ const el = nodes[i];
589
+ if (el.textContent) el.textContent = MASK_CHAR.repeat(6);
590
+ }
591
+ } catch (e) {
592
+ }
593
+ }
594
+ }
595
+ function materializeFormState(source, clone) {
596
+ try {
597
+ const src = source.querySelectorAll("input, textarea, select");
598
+ const dst = clone.querySelectorAll("input, textarea, select");
599
+ const n = Math.min(src.length, dst.length);
600
+ for (let i = 0; i < n; i++) {
601
+ const s = src[i];
602
+ const d = dst[i];
603
+ if (s.tagName === "SELECT") {
604
+ d.setAttribute("data-bugban-selected", String(s.selectedIndex));
605
+ continue;
606
+ }
607
+ if (typeof s.value === "string" && s.value !== "") d.setAttribute("value", s.value);
608
+ if (s.checked) d.setAttribute("checked", "checked");
609
+ }
610
+ } catch (e) {
611
+ }
612
+ }
613
+ function collectCss(maxBytes) {
614
+ let css = "";
615
+ try {
616
+ const sheets = document.styleSheets;
617
+ for (let i = 0; i < sheets.length; i++) {
618
+ if (css.length > maxBytes) break;
619
+ try {
620
+ const rules = sheets[i].cssRules;
621
+ if (!rules) continue;
622
+ for (let j = 0; j < rules.length; j++) {
623
+ css += rules[j].cssText + "\n";
624
+ if (css.length > maxBytes) break;
625
+ }
626
+ } catch (e) {
627
+ continue;
628
+ }
629
+ }
630
+ } catch (e) {
631
+ }
632
+ return css.length > maxBytes ? css.slice(0, maxBytes) : css;
633
+ }
634
+ function takeSnapshot(opts) {
635
+ if (!isBrowser || !opts.enabled) return null;
636
+ try {
637
+ const source = document.documentElement;
638
+ if (!source) return null;
639
+ const clone = source.cloneNode(true);
640
+ materializeFormState(source, clone);
641
+ if (opts.maskInputs) maskFormFields(clone);
642
+ if (opts.maskSelectors && opts.maskSelectors.length) {
643
+ maskSelectors(clone, opts.maskSelectors);
644
+ }
645
+ for (const tag of DROP_TAGS) {
646
+ const nodes = clone.getElementsByTagName(tag);
647
+ while (nodes.length > 0) {
648
+ const n = nodes[0];
649
+ if (n.parentNode) n.parentNode.removeChild(n);
650
+ else break;
651
+ }
652
+ }
653
+ try {
654
+ const head = clone.querySelector("head");
655
+ if (head && !head.querySelector("base")) {
656
+ const base = document.createElement("base");
657
+ base.setAttribute("href", document.baseURI || location.href);
658
+ head.insertBefore(base, head.firstChild);
659
+ }
660
+ } catch (e) {
661
+ }
662
+ let html = clone.outerHTML || "";
663
+ if (html.length > opts.maxBytes) {
664
+ return null;
665
+ }
666
+ const css = collectCss(Math.max(0, opts.maxBytes - html.length));
667
+ return {
668
+ kind: "dom",
669
+ html,
670
+ css: css || void 0,
671
+ viewport: {
672
+ width: window.innerWidth,
673
+ height: window.innerHeight,
674
+ scrollX: window.scrollX || 0,
675
+ scrollY: window.scrollY || 0
676
+ },
677
+ url: location.href,
678
+ taken_at: nowIso()
679
+ };
680
+ } catch (e) {
681
+ return null;
682
+ }
683
+ }
684
+
685
+ // src/vitals.ts
686
+ var THRESHOLDS = {
687
+ LCP: [2500, 4e3],
688
+ FCP: [1800, 3e3],
689
+ CLS: [0.1, 0.25],
690
+ INP: [200, 500],
691
+ TTFB: [800, 1800]
692
+ };
693
+ function rate(name, value) {
694
+ const t = THRESHOLDS[name];
695
+ if (!t) return void 0;
696
+ if (value <= t[0]) return "good";
697
+ if (value <= t[1]) return "needs-improvement";
698
+ return "poor";
699
+ }
700
+ var VitalsCollector = class {
701
+ constructor(opts, flush2) {
702
+ this.opts = opts;
703
+ this.flush = flush2;
704
+ this.metrics = [];
705
+ this.observers = [];
706
+ this.flushed = false;
707
+ /** LCP/CLS/INP are "final value wins" — keyed so later values replace. */
708
+ this.latest = {};
709
+ }
710
+ start() {
711
+ if (!isBrowser || !this.opts.enabled) return;
712
+ if (typeof PerformanceObserver !== "function") return;
713
+ this.observeTtfb();
714
+ this.observePaint();
715
+ this.observeLcp();
716
+ this.observeCls();
717
+ this.observeInp();
718
+ this.observeLongTasks();
719
+ this.observeResources();
720
+ this.installFlushTriggers();
721
+ }
722
+ stop() {
723
+ for (const o of this.observers) safe(() => o.disconnect());
724
+ this.observers = [];
725
+ }
726
+ /** Manually add a metric — adapters use this for component render times. */
727
+ push(metric) {
728
+ var _a, _b, _c;
729
+ try {
730
+ this.metrics.push({
731
+ ...metric,
732
+ rating: (_a = metric.rating) != null ? _a : rate(metric.name, metric.value),
733
+ url: (_b = metric.url) != null ? _b : location.href,
734
+ occurred_at: (_c = metric.occurred_at) != null ? _c : nowIso()
735
+ });
736
+ } catch (e) {
737
+ }
738
+ }
739
+ set(name, value, extra = {}) {
740
+ this.latest[name] = {
741
+ name,
742
+ value: Math.round(value * 1e3) / 1e3,
743
+ rating: rate(name, value),
744
+ url: safe(() => location.href),
745
+ occurred_at: nowIso(),
746
+ ...extra
747
+ };
748
+ }
749
+ observe(type, cb, buffered = true) {
750
+ safe(() => {
751
+ const obs = new PerformanceObserver((list) => safe(() => cb(list.getEntries())));
752
+ obs.observe({ type, buffered });
753
+ this.observers.push(obs);
754
+ });
755
+ }
756
+ observeTtfb() {
757
+ safe(() => {
758
+ const nav = performance.getEntriesByType("navigation")[0];
759
+ if (nav && typeof nav.responseStart === "number" && nav.responseStart > 0) {
760
+ this.set("TTFB", nav.responseStart, { navigation_type: nav.type });
761
+ }
762
+ });
763
+ }
764
+ observePaint() {
765
+ this.observe("paint", (entries) => {
766
+ for (const e of entries) {
767
+ if (e.name === "first-contentful-paint") this.set("FCP", e.startTime);
768
+ }
769
+ });
770
+ }
771
+ observeLcp() {
772
+ this.observe("largest-contentful-paint", (entries) => {
773
+ const last = entries[entries.length - 1];
774
+ if (!last) return;
775
+ this.set("LCP", last.startTime, {
776
+ target: selectorFor(last.element),
777
+ data: { size: last.size, url: last.url || void 0 }
778
+ });
779
+ });
780
+ }
781
+ observeCls() {
782
+ let clsValue = 0;
783
+ let sessionValue = 0;
784
+ let sessionEntries = [];
785
+ this.observe("layout-shift", (entries) => {
786
+ var _a;
787
+ for (const entry of entries) {
788
+ if (entry.hadRecentInput) continue;
789
+ const first = sessionEntries[0];
790
+ const last = sessionEntries[sessionEntries.length - 1];
791
+ if (sessionValue && entry.startTime - last.startTime < 1e3 && entry.startTime - first.startTime < 5e3) {
792
+ sessionValue += entry.value;
793
+ sessionEntries.push(entry);
794
+ } else {
795
+ sessionValue = entry.value;
796
+ sessionEntries = [entry];
797
+ }
798
+ if (sessionValue > clsValue) {
799
+ clsValue = sessionValue;
800
+ const worst = sessionEntries.slice().sort((a, b) => b.value - a.value)[0];
801
+ this.set("CLS", clsValue, {
802
+ target: ((_a = worst == null ? void 0 : worst.sources) == null ? void 0 : _a[0]) ? selectorFor(worst.sources[0].node) : void 0
803
+ });
804
+ }
805
+ }
806
+ });
807
+ }
808
+ observeInp() {
809
+ let worst = 0;
810
+ this.observe("event", (entries) => {
811
+ for (const entry of entries) {
812
+ const duration = entry.duration;
813
+ if (typeof duration !== "number" || duration <= worst) continue;
814
+ worst = duration;
815
+ this.set("INP", duration, {
816
+ target: selectorFor(entry.target),
817
+ data: { interaction: entry.name }
818
+ });
819
+ }
820
+ });
821
+ }
822
+ observeLongTasks() {
823
+ this.observe("longtask", (entries) => {
824
+ var _a, _b;
825
+ for (const e of entries) {
826
+ if (e.duration < this.opts.longTaskMs) continue;
827
+ this.push({
828
+ name: "LONG_TASK",
829
+ value: Math.round(e.duration),
830
+ rating: e.duration > 200 ? "poor" : "needs-improvement",
831
+ target: ((_b = (_a = e.attribution) == null ? void 0 : _a[0]) == null ? void 0 : _b.containerName) || void 0,
832
+ data: { start: Math.round(e.startTime) }
833
+ });
834
+ }
835
+ });
836
+ }
837
+ observeResources() {
838
+ this.observe("resource", (entries) => {
839
+ for (const e of entries) {
840
+ if (e.duration < this.opts.slowResourceMs) continue;
841
+ if (e.name.indexOf("/api/ingest/") !== -1) continue;
842
+ this.push({
843
+ name: "RESOURCE",
844
+ value: Math.round(e.duration),
845
+ rating: e.duration > 3e3 ? "poor" : "needs-improvement",
846
+ target: e.name,
847
+ data: {
848
+ type: e.initiatorType,
849
+ size: e.transferSize || void 0,
850
+ cached: e.transferSize === 0 && e.decodedBodySize > 0
851
+ }
852
+ });
853
+ }
854
+ });
855
+ }
856
+ /**
857
+ * Flush when the page is hidden. `visibilitychange` fires reliably on
858
+ * mobile where `beforeunload` often does not; `pagehide` covers bfcache.
859
+ */
860
+ installFlushTriggers() {
861
+ const doFlush = () => this.flushNow();
862
+ safe(() => {
863
+ document.addEventListener(
864
+ "visibilitychange",
865
+ () => {
866
+ if (document.visibilityState === "hidden") doFlush();
867
+ },
868
+ { capture: true }
869
+ );
870
+ window.addEventListener("pagehide", doFlush, { capture: true });
871
+ });
872
+ }
873
+ flushNow() {
874
+ if (this.flushed) return;
875
+ try {
876
+ const all = this.metrics.concat(Object.keys(this.latest).map((k) => this.latest[k]));
877
+ if (all.length === 0) return;
878
+ this.flushed = true;
879
+ this.metrics = [];
880
+ this.latest = {};
881
+ this.flush(all);
882
+ } catch (e) {
883
+ }
884
+ }
885
+ };
886
+
887
+ // src/integrations/console.ts
888
+ var LEVEL_MAP = {
889
+ log: "info",
890
+ info: "info",
891
+ debug: "debug",
892
+ warn: "warning",
893
+ error: "error"
894
+ };
895
+ function installConsole(opts, hooks) {
896
+ if (typeof console === "undefined") return () => {
897
+ };
898
+ const originals = {};
899
+ let inside = false;
900
+ for (const method of opts.levels) {
901
+ const original = console[method];
902
+ if (typeof original !== "function") continue;
903
+ originals[method] = original;
904
+ console[method] = function(...args) {
905
+ try {
906
+ if (!inside) {
907
+ inside = true;
908
+ const level = LEVEL_MAP[method] || "info";
909
+ const message = args.map((a) => typeof a === "string" ? a : truncate(a, 200)).join(" ");
910
+ hooks.breadcrumb(level, truncate(message, 500), args);
911
+ if ((method === "error" || method === "warn") && opts.captureAsEvent.indexOf(method) !== -1) {
912
+ hooks.capture(level, message, args);
913
+ }
914
+ inside = false;
915
+ }
916
+ } catch (e) {
917
+ inside = false;
918
+ }
919
+ return safe(() => original.apply(console, args));
920
+ };
921
+ }
922
+ return () => {
923
+ for (const m in originals) {
924
+ safe(() => {
925
+ console[m] = originals[m];
926
+ });
927
+ }
928
+ };
929
+ }
930
+
931
+ // src/redact.ts
932
+ var DEFAULT_REDACT_KEYS = [
933
+ "password",
934
+ "passwd",
935
+ "pwd",
936
+ "secret",
937
+ "token",
938
+ "access_token",
939
+ "refresh_token",
940
+ "api_key",
941
+ "apikey",
942
+ "authorization",
943
+ "auth",
944
+ "cookie",
945
+ "set-cookie",
946
+ "session",
947
+ "csrf",
948
+ "xsrf",
949
+ "card",
950
+ "card_number",
951
+ "cardnumber",
952
+ "cvv",
953
+ "cvc",
954
+ "pin",
955
+ "ssn",
956
+ "iban"
957
+ ];
958
+ var REDACTED = "[redacted]";
959
+ function isSensitiveKey(key, extra) {
960
+ const k = key.toLowerCase();
961
+ for (const s of DEFAULT_REDACT_KEYS) {
962
+ if (k.indexOf(s) !== -1) return true;
963
+ }
964
+ for (const s of extra) {
965
+ if (s && k.indexOf(s.toLowerCase()) !== -1) return true;
966
+ }
967
+ return false;
968
+ }
969
+ function redact(value, extraKeys = [], depth = 0) {
970
+ if (depth > 6 || value == null) return value;
971
+ try {
972
+ if (Array.isArray(value)) {
973
+ return value.slice(0, 100).map((v) => redact(v, extraKeys, depth + 1));
974
+ }
975
+ if (typeof value === "object") {
976
+ const src = value;
977
+ const out = {};
978
+ let count = 0;
979
+ for (const key in src) {
980
+ if (!Object.prototype.hasOwnProperty.call(src, key)) continue;
981
+ if (++count > 100) break;
982
+ out[key] = isSensitiveKey(key, extraKeys) ? REDACTED : redact(src[key], extraKeys, depth + 1);
983
+ }
984
+ return out;
985
+ }
986
+ } catch (e) {
987
+ }
988
+ return value;
989
+ }
990
+ function redactUrl(url, extraKeys = []) {
991
+ try {
992
+ const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(url);
993
+ const u = new URL(url, hasScheme ? void 0 : "http://localhost");
994
+ if (u.username || u.password) {
995
+ u.username = "";
996
+ u.password = "";
997
+ }
998
+ u.searchParams.forEach((_v, k) => {
999
+ if (isSensitiveKey(k, extraKeys)) u.searchParams.set(k, REDACTED);
1000
+ });
1001
+ return hasScheme ? u.toString() : u.pathname + u.search + u.hash;
1002
+ } catch (e) {
1003
+ return url;
1004
+ }
1005
+ }
1006
+
1007
+ // src/integrations/network.ts
1008
+ function installNetwork(opts, hooks, redactKeys, ownHost) {
1009
+ const undo = [];
1010
+ const ignored = (url) => {
1011
+ if (url.indexOf("/api/ingest/") !== -1) return true;
1012
+ if (ownHost && url.indexOf(ownHost) === 0) return true;
1013
+ return matchesAny(url, opts.ignoreUrls);
1014
+ };
1015
+ const record = (method, rawUrl, status, startedAt, errored) => {
1016
+ try {
1017
+ const url = redactUrl(rawUrl, redactKeys);
1018
+ const duration = Math.round(now() - startedAt);
1019
+ const failed = errored || typeof status === "number" && status >= 400;
1020
+ const data = {
1021
+ method,
1022
+ url,
1023
+ status_code: status,
1024
+ duration_ms: duration
1025
+ };
1026
+ const label = `${method} ${url} \u2192 ${errored ? "failed" : status} (${duration}ms)`;
1027
+ hooks.breadcrumb(label, data, failed);
1028
+ if (opts.captureFailures && (errored || status !== void 0 && status >= 500)) {
1029
+ hooks.capture(label, data);
1030
+ }
1031
+ } catch (e) {
1032
+ }
1033
+ };
1034
+ if (opts.enabled && typeof fetch === "function") {
1035
+ const original = fetch;
1036
+ const patched = function(input, init2) {
1037
+ let url = "";
1038
+ let method = "GET";
1039
+ const started = now();
1040
+ try {
1041
+ url = typeof input === "string" ? input : input && input.url ? input.url : String(input);
1042
+ method = String(init2 && init2.method || input && input.method || "GET").toUpperCase();
1043
+ } catch (e) {
1044
+ }
1045
+ if (!url || ignored(url)) return original.apply(this, [input, init2]);
1046
+ return original.apply(this, [input, init2]).then(
1047
+ (res) => {
1048
+ record(method, url, res ? res.status : void 0, started, false);
1049
+ return res;
1050
+ },
1051
+ (err) => {
1052
+ record(method, url, void 0, started, true);
1053
+ throw err;
1054
+ }
1055
+ );
1056
+ };
1057
+ globalThis.fetch = patched;
1058
+ undo.push(() => {
1059
+ globalThis.fetch = original;
1060
+ });
1061
+ }
1062
+ if (opts.enabled && typeof XMLHttpRequest === "function") {
1063
+ const proto = XMLHttpRequest.prototype;
1064
+ const origOpen = proto.open;
1065
+ const origSend = proto.send;
1066
+ proto.open = function(method, url, ...rest) {
1067
+ safe(() => {
1068
+ this.__bugban = { method: String(method || "GET").toUpperCase(), url: String(url || "") };
1069
+ });
1070
+ return origOpen.apply(this, [method, url, ...rest]);
1071
+ };
1072
+ proto.send = function(...args) {
1073
+ const meta = safe(() => this.__bugban);
1074
+ if (meta && meta.url && !ignored(meta.url)) {
1075
+ const started = now();
1076
+ safe(() => {
1077
+ this.addEventListener("loadend", () => {
1078
+ const status = this.status || void 0;
1079
+ record(meta.method, meta.url, status, started, !status);
1080
+ });
1081
+ });
1082
+ }
1083
+ return origSend.apply(this, args);
1084
+ };
1085
+ undo.push(() => {
1086
+ proto.open = origOpen;
1087
+ proto.send = origSend;
1088
+ });
1089
+ }
1090
+ return () => {
1091
+ for (const fn of undo) safe(fn);
1092
+ };
1093
+ }
1094
+ function now() {
1095
+ try {
1096
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
1097
+ } catch (e) {
1098
+ return Date.now();
1099
+ }
1100
+ }
1101
+
1102
+ // src/integrations/globals.ts
1103
+ function installGlobalHandlers(hooks) {
1104
+ const undo = [];
1105
+ if (isBrowser) {
1106
+ const onError = (event) => {
1107
+ safe(() => {
1108
+ const err = event.error || {
1109
+ name: "Error",
1110
+ message: event.message,
1111
+ stack: void 0
1112
+ };
1113
+ hooks.capture(err, { handled: false, type: "onerror" });
1114
+ });
1115
+ };
1116
+ const onRejection = (event) => {
1117
+ safe(() => {
1118
+ const reason = event.reason;
1119
+ hooks.capture(reason, { handled: false, type: "unhandledrejection" });
1120
+ });
1121
+ };
1122
+ safe(() => window.addEventListener("error", onError, true));
1123
+ safe(() => window.addEventListener("unhandledrejection", onRejection, true));
1124
+ undo.push(() => {
1125
+ safe(() => window.removeEventListener("error", onError, true));
1126
+ safe(() => window.removeEventListener("unhandledrejection", onRejection, true));
1127
+ });
1128
+ }
1129
+ if (isNode) {
1130
+ const onUncaught = (err) => {
1131
+ hooks.capture(err, { handled: false, type: "uncaughtException" });
1132
+ safe(() => {
1133
+ void hooks.flush().then(() => {
1134
+ if (process.listenerCount("uncaughtException") <= 1) {
1135
+ process.exit(1);
1136
+ }
1137
+ });
1138
+ });
1139
+ };
1140
+ const onUnhandled = (reason) => {
1141
+ hooks.capture(reason, { handled: false, type: "unhandledRejection" });
1142
+ };
1143
+ safe(() => process.on("uncaughtException", onUncaught));
1144
+ safe(() => process.on("unhandledRejection", onUnhandled));
1145
+ safe(() => process.on("beforeExit", () => void hooks.flush()));
1146
+ undo.push(() => {
1147
+ safe(() => process.off("uncaughtException", onUncaught));
1148
+ safe(() => process.off("unhandledRejection", onUnhandled));
1149
+ });
1150
+ }
1151
+ return () => {
1152
+ for (const fn of undo) safe(fn);
1153
+ };
1154
+ }
1155
+
1156
+ // src/integrations/dom.ts
1157
+ function labelFor(el) {
1158
+ return safe(() => {
1159
+ const aria = el.getAttribute("aria-label");
1160
+ if (aria) return truncate(aria, 60);
1161
+ const tag = el.tagName.toLowerCase();
1162
+ if (tag === "input" || tag === "textarea" || tag === "select") {
1163
+ const ph = el.getAttribute("placeholder") || el.getAttribute("name") || "";
1164
+ return ph ? `${tag}[${truncate(ph, 40)}]` : tag;
1165
+ }
1166
+ const text = (el.textContent || "").trim().replace(/\s+/g, " ");
1167
+ return text ? truncate(text, 60) : tag;
1168
+ }) || "element";
1169
+ }
1170
+ function installDomBreadcrumbs(hooks) {
1171
+ if (!isBrowser) return () => {
1172
+ };
1173
+ const undo = [];
1174
+ const onClick = (event) => {
1175
+ safe(() => {
1176
+ const target = event.target;
1177
+ if (!target || typeof target.tagName !== "string") return;
1178
+ let el = target;
1179
+ for (let i = 0; i < 4 && el; i++) {
1180
+ const tag = el.tagName.toLowerCase();
1181
+ if (tag === "button" || tag === "a" || el.getAttribute("role") === "button") break;
1182
+ el = el.parentElement;
1183
+ }
1184
+ const chosen = el || target;
1185
+ hooks.click(labelFor(chosen), {
1186
+ selector: selectorFor(chosen),
1187
+ href: safe(() => chosen.getAttribute("href")) || void 0
1188
+ });
1189
+ });
1190
+ };
1191
+ safe(() => document.addEventListener("click", onClick, { capture: true, passive: true }));
1192
+ undo.push(() => safe(() => document.removeEventListener("click", onClick, true)));
1193
+ let current = safe(() => location.href) || "";
1194
+ const report = () => {
1195
+ safe(() => {
1196
+ const next = location.href;
1197
+ if (next !== current) {
1198
+ hooks.navigate(current, next);
1199
+ current = next;
1200
+ }
1201
+ });
1202
+ };
1203
+ safe(() => {
1204
+ const history = window.history;
1205
+ const origPush = history.pushState;
1206
+ const origReplace = history.replaceState;
1207
+ if (typeof origPush === "function") {
1208
+ history.pushState = function(...args) {
1209
+ const r = origPush.apply(this, args);
1210
+ report();
1211
+ return r;
1212
+ };
1213
+ undo.push(() => {
1214
+ history.pushState = origPush;
1215
+ });
1216
+ }
1217
+ if (typeof origReplace === "function") {
1218
+ history.replaceState = function(...args) {
1219
+ const r = origReplace.apply(this, args);
1220
+ report();
1221
+ return r;
1222
+ };
1223
+ undo.push(() => {
1224
+ history.replaceState = origReplace;
1225
+ });
1226
+ }
1227
+ });
1228
+ safe(() => window.addEventListener("popstate", report));
1229
+ undo.push(() => safe(() => window.removeEventListener("popstate", report)));
1230
+ return () => {
1231
+ for (const fn of undo) safe(fn);
1232
+ };
1233
+ }
1234
+
1235
+ // src/ping.ts
1236
+ var MARKER_KEY = "bugban.ping";
1237
+ var memoryMarker = false;
1238
+ async function alreadyPinged(apiKey, version) {
1239
+ const stamp = `${apiKey.slice(0, 12)}:${version}`;
1240
+ if (isBrowser) {
1241
+ const stored = safe(() => localStorage.getItem(MARKER_KEY));
1242
+ if (stored === stamp) return true;
1243
+ safe(() => localStorage.setItem(MARKER_KEY, stamp));
1244
+ if (memoryMarker) return true;
1245
+ memoryMarker = true;
1246
+ return false;
1247
+ }
1248
+ if (isNode) {
1249
+ try {
1250
+ const [fs, os, path] = await Promise.all([
1251
+ nodeModule("fs"),
1252
+ nodeModule("os"),
1253
+ nodeModule("path")
1254
+ ]);
1255
+ if (!fs || !os || !path || typeof fs.existsSync !== "function") {
1256
+ if (memoryMarker) return true;
1257
+ memoryMarker = true;
1258
+ return false;
1259
+ }
1260
+ const key = toHex(stamp).slice(0, 32);
1261
+ const file = path.join(os.tmpdir(), `.bugban-ping-${key}`);
1262
+ if (fs.existsSync(file)) return true;
1263
+ fs.writeFileSync(file, stamp);
1264
+ return false;
1265
+ } catch (e) {
1266
+ return false;
1267
+ }
1268
+ }
1269
+ if (memoryMarker) return true;
1270
+ memoryMarker = true;
1271
+ return false;
1272
+ }
1273
+ function toHex(s) {
1274
+ let out = "";
1275
+ for (let i = 0; i < s.length; i++) {
1276
+ out += s.charCodeAt(i).toString(16);
1277
+ }
1278
+ return out;
1279
+ }
1280
+ async function sendInstallPing(opts, transport) {
1281
+ if (!opts.installPing || !opts.enabled) return;
1282
+ try {
1283
+ if (await alreadyPinged(opts.apiKey, SDK_VERSION)) return;
1284
+ const pkg = await appPackageInfo();
1285
+ const host = await hostname();
1286
+ const payload = {
1287
+ sdk: SDK_NAME,
1288
+ sdk_version: SDK_VERSION,
1289
+ framework: opts.framework || runtimeName(),
1290
+ framework_version: opts.frameworkVersion,
1291
+ environment: opts.environment,
1292
+ hostname: host,
1293
+ // The app's own identity. In Node this is read from package.json; in
1294
+ // the browser there is no filesystem, so it comes from `release`
1295
+ // (usually wired to the bundler's define) and the page host.
1296
+ app_name: pkg.name || (isBrowser ? safe(() => location.hostname) : void 0),
1297
+ app_version: opts.release || pkg.version,
1298
+ // Where the SDK is running, and that runtime's version: Node's
1299
+ // process.version, or the browser UA. The panel shows this the same
1300
+ // way it shows the PHP version for the PHP SDKs.
1301
+ runtime: runtimeName(),
1302
+ runtime_version: runtimeVersion()
1303
+ };
1304
+ for (const k in payload) {
1305
+ if (payload[k] == null || payload[k] === "") delete payload[k];
1306
+ }
1307
+ void transport.send("/api/ingest/ping", payload);
1308
+ } catch (e) {
1309
+ }
1310
+ }
1311
+
1312
+ // src/client.ts
1313
+ var BugbanClient = class {
1314
+ constructor(options = {}) {
1315
+ this.teardown = [];
1316
+ this.user = null;
1317
+ this.context = {};
1318
+ this.sessionId = randomId();
1319
+ this.startedAt = Date.now();
1320
+ /** Events waiting to be sent, coalesced so an error storm is one request. */
1321
+ this.queue = [];
1322
+ this.flushTimer = null;
1323
+ /** Guard against reporting an error that our own reporting caused. */
1324
+ this.sending = false;
1325
+ this.started = false;
1326
+ this.opts = resolveOptions(options);
1327
+ this.transport = new Transport(this.opts.host, this.opts.apiKey, this.opts.debug);
1328
+ this.crumbs = new BreadcrumbBuffer(this.opts.maxBreadcrumbs);
1329
+ }
1330
+ get options() {
1331
+ return this.opts;
1332
+ }
1333
+ /** Wire up integrations. Safe to call twice — the second call is ignored. */
1334
+ start() {
1335
+ if (this.started) return;
1336
+ this.started = true;
1337
+ if (!this.opts.enabled) {
1338
+ this.debug("disabled (no API key)");
1339
+ return;
1340
+ }
1341
+ safe(() => this.installIntegrations());
1342
+ safe(() => sendInstallPing(this.opts, this.transport));
1343
+ }
1344
+ installIntegrations() {
1345
+ const o = this.opts;
1346
+ if (o.globalHandlers) {
1347
+ this.teardown.push(
1348
+ installGlobalHandlers({
1349
+ capture: (err, extra) => this.capture(err, extra),
1350
+ flush: () => this.flush()
1351
+ })
1352
+ );
1353
+ }
1354
+ if (o.console) {
1355
+ this.teardown.push(
1356
+ installConsole(o.console, {
1357
+ breadcrumb: (level, message) => this.addBreadcrumb({ type: "console", level, message }),
1358
+ capture: (level, message, args) => {
1359
+ const real = args.find((a) => a instanceof Error);
1360
+ this.capture(real != null ? real : message, {
1361
+ handled: true,
1362
+ type: "console",
1363
+ level
1364
+ });
1365
+ }
1366
+ })
1367
+ );
1368
+ }
1369
+ if (o.network) {
1370
+ this.teardown.push(
1371
+ installNetwork(
1372
+ o.network,
1373
+ {
1374
+ breadcrumb: (message, data, failed) => this.addBreadcrumb({
1375
+ type: "http",
1376
+ level: failed ? "error" : "info",
1377
+ message,
1378
+ data
1379
+ }),
1380
+ capture: (message, data) => this.capture(new Error(message), {
1381
+ handled: true,
1382
+ type: "http",
1383
+ context: data
1384
+ })
1385
+ },
1386
+ o.redactKeys,
1387
+ o.host
1388
+ )
1389
+ );
1390
+ }
1391
+ if (o.domBreadcrumbs) {
1392
+ this.teardown.push(
1393
+ installDomBreadcrumbs({
1394
+ click: (label, data) => this.addBreadcrumb({ type: "click", message: `Clicked ${label}`, data }),
1395
+ navigate: (from, to) => this.addBreadcrumb({
1396
+ type: "navigation",
1397
+ message: `${redactUrl(from, o.redactKeys)} \u2192 ${redactUrl(to, o.redactKeys)}`
1398
+ })
1399
+ })
1400
+ );
1401
+ }
1402
+ if (o.vitals && isBrowser) {
1403
+ if (sampled(o.vitals.sampleRate)) {
1404
+ this.vitals = new VitalsCollector(o.vitals, (metrics) => this.sendVitals(metrics));
1405
+ this.vitals.start();
1406
+ this.teardown.push(() => {
1407
+ var _a;
1408
+ return (_a = this.vitals) == null ? void 0 : _a.stop();
1409
+ });
1410
+ } else {
1411
+ this.debug("vitals not sampled for this session");
1412
+ }
1413
+ }
1414
+ if (isBrowser) {
1415
+ this.teardown.push(this.installUnloadFlush());
1416
+ }
1417
+ }
1418
+ /**
1419
+ * Send whatever is queued before the page goes away.
1420
+ *
1421
+ * Events are batched with a short debounce so an error loop becomes one
1422
+ * request instead of hundreds. That debounce loses the last batch whenever
1423
+ * the user navigates immediately after the error — which is the common
1424
+ * case, not an edge case: click a link, something throws, the document is
1425
+ * torn down before the timer fires. Observed exactly that while testing:
1426
+ * an uncaught TypeError never arrived while the beacon-based vitals from the
1427
+ * same page did.
1428
+ *
1429
+ * `visibilitychange` is the reliable signal (mobile Safari often skips
1430
+ * `beforeunload`), and `pagehide` covers the bfcache case. Both drain with
1431
+ * `beacon: true`, the only delivery a closing document is guaranteed to
1432
+ * complete.
1433
+ */
1434
+ installUnloadFlush() {
1435
+ const onHide = () => {
1436
+ if (document.visibilityState === "hidden") void this.drain(true);
1437
+ };
1438
+ const onPageHide = () => void this.drain(true);
1439
+ safe(() => document.addEventListener("visibilitychange", onHide, true));
1440
+ safe(() => window.addEventListener("pagehide", onPageHide, true));
1441
+ return () => {
1442
+ safe(() => document.removeEventListener("visibilitychange", onHide, true));
1443
+ safe(() => window.removeEventListener("pagehide", onPageHide, true));
1444
+ };
1445
+ }
1446
+ // --- public API ---------------------------------------------------------
1447
+ addBreadcrumb(crumb) {
1448
+ if (!this.opts.enabled) return;
1449
+ safe(() => {
1450
+ let c = {
1451
+ ...crumb,
1452
+ data: crumb.data ? redact(crumb.data, this.opts.redactKeys) : void 0,
1453
+ timestamp: crumb.timestamp || nowIso()
1454
+ };
1455
+ if (this.opts.beforeBreadcrumb) {
1456
+ const out = this.opts.beforeBreadcrumb(c);
1457
+ if (!out) return;
1458
+ c = out;
1459
+ }
1460
+ this.crumbs.add(c);
1461
+ });
1462
+ }
1463
+ setUser(user) {
1464
+ safe(() => {
1465
+ this.user = user ? redact(user, this.opts.redactKeys) : null;
1466
+ });
1467
+ }
1468
+ setContext(key, value) {
1469
+ safe(() => {
1470
+ this.context[key] = redact(value, this.opts.redactKeys);
1471
+ });
1472
+ }
1473
+ captureMessage(message, level = "info") {
1474
+ this.capture(new Error(message), { handled: true, level, type: "message" });
1475
+ }
1476
+ /** Report an error. Never throws, never blocks. */
1477
+ capture(error, extra = {}) {
1478
+ if (!this.opts.enabled) return;
1479
+ if (this.sending) return;
1480
+ safe(() => {
1481
+ var _a;
1482
+ const norm = normalizeError(error);
1483
+ if (this.opts.ignoreErrors.length && matchesAny(norm.message, this.opts.ignoreErrors)) {
1484
+ return;
1485
+ }
1486
+ if (!sampled(this.opts.sampleRate)) return;
1487
+ const frames = parseStack(norm.stack);
1488
+ const top = frames.find((f) => f.in_app) || frames[0];
1489
+ let event = {
1490
+ exception_class: norm.name,
1491
+ message: truncate(norm.message, 2e3),
1492
+ level: extra.level || "error",
1493
+ type: extra.type || "error",
1494
+ handled: (_a = extra.handled) != null ? _a : true,
1495
+ file: top == null ? void 0 : top.file,
1496
+ line: top == null ? void 0 : top.line,
1497
+ stacktrace: frames,
1498
+ release: this.opts.release,
1499
+ environment: this.opts.environment,
1500
+ user: this.user,
1501
+ breadcrumbs: this.crumbs.all(),
1502
+ device: deviceInfo(),
1503
+ session: {
1504
+ id: this.sessionId,
1505
+ started_at: new Date(this.startedAt).toISOString(),
1506
+ duration_ms: Date.now() - this.startedAt
1507
+ },
1508
+ request: extra.request || this.requestInfo(),
1509
+ context: {
1510
+ ...this.context,
1511
+ ...extra.context ? redact(extra.context, this.opts.redactKeys) : {}
1512
+ },
1513
+ snapshot: this.opts.snapshot ? takeSnapshot(this.opts.snapshot) : null
1514
+ };
1515
+ if (this.opts.beforeSend) {
1516
+ const out = this.opts.beforeSend(event);
1517
+ if (!out) return;
1518
+ event = out;
1519
+ }
1520
+ this.enqueue(event);
1521
+ });
1522
+ }
1523
+ /** Flush anything queued. Await it before a process exits. */
1524
+ async flush() {
1525
+ if (this.flushTimer) {
1526
+ clearTimeout(this.flushTimer);
1527
+ this.flushTimer = null;
1528
+ }
1529
+ await this.drain(false);
1530
+ safe(() => {
1531
+ var _a;
1532
+ return (_a = this.vitals) == null ? void 0 : _a.flushNow();
1533
+ });
1534
+ }
1535
+ /** Remove every hook the SDK installed. */
1536
+ close() {
1537
+ for (const fn of this.teardown) safe(fn);
1538
+ this.teardown = [];
1539
+ this.started = false;
1540
+ }
1541
+ /** Adapters use this to report render timings alongside browser vitals. */
1542
+ recordMetric(metric) {
1543
+ safe(() => {
1544
+ var _a;
1545
+ return (_a = this.vitals) == null ? void 0 : _a.push(metric);
1546
+ });
1547
+ }
1548
+ // --- internals ----------------------------------------------------------
1549
+ requestInfo() {
1550
+ if (!isBrowser) return void 0;
1551
+ return safe(() => ({
1552
+ url: redactUrl(location.href, this.opts.redactKeys),
1553
+ referrer: document.referrer ? redactUrl(document.referrer, this.opts.redactKeys) : void 0,
1554
+ method: "GET"
1555
+ }));
1556
+ }
1557
+ enqueue(event) {
1558
+ this.queue.push(event);
1559
+ if (this.queue.length > 50) this.queue.splice(0, this.queue.length - 50);
1560
+ if (this.flushTimer) return;
1561
+ this.flushTimer = setTimeout(() => {
1562
+ this.flushTimer = null;
1563
+ void this.drain(false);
1564
+ }, 300);
1565
+ safe(() => {
1566
+ const t = this.flushTimer;
1567
+ if (t && typeof t.unref === "function") t.unref();
1568
+ });
1569
+ }
1570
+ async drain(beacon) {
1571
+ if (this.queue.length === 0) return;
1572
+ const batch = this.queue.splice(0, this.queue.length);
1573
+ this.sending = true;
1574
+ try {
1575
+ await this.transport.send("/api/ingest/events", { events: batch }, { beacon });
1576
+ } finally {
1577
+ this.sending = false;
1578
+ }
1579
+ }
1580
+ sendVitals(metrics) {
1581
+ safe(() => {
1582
+ const queries = [];
1583
+ const vitals = [];
1584
+ for (const m of metrics) {
1585
+ if (m.name === "SLOW_QUERY") {
1586
+ const d = m.data || {};
1587
+ queries.push({
1588
+ sql: d.sql,
1589
+ duration_ms: m.value,
1590
+ connection: d.connection,
1591
+ bindings: d.bindings,
1592
+ url: d.url,
1593
+ method: d.method,
1594
+ occurred_at: m.occurred_at
1595
+ });
1596
+ } else {
1597
+ vitals.push(m);
1598
+ }
1599
+ }
1600
+ const url = safe(() => redactUrl(location.href, this.opts.redactKeys));
1601
+ if (queries.length) {
1602
+ void this.transport.send("/api/ingest/queries", { queries }, { beacon: true });
1603
+ }
1604
+ if (vitals.length) {
1605
+ void this.transport.send(
1606
+ "/api/ingest/vitals",
1607
+ {
1608
+ vitals,
1609
+ url,
1610
+ release: this.opts.release,
1611
+ environment: this.opts.environment,
1612
+ session_id: this.sessionId,
1613
+ device: deviceInfo()
1614
+ },
1615
+ // The page is usually unloading when vitals flush — beacon
1616
+ // is the only delivery that survives that.
1617
+ { beacon: true }
1618
+ );
1619
+ }
1620
+ });
1621
+ }
1622
+ debug(...args) {
1623
+ if (!this.opts.debug) return;
1624
+ safe(() => console.info("[bugban]", ...args));
1625
+ }
1626
+ };
1627
+
1628
+ // src/index.ts
1629
+ var client = null;
1630
+ function init(options = {}) {
1631
+ if (client) safe(() => client.close());
1632
+ client = new BugbanClient(options);
1633
+ client.start();
1634
+ return client;
1635
+ }
1636
+ function getClient() {
1637
+ return client;
1638
+ }
1639
+ function capture(error, extra) {
1640
+ client == null ? void 0 : client.capture(error, extra);
1641
+ }
1642
+ function captureMessage(message, level = "info") {
1643
+ client == null ? void 0 : client.captureMessage(message, level);
1644
+ }
1645
+ function setUser(user) {
1646
+ client == null ? void 0 : client.setUser(user);
1647
+ }
1648
+ function setContext(key, value) {
1649
+ client == null ? void 0 : client.setContext(key, value);
1650
+ }
1651
+ function addBreadcrumb(crumb) {
1652
+ client == null ? void 0 : client.addBreadcrumb(crumb);
1653
+ }
1654
+ function recordMetric(metric) {
1655
+ client == null ? void 0 : client.recordMetric(metric);
1656
+ }
1657
+ async function flush() {
1658
+ await (client == null ? void 0 : client.flush());
1659
+ }
1660
+ function close() {
1661
+ client == null ? void 0 : client.close();
1662
+ client = null;
1663
+ }
1664
+ var Bugban = {
1665
+ VERSION: SDK_VERSION,
1666
+ init,
1667
+ getClient,
1668
+ capture,
1669
+ captureMessage,
1670
+ setUser,
1671
+ setContext,
1672
+ addBreadcrumb,
1673
+ recordMetric,
1674
+ flush,
1675
+ close
1676
+ };
1677
+
1678
+ exports.Bugban = Bugban;
1679
+ exports.BugbanClient = BugbanClient;
1680
+ exports.SDK_NAME = SDK_NAME;
1681
+ exports.SDK_VERSION = SDK_VERSION;
1682
+ exports.addBreadcrumb = addBreadcrumb;
1683
+ exports.capture = capture;
1684
+ exports.captureMessage = captureMessage;
1685
+ exports.close = close;
1686
+ exports.dependencyVersion = dependencyVersion;
1687
+ exports.flush = flush;
1688
+ exports.getClient = getClient;
1689
+ exports.init = init;
1690
+ exports.normalizeError = normalizeError;
1691
+ exports.parseStack = parseStack;
1692
+ exports.recordMetric = recordMetric;
1693
+ exports.redact = redact;
1694
+ exports.redactUrl = redactUrl;
1695
+ exports.runtimeName = runtimeName;
1696
+ exports.runtimeVersion = runtimeVersion;
1697
+ exports.setContext = setContext;
1698
+ exports.setUser = setUser;
1699
+ //# sourceMappingURL=index.cjs.map
1700
+ //# sourceMappingURL=index.cjs.map