@sphoro/bugcatch-web-sdk 0.17.0 → 0.18.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 (51) hide show
  1. package/README.md +69 -4
  2. package/dist/bugcatch-call.es.js +586 -0
  3. package/dist/bugcatch-call.es.js.map +1 -0
  4. package/dist/bugcatch-call.umd.js +2 -0
  5. package/dist/bugcatch-call.umd.js.map +1 -0
  6. package/dist/bugcatch-core.es.js +3214 -0
  7. package/dist/bugcatch-core.es.js.map +1 -0
  8. package/dist/bugcatch-panel.es.js +2402 -0
  9. package/dist/bugcatch-panel.es.js.map +1 -0
  10. package/dist/bugcatch-panel.umd.js +1240 -0
  11. package/dist/bugcatch-panel.umd.js.map +1 -0
  12. package/dist/bugcatch.es.js +2 -5625
  13. package/dist/bugcatch.es.js.map +1 -1
  14. package/dist/bugcatch.standalone.umd.js +1536 -0
  15. package/dist/bugcatch.standalone.umd.js.map +1 -0
  16. package/dist/bugcatch.umd.js +89 -1224
  17. package/dist/bugcatch.umd.js.map +1 -1
  18. package/package.json +6 -6
  19. package/src/announcements.js +147 -0
  20. package/src/busy.js +18 -0
  21. package/src/call.js +7 -0
  22. package/src/capture.js +9 -0
  23. package/src/chunks.js +56 -0
  24. package/src/gesture.js +33 -0
  25. package/src/icons.js +18 -0
  26. package/src/identity.js +13 -8
  27. package/src/incoming-call.js +8 -30
  28. package/src/index.d.ts +50 -0
  29. package/src/index.js +138 -35
  30. package/src/lazy.js +23 -0
  31. package/src/notice.js +68 -0
  32. package/src/panel/call.js +242 -0
  33. package/src/panel/compose.js +416 -0
  34. package/src/panel/composer.js +235 -0
  35. package/src/panel/icons.js +34 -0
  36. package/src/panel/index.js +251 -0
  37. package/src/panel/known.js +130 -0
  38. package/src/panel/messages.js +176 -0
  39. package/src/panel/notices.js +34 -0
  40. package/src/panel/picks.js +61 -0
  41. package/src/panel/refs.js +321 -0
  42. package/src/panel/render.js +124 -0
  43. package/src/panel/signin.js +228 -0
  44. package/src/panel/styles.js +960 -0
  45. package/src/panel/thread.js +384 -0
  46. package/src/session.js +8 -3
  47. package/src/umd/call.js +6 -0
  48. package/src/umd/lazy.js +98 -0
  49. package/src/umd/panel.js +6 -0
  50. package/src/updates.js +20 -7
  51. package/src/widget.js +250 -3260
@@ -0,0 +1,3214 @@
1
+ var fe = (t) => {
2
+ throw TypeError(t);
3
+ };
4
+ var ht = (t, e, n) => e.has(t) || fe("Cannot " + n);
5
+ var pe = (t, e, n) => e.has(t) ? fe("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(t) : e.set(t, n);
6
+ var x = (t, e, n) => (ht(t, e, "access private method"), n);
7
+ const Y = "[REDACTED]", ft = /* @__PURE__ */ new Set([
8
+ "auth",
9
+ "bearer",
10
+ "session",
11
+ "sid",
12
+ "phpsessid",
13
+ "otp",
14
+ "pass",
15
+ "passwd",
16
+ "pwd",
17
+ "sig",
18
+ "codeverifier"
19
+ ]), pt = [
20
+ "token",
21
+ "secret",
22
+ "password",
23
+ "passwd",
24
+ "apikey",
25
+ "signature",
26
+ "credential",
27
+ "sessionid",
28
+ "privatekey",
29
+ "accesskey",
30
+ "authorization"
31
+ ], gt = /* @__PURE__ */ new Set(["key", "code", "state", "nonce", "salt", "hash"]), Ae = [
32
+ /eyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]+/g,
33
+ // JWT
34
+ /(?:AKIA|ASIA|AROA|AIDA|AGPA|ANPA|ANVA|APKA)[0-9A-Z]{16}/g,
35
+ // AWS key id
36
+ /AIza[\w-]{35}/g,
37
+ // Google API key
38
+ /gh[pousr]_[A-Za-z0-9]{36,}/g,
39
+ // GitHub token
40
+ /github_pat_\w{22,}/g,
41
+ // GitHub fine-grained PAT
42
+ /[sprk]k_(?:live|test)_[0-9A-Za-z]{10,}/g,
43
+ // Stripe
44
+ /xox[abposr]-[A-Za-z0-9-]{10,}/g,
45
+ // Slack
46
+ /sk-ant-[\w-]{20,}/g,
47
+ // Anthropic
48
+ /sk-[\w-]{20,}/g,
49
+ // OpenAI
50
+ /glpat-[\w-]{20,}/g,
51
+ // GitLab
52
+ /shp(?:at|ss|ca|pa)_[A-Za-z0-9]{32}/g
53
+ // Shopify
54
+ ], mt = /\bBearer\s+[\w.~+/=-]{8,}/gi, wt = /^([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^/?#@]+)@/;
55
+ function je(t) {
56
+ try {
57
+ return decodeURIComponent(t.replace(/\+/g, " "));
58
+ } catch {
59
+ return t;
60
+ }
61
+ }
62
+ function bt(t) {
63
+ return je(t).toLowerCase().replace(/[^a-z0-9]/g, "");
64
+ }
65
+ function yt(t) {
66
+ return t.length >= 20 ? !0 : Ae.some((e) => (e.lastIndex = 0, e.test(t)));
67
+ }
68
+ function vt(t, e) {
69
+ const n = bt(t);
70
+ if (!n) return !1;
71
+ if (ft.has(n)) return !0;
72
+ for (const o of pt) if (n.includes(o)) return !0;
73
+ return gt.has(n) ? yt(je(e)) : !1;
74
+ }
75
+ function G(t) {
76
+ return t.split("&").map((e) => {
77
+ const n = e.indexOf("=");
78
+ if (n === -1) return e;
79
+ const o = e.slice(0, n), r = e.slice(n + 1);
80
+ return r && vt(o, r) ? `${o}=${Y}` : e;
81
+ }).join("&");
82
+ }
83
+ function Oe(t) {
84
+ let e = t;
85
+ for (const n of Ae)
86
+ n.lastIndex = 0, e = e.replace(n, Y);
87
+ return e;
88
+ }
89
+ function Me(t) {
90
+ if (t == null) return t;
91
+ let e = String(t);
92
+ if (!e) return e;
93
+ e = e.replace(wt, `$1${Y}@`);
94
+ const n = e.indexOf("#");
95
+ let o = n === -1 ? e : e.slice(0, n), r = n === -1 ? "" : e.slice(n);
96
+ const i = o.indexOf("?");
97
+ if (i !== -1 && (o = o.slice(0, i + 1) + G(o.slice(i + 1))), r.length > 1) {
98
+ const a = r.slice(1), s = a.indexOf("?");
99
+ s !== -1 ? r = `#${a.slice(0, s + 1)}${G(a.slice(s + 1))}` : a.includes("=") && (r = `#${G(a)}`);
100
+ }
101
+ return Oe(o + r);
102
+ }
103
+ function kt(t) {
104
+ if (t == null) return t;
105
+ let e = String(t);
106
+ return e && (e = e.replace(/[A-Za-z][A-Za-z0-9+.-]{0,30}:\/\/[^\s"'<>)\]]+/g, (n) => Me(n)), e = e.replace(mt, `Bearer ${Y}`), Oe(e));
107
+ }
108
+ function D(t) {
109
+ const e = [];
110
+ return {
111
+ push(n) {
112
+ e.push(n), e.length > t && e.splice(0, e.length - t);
113
+ },
114
+ all: () => e.slice(),
115
+ clear: () => {
116
+ e.length = 0;
117
+ },
118
+ get size() {
119
+ return e.length;
120
+ }
121
+ };
122
+ }
123
+ const ge = ["log", "info", "warn", "error", "debug"];
124
+ function xt(t, { levels: e = ["warn", "error", "log", "info"] } = {}) {
125
+ const n = {};
126
+ for (const o of ge)
127
+ n[o] = console[o], e.includes(o) && (console[o] = (...r) => {
128
+ try {
129
+ t.push({
130
+ level: o,
131
+ text: r.map(Re).join(" ").slice(0, 2e3),
132
+ at: (/* @__PURE__ */ new Date()).toISOString()
133
+ });
134
+ } catch {
135
+ }
136
+ n[o].apply(console, r);
137
+ });
138
+ return () => {
139
+ for (const o of ge) console[o] = n[o];
140
+ };
141
+ }
142
+ function It(t, { slowMs: e = 2e3 } = {}) {
143
+ const n = window.fetch, o = XMLHttpRequest.prototype.open, r = XMLHttpRequest.prototype.send;
144
+ return n && (window.fetch = async function(...a) {
145
+ var f, p, d;
146
+ const s = performance.now(), c = typeof a[0] == "string" ? a[0] : (f = a[0]) == null ? void 0 : f.url, l = ((p = a[1]) == null ? void 0 : p.method) || (typeof a[0] == "object" ? (d = a[0]) == null ? void 0 : d.method : "GET") || "GET";
147
+ try {
148
+ const u = await n.apply(this, a);
149
+ return Q(t, { url: c, method: l, status: u.status, ms: Math.round(performance.now() - s), slowMs: e }), u;
150
+ } catch (u) {
151
+ throw Q(t, { url: c, method: l, status: 0, error: String((u == null ? void 0 : u.message) || u), ms: Math.round(performance.now() - s), slowMs: e }), u;
152
+ }
153
+ }), XMLHttpRequest.prototype.open = function(a, s, ...c) {
154
+ return this.__bc = { method: a, url: s, started: 0 }, o.call(this, a, s, ...c);
155
+ }, XMLHttpRequest.prototype.send = function(...a) {
156
+ return this.__bc && (this.__bc.started = performance.now(), this.addEventListener("loadend", () => {
157
+ Q(t, {
158
+ url: this.__bc.url,
159
+ method: this.__bc.method,
160
+ status: this.status,
161
+ ms: Math.round(performance.now() - this.__bc.started),
162
+ slowMs: e
163
+ });
164
+ })), r.apply(this, a);
165
+ }, () => {
166
+ n && (window.fetch = n), XMLHttpRequest.prototype.open = o, XMLHttpRequest.prototype.send = r;
167
+ };
168
+ }
169
+ function Q(t, { url: e, method: n, status: o, ms: r, error: i, slowMs: a }) {
170
+ try {
171
+ t.push({
172
+ // Scrubbed before it is buffered, not before it is sent: a credential
173
+ // that never enters the ring buffer cannot leave with a report, and
174
+ // cannot be read back out of memory by the page either. Redact first,
175
+ // then truncate — truncating first would split a token into something
176
+ // no pattern recognises, and the fragment would still be a fragment of
177
+ // a live credential.
178
+ url: Me(String(e)).slice(0, 500),
179
+ method: n,
180
+ status: o,
181
+ ms: r,
182
+ error: i === void 0 ? void 0 : kt(i),
183
+ // Flagged so triage can spot the interesting rows without reading everything.
184
+ failed: o === 0 || o >= 400,
185
+ slow: r >= a,
186
+ at: (/* @__PURE__ */ new Date()).toISOString()
187
+ });
188
+ } catch {
189
+ }
190
+ }
191
+ function St(t) {
192
+ const e = (o) => {
193
+ const r = String(o.message || "Error"), i = o.filename ? ` (${o.filename}:${o.lineno}:${o.colno})` : "";
194
+ t.push({
195
+ level: "error",
196
+ text: (/^uncaught/i.test(r) ? r : `Uncaught ${r}`) + i,
197
+ at: (/* @__PURE__ */ new Date()).toISOString()
198
+ });
199
+ }, n = (o) => {
200
+ t.push({
201
+ level: "error",
202
+ text: `Unhandled rejection: ${Re(o.reason)}`,
203
+ at: (/* @__PURE__ */ new Date()).toISOString()
204
+ });
205
+ };
206
+ return window.addEventListener("error", e), window.addEventListener("unhandledrejection", n), () => {
207
+ window.removeEventListener("error", e), window.removeEventListener("unhandledrejection", n);
208
+ };
209
+ }
210
+ async function me() {
211
+ const t = Le();
212
+ try {
213
+ if (navigator.getBattery) {
214
+ const e = await navigator.getBattery();
215
+ t.battery = Math.round(e.level * 100), t.charging = e.charging;
216
+ }
217
+ } catch {
218
+ }
219
+ return t;
220
+ }
221
+ function Le() {
222
+ var e, n, o, r, i, a, s, c, l;
223
+ const t = window.navigator || {};
224
+ return {
225
+ userAgent: t.userAgent,
226
+ platform: t.platform,
227
+ language: t.language,
228
+ languages: t.languages ? [...t.languages] : null,
229
+ online: t.onLine,
230
+ cookieEnabled: t.cookieEnabled,
231
+ maxTouchPoints: t.maxTouchPoints ?? null,
232
+ viewport: { width: window.innerWidth, height: window.innerHeight },
233
+ screen: {
234
+ width: (e = window.screen) == null ? void 0 : e.width,
235
+ height: (n = window.screen) == null ? void 0 : n.height,
236
+ availWidth: (o = window.screen) == null ? void 0 : o.availWidth,
237
+ availHeight: (r = window.screen) == null ? void 0 : r.availHeight,
238
+ colorDepth: (i = window.screen) == null ? void 0 : i.colorDepth,
239
+ orientation: ((s = (a = window.screen) == null ? void 0 : a.orientation) == null ? void 0 : s.type) || null
240
+ },
241
+ devicePixelRatio: window.devicePixelRatio,
242
+ referrer: document.referrer || null,
243
+ title: document.title,
244
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
245
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
246
+ timezoneOffsetMinutes: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
247
+ // A hint, not a guarantee: `webdriver` is the only broadly-available signal
248
+ // that something other than a person is driving the page.
249
+ browserMode: t.webdriver ? "Automated" : "Normal",
250
+ doNotTrack: t.doNotTrack || null,
251
+ hardwareConcurrency: t.hardwareConcurrency ?? null,
252
+ deviceMemory: t.deviceMemory ?? null,
253
+ connection: t.connection ? {
254
+ effectiveType: t.connection.effectiveType,
255
+ downlink: t.connection.downlink,
256
+ rtt: t.connection.rtt,
257
+ saveData: t.connection.saveData
258
+ } : null,
259
+ prefersColorScheme: (c = window.matchMedia) != null && c.call(window, "(prefers-color-scheme: dark)").matches ? "dark" : "light",
260
+ prefersReducedMotion: ((l = window.matchMedia) == null ? void 0 : l.call(window, "(prefers-reduced-motion: reduce)").matches) || !1
261
+ };
262
+ }
263
+ function Re(t) {
264
+ if (typeof t == "string") return t;
265
+ if (t instanceof Error) return `${t.name}: ${t.message}`;
266
+ try {
267
+ return JSON.stringify(t);
268
+ } catch {
269
+ return String(t);
270
+ }
271
+ }
272
+ const Ct = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11.5a7.5 7 0 0 1 7.5-7h1a7.5 7 0 0 1 0 14H9l-4.2 2.6a.5.5 0 0 1-.76-.5l.5-3.1A7 7 0 0 1 4 11.5z"/><path d="M8.5 11.5h.01M12 11.5h.01M15.5 11.5h.01" stroke-width="2.6"/></svg>', Bt = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9.5l6 6 6-6"/></svg>', Et = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4.2"/><path d="M12 2.6v2.2M12 19.2v2.2M2.6 12h2.2M19.2 12h2.2M5.4 5.4l1.6 1.6M17 17l1.6 1.6M18.6 5.4L17 7M7 17l-1.6 1.6"/></svg>', $t = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13.4A8.2 8.2 0 1 1 10.6 4a6.6 6.6 0 0 0 9.4 9.4z"/></svg>', Tt = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2.8" y="4.4" width="18.4" height="12.6" rx="2.2"/><path d="M8.6 20.2h6.8"/><path d="M12 4.4v12.6"/><path d="M12 7.2a3.6 3.6 0 0 1 0 7.2z" fill="currentColor" stroke="none"/></svg>', Ke = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5.2M12 16.3h.01"/></svg>', Ue = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>', At = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.8l4.6 4.4L19 7"/></svg>', Ne = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 11v5.5M12 7.7h.01"/></svg>', jt = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.3 4.2a2 2 0 0 1 3.4 0l7.6 13.1a2 2 0 0 1-1.7 3H4.4a2 2 0 0 1-1.7-3z"/><path d="M12 9.5v4.2M12 17h.01"/></svg>';
273
+ function De(t, e) {
274
+ console.warn(`[BugCatch] ${t}`, ...e ? [e] : []);
275
+ const n = new Error("[BugCatch] part of the widget could not be loaded — check the connection and try again.");
276
+ return e && (n.cause = e), n;
277
+ }
278
+ function _e(t) {
279
+ let e = null;
280
+ return (...n) => (e || (e = t(...n).catch((o) => {
281
+ throw e = null, o;
282
+ })), e);
283
+ }
284
+ const we = _e(() => import("./bugcatch-panel.es.js").catch((t) => {
285
+ throw De("the report panel could not be loaded.", t);
286
+ })), _ = _e(() => import("./bugcatch-call.es.js").catch((t) => {
287
+ throw De("the calling code could not be loaded.", t);
288
+ })), Ot = {
289
+ info: Ne,
290
+ success: At,
291
+ warning: jt,
292
+ critical: Ke
293
+ };
294
+ function Mt(t, { where: e, onDismiss: n }) {
295
+ const o = document.createElement("div");
296
+ o.className = `notice tone-${t.tone} at-${e}`, o.setAttribute("role", "status");
297
+ const r = document.createElement("span");
298
+ r.className = "notice-ico", r.setAttribute("aria-hidden", "true"), r.innerHTML = Ot[t.tone] || Ne;
299
+ const i = document.createElement("div");
300
+ i.className = "notice-text";
301
+ const a = document.createElement("strong");
302
+ if (a.textContent = t.title, i.append(a), t.body) {
303
+ const s = document.createElement("p");
304
+ s.textContent = t.body, i.append(s);
305
+ }
306
+ if (t.ctaHref && t.ctaLabel) {
307
+ const s = document.createElement("a");
308
+ s.className = "notice-cta", s.href = t.ctaHref, s.textContent = t.ctaLabel, /^https?:/i.test(t.ctaHref) && (s.target = "_blank", s.rel = "noopener noreferrer"), i.append(s);
309
+ }
310
+ if (o.append(r, i), t.dismissible && n) {
311
+ const s = document.createElement("button");
312
+ s.type = "button", s.className = "notice-x", s.setAttribute("aria-label", "Dismiss this notice"), s.innerHTML = Ue, s.addEventListener("click", () => n(t)), o.append(s);
313
+ }
314
+ return o;
315
+ }
316
+ const be = {
317
+ "bottom-right": "bottom:20px;right:20px;",
318
+ "bottom-left": "bottom:20px;left:20px;",
319
+ "top-right": "top:20px;right:20px;",
320
+ "top-left": "top:20px;left:20px;"
321
+ }, Pe = [
322
+ { value: "light", label: "Light", icon: Et },
323
+ { value: "dark", label: "Dark", icon: $t },
324
+ { value: "auto", label: "System default", icon: Tt }
325
+ ], He = "bugcatch:theme";
326
+ function Lt() {
327
+ try {
328
+ const t = localStorage.getItem(He);
329
+ return Pe.some((e) => e.value === t) ? t : null;
330
+ } catch {
331
+ return null;
332
+ }
333
+ }
334
+ function Rt(t) {
335
+ try {
336
+ localStorage.setItem(He, t);
337
+ } catch {
338
+ }
339
+ }
340
+ const Kt = (t) => `
341
+ :host { all: initial; }
342
+ :host {
343
+ --bc-accent: ${t};
344
+ --bc-on-accent: #fff;
345
+ --bc-bg: #fff;
346
+ --bc-elevated: #fff;
347
+ --bc-fg: #14161c;
348
+ --bc-muted: #5f6673;
349
+ --bc-faint: #9aa1ad;
350
+ --bc-border: #e6e8ee;
351
+ --bc-subtle: #f4f5f8;
352
+ --bc-raised: #fff;
353
+ --bc-danger: #dc2626;
354
+ --bc-ok: #059669;
355
+ --bc-warn: #b45309;
356
+ /* The lockup's own two colours, kept off --bc-accent on purpose: that one is
357
+ the customer's brand, and this mark is ours. Values are the light cut of
358
+ brand/sphoro-bugcatch-primary.svg, pulled up from near-black so the mark
359
+ identifies itself without out-shouting the host's page. */
360
+ --bc-brand-ink: #4b5563;
361
+ --bc-brand-mark: #5647ee;
362
+ --bc-shadow: 0 24px 64px rgba(10,12,16,.22), 0 4px 14px rgba(10,12,16,.08);
363
+ --bc-shadow-sm: 0 1px 2px rgba(10,12,16,.06), 0 1px 1px rgba(10,12,16,.04);
364
+ --bc-dur: 240ms;
365
+ --bc-ease: cubic-bezier(.22,1,.36,1);
366
+ /* The system everything below is drawn from. Derived tokens are written in
367
+ terms of the ones above, so a host that overrides --bc-accent or the
368
+ palette gets a coherent result without touching these. */
369
+ --bc-accent-soft: color-mix(in srgb, var(--bc-accent) 10%, var(--bc-bg));
370
+ --bc-accent-line: color-mix(in srgb, var(--bc-accent) 32%, var(--bc-border));
371
+ --bc-ring: 0 0 0 3px color-mix(in srgb, var(--bc-accent) 22%, transparent);
372
+ --bc-r-sm: 8px;
373
+ --bc-r-md: 12px;
374
+ --bc-r-lg: 18px;
375
+ --bc-r-xl: 22px;
376
+ --bc-fs-xs: 11.5px;
377
+ --bc-fs-sm: 12.5px;
378
+ --bc-fs-md: 14px;
379
+ --bc-fs-lg: 16px;
380
+ --bc-pad: 18px;
381
+ }
382
+ :host(.bc-dark) {
383
+ --bc-bg: #14161c;
384
+ --bc-elevated: #1c1f27;
385
+ --bc-fg: #eef0f4;
386
+ --bc-muted: #a3aab6;
387
+ --bc-faint: #6f7682;
388
+ --bc-border: #2a2e38;
389
+ --bc-subtle: #1f222a;
390
+ --bc-raised: #363d4b;
391
+ --bc-warn: #f59e0b;
392
+ --bc-brand-ink: #9aa1ad;
393
+ --bc-brand-mark: #9b8fff;
394
+ --bc-shadow: 0 24px 64px rgba(0,0,0,.6), 0 4px 14px rgba(0,0,0,.4);
395
+ --bc-shadow-sm: 0 1px 2px rgba(0,0,0,.4);
396
+ --bc-accent-soft: color-mix(in srgb, var(--bc-accent) 18%, var(--bc-bg));
397
+ }
398
+ @media (prefers-color-scheme: dark) {
399
+ :host(.bc-auto) {
400
+ --bc-bg: #14161c;
401
+ --bc-elevated: #1c1f27;
402
+ --bc-fg: #eef0f4;
403
+ --bc-muted: #a3aab6;
404
+ --bc-faint: #6f7682;
405
+ --bc-border: #2a2e38;
406
+ --bc-subtle: #1f222a;
407
+ --bc-raised: #363d4b;
408
+ --bc-warn: #f59e0b;
409
+ --bc-brand-ink: #9aa1ad;
410
+ --bc-brand-mark: #9b8fff;
411
+ --bc-shadow: 0 24px 64px rgba(0,0,0,.6), 0 4px 14px rgba(0,0,0,.4);
412
+ --bc-shadow-sm: 0 1px 2px rgba(0,0,0,.4);
413
+ --bc-accent-soft: color-mix(in srgb, var(--bc-accent) 18%, var(--bc-bg));
414
+ }
415
+ }
416
+
417
+ * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif; }
418
+ svg { width: 1em; height: 1em; display: block; }
419
+ /* The hidden attribute has to actually hide. Nearly every element in here is given a
420
+ display by a rule below, and any of them beats the attribute's own UA style
421
+ — which is why .dock and .launcher each had to say so themselves. Said once,
422
+ for all of them, so the next element hidden from JavaScript works. */
423
+ [hidden] { display: none !important; }
424
+ .sr-only {
425
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
426
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
427
+ }
428
+
429
+ /* One focus ring for everything clickable, so keyboard users never have to
430
+ guess where they are. Fields draw their own ring (a glow, not an outline). */
431
+ button:focus-visible, a:focus-visible, select:focus-visible, [tabindex]:focus-visible {
432
+ outline: 2px solid var(--bc-accent); outline-offset: 2px;
433
+ }
434
+
435
+ /* Motion is decoration here — every animation below is an entrance or a state
436
+ hint, so switching them off costs nothing but the polish. */
437
+ @media (prefers-reduced-motion: reduce) {
438
+ *, *::before, *::after {
439
+ animation-duration: .01ms !important;
440
+ animation-iteration-count: 1 !important;
441
+ transition-duration: .01ms !important;
442
+ }
443
+ }
444
+
445
+ @keyframes bc-launcher-in { from { opacity: 0; transform: translateY(12px) scale(.86) } to { opacity: 1; transform: none } }
446
+ @keyframes bc-item-in { from { opacity: 0; transform: translateY(8px) scale(.96) } to { opacity: 1; transform: none } }
447
+
448
+ /* ---------- dock: theme switch stacked above the launcher ---------- */
449
+ .dock {
450
+ position: fixed; z-index: 2147483000;
451
+ display: flex; flex-direction: column; gap: 10px;
452
+ }
453
+ .dock[hidden] { display: none; }
454
+ /* While the panel is open the switch rises above the scrim so it stays usable
455
+ — the panel shares the shadow root, so a change re-themes the open form. */
456
+ .dock.over { z-index: 2147483002; }
457
+ .dock.at-right { align-items: flex-end; }
458
+ .dock.at-left { align-items: flex-start; }
459
+ /* Anchored to the top, "above the launcher" would be off-screen, so the
460
+ switch goes under it and the launcher stays the outermost thing. */
461
+ .dock.at-top { flex-direction: column-reverse; }
462
+
463
+ .themer {
464
+ display: inline-flex; align-items: center; gap: 2px;
465
+ padding: 3px; border-radius: 999px;
466
+ background: var(--bc-elevated); border: 1px solid var(--bc-border);
467
+ box-shadow: 0 6px 18px rgba(10,12,16,.14), 0 1px 3px rgba(10,12,16,.08);
468
+ animation: bc-launcher-in 420ms var(--bc-ease) both;
469
+ }
470
+ .themer button {
471
+ display: grid; place-items: center;
472
+ width: 28px; height: 28px; padding: 0;
473
+ border: none; border-radius: 999px; background: transparent;
474
+ color: var(--bc-muted); cursor: pointer;
475
+ transition: background var(--bc-dur) var(--bc-ease), color var(--bc-dur) var(--bc-ease), transform 140ms var(--bc-ease);
476
+ }
477
+ .themer button svg { font-size: 15px; }
478
+ .themer button:hover { background: var(--bc-subtle); color: var(--bc-fg); }
479
+ .themer button:active { transform: scale(.92); }
480
+ .themer button[aria-pressed="true"] { background: var(--bc-accent); color: var(--bc-on-accent); }
481
+ .themer button[aria-pressed="true"]:hover { background: var(--bc-accent); filter: brightness(1.06); }
482
+
483
+ /* ---------- launcher ---------- */
484
+ .launcher {
485
+ position: relative;
486
+ display: inline-flex; align-items: center; gap: 9px;
487
+ height: 48px; padding: 0 20px 0 16px; border: none; border-radius: 999px;
488
+ background: var(--bc-accent); color: var(--bc-on-accent);
489
+ font-size: var(--bc-fs-md); font-weight: 620; line-height: 1; letter-spacing: -.01em;
490
+ cursor: pointer;
491
+ box-shadow: 0 10px 28px color-mix(in srgb, var(--bc-accent) 34%, rgba(10,12,16,.2)), 0 2px 6px rgba(10,12,16,.16);
492
+ transition: transform var(--bc-dur) var(--bc-ease), box-shadow var(--bc-dur) var(--bc-ease), filter 160ms linear, padding var(--bc-dur) var(--bc-ease);
493
+ animation: bc-launcher-in 420ms var(--bc-ease) both;
494
+ }
495
+ /* Two glyphs in one box: the bubble when closed, a chevron when open, turned
496
+ into each other so the button reads as the panel's own handle. */
497
+ .launcher .ico { position: relative; width: 20px; height: 20px; flex: none; }
498
+ .launcher .ico > span {
499
+ position: absolute; inset: 0;
500
+ transition: transform 320ms var(--bc-ease), opacity 200ms ease;
501
+ }
502
+ .launcher .ico svg { width: 20px; height: 20px; }
503
+ .launcher .ico .when-open { opacity: 0; transform: rotate(-90deg) scale(.6); }
504
+ .launcher[aria-expanded="true"] .ico .when-closed { opacity: 0; transform: rotate(90deg) scale(.6); }
505
+ .launcher[aria-expanded="true"] .ico .when-open { opacity: 1; transform: none; }
506
+ .launcher:hover { transform: translateY(-2px); filter: brightness(1.06); }
507
+ .launcher:active { transform: translateY(0) scale(.97); transition-duration: 90ms; }
508
+ .launcher:focus-visible { outline-offset: 3px; }
509
+ .launcher[hidden] { display: none; }
510
+
511
+ /* ---------- messages ----------
512
+ The panel's banner, and the load-failure card below — which is drawn when
513
+ the panel's own sheet never arrived, so these rules live in this one. */
514
+ .msg {
515
+ display: flex; align-items: flex-start; gap: 8px;
516
+ font-size: var(--bc-fs-sm); line-height: 1.45; padding: 10px 12px; border-radius: var(--bc-r-md); margin-bottom: 14px;
517
+ animation: bc-item-in 240ms var(--bc-ease) both;
518
+ }
519
+ .msg svg { flex: none; font-size: 15px; margin-top: 1px; }
520
+ .msg.err { background: color-mix(in srgb, var(--bc-danger) 10%, var(--bc-bg)); color: var(--bc-danger); border: 1px solid color-mix(in srgb, var(--bc-danger) 30%, transparent); }
521
+ .msg.info { background: var(--bc-subtle); color: var(--bc-muted); border: 1px solid var(--bc-border); }
522
+
523
+ /* ---------- announcements ----------
524
+ What the project's team is telling visitors (notice.js): beside the launcher
525
+ for the ones written for that, and at the top of the panel for all of them.
526
+ In this sheet because the card beside the button exists before the panel's
527
+ does. The tone is a colour and an icon; the site's accent is the info one. */
528
+ .notice {
529
+ --tone: var(--bc-accent);
530
+ display: flex; align-items: flex-start; gap: 10px;
531
+ padding: 12px 12px 12px 14px; border-radius: var(--bc-r-md);
532
+ background: color-mix(in srgb, var(--tone) 7%, var(--bc-elevated));
533
+ border: 1px solid color-mix(in srgb, var(--tone) 28%, var(--bc-border));
534
+ color: var(--bc-fg);
535
+ font-size: var(--bc-fs-sm); line-height: 1.45; text-align: left;
536
+ animation: bc-item-in 240ms var(--bc-ease) both;
537
+ }
538
+ .notice.tone-success { --tone: var(--bc-ok); }
539
+ .notice.tone-warning { --tone: var(--bc-warn); }
540
+ .notice.tone-critical { --tone: var(--bc-danger); }
541
+ .notice-ico { flex: none; color: var(--tone); font-size: 16px; margin-top: 1px; }
542
+ .notice-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
543
+ .notice-text strong { display: block; font-weight: 650; letter-spacing: -.005em; }
544
+ .notice-text p { margin: 2px 0 0; color: var(--bc-muted); white-space: pre-line; }
545
+ .notice-cta {
546
+ display: inline-block; margin-top: 6px;
547
+ color: var(--tone); font-weight: 620; text-decoration: none;
548
+ }
549
+ .notice-cta:hover { text-decoration: underline; text-underline-offset: 2px; }
550
+ .notice-x {
551
+ flex: none; display: grid; place-items: center;
552
+ width: 26px; height: 26px; margin: -4px -4px 0 0; padding: 0;
553
+ border: 0; border-radius: 999px; background: transparent;
554
+ color: var(--bc-muted); cursor: pointer;
555
+ }
556
+ .notice-x:hover { background: color-mix(in srgb, var(--tone) 12%, transparent); color: var(--bc-fg); }
557
+ .notice-x svg { font-size: 14px; }
558
+ /* Beside the button it floats over somebody else's page: opaque, lifted, and
559
+ never taller than a glance. The whole text is in the panel. */
560
+ .notice.at-launcher {
561
+ width: min(320px, calc(100vw - 40px));
562
+ box-shadow: 0 10px 28px rgba(10,12,16,.14), 0 1px 3px rgba(10,12,16,.08);
563
+ }
564
+ .notice.at-launcher .notice-text p {
565
+ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden;
566
+ }
567
+ .notice.at-panel { margin-bottom: 10px; }
568
+
569
+ /* The dock sits in a corner of a page the widget does not own. On a notched
570
+ phone that corner is under the home indicator, and in landscape it is under
571
+ the camera housing. env() is 0 everywhere else, so the offsets are unchanged
572
+ on hardware that has no insets. */
573
+ .dock {
574
+ padding-bottom: env(safe-area-inset-bottom, 0px);
575
+ padding-right: env(safe-area-inset-right, 0px);
576
+ padding-left: env(safe-area-inset-left, 0px);
577
+ }
578
+
579
+ /* The dock's own share of the phone and touch rules; the panel's sheet carries
580
+ the rest. */
581
+ @media (max-width: 480px) {
582
+ /* The panel is a full-screen sheet with its own close button, and a launcher
583
+ floating over its send button would be in the way. */
584
+ .dock.over { display: none; }
585
+ }
586
+ @media (pointer: coarse) {
587
+ /* The theme switch's buttons are small glyphs whose boxes cannot grow without
588
+ moving the launcher under them, so each keeps its size and gets an
589
+ invisible target drawn around it instead. */
590
+ .themer button { position: relative; }
591
+ .themer button::after {
592
+ content: '';
593
+ position: absolute;
594
+ inset: -8px;
595
+ }
596
+ /* The themer's three buttons sit side by side, so their expanders would
597
+ overlap each other without a gap to grow into. */
598
+ .themer { gap: 4px; }
599
+ .notice-x { width: 36px; height: 36px; margin: -9px -8px -6px 0; }
600
+ }
601
+ /* Hover is a lie on a touchscreen: it latches on tap and stays. The launcher's
602
+ lift in particular was left applied after the panel closed. */
603
+ @media (hover: none) {
604
+ .launcher:hover { transform: none; filter: none; }
605
+ .themer button:hover { transform: none; }
606
+ }
607
+
608
+ /* ---------- while the panel's code is on its way ----------
609
+ Usually it is already here — it is fetched once the page is idle, and again
610
+ as a pointer reaches the button. On a slow first press the launcher says it
611
+ heard, rather than looking as though it did nothing. */
612
+ .launcher[aria-busy="true"] { cursor: progress; }
613
+ .launcher[aria-busy="true"] .ico { animation: bc-busy 700ms ease-in-out infinite alternate; }
614
+ @keyframes bc-busy { to { opacity: .4; } }
615
+
616
+ /* ---------- the panel's code would not load ----------
617
+ Said above the launcher, in the form's own error style, with the retry: the
618
+ network dropped, or a Content-Security-Policy refused the file. */
619
+ .loadfail {
620
+ margin: 0; max-width: min(320px, calc(100vw - 40px));
621
+ align-items: center; box-shadow: var(--bc-shadow-sm);
622
+ }
623
+ .loadfail > span { flex: 1; }
624
+ .loadfail button {
625
+ flex: none; padding: 2px 4px; border: 0; border-radius: 6px;
626
+ background: none; color: inherit; font: inherit; font-weight: 650; cursor: pointer;
627
+ }
628
+ .loadfail button[data-act="retry"] { text-decoration: underline; text-underline-offset: 2px; }
629
+ .loadfail button svg { font-size: 13px; margin: 0; }
630
+ `;
631
+ function Ut(t) {
632
+ const e = document.createElement("div");
633
+ e.setAttribute("data-bugcatch", "widget");
634
+ let n = Lt() || Nt(t.config.theme);
635
+ e.className = ye(n);
636
+ const o = e.attachShadow({ mode: "open" }), r = document.createElement("style");
637
+ r.textContent = Kt(t.config.accent || "#4F46E5"), o.appendChild(r);
638
+ const i = be[t.config.widgetPosition] ? t.config.widgetPosition : "bottom-right", a = document.createElement("div");
639
+ a.className = `dock ${i.includes("right") ? "at-right" : "at-left"}` + (i.startsWith("top") ? " at-top" : ""), a.style.cssText = be[i];
640
+ const s = document.createElement("div");
641
+ s.className = "themer", s.setAttribute("role", "group"), s.setAttribute("aria-label", "Report widget theme"), s.innerHTML = Pe.map((w) => `<button type="button" data-theme="${w.value}" title="${w.label}" aria-label="${w.label}" aria-pressed="${w.value === n}">${w.icon}</button>`).join(""), s.addEventListener("click", (w) => {
642
+ const k = w.target.closest("button[data-theme]");
643
+ !k || k.dataset.theme === n || (n = k.dataset.theme, e.className = ye(n), Rt(n), s.querySelectorAll("button").forEach((B) => {
644
+ B.setAttribute("aria-pressed", String(B.dataset.theme === n));
645
+ }));
646
+ });
647
+ const c = document.createElement("button");
648
+ c.className = "launcher", c.type = "button", c.innerHTML = `<span class="ico" aria-hidden="true"><span class="when-closed">${Ct}</span><span class="when-open">${Bt}</span></span><span>Report a bug</span>`, c.setAttribute("aria-haspopup", "dialog"), c.setAttribute("aria-expanded", "false");
649
+ let l = null;
650
+ c.addEventListener("click", () => {
651
+ if (l) {
652
+ l.close();
653
+ return;
654
+ }
655
+ l = f();
656
+ });
657
+ function f() {
658
+ S(), v();
659
+ let w = null, k = !1;
660
+ const B = () => {
661
+ k = !0, c.removeAttribute("aria-busy"), l = null;
662
+ }, b = {
663
+ close: () => w ? w.close() : B(),
664
+ destroy: () => w ? w.destroy() : B()
665
+ };
666
+ return c.setAttribute("aria-busy", "true"), we(t.config.chunkBase).then(({ openPanel: y }) => {
667
+ k || (c.removeAttribute("aria-busy"), a.classList.add("over"), c.setAttribute("aria-expanded", "true"), w = y({
668
+ root: o,
669
+ host: e,
670
+ dock: a,
671
+ client: t,
672
+ preloadCall: u,
673
+ onClose: () => {
674
+ l = null, a.classList.remove("over"), c.setAttribute("aria-expanded", "false");
675
+ }
676
+ }));
677
+ }, () => {
678
+ k || (B(), $());
679
+ }), b;
680
+ }
681
+ const p = () => {
682
+ we(t.config.chunkBase).catch(() => {
683
+ });
684
+ };
685
+ for (const w of ["pointerenter", "touchstart", "focus"])
686
+ c.addEventListener(w, p, { once: !0, passive: !0 });
687
+ const d = ve(p);
688
+ function u() {
689
+ _(t.config.chunkBase).catch(() => {
690
+ });
691
+ }
692
+ let g = null;
693
+ function $() {
694
+ S(), g = document.createElement("div"), g.className = "msg err loadfail", g.setAttribute("role", "alert"), g.innerHTML = `${Ke}<span>The report form didn't load. Check your connection and try again.</span><button type="button" data-act="retry">Try again</button><button type="button" data-act="dismiss" aria-label="Dismiss">${Ue}</button>`, g.querySelector('[data-act="retry"]').addEventListener("click", () => {
695
+ c.focus(), l || (l = f());
696
+ }), g.querySelector('[data-act="dismiss"]').addEventListener("click", () => {
697
+ c.focus(), S();
698
+ }), a.prepend(g);
699
+ }
700
+ function S() {
701
+ g == null || g.remove(), g = null;
702
+ }
703
+ let h = null;
704
+ function I(w) {
705
+ v(), h = Mt(w, {
706
+ where: "launcher",
707
+ onDismiss: (k) => {
708
+ t.dismissAnnouncement(k), c.focus(), v();
709
+ }
710
+ }), g ? g.after(h) : a.prepend(h);
711
+ }
712
+ function v() {
713
+ h == null || h.remove(), h = null;
714
+ }
715
+ const C = t.config.announcements === !1 ? () => {
716
+ } : ve(() => {
717
+ t.announcements().then((w) => {
718
+ const k = w.find((B) => B.placement === "launcher");
719
+ k && !l && e.isConnected && I(k);
720
+ }).catch(() => {
721
+ });
722
+ });
723
+ return a.append(s, c), o.appendChild(a), document.body.appendChild(e), t.widgetControls = {
724
+ open: () => {
725
+ l || c.click();
726
+ },
727
+ close: () => {
728
+ l == null || l.close();
729
+ },
730
+ isOpen: () => !!l
731
+ }, () => {
732
+ d(), C(), l == null || l.destroy(), e.remove(), t.widgetControls = null;
733
+ };
734
+ }
735
+ function Nt(t) {
736
+ return t === "dark" || t === "light" ? t : "auto";
737
+ }
738
+ function ye(t) {
739
+ return t === "dark" ? "bc-dark" : t === "light" ? "bc-light" : "bc-auto";
740
+ }
741
+ function ve(t) {
742
+ let e = () => {
743
+ };
744
+ const n = () => {
745
+ if (typeof requestIdleCallback == "function") {
746
+ const o = requestIdleCallback(t, { timeout: 5e3 });
747
+ e = () => cancelIdleCallback(o);
748
+ } else {
749
+ const o = setTimeout(t, 1500);
750
+ e = () => clearTimeout(o);
751
+ }
752
+ };
753
+ return document.readyState === "complete" ? n() : (window.addEventListener("load", n, { once: !0 }), e = () => window.removeEventListener("load", n)), () => e();
754
+ }
755
+ const Dt = "Too many requests are being sent from here at the moment. Please try again in a minute.";
756
+ function L(t) {
757
+ return t || Dt;
758
+ }
759
+ const _t = typeof window < "u" && window.fetch ? window.fetch.bind(window) : null, Pt = typeof XMLHttpRequest < "u" ? XMLHttpRequest.prototype.open : null, Ht = typeof XMLHttpRequest < "u" ? XMLHttpRequest.prototype.send : null, zt = [
760
+ "image/png",
761
+ "image/jpeg",
762
+ "image/webp",
763
+ "image/gif",
764
+ "video/webm",
765
+ "video/mp4",
766
+ "audio/webm",
767
+ "audio/ogg",
768
+ "audio/mpeg",
769
+ "audio/mp4",
770
+ "audio/wav",
771
+ "application/pdf",
772
+ "text/plain",
773
+ "text/csv",
774
+ "application/json",
775
+ "application/zip",
776
+ "application/msword",
777
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
778
+ "application/vnd.ms-excel",
779
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
780
+ ], ke = [
781
+ "image/png",
782
+ "image/jpeg",
783
+ "image/webp",
784
+ "video/webm",
785
+ "video/mp4"
786
+ ], oo = () => {
787
+ var t;
788
+ return !!((t = navigator == null ? void 0 : navigator.mediaDevices) != null && t.getUserMedia && typeof MediaRecorder < "u");
789
+ }, io = () => {
790
+ var t;
791
+ return !!((t = navigator == null ? void 0 : navigator.mediaDevices) != null && t.getDisplayMedia && typeof MediaRecorder < "u");
792
+ }, ro = () => {
793
+ var t;
794
+ return !!((t = navigator == null ? void 0 : navigator.mediaDevices) != null && t.getDisplayMedia);
795
+ };
796
+ function ee(t, e = zt) {
797
+ const n = String(t || "").split(";")[0].trim().toLowerCase();
798
+ return e.includes(n) ? n : null;
799
+ }
800
+ async function ao() {
801
+ const t = await navigator.mediaDevices.getDisplayMedia({
802
+ video: { frameRate: 1 },
803
+ audio: !1
804
+ });
805
+ try {
806
+ const e = t.getVideoTracks()[0], n = document.createElement("video");
807
+ n.srcObject = t, n.muted = !0, n.playsInline = !0, await n.play(), await new Promise((s) => requestAnimationFrame(() => requestAnimationFrame(s)));
808
+ const { width: o, height: r } = e.getSettings(), i = document.createElement("canvas");
809
+ i.width = o || n.videoWidth, i.height = r || n.videoHeight, i.getContext("2d").drawImage(n, 0, 0, i.width, i.height);
810
+ const a = await new Promise((s) => i.toBlob(s, "image/png"));
811
+ if (!a) throw new Error("The browser returned an empty screenshot.");
812
+ return { blob: a, mime: "image/png", kind: "SCREENSHOT" };
813
+ } finally {
814
+ t.getTracks().forEach((e) => e.stop());
815
+ }
816
+ }
817
+ async function so({ maxMs: t = 12e4, onTick: e, onAutoStop: n } = {}) {
818
+ const o = await navigator.mediaDevices.getDisplayMedia({
819
+ video: { frameRate: 15 },
820
+ audio: !1
821
+ }), r = ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm"].find((u) => MediaRecorder.isTypeSupported(u)) || "video/webm", i = new MediaRecorder(o, { mimeType: r, videoBitsPerSecond: 25e5 }), a = [];
822
+ i.ondataavailable = (u) => {
823
+ u.data.size && a.push(u.data);
824
+ }, i.start(1e3);
825
+ const s = Date.now(), c = setInterval(() => e == null ? void 0 : e(Math.floor((Date.now() - s) / 1e3)), 500), l = () => {
826
+ clearInterval(c), clearTimeout(d), o.getTracks().forEach((u) => u.stop());
827
+ }, f = new Promise((u) => {
828
+ i.onstop = () => {
829
+ l(), u(new Blob(a, { type: r }));
830
+ };
831
+ }), p = () => {
832
+ i.state !== "inactive" && i.stop(), n == null || n();
833
+ };
834
+ o.getVideoTracks()[0].addEventListener("ended", p);
835
+ const d = setTimeout(p, t);
836
+ return {
837
+ mime: r,
838
+ maxMs: t,
839
+ async stop() {
840
+ return clearTimeout(d), i.state !== "inactive" && i.stop(), { blob: await f, mime: r, kind: "VIDEO" };
841
+ },
842
+ cancel() {
843
+ l(), i.state !== "inactive" && i.stop();
844
+ }
845
+ };
846
+ }
847
+ async function co({ maxMs: t = 12e4, onTick: e, onAutoStop: n } = {}) {
848
+ const o = await navigator.mediaDevices.getUserMedia({ audio: !0 }), r = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4", "audio/ogg"].find((u) => MediaRecorder.isTypeSupported(u)) || "", i = new MediaRecorder(o, r ? { mimeType: r } : void 0), a = [];
849
+ i.ondataavailable = (u) => {
850
+ u.data.size && a.push(u.data);
851
+ }, i.start(1e3);
852
+ const s = Date.now(), c = setInterval(() => e == null ? void 0 : e(Math.floor((Date.now() - s) / 1e3)), 500), l = () => {
853
+ clearInterval(c), clearTimeout(d), o.getTracks().forEach((u) => u.stop());
854
+ }, f = new Promise((u) => {
855
+ i.onstop = () => {
856
+ l(), u(new Blob(a, { type: i.mimeType || r }));
857
+ };
858
+ }), d = setTimeout(() => {
859
+ i.state !== "inactive" && i.stop(), n == null || n();
860
+ }, t);
861
+ return {
862
+ mime: i.mimeType || r,
863
+ maxMs: t,
864
+ async stop() {
865
+ clearTimeout(d), i.state !== "inactive" && i.stop();
866
+ const u = await f;
867
+ return { blob: u, mime: u.type || i.mimeType || r, kind: "AUDIO" };
868
+ },
869
+ cancel() {
870
+ l(), i.state !== "inactive" && i.stop();
871
+ }
872
+ };
873
+ }
874
+ async function Ft({ apiBase: t, apiKey: e, blob: n, mime: o, onProgress: r }) {
875
+ var l;
876
+ const i = ee(o || n.type);
877
+ if (!i)
878
+ throw new Error(`[BugCatch] ${o || n.type || "that file"} can't be attached.`);
879
+ const a = await _t(`${t}/ingest/presign`, {
880
+ method: "POST",
881
+ headers: { "Content-Type": "application/json", "X-BugCatch-Key": e },
882
+ body: JSON.stringify({ mime: i })
883
+ });
884
+ if (a.status === 429) {
885
+ let f = "";
886
+ try {
887
+ f = ((l = await a.json()) == null ? void 0 : l.message) || "";
888
+ } catch {
889
+ }
890
+ throw new Error(`[BugCatch] ${L(typeof f == "string" ? f : "")}`);
891
+ }
892
+ if (!a.ok) throw new Error(`[BugCatch] could not start the upload (HTTP ${a.status}).`);
893
+ const { key: s, uploadUrl: c } = await a.json();
894
+ return await Xt(c, n, i, r), { storageKey: s, mime: i };
895
+ }
896
+ function Xt(t, e, n, o) {
897
+ return new Promise((r, i) => {
898
+ const a = new XMLHttpRequest();
899
+ Pt.call(a, "PUT", t, !0), a.setRequestHeader("Content-Type", n), a.upload.onprogress = (s) => {
900
+ s.lengthComputable && (o == null || o(s.loaded / s.total));
901
+ }, a.onload = () => {
902
+ a.status >= 200 && a.status < 300 ? r() : i(new Error(`[BugCatch] upload failed (HTTP ${a.status}).`));
903
+ }, a.onerror = () => i(new Error(
904
+ "[BugCatch] the browser blocked the upload to storage. If this is a new environment, the media bucket is probably missing its CORS policy."
905
+ )), a.onabort = () => i(new Error("[BugCatch] upload cancelled.")), Ht.call(a, e);
906
+ });
907
+ }
908
+ function lo(t) {
909
+ return t < 1024 ? `${t} B` : t < 1024 * 1024 ? `${(t / 1024).toFixed(0)} KB` : `${(t / 1024 / 1024).toFixed(1)} MB`;
910
+ }
911
+ function uo(t) {
912
+ const e = Math.floor(t / 60), n = t % 60;
913
+ return `${e}:${String(n).padStart(2, "0")}`;
914
+ }
915
+ const X = "bugcatch.watch", xe = [2e3, 5e3, 15e3, 3e4, 6e4], qt = 5e3;
916
+ let U = null;
917
+ function Wt(t) {
918
+ U = t || null;
919
+ }
920
+ const se = () => U ? `${X}:${U}` : X;
921
+ function ce() {
922
+ return U ? `?projectId=${encodeURIComponent(U)}` : "";
923
+ }
924
+ function E() {
925
+ try {
926
+ const t = window.localStorage;
927
+ return (t == null ? void 0 : t.getItem(se())) || (t == null ? void 0 : t.getItem(X)) || null;
928
+ } catch {
929
+ return null;
930
+ }
931
+ }
932
+ function te(t) {
933
+ var e;
934
+ if (t)
935
+ try {
936
+ (e = window.localStorage) == null || e.setItem(se(), t);
937
+ } catch {
938
+ }
939
+ }
940
+ function M() {
941
+ var t, e;
942
+ try {
943
+ (t = window.localStorage) == null || t.removeItem(se()), (e = window.localStorage) == null || e.removeItem(X);
944
+ } catch {
945
+ }
946
+ ze();
947
+ }
948
+ const q = "bugcatch.unread";
949
+ function Jt(t) {
950
+ return [(t == null ? void 0 : t.bugId) || "report", (t == null ? void 0 : t.kind) || "status", (t == null ? void 0 : t.at) || ""].join("|");
951
+ }
952
+ function N() {
953
+ var t;
954
+ try {
955
+ const e = (t = window.localStorage) == null ? void 0 : t.getItem(q), n = e ? JSON.parse(e) : [];
956
+ return Array.isArray(n) ? n.filter((o) => o && typeof o.id == "string") : [];
957
+ } catch {
958
+ return [];
959
+ }
960
+ }
961
+ function V(t) {
962
+ var e, n;
963
+ try {
964
+ t.length ? (e = window.localStorage) == null || e.setItem(q, JSON.stringify(t)) : (n = window.localStorage) == null || n.removeItem(q);
965
+ } catch {
966
+ }
967
+ }
968
+ function Yt(t) {
969
+ const e = { ...t, id: Jt(t) }, n = N().filter((o) => o.id !== e.id);
970
+ return n.push(e), V(n.slice(-20)), e;
971
+ }
972
+ function Vt(t) {
973
+ const e = N().filter((n) => n.id !== t);
974
+ return V(e), e;
975
+ }
976
+ function Zt(t) {
977
+ const e = N().filter((n) => n.bugId !== t);
978
+ return V(e), e;
979
+ }
980
+ function ze() {
981
+ return V([]), [];
982
+ }
983
+ function Gt(t, e) {
984
+ let n = t, o = n.indexOf(`
985
+
986
+ `);
987
+ for (; o !== -1; ) {
988
+ const r = n.slice(0, o);
989
+ n = n.slice(o + 2);
990
+ const i = r.split(`
991
+ `).filter((a) => a.startsWith("data:")).map((a) => a.slice(5).trim()).join(`
992
+ `);
993
+ if (i)
994
+ try {
995
+ e(JSON.parse(i));
996
+ } catch {
997
+ }
998
+ o = n.indexOf(`
999
+
1000
+ `);
1001
+ }
1002
+ return n;
1003
+ }
1004
+ async function Qt({ apiBase: t }) {
1005
+ const e = E();
1006
+ if (!e) return { reporter: null, reports: [] };
1007
+ const n = await fetch(`${t}/ingest/watch/reports${ce()}`, {
1008
+ headers: { Authorization: `Bearer ${e}` }
1009
+ });
1010
+ if (n.status === 401)
1011
+ return M(), { reporter: null, reports: [] };
1012
+ if (!n.ok) throw new Error(`[BugCatch] could not load your reports (HTTP ${n.status})`);
1013
+ return n.json();
1014
+ }
1015
+ async function en({ apiBase: t, bugId: e }) {
1016
+ const n = E();
1017
+ if (!n) return null;
1018
+ const o = await fetch(`${t}/ingest/watch/reports/${encodeURIComponent(e)}${ce()}`, {
1019
+ headers: { Authorization: `Bearer ${n}` }
1020
+ });
1021
+ if (o.status === 401)
1022
+ return M(), null;
1023
+ if (o.status === 404) return null;
1024
+ if (!o.ok) throw new Error(`[BugCatch] could not load that report (HTTP ${o.status})`);
1025
+ return o.json();
1026
+ }
1027
+ async function tn({ apiBase: t, bugId: e, body: n, attachments: o }) {
1028
+ const r = E();
1029
+ if (!r) throw new Error("[BugCatch] nothing filed from this browser yet.");
1030
+ const i = await fetch(
1031
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/messages`,
1032
+ {
1033
+ method: "POST",
1034
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${r}` },
1035
+ body: JSON.stringify({
1036
+ ...n ? { body: n } : {},
1037
+ ...o != null && o.length ? { attachments: o } : {}
1038
+ })
1039
+ }
1040
+ );
1041
+ if (i.status === 401)
1042
+ throw M(), new Error("[BugCatch] this browser can no longer prove it filed that report.");
1043
+ if (!i.ok) {
1044
+ let a = "";
1045
+ try {
1046
+ const s = await i.json();
1047
+ a = Array.isArray(s.message) ? s.message.join(", ") : s.message || "";
1048
+ } catch {
1049
+ }
1050
+ throw i.status === 429 ? new Error(L(a)) : new Error(a || `[BugCatch] could not send that (HTTP ${i.status}).`);
1051
+ }
1052
+ return i.json();
1053
+ }
1054
+ async function nn({ apiBase: t, apiKey: e, projectId: n }) {
1055
+ const o = { agent: !1, live: !1 };
1056
+ try {
1057
+ const r = await fetch(
1058
+ `${t}/ingest/calls?projectId=${encodeURIComponent(n)}`,
1059
+ { headers: { "X-BugCatch-Key": e } }
1060
+ );
1061
+ if (!r.ok) return o;
1062
+ const i = await r.json();
1063
+ return i != null && i.available ? i.kinds && typeof i.kinds == "object" ? { agent: !1, live: !!i.kinds.live } : { agent: !1, live: i.kind === "live" } : o;
1064
+ } catch {
1065
+ return o;
1066
+ }
1067
+ }
1068
+ async function on({ apiBase: t, apiKey: e, token: n, payload: o, watchToken: r }) {
1069
+ const i = await fetch(`${t}/ingest/calls`, {
1070
+ method: "POST",
1071
+ headers: {
1072
+ "Content-Type": "application/json",
1073
+ "X-BugCatch-Key": e,
1074
+ // The same header `report()` sends, and for the same reason: this route
1075
+ // files a report too. Without it a project that requires sign-in refuses
1076
+ // the call outright — and the reporter is told to sign in by a panel that
1077
+ // is saying, an inch below the message, who they are signed in as.
1078
+ ...n ? { "X-BugCatch-Session": n } : {}
1079
+ },
1080
+ // The watch token this browser holds, so the one filing hands back covers
1081
+ // the reports it already could — see withHeldToken in index.js.
1082
+ body: JSON.stringify(r ? { ...o, watchToken: r } : o)
1083
+ });
1084
+ if (!i.ok) {
1085
+ let a = "";
1086
+ try {
1087
+ const c = await i.json();
1088
+ a = Array.isArray(c.message) ? c.message.join(", ") : c.message || "";
1089
+ } catch {
1090
+ }
1091
+ const s = new Error(
1092
+ i.status === 429 ? L(a) : a || `[BugCatch] the call could not be started (HTTP ${i.status}).`
1093
+ );
1094
+ throw s.status = i.status, s;
1095
+ }
1096
+ return i.json();
1097
+ }
1098
+ async function Ie({ apiBase: t, bugId: e }) {
1099
+ const n = E();
1100
+ if (!n) return !1;
1101
+ try {
1102
+ const o = await fetch(
1103
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/live-call`,
1104
+ { headers: { Authorization: `Bearer ${n}` } }
1105
+ );
1106
+ if (!o.ok) return !1;
1107
+ const r = await o.json();
1108
+ return !!(r != null && r.offered);
1109
+ } catch {
1110
+ return !1;
1111
+ }
1112
+ }
1113
+ async function rn({ apiBase: t, bugId: e }) {
1114
+ const n = E();
1115
+ if (!n) throw new Error("[BugCatch] nothing filed from this browser yet.");
1116
+ const o = await fetch(
1117
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/live-call`,
1118
+ { method: "POST", headers: { Authorization: `Bearer ${n}` } }
1119
+ );
1120
+ if (o.status === 401)
1121
+ throw M(), new Error("[BugCatch] this browser can no longer prove it filed that report.");
1122
+ if (!o.ok) {
1123
+ let r = "";
1124
+ try {
1125
+ const i = await o.json();
1126
+ r = Array.isArray(i.message) ? i.message.join(", ") : i.message || "";
1127
+ } catch {
1128
+ }
1129
+ throw new Error(r || `[BugCatch] the call could not be started (HTTP ${o.status}).`);
1130
+ }
1131
+ return o.json();
1132
+ }
1133
+ async function an({ apiBase: t, bugId: e, callId: n }) {
1134
+ const o = E();
1135
+ if (o)
1136
+ try {
1137
+ await fetch(
1138
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/live-call/${encodeURIComponent(n)}/give-up`,
1139
+ { method: "POST", headers: { Authorization: `Bearer ${o}` } }
1140
+ );
1141
+ } catch {
1142
+ }
1143
+ }
1144
+ async function sn({ apiBase: t, bugId: e, callId: n }) {
1145
+ const o = E();
1146
+ if (!o) throw new Error("[BugCatch] nothing filed from this browser yet.");
1147
+ const r = await fetch(
1148
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/live-call/${encodeURIComponent(n)}/answer`,
1149
+ { method: "POST", headers: { Authorization: `Bearer ${o}` } }
1150
+ );
1151
+ if (r.status === 401)
1152
+ throw M(), new Error("[BugCatch] this browser can no longer prove it filed that report.");
1153
+ if (r.status === 409)
1154
+ throw new Error("[BugCatch] that call is no longer ringing.");
1155
+ if (!r.ok) {
1156
+ let i = "";
1157
+ try {
1158
+ const a = await r.json();
1159
+ i = Array.isArray(a.message) ? a.message.join(", ") : a.message || "";
1160
+ } catch {
1161
+ }
1162
+ throw new Error(i || `[BugCatch] the call could not be answered (HTTP ${r.status}).`);
1163
+ }
1164
+ return r.json();
1165
+ }
1166
+ async function Se({ apiBase: t, bugId: e, callId: n }) {
1167
+ const o = E();
1168
+ if (o)
1169
+ try {
1170
+ await fetch(
1171
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/live-call/${encodeURIComponent(n)}/decline`,
1172
+ { method: "POST", headers: { Authorization: `Bearer ${o}` } }
1173
+ );
1174
+ } catch {
1175
+ }
1176
+ }
1177
+ function cn({ apiBase: t, onUpdate: e }) {
1178
+ let n = !0, o = 0, r = null;
1179
+ async function i() {
1180
+ const a = E();
1181
+ if (!a) throw new Error("nothing filed from this browser yet");
1182
+ r = new AbortController();
1183
+ const s = await fetch(`${t}/ingest/watch/stream${ce()}`, {
1184
+ headers: { Authorization: `Bearer ${a}`, Accept: "text/event-stream" },
1185
+ signal: r.signal
1186
+ });
1187
+ if (s.status === 401)
1188
+ throw n = !1, M(), new Error("subscription no longer valid");
1189
+ if (!s.ok || !s.body) throw new Error(`stream failed (${s.status})`);
1190
+ const c = Date.now(), l = s.body.getReader(), f = new TextDecoder();
1191
+ let p = "";
1192
+ try {
1193
+ for (; ; ) {
1194
+ const { value: d, done: u } = await l.read();
1195
+ if (u) break;
1196
+ p += f.decode(d, { stream: !0 }), p = Gt(p, (g) => {
1197
+ if (g.type !== "ping")
1198
+ try {
1199
+ e(g);
1200
+ } catch {
1201
+ }
1202
+ });
1203
+ }
1204
+ } finally {
1205
+ Date.now() - c >= qt && (o = 0);
1206
+ }
1207
+ }
1208
+ return (async () => {
1209
+ for (; n; ) {
1210
+ try {
1211
+ await i();
1212
+ } catch (s) {
1213
+ if ((s == null ? void 0 : s.name) === "AbortError") return;
1214
+ }
1215
+ if (!n) return;
1216
+ const a = xe[Math.min(o, xe.length - 1)];
1217
+ o += 1, await new Promise((s) => setTimeout(s, a));
1218
+ }
1219
+ })(), () => {
1220
+ n = !1, r == null || r.abort(), r = null;
1221
+ };
1222
+ }
1223
+ const ln = {
1224
+ OPEN: "is open",
1225
+ IN_PROGRESS: "is being worked on",
1226
+ RESOLVED: "has been fixed",
1227
+ CLOSED: "has been closed"
1228
+ }, un = {
1229
+ status: null,
1230
+ // falls back to the host's configured accent
1231
+ severity: "#e8912a",
1232
+ assignment: "#4da3ff",
1233
+ reply: "#35b87f"
1234
+ };
1235
+ function Ce(t) {
1236
+ return t != null && t.summary ? t.summary : `Your report ${ln[t == null ? void 0 : t.status] || "was updated"}`;
1237
+ }
1238
+ function dn(t) {
1239
+ var e;
1240
+ return (t == null ? void 0 : t.kind) === "reply" && ((e = t.reply) != null && e.body) ? `${t.reply.author ? `${t.reply.author}: ` : ""}${t.reply.body}` : (t == null ? void 0 : t.title) || "";
1241
+ }
1242
+ function hn(t) {
1243
+ const e = Date.parse(t || "");
1244
+ if (!Number.isFinite(e)) return "";
1245
+ const n = Math.round((Date.now() - e) / 6e4);
1246
+ if (n < 1) return "just now";
1247
+ if (n < 60) return `${n}m ago`;
1248
+ const o = Math.round(n / 60);
1249
+ if (o < 24) return `${o}h ago`;
1250
+ const r = Math.round(o / 24);
1251
+ return r === 1 ? "yesterday" : `${r}d ago`;
1252
+ }
1253
+ function fn() {
1254
+ var t;
1255
+ try {
1256
+ const e = document.querySelector('[data-bugcatch="widget"]'), n = (t = e == null ? void 0 : e.shadowRoot) == null ? void 0 : t.querySelector(".dock"), o = (n == null ? void 0 : n.getBoundingClientRect().height) || 0;
1257
+ return o ? Math.round(o) + 12 : 0;
1258
+ } catch {
1259
+ return 0;
1260
+ }
1261
+ }
1262
+ function pn({
1263
+ accent: t = "#4F46E5",
1264
+ position: e = "bottom-right",
1265
+ onRead: n = null,
1266
+ onReadAll: o = null,
1267
+ onCall: r = null
1268
+ } = {}) {
1269
+ const [i, a] = String(e).split("-"), s = i === "top", c = a === "left", l = /* @__PURE__ */ new Map();
1270
+ let f = null, p = null;
1271
+ function d() {
1272
+ return f != null && f.isConnected || (f = document.createElement("div"), f.setAttribute("data-bugcatch", "updates"), f.style.cssText = [
1273
+ "position:fixed",
1274
+ `${s ? "top" : "bottom"}:${20 + fn()}px`,
1275
+ `${c ? "left" : "right"}:20px`,
1276
+ // Under the widget's dock and its panel scrim, both of which are higher.
1277
+ // A notification is worth interrupting a page for, not the report being
1278
+ // read on top of it.
1279
+ "z-index:2147482998",
1280
+ "display:flex",
1281
+ "flex-direction:column",
1282
+ "gap:10px",
1283
+ `align-items:${c ? "flex-start" : "flex-end"}`,
1284
+ "width:min(320px,calc(100vw - 40px))",
1285
+ // The gaps between cards belong to the page underneath.
1286
+ "pointer-events:none"
1287
+ ].join(";"), document.body.appendChild(f)), f;
1288
+ }
1289
+ function u(h) {
1290
+ if (!h._bcShown) {
1291
+ h.remove();
1292
+ return;
1293
+ }
1294
+ h.style.opacity = "0", h.style.transform = `translateY(${s ? -8 : 8}px)`, setTimeout(() => h.remove(), 250);
1295
+ }
1296
+ function g(h, I) {
1297
+ const v = document.createElement("button");
1298
+ return v.type = "button", v.textContent = h, v.style.cssText = [
1299
+ "padding:5px 11px",
1300
+ `border:${I ? "0" : "1px solid rgba(236,238,242,.24)"}`,
1301
+ "border-radius:6px",
1302
+ `background:${I ? t : "transparent"}`,
1303
+ `color:${I ? "#fff" : "#eceef2"}`,
1304
+ "font:600 12px/1.4 inherit",
1305
+ "cursor:pointer",
1306
+ "white-space:nowrap"
1307
+ ].join(";"), v;
1308
+ }
1309
+ function $(h) {
1310
+ var b;
1311
+ const I = document.createElement("div");
1312
+ I.setAttribute("role", "status"), I.style.cssText = [
1313
+ "box-sizing:border-box",
1314
+ "width:100%",
1315
+ "padding:12px 14px",
1316
+ "border-radius:10px",
1317
+ "background:#1b1e26",
1318
+ "color:#eceef2",
1319
+ 'font:13px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
1320
+ "box-shadow:0 12px 30px rgba(0,0,0,0.45)",
1321
+ `border-left:3px solid ${un[h == null ? void 0 : h.kind] || t}`,
1322
+ "opacity:0",
1323
+ `transform:translateY(${s ? -8 : 8}px)`,
1324
+ "transition:opacity .2s ease,transform .2s ease",
1325
+ "pointer-events:auto"
1326
+ ].join(";");
1327
+ const v = document.createElement("div");
1328
+ v.style.cssText = "font-weight:600;margin-bottom:2px", v.textContent = Ce(h);
1329
+ const C = document.createElement("div");
1330
+ C.style.cssText = (h == null ? void 0 : h.kind) === "reply" ? "color:#a4abba;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden" : "color:#a4abba", C.textContent = dn(h);
1331
+ const w = document.createElement("div");
1332
+ w.style.cssText = "display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap";
1333
+ const k = document.createElement("span");
1334
+ k.style.cssText = "color:#7b8394;font-size:11px;margin-right:auto", k.textContent = hn(h == null ? void 0 : h.at);
1335
+ const B = g("Mark as read", !1);
1336
+ if (B.setAttribute("aria-label", `Mark as read: ${Ce(h)}`), B.addEventListener("click", () => n == null ? void 0 : n(h.id)), w.append(k, B), (b = h == null ? void 0 : h.call) != null && b.offered && typeof r == "function") {
1337
+ const y = g("Talk it through", !0);
1338
+ y.addEventListener("click", () => r(h)), w.insertBefore(y, B);
1339
+ }
1340
+ return I.append(v, C, w), I;
1341
+ }
1342
+ function S(h) {
1343
+ const I = document.createElement("div");
1344
+ I.style.cssText = [
1345
+ "box-sizing:border-box",
1346
+ "width:100%",
1347
+ "display:flex",
1348
+ "align-items:center",
1349
+ "gap:8px",
1350
+ "padding:8px 12px",
1351
+ "border-radius:10px",
1352
+ "background:#1b1e26",
1353
+ "color:#a4abba",
1354
+ 'font:12px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
1355
+ "box-shadow:0 12px 30px rgba(0,0,0,0.45)",
1356
+ "pointer-events:auto"
1357
+ ].join(";");
1358
+ const v = document.createElement("span");
1359
+ v.style.cssText = "margin-right:auto", v.textContent = `${h} more update${h === 1 ? "" : "s"}`;
1360
+ const C = g("Mark all as read", !1);
1361
+ return C.style.cssText += ";padding:4px 9px", C.addEventListener("click", () => o == null ? void 0 : o()), I.append(v, C), I;
1362
+ }
1363
+ return {
1364
+ /**
1365
+ * Draw what is unread, oldest first.
1366
+ *
1367
+ * The newest card sits nearest the corner the widget is in, which is the
1368
+ * corner the reporter's eye is already trained on — so the stack grows away
1369
+ * from the launcher rather than over it.
1370
+ */
1371
+ render(h) {
1372
+ const I = Array.isArray(h) ? h : [];
1373
+ if (!I.length) {
1374
+ for (const y of l.values()) u(y);
1375
+ l.clear(), p == null || p.remove(), p = null, f == null || f.remove(), f = null;
1376
+ return;
1377
+ }
1378
+ const v = I.slice(-3), C = new Set(v.map((y) => y.id)), w = d();
1379
+ for (const [y, T] of l)
1380
+ C.has(y) || (u(T), l.delete(y));
1381
+ const k = [];
1382
+ for (const y of v) {
1383
+ if (l.has(y.id)) continue;
1384
+ const T = $(y);
1385
+ l.set(y.id, T), k.push(T);
1386
+ }
1387
+ const B = s ? [...v].reverse() : v;
1388
+ for (const y of B) w.appendChild(l.get(y.id));
1389
+ const b = I.length - v.length;
1390
+ b > 0 ? (p == null || p.remove(), p = S(b), s ? w.appendChild(p) : w.insertBefore(p, w.firstChild)) : (p == null || p.remove(), p = null), requestAnimationFrame(() => {
1391
+ for (const y of k)
1392
+ y.isConnected && (y._bcShown = !0, y.style.opacity = "1", y.style.transform = "translateY(0)");
1393
+ });
1394
+ },
1395
+ destroy() {
1396
+ l.clear(), p = null, f == null || f.remove(), f = null;
1397
+ }
1398
+ };
1399
+ }
1400
+ async function gn({ apiBase: t, apiKey: e, projectId: n, sessionToken: o }) {
1401
+ try {
1402
+ const r = await fetch(
1403
+ `${t}/ingest/known-issues?projectId=${encodeURIComponent(n)}`,
1404
+ { headers: le(e, o) }
1405
+ );
1406
+ if (!r.ok) return [];
1407
+ const i = await r.json();
1408
+ return Array.isArray(i == null ? void 0 : i.issues) ? i.issues : [];
1409
+ } catch {
1410
+ return [];
1411
+ }
1412
+ }
1413
+ async function mn({ apiBase: t, apiKey: e, projectId: n, q: o, sessionToken: r, signal: i }) {
1414
+ try {
1415
+ const a = `${t}/ingest/known-issues/match?projectId=${encodeURIComponent(n)}&q=${encodeURIComponent(o)}`, s = await fetch(a, { headers: le(e, r), signal: i });
1416
+ if (!s.ok) return [];
1417
+ const c = await s.json();
1418
+ return Array.isArray(c == null ? void 0 : c.issues) ? c.issues : [];
1419
+ } catch {
1420
+ return [];
1421
+ }
1422
+ }
1423
+ async function wn({
1424
+ apiBase: t,
1425
+ apiKey: e,
1426
+ projectId: n,
1427
+ bugId: o,
1428
+ reporterEmail: r,
1429
+ reporterName: i,
1430
+ metadata: a,
1431
+ logs: s,
1432
+ sessionToken: c
1433
+ }) {
1434
+ const l = E(), f = await fetch(
1435
+ `${t}/ingest/known-issues/${encodeURIComponent(o)}/join`,
1436
+ {
1437
+ method: "POST",
1438
+ headers: le(e, c),
1439
+ body: JSON.stringify({
1440
+ projectId: n,
1441
+ reporterEmail: r,
1442
+ reporterName: i,
1443
+ metadata: a,
1444
+ logs: s,
1445
+ ...l ? { watchToken: l } : {}
1446
+ })
1447
+ }
1448
+ );
1449
+ if (!f.ok) throw new Error(await bn(f));
1450
+ const p = await f.json();
1451
+ return p != null && p.watchToken && te(p.watchToken), p;
1452
+ }
1453
+ function le(t, e) {
1454
+ return {
1455
+ "Content-Type": "application/json",
1456
+ "X-BugCatch-Key": t,
1457
+ ...e ? { "X-BugCatch-Session": e } : {}
1458
+ };
1459
+ }
1460
+ async function bn(t) {
1461
+ let e = "";
1462
+ try {
1463
+ const n = await t.json();
1464
+ e = Array.isArray(n.message) ? n.message.join(", ") : n.message || "";
1465
+ } catch {
1466
+ }
1467
+ return t.status === 429 ? L(e) : e || `Could not add you to this issue (HTTP ${t.status})`;
1468
+ }
1469
+ async function yn({ apiBase: t, bugId: e, ...n }) {
1470
+ const o = E();
1471
+ if (!o) throw new Error("This browser cannot access that report.");
1472
+ const r = await fetch(`${t}/ingest/watch/reports/${encodeURIComponent(e)}/verification`, {
1473
+ method: "POST",
1474
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${o}` },
1475
+ body: JSON.stringify(n)
1476
+ }), i = await r.json().catch(() => ({}));
1477
+ if (!r.ok) throw new Error(i.message || "Could not record feedback. Please refresh and try again.");
1478
+ return i;
1479
+ }
1480
+ const vn = "bugcatch.session.", ue = (t) => `${vn}${t}`, de = /* @__PURE__ */ new Map();
1481
+ function j(t) {
1482
+ var e;
1483
+ if (!t) return null;
1484
+ try {
1485
+ const n = (e = window.localStorage) == null ? void 0 : e.getItem(ue(t));
1486
+ if (n) {
1487
+ const o = JSON.parse(n);
1488
+ if (o != null && o.token) return o;
1489
+ }
1490
+ } catch {
1491
+ }
1492
+ return de.get(t) || null;
1493
+ }
1494
+ function kn(t, e) {
1495
+ var o;
1496
+ if (!t || !(e != null && e.token)) return;
1497
+ const n = {
1498
+ token: e.token,
1499
+ email: e.email || null,
1500
+ name: e.name || null
1501
+ };
1502
+ de.set(t, n);
1503
+ try {
1504
+ (o = window.localStorage) == null || o.setItem(ue(t), JSON.stringify(n));
1505
+ } catch {
1506
+ }
1507
+ }
1508
+ function ne(t) {
1509
+ var e;
1510
+ if (t) {
1511
+ de.delete(t);
1512
+ try {
1513
+ (e = window.localStorage) == null || e.removeItem(ue(t));
1514
+ } catch {
1515
+ }
1516
+ }
1517
+ }
1518
+ function Z({ apiKey: t, token: e }) {
1519
+ return {
1520
+ "Content-Type": "application/json",
1521
+ "X-BugCatch-Key": t,
1522
+ // Its own header rather than Authorization, which on these routes already
1523
+ // carries the API key. The two answer different questions and a request
1524
+ // carries both.
1525
+ ...e ? { "X-BugCatch-Session": e } : {}
1526
+ };
1527
+ }
1528
+ async function Fe(t) {
1529
+ let e = "";
1530
+ try {
1531
+ const n = await t.json();
1532
+ e = Array.isArray(n.message) ? n.message.join(", ") : n.message || "";
1533
+ } catch {
1534
+ }
1535
+ return t.status === 429 ? L(e) : e || `Request failed (HTTP ${t.status})`;
1536
+ }
1537
+ async function xn({ apiBase: t, apiKey: e, projectId: n, token: o }) {
1538
+ const r = { mode: "off", signedIn: null, signedOutReason: null };
1539
+ try {
1540
+ const i = await fetch(
1541
+ `${t}/ingest/login?projectId=${encodeURIComponent(n)}`,
1542
+ { headers: Z({ apiKey: e, token: o }) }
1543
+ );
1544
+ if (!i.ok) return r;
1545
+ const a = await i.json();
1546
+ return {
1547
+ mode: a.mode === "optional" || a.mode === "required" ? a.mode : "off",
1548
+ signedIn: a.signedIn || null,
1549
+ signedOutReason: a.signedOutReason || null
1550
+ };
1551
+ } catch {
1552
+ return r;
1553
+ }
1554
+ }
1555
+ async function In({ apiBase: t, apiKey: e, projectId: n, email: o, name: r }) {
1556
+ const i = await fetch(`${t}/ingest/login/start`, {
1557
+ method: "POST",
1558
+ headers: Z({ apiKey: e }),
1559
+ body: JSON.stringify({ projectId: n, email: o, ...r ? { name: r } : {} })
1560
+ });
1561
+ if (!i.ok) throw new Error(await Fe(i));
1562
+ return i.json();
1563
+ }
1564
+ async function Sn({ apiBase: t, apiKey: e, projectId: n, email: o, code: r }) {
1565
+ const i = await fetch(`${t}/ingest/login`, {
1566
+ method: "POST",
1567
+ headers: Z({ apiKey: e }),
1568
+ body: JSON.stringify({ projectId: n, email: o, code: r })
1569
+ });
1570
+ if (!i.ok) throw new Error(await Fe(i));
1571
+ const a = await i.json(), s = { token: a.sessionToken, email: a.email, name: a.name };
1572
+ return kn(n, s), s;
1573
+ }
1574
+ async function Cn({ apiBase: t, apiKey: e, projectId: n, token: o }) {
1575
+ if (ne(n), !o) return { signedOut: !0 };
1576
+ try {
1577
+ await fetch(`${t}/ingest/logout`, {
1578
+ method: "POST",
1579
+ headers: Z({ apiKey: e, token: o }),
1580
+ body: JSON.stringify({ projectId: n })
1581
+ });
1582
+ } catch {
1583
+ }
1584
+ return { signedOut: !0 };
1585
+ }
1586
+ let Xe = !1;
1587
+ if (typeof window < "u" && window.addEventListener)
1588
+ for (const t of ["pointerdown", "keydown", "touchstart"])
1589
+ window.addEventListener(t, () => {
1590
+ Xe = !0;
1591
+ }, {
1592
+ once: !0,
1593
+ capture: !0,
1594
+ passive: !0
1595
+ });
1596
+ function Bn() {
1597
+ return Xe;
1598
+ }
1599
+ const En = 2e4, qe = 120, oe = 200;
1600
+ function We(t) {
1601
+ if (typeof t != "string" || !t.trim() || t.length > 400) return null;
1602
+ try {
1603
+ return new RegExp(t.trim(), "gi");
1604
+ } catch {
1605
+ return null;
1606
+ }
1607
+ }
1608
+ const Je = (t) => String(t).replace(/\s+/g, " ").trim().slice(0, qe);
1609
+ function Ye(t, e) {
1610
+ const n = t && t.normalize;
1611
+ if (n === "phone") {
1612
+ const o = e.replace(/\D/g, "");
1613
+ return o.length === 10 ? o : o.length === 12 && o.startsWith("91") ? o.slice(2) : o.length === 11 && o.startsWith("0") ? o.slice(1) : e;
1614
+ }
1615
+ return n === "upper" ? e.toUpperCase() : e;
1616
+ }
1617
+ function Ve(t) {
1618
+ const e = t.length > 1 && t[1] !== void 0 ? t[1] : t[0];
1619
+ return typeof e != "string" || !e.trim() ? null : { value: Je(e), start: t.index, end: t.index + t[0].length };
1620
+ }
1621
+ function $n(t, e) {
1622
+ return ((t.slice(e.start, e.valueStart) + t.slice(e.valueEnd, e.end)).match(/[a-z]/gi) || []).length;
1623
+ }
1624
+ const Tn = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[A-Za-z0-9][\w-]{5,40}/gi, Be = /[\w-]/;
1625
+ function ho(t, e) {
1626
+ const n = (t || []).filter((d) => d && d.scan !== !1 && d.pattern), o = [], r = [], i = (e || []).map((d) => typeof d == "string" ? d.slice(0, En) : "");
1627
+ i.forEach((d, u) => {
1628
+ d && n.forEach((g, $) => {
1629
+ const S = We(g.pattern);
1630
+ if (!S) return;
1631
+ let h, I = 0;
1632
+ for (S.lastIndex = 0; (h = S.exec(d)) !== null && !(++I > oe); ) {
1633
+ if (h[0] === "") {
1634
+ S.lastIndex += 1;
1635
+ continue;
1636
+ }
1637
+ const v = Ve(h);
1638
+ if (!v) continue;
1639
+ const C = h.length > 1 && h[1] !== void 0 ? h[1] : h[0], w = h.index + Math.max(0, h[0].indexOf(C)), k = {
1640
+ block: u,
1641
+ order: $,
1642
+ key: g.key,
1643
+ label: g.label,
1644
+ value: Ye(g, v.value),
1645
+ start: v.start,
1646
+ end: v.end,
1647
+ valueStart: w,
1648
+ valueEnd: w + C.length
1649
+ };
1650
+ k.weight = $n(d, k), o.push(k);
1651
+ }
1652
+ });
1653
+ });
1654
+ const a = o.filter((d) => !o.some((u) => u !== d && u.block === d.block && u.key !== d.key && u.valueStart <= d.valueStart && u.valueEnd >= d.valueEnd && u.weight > d.weight)), s = /* @__PURE__ */ new Map();
1655
+ for (const d of a) {
1656
+ const u = d.value.toLowerCase();
1657
+ s.has(u) || s.set(u, { value: d.value, keys: /* @__PURE__ */ new Map() });
1658
+ const g = s.get(u);
1659
+ g.keys.has(d.key) || g.keys.set(d.key, { key: d.key, label: d.label, order: d.order });
1660
+ }
1661
+ const c = [], l = [], f = /* @__PURE__ */ new Map();
1662
+ for (const d of s.values()) {
1663
+ const u = [...d.keys.values()].sort((S, h) => S.order - h.order);
1664
+ if (u.length > 1) {
1665
+ l.push({ value: d.value, kinds: u.map(({ key: S, label: h }) => ({ key: S, label: h })) });
1666
+ continue;
1667
+ }
1668
+ const [{ key: g, label: $ }] = u;
1669
+ f.has(g) || f.set(g, []), f.get(g).push({ key: g, label: $, value: d.value });
1670
+ }
1671
+ for (const d of f.values())
1672
+ if (d.length === 1) c.push(d[0]);
1673
+ else for (const u of d) l.push({ value: u.value, kinds: [{ key: u.key, label: u.label }] });
1674
+ const p = new Set(s.keys());
1675
+ return i.forEach((d, u) => {
1676
+ if (!d) return;
1677
+ const g = o.filter((I) => I.block === u), $ = new RegExp(Tn.source, "gi");
1678
+ let S, h = 0;
1679
+ for (; (S = $.exec(d)) !== null && ++h <= oe; ) {
1680
+ const I = S.index, v = I + S[0].length;
1681
+ if (g.some((y) => I < y.end && v > y.start)) continue;
1682
+ const C = d[I - 1] || "", w = d[v] || "";
1683
+ if (Be.test(C) || Be.test(w) || C === "/" || C === "@" || w === "@" || C === "." || w === "." && /\w/.test(d[v + 1] || "") || !/\d/.test(S[0])) continue;
1684
+ const k = Je(S[0]), B = k.toLowerCase();
1685
+ if (p.has(B)) continue;
1686
+ const b = (t || []).filter((y) => y && An(y, k)).map(({ key: y, label: T }) => ({ key: y, label: T }));
1687
+ b.length && (p.add(B), r.push({ value: k, kinds: b }));
1688
+ }
1689
+ }), { found: c, conflicts: l, loose: r };
1690
+ }
1691
+ function An(t, e) {
1692
+ const n = typeof e == "string" ? e.trim() : "";
1693
+ if (!n || n.length > qe) return null;
1694
+ const o = We(t && (t.valuePattern || t.pattern));
1695
+ if (!o) return null;
1696
+ let r, i = 0;
1697
+ for (o.lastIndex = 0; (r = o.exec(n)) !== null && !(++i > oe); ) {
1698
+ if (r[0] === "") {
1699
+ o.lastIndex += 1;
1700
+ continue;
1701
+ }
1702
+ const a = r.index + r[0].length === n.length, s = r.index === 0 || r.index === 1;
1703
+ if (a && s) {
1704
+ const c = Ve(r);
1705
+ if (c) return Ye(t, c.value);
1706
+ }
1707
+ }
1708
+ return null;
1709
+ }
1710
+ async function jn({ apiBase: t, apiKey: e, projectId: n }) {
1711
+ const o = { policy: "off", fields: [] };
1712
+ try {
1713
+ const r = await fetch(
1714
+ `${t}/ingest/reference-fields?projectId=${encodeURIComponent(n)}`,
1715
+ { headers: { "X-BugCatch-Key": e } }
1716
+ );
1717
+ if (!r.ok) return o;
1718
+ const i = await r.json(), a = i.policy === "optional" || i.policy === "required" ? i.policy : "off", s = Array.isArray(i.fields) ? i.fields : [];
1719
+ return s.length ? { policy: a, fields: s } : o;
1720
+ } catch {
1721
+ return o;
1722
+ }
1723
+ }
1724
+ const fo = /Did this answer your question\?/i;
1725
+ async function On({ apiBase: t, apiKey: e, projectId: n }) {
1726
+ try {
1727
+ const o = await fetch(
1728
+ `${t}/ingest/agent-issues?projectId=${encodeURIComponent(n)}`,
1729
+ { headers: { "X-BugCatch-Key": e } }
1730
+ );
1731
+ if (!o.ok) return [];
1732
+ const r = await o.json();
1733
+ return (Array.isArray(r == null ? void 0 : r.issues) ? r.issues : []).filter((i) => i && typeof i.key == "string" && typeof i.title == "string").map((i) => ({ key: i.key, title: i.title, hint: typeof i.hint == "string" ? i.hint : "" }));
1734
+ } catch {
1735
+ return [];
1736
+ }
1737
+ }
1738
+ const Mn = 5 * 6e4, Ze = "bugcatch:announcements:seen", Ln = 40, Rn = ["info", "success", "warning", "critical"], Ge = (t) => `bugcatch:announcements:${t}`;
1739
+ async function Kn({
1740
+ apiBase: t,
1741
+ apiKey: e,
1742
+ projectId: n,
1743
+ fresh: o = !1
1744
+ }) {
1745
+ var r;
1746
+ if (!o) {
1747
+ const i = zn(n);
1748
+ if (i) return i;
1749
+ }
1750
+ try {
1751
+ const i = await fetch(
1752
+ `${t}/ingest/announcements?projectId=${encodeURIComponent(n)}`,
1753
+ { headers: { "X-BugCatch-Key": e } }
1754
+ ), a = i.ok ? Hn((r = await i.json()) == null ? void 0 : r.announcements) : [];
1755
+ return Fn(n, a), a;
1756
+ } catch {
1757
+ return [];
1758
+ }
1759
+ }
1760
+ function Un(t, { on: e = _n(), seen: n = et() } = {}) {
1761
+ return (t || []).filter((o) => Pn(o, e) && !(o.dismissible && n.includes(Qe(o))));
1762
+ }
1763
+ function Nn(t) {
1764
+ if (!(!(t != null && t.id) || !t.dismissible))
1765
+ try {
1766
+ const e = Qe(t), n = [...et().filter((o) => o !== e), e];
1767
+ localStorage.setItem(Ze, JSON.stringify(n.slice(-Ln)));
1768
+ } catch {
1769
+ }
1770
+ }
1771
+ function Dn(t) {
1772
+ return typeof t != "string" || !t || t.startsWith("//") ? null : t.startsWith("/") || /^(https?:\/\/|mailto:)./i.test(t) ? t : null;
1773
+ }
1774
+ function _n(t = /* @__PURE__ */ new Date()) {
1775
+ const e = (n) => String(n).padStart(2, "0");
1776
+ return `${t.getFullYear()}-${e(t.getMonth() + 1)}-${e(t.getDate())}`;
1777
+ }
1778
+ function Pn(t, e) {
1779
+ return !(t.startsOn && e < t.startsOn || t.endsOn && e > t.endsOn);
1780
+ }
1781
+ function Qe(t) {
1782
+ return `${t.id}#${t.revision || 0}`;
1783
+ }
1784
+ function Hn(t) {
1785
+ return (Array.isArray(t) ? t : []).filter((e) => e && typeof e.id == "string" && typeof e.title == "string" && e.title.trim()).map((e) => {
1786
+ const n = e.placement === "launcher" ? "launcher" : "panel", o = Dn(e.ctaHref);
1787
+ return {
1788
+ id: e.id,
1789
+ revision: Number.isInteger(e.revision) ? e.revision : 0,
1790
+ title: e.title,
1791
+ body: typeof e.body == "string" ? e.body : "",
1792
+ tone: Rn.includes(e.tone) ? e.tone : "info",
1793
+ // Both halves, or neither, as the API saves them.
1794
+ ctaLabel: o && typeof e.ctaLabel == "string" ? e.ctaLabel : "",
1795
+ ctaHref: o && typeof e.ctaLabel == "string" ? o : "",
1796
+ placement: n,
1797
+ dismissible: n === "launcher" || e.dismissible !== !1,
1798
+ startsOn: typeof e.startsOn == "string" ? e.startsOn : "",
1799
+ endsOn: typeof e.endsOn == "string" ? e.endsOn : ""
1800
+ };
1801
+ });
1802
+ }
1803
+ function et() {
1804
+ try {
1805
+ const t = JSON.parse(localStorage.getItem(Ze) || "[]");
1806
+ return Array.isArray(t) ? t.filter((e) => typeof e == "string") : [];
1807
+ } catch {
1808
+ return [];
1809
+ }
1810
+ }
1811
+ function zn(t) {
1812
+ try {
1813
+ const e = JSON.parse(localStorage.getItem(Ge(t)) || "null");
1814
+ if (!e || !Array.isArray(e.list) || typeof e.at != "number") return null;
1815
+ const n = Date.now() - e.at;
1816
+ return n >= 0 && n < Mn ? e.list : null;
1817
+ } catch {
1818
+ return null;
1819
+ }
1820
+ }
1821
+ function Fn(t, e) {
1822
+ try {
1823
+ localStorage.setItem(Ge(t), JSON.stringify({ at: Date.now(), list: e }));
1824
+ } catch {
1825
+ }
1826
+ }
1827
+ const W = 120, Xn = 500, J = 30, qn = 1e3, he = {
1828
+ loanId: ["loanId", "loanNumber"],
1829
+ applicationId: ["applicationId"],
1830
+ customerNumber: ["customerNumber", "customerId"]
1831
+ }, Wn = /[\u0000-\u001f\u007f]/;
1832
+ function Jn(t) {
1833
+ if (typeof t == "number") return Number.isFinite(t) ? String(t) : null;
1834
+ if (typeof t != "string") return null;
1835
+ const e = t.trim();
1836
+ return !e || e.length > W || Wn.test(e) ? null : e;
1837
+ }
1838
+ function A(t) {
1839
+ return t === null ? "null" : Array.isArray(t) ? "an array" : typeof t == "object" ? "an object" : `a ${typeof t}`;
1840
+ }
1841
+ function ie(t, e, n, o, r) {
1842
+ for (const i of n) {
1843
+ if (!(i in t) || t[i] === void 0 || t[i] === null || t[i] === "") continue;
1844
+ const a = Jn(t[i]);
1845
+ if (a) return a;
1846
+ const s = t[i];
1847
+ r(typeof s == "string" && s.trim().length > W ? `${o}.${i} is longer than ${W} characters and was dropped — pass the identifier itself, not a sentence or a URL.` : `${o}.${i} should be a string or number, got ${A(s)} — it was dropped.`);
1848
+ }
1849
+ return null;
1850
+ }
1851
+ function tt(t) {
1852
+ if (t === null || typeof t == "boolean") return { ok: !0, value: t };
1853
+ if (typeof t == "number") return Number.isFinite(t) ? { ok: !0, value: t } : { ok: !1 };
1854
+ if (typeof t == "string") return { ok: !0, value: t.slice(0, Xn) };
1855
+ if (typeof t == "object")
1856
+ try {
1857
+ const e = JSON.stringify(t);
1858
+ if (e && e.length <= qn) return { ok: !0, value: JSON.parse(e) };
1859
+ } catch {
1860
+ }
1861
+ return { ok: !1 };
1862
+ }
1863
+ const nt = ["email", "name", "phone"], Yn = /* @__PURE__ */ new Set(["id", "userId", ...nt, ...Object.values(he).flat()]);
1864
+ function Ee(t, e = () => {
1865
+ }) {
1866
+ if (t == null) return null;
1867
+ if (typeof t != "object" || Array.isArray(t))
1868
+ return e(`identify() takes an object like { id, email, name } or null, got ${A(t)} — ignored.`), null;
1869
+ const n = {};
1870
+ try {
1871
+ const o = ie(t, "id", ["id", "userId"], "identify()", e);
1872
+ o && (n.id = o);
1873
+ for (const i of nt) {
1874
+ const a = t[i];
1875
+ if (a == null || a === "") continue;
1876
+ if (i === "phone" && typeof a == "number" && Number.isFinite(a)) {
1877
+ n.phone = String(a);
1878
+ continue;
1879
+ }
1880
+ if (typeof a != "string") {
1881
+ e(`identify().${i} should be a string, got ${A(a)} — it was dropped.`);
1882
+ continue;
1883
+ }
1884
+ const s = a.trim().slice(0, i === "name" ? 200 : W);
1885
+ s && (i === "email" && !s.includes("@") && e(`identify().email "${s}" does not look like an email address — reports cannot be threaded or followed up without one.`), n[i] = s);
1886
+ }
1887
+ for (const [i, a] of Object.entries(he)) {
1888
+ const s = ie(t, i, a, "identify()", e);
1889
+ s && (n[i] = s);
1890
+ }
1891
+ let r = 0;
1892
+ for (const [i, a] of Object.entries(t)) {
1893
+ if (Yn.has(i) || a === void 0 || typeof a == "function") continue;
1894
+ if (r >= J) {
1895
+ e(`identify() was given more than ${J} extra fields; the rest were dropped.`);
1896
+ break;
1897
+ }
1898
+ const s = tt(a);
1899
+ if (!s.ok) {
1900
+ e(`identify().${i} could not be sent (${A(a)} too large or not serialisable) — dropped.`);
1901
+ continue;
1902
+ }
1903
+ n[i] = s.value, r += 1;
1904
+ }
1905
+ } catch {
1906
+ }
1907
+ return !n.id && !n.email && e("identify() was called without an id or email — reports will not be attributed to anyone, and nothing can be looked up by user."), n;
1908
+ }
1909
+ function $e(t, e, n = () => {
1910
+ }) {
1911
+ if (e === null) return {};
1912
+ const o = { ...t || {} };
1913
+ if (e === void 0) return o;
1914
+ if (typeof e != "object" || Array.isArray(e))
1915
+ return n(`setContext() takes an object like { loanId, screen } or null, got ${A(e)} — ignored.`), o;
1916
+ try {
1917
+ const r = /* @__PURE__ */ new Map();
1918
+ for (const [i, a] of Object.entries(he)) for (const s of a) r.set(s, i);
1919
+ for (const [i, a] of Object.entries(e)) {
1920
+ const s = r.get(i);
1921
+ if (a == null) {
1922
+ delete o[s || i];
1923
+ continue;
1924
+ }
1925
+ if (s) {
1926
+ const l = ie({ [i]: a }, s, [i], "setContext()", n);
1927
+ l ? o[s] = l : delete o[s];
1928
+ continue;
1929
+ }
1930
+ if (typeof a == "function") continue;
1931
+ if (!(i in o) && Object.keys(o).length >= J) {
1932
+ n(`setContext() holds at most ${J} keys; "${i}" was dropped.`);
1933
+ continue;
1934
+ }
1935
+ const c = tt(a);
1936
+ if (!c.ok) {
1937
+ n(`setContext().${i} could not be sent (${A(a)} too large or not serialisable) — dropped.`);
1938
+ continue;
1939
+ }
1940
+ o[i] = c.value;
1941
+ }
1942
+ } catch {
1943
+ }
1944
+ return o;
1945
+ }
1946
+ const ot = [
1947
+ "apiBase",
1948
+ "apiKey",
1949
+ "projectId",
1950
+ "consoleLimit",
1951
+ "networkLimit",
1952
+ "captureConsole",
1953
+ "captureNetwork",
1954
+ "captureErrors",
1955
+ "widget",
1956
+ "widgetPosition",
1957
+ "theme",
1958
+ "accent",
1959
+ "attachments",
1960
+ "maxAttachments",
1961
+ "maxAttachmentBytes",
1962
+ "maxRecordingMs",
1963
+ "beforeSend",
1964
+ "user",
1965
+ "context",
1966
+ "updates",
1967
+ "updateToast",
1968
+ "onUpdate",
1969
+ "voice",
1970
+ "onCallState",
1971
+ "onIncomingCall",
1972
+ "onReportSubmitted",
1973
+ "onAssistantPhase",
1974
+ "onError",
1975
+ "debug",
1976
+ "drafts",
1977
+ "knownIssues",
1978
+ "agentIssues",
1979
+ "release",
1980
+ "environment",
1981
+ "buildId",
1982
+ "chunkBase",
1983
+ "announcements"
1984
+ ], Vn = [
1985
+ "beforeSend",
1986
+ "onUpdate",
1987
+ "onCallState",
1988
+ "onIncomingCall",
1989
+ "onReportSubmitted",
1990
+ "onAssistantPhase",
1991
+ "onError"
1992
+ ];
1993
+ function Zn(t) {
1994
+ const e = t.toLowerCase().replace(/[-_]/g, "");
1995
+ return ot.find((n) => n.toLowerCase() === e) || null;
1996
+ }
1997
+ const Te = /^(your[_-]|<|xxx|todo|changeme)/i, Gn = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1998
+ function Qn(t, { pageHost: e = "" } = {}) {
1999
+ const n = [], o = [];
2000
+ if (!t || typeof t != "object")
2001
+ return n.push("init() takes an options object: BugCatch.init({ apiBase, apiKey, projectId })."), { errors: n, warnings: o };
2002
+ const { apiKey: r, projectId: i, apiBase: a } = t;
2003
+ r ? typeof r != "string" ? n.push(`apiKey must be a string, got ${A(r)}.`) : Te.test(r) ? n.push(`apiKey is still the placeholder "${r}" — paste the key you saved when you created it.`) : Gn.test(r) ? o.push("apiKey looks like a project id — did you swap apiKey and projectId?") : r.startsWith("bc_") || o.push('apiKey does not look like a BugCatch key (they start with "bc_live_") — reports will be refused with HTTP 401.') : n.push("apiKey is required — create one under Workspace Settings → API Keys."), i ? typeof i != "string" ? n.push(`projectId must be a string, got ${A(i)}.`) : Te.test(i) ? n.push(`projectId is still the placeholder "${i}".`) : i.startsWith("bc_") ? o.push("projectId looks like an API key — did you swap apiKey and projectId?") : /\s/.test(i) && o.push("projectId contains whitespace — check it was copied cleanly.") : n.push("projectId is required — copy it from Project Settings → Web SDK."), a == null ? !e || /^(localhost|127\.|0\.0\.0\.0|\[::1\])/.test(e) || o.push("apiBase is not set, so reports go to http://localhost:3000 — set apiBase to your BugCatch API URL.") : (typeof a != "string" || !/^https?:\/\//.test(a)) && o.push(`apiBase should be an absolute http(s) URL, got ${JSON.stringify(a)}.`);
2004
+ for (const s of Object.keys(t)) {
2005
+ if (ot.includes(s)) continue;
2006
+ const c = Zn(s);
2007
+ o.push(c ? `unknown option "${s}" — did you mean "${c}"?` : `unknown option "${s}" is ignored.`);
2008
+ }
2009
+ for (const s of Vn) {
2010
+ const c = t[s];
2011
+ c != null && typeof c != "function" && o.push(`${s} should be a function, got ${A(c)} — it will not be called.`);
2012
+ }
2013
+ return { errors: n, warnings: o };
2014
+ }
2015
+ function eo(t) {
2016
+ return typeof t == "string" ? t.replace(/\/+$/, "") : t;
2017
+ }
2018
+ const K = {
2019
+ name: "@sphoro/bugcatch-web-sdk",
2020
+ version: "0.18.0"
2021
+ }, R = {
2022
+ apiBase: "http://localhost:3000",
2023
+ apiKey: null,
2024
+ projectId: null,
2025
+ consoleLimit: 100,
2026
+ networkLimit: 50,
2027
+ captureConsole: !0,
2028
+ captureNetwork: !0,
2029
+ captureErrors: !0,
2030
+ // Shows the floating "Report a bug" button. Turn off to drive it yourself.
2031
+ widget: !0,
2032
+ widgetPosition: "bottom-right",
2033
+ // 'auto' follows the visitor's system setting; 'light' / 'dark' pin it.
2034
+ theme: "auto",
2035
+ accent: "#4F46E5",
2036
+ // Draw what the project's team is telling visitors — written on the Widget
2037
+ // announcements settings page — in the panel, and beside the button for the
2038
+ // ones written for that. false draws nothing; announcements() still answers,
2039
+ // for a site that shows them its own way.
2040
+ announcements: !0,
2041
+ // Screenshot / screen recording / file attach. Capture needs getDisplayMedia,
2042
+ // so it is offered only where the browser has it — file attach always works.
2043
+ attachments: !0,
2044
+ maxAttachments: 5,
2045
+ maxAttachmentBytes: 25 * 1024 * 1024,
2046
+ maxRecordingMs: 12e4,
2047
+ // Called before send; return false to cancel, or mutate the payload to scrub.
2048
+ beforeSend: null,
2049
+ user: null,
2050
+ // Tell the reporter when a bug they filed changes status — live, in your
2051
+ // app, where they already are. Off means no connection is opened at all.
2052
+ updates: !0,
2053
+ // Show the built-in notification when an update arrives. It stays until the
2054
+ // reporter marks it read — including across a reload — so an update is not
2055
+ // lost to whoever happened to be looking at the tab. Turn it off and drive
2056
+ // the notification yourself from onUpdate and unreadUpdates().
2057
+ updateToast: !0,
2058
+ // Called with { id, bugId, title, status, at } whenever one of this browser's
2059
+ // reports moves. Runs whether or not the built-in notification is on; `id` is
2060
+ // what markUpdateRead() takes.
2061
+ onUpdate: null,
2062
+ // Offer a call to the team when a report is picked up, if the workspace has
2063
+ // sphoro.voice configured. Nothing is dialled and no permission is asked for
2064
+ // until somebody presses the button — see startCall().
2065
+ voice: !0,
2066
+ // Called as a call moves: 'ringing', 'connecting', 'connected', 'missed',
2067
+ // 'ended', 'failed'. The SDK shows nothing of its own beyond the offer, so an
2068
+ // integration that wants a call UI builds it from this.
2069
+ onCallState: null,
2070
+ // Called with { bugId, callId, from, project, reference, ringsUntil,
2071
+ // recorded } when somebody on the team rings this page about a report filed
2072
+ // from it. The SDK has already drawn the card by then — this is for a host
2073
+ // application that wants to duck its own audio or pause a video, not a hook
2074
+ // that can suppress a call the person can see ringing. Set `voice: false` to
2075
+ // switch the whole thing off.
2076
+ onIncomingCall: null,
2077
+ // Called with { bugId, title, via: 'form' | 'call', result } once a report
2078
+ // has been accepted — from the widget, from report(), or from pressing Call.
2079
+ // The same thing is emitted as the 'report' event.
2080
+ onReportSubmitted: null,
2081
+ // Called with { bugId, phase, title, detail } when the AI assistant working
2082
+ // a report moves: 'analyzing' → 'needs_info' | 'confirming' | 'reviewing' |
2083
+ // 'resolving' | 'resolved'. Seen whenever the report is fetched — the widget
2084
+ // does while it is open; myReport(id) does for an integration's own view.
2085
+ onAssistantPhase: null,
2086
+ // Called with { action, error } when something the SDK did on the page's
2087
+ // behalf failed (a report that was refused, a listener that threw). Also the
2088
+ // 'error' event.
2089
+ onError: null,
2090
+ // Business context for the screen the SDK starts on — see setContext().
2091
+ context: null,
2092
+ // The script-tag build fetches the report panel and the call code when they
2093
+ // are first needed, from the folder it was itself loaded from. Set this to
2094
+ // that folder's URL when it cannot tell — a copy of the bundle inlined into
2095
+ // the page, or served from somewhere other than beside its dist/ siblings.
2096
+ // The ES build ignores it: the bundler decides where its chunks live.
2097
+ chunkBase: null,
2098
+ // Log what the SDK is doing (identify, context, each report's shape) to the
2099
+ // console. Off in production; the warnings about misconfiguration are
2100
+ // printed either way.
2101
+ debug: !1
2102
+ };
2103
+ function to(t) {
2104
+ const e = E();
2105
+ return e ? { ...t, watchToken: e } : t;
2106
+ }
2107
+ var m, P, rt, O, at, re, H, z, ae, st, ct, lt, F, ut;
2108
+ class it {
2109
+ constructor() {
2110
+ pe(this, m);
2111
+ this.config = { ...R }, this.started = !1, this.teardown = [], this.consoleBuffer = D(R.consoleLimit), this.networkBuffer = D(R.networkLimit), this.stopWatching = null, this.notifier = null, this.call = null, this.callStarting = !1, this.ringing = null, this.listeners = /* @__PURE__ */ new Map(), this.context = {}, this.warned = /* @__PURE__ */ new Set(), this.phases = /* @__PURE__ */ new Map();
2112
+ }
2113
+ /** A misconfiguration, said once. Always printed — it is what saves a day of guessing. */
2114
+ warn(e) {
2115
+ this.warned.has(e) || (this.warned.add(e), console.warn(`[BugCatch] ${e}`));
2116
+ }
2117
+ /** What the SDK is doing, for `debug: true`. */
2118
+ debugLog(...e) {
2119
+ this.config.debug && console.info("[BugCatch]", ...e);
2120
+ }
2121
+ init(e = {}) {
2122
+ if (this.started)
2123
+ return console.warn("[BugCatch] init() called twice — ignoring the second call."), this;
2124
+ const { errors: n, warnings: o } = Qn(e, {
2125
+ pageHost: typeof location < "u" ? location.hostname : ""
2126
+ });
2127
+ if (n.length) throw new Error(`[BugCatch] ${n[0]}`);
2128
+ for (const i of o) this.warn(i);
2129
+ const r = this.config.user;
2130
+ return this.config = { ...R, ...e, apiBase: eo(e.apiBase ?? R.apiBase) }, this.config.user = e.user !== void 0 ? Ee(e.user, (i) => this.warn(i.replace("identify()", "init({ user })"))) : r || null, e.context && this.setContext(e.context), this.consoleBuffer = D(this.config.consoleLimit), this.networkBuffer = D(this.config.networkLimit), this.config.captureConsole && this.teardown.push(xt(this.consoleBuffer)), this.config.captureNetwork && this.teardown.push(It(this.networkBuffer)), this.config.captureErrors && this.teardown.push(St(this.consoleBuffer)), Wt(this.config.projectId), this.config.widget && this.teardown.push(Ut(this)), this.config.updates && E() && (this.startWatching(), queueMicrotask(() => x(this, m, O).call(this))), this.started = !0, x(this, m, rt).call(this), this.debugLog("initialised", {
2131
+ version: K.version,
2132
+ apiBase: this.config.apiBase,
2133
+ projectId: this.config.projectId,
2134
+ user: this.config.user,
2135
+ context: this.context
2136
+ }), this;
2137
+ }
2138
+ /**
2139
+ * Open the update stream. Idempotent — init() and a first report both reach
2140
+ * here, and a second connection would double every notification.
2141
+ */
2142
+ startWatching() {
2143
+ if (this.stopWatching) return this;
2144
+ this.stopWatching = cn({
2145
+ apiBase: this.config.apiBase,
2146
+ onUpdate: (n) => {
2147
+ if (n != null && n.incomingCall) {
2148
+ x(this, m, lt).call(this, n);
2149
+ return;
2150
+ }
2151
+ if (n != null && n.callWithdrawn) {
2152
+ x(this, m, F).call(this, n.callWithdrawn.callId);
2153
+ return;
2154
+ }
2155
+ const o = Yt(n);
2156
+ typeof this.config.onUpdate == "function" && this.config.onUpdate(o), this.emit("update", o), x(this, m, O).call(this);
2157
+ }
2158
+ }), this.teardown.push(() => {
2159
+ var n;
2160
+ return (n = this.stopWatching) == null ? void 0 : n.call(this);
2161
+ }), this.teardown.push(() => x(this, m, F).call(this));
2162
+ const e = (n) => {
2163
+ (n.key === q || n.key === null) && x(this, m, O).call(this);
2164
+ };
2165
+ return window.addEventListener("storage", e), this.teardown.push(() => window.removeEventListener("storage", e)), this;
2166
+ }
2167
+ /**
2168
+ * Subscribe to updates on this browser's reports. Returns an unsubscribe.
2169
+ *
2170
+ * const off = BugCatch.on('update', (u) => console.log(u.summary));
2171
+ *
2172
+ * Alongside the `onUpdate` config option rather than replacing it: that one
2173
+ * is the single handler an integration sets at init, this one lets several
2174
+ * parts of an app listen independently — a badge, a toast of your own, an
2175
+ * open report view — without them having to agree on one callback.
2176
+ */
2177
+ on(e, n) {
2178
+ if (typeof n != "function") return () => {
2179
+ };
2180
+ const o = this.listeners.get(e) || /* @__PURE__ */ new Set();
2181
+ return o.add(n), this.listeners.set(e, o), () => this.off(e, n);
2182
+ }
2183
+ off(e, n) {
2184
+ var o;
2185
+ return (o = this.listeners.get(e)) == null || o.delete(n), this;
2186
+ }
2187
+ /** Notify listeners; one that throws must not stop the rest. */
2188
+ emit(e, n) {
2189
+ for (const o of this.listeners.get(e) || [])
2190
+ try {
2191
+ o(n);
2192
+ } catch (r) {
2193
+ console.error(`[BugCatch] a '${e}' listener threw:`, r);
2194
+ }
2195
+ }
2196
+ /**
2197
+ * Updates this browser has been told about and nobody has read yet, oldest
2198
+ * first.
2199
+ *
2200
+ * BugCatch.unreadUpdates().length // a badge on your own bell
2201
+ *
2202
+ * Survives a reload: the stream replays nothing when it reconnects, so an
2203
+ * update that arrived while the tab was closed would otherwise be gone.
2204
+ * Each carries the `id` the mark-read calls take.
2205
+ */
2206
+ unreadUpdates() {
2207
+ return N();
2208
+ }
2209
+ /**
2210
+ * Mark one update read. It stops being shown, here and on the next load.
2211
+ *
2212
+ * BugCatch.on('update', (u) => myBell.add(u, () => BugCatch.markUpdateRead(u.id)));
2213
+ *
2214
+ * Read is something the reporter says, not something a timer decides — which
2215
+ * is the whole reason these are held rather than shown for eight seconds and
2216
+ * dropped. An integration rendering its own notifications is the one that
2217
+ * knows when its reader has seen one, so it has to say.
2218
+ */
2219
+ markUpdateRead(e) {
2220
+ return Vt(e), x(this, m, O).call(this), this;
2221
+ }
2222
+ /**
2223
+ * Mark everything about one report read.
2224
+ *
2225
+ * Opening the report is reading its updates, so the widget calls this when
2226
+ * the thread opens; an integration with its own report view should too.
2227
+ */
2228
+ markReportRead(e) {
2229
+ return Zt(e), x(this, m, O).call(this), this;
2230
+ }
2231
+ /** Mark everything read. The "clear all" behind your own notification list. */
2232
+ markUpdatesRead() {
2233
+ return ze(), x(this, m, O).call(this), this;
2234
+ }
2235
+ /**
2236
+ * One report in full, with its timeline — every step from filing to now.
2237
+ *
2238
+ * const report = await BugCatch.myReport(id);
2239
+ * report.timeline // [{ kind: 'filed' | 'status' | 'severity' | ... , summary, at }]
2240
+ *
2241
+ * Resolves to null when this browser cannot prove it filed that report.
2242
+ */
2243
+ async verifyFix(e, { result: n, resolvedAt: o, note: r }) {
2244
+ const i = n === "STILL_HAPPENING" ? await x(this, m, z).call(this, {}) : {};
2245
+ return yn({ apiBase: this.config.apiBase, bugId: e, result: n, resolvedAt: o, note: r, metadata: i.metadata, logs: i.logs });
2246
+ }
2247
+ async myReport(e) {
2248
+ const n = await en({ apiBase: this.config.apiBase, bugId: e });
2249
+ return x(this, m, at).call(this, e, n == null ? void 0 : n.assistant), n;
2250
+ }
2251
+ /**
2252
+ * Write back on a report this browser filed.
2253
+ *
2254
+ * const key = await BugCatch.uploadAttachment(file, file.type);
2255
+ * await BugCatch.replyToReport(id, {
2256
+ * body: 'Still happening on Safari',
2257
+ * attachments: [{ ...key, name: file.name, size: file.size }],
2258
+ * });
2259
+ *
2260
+ * The widget's own conversation view calls this; it is public so an
2261
+ * integration rendering its own can too. Resolves to the stored message, so
2262
+ * the caller renders what the server kept rather than what it hoped it sent.
2263
+ */
2264
+ replyToReport(e, { body: n, attachments: o } = {}) {
2265
+ return tn({ apiBase: this.config.apiBase, bugId: e, body: n, attachments: o });
2266
+ }
2267
+ /**
2268
+ * Whether this visitor can report something by talking about it instead.
2269
+ *
2270
+ * if (await BugCatch.canCallNow()) showYourOwnCallButton();
2271
+ *
2272
+ * Two conditions, and both have to hold before anything is drawn. The project
2273
+ * has to have calling configured — false on most deployments — and this
2274
+ * browser has to know who the visitor is, because a call with nobody to
2275
+ * attribute it to has no watch token to follow it with and nobody to tell
2276
+ * afterwards what happened to the report. That is the same limit every other
2277
+ * follow-up feature in this SDK has: it all hangs off `identify()`.
2278
+ */
2279
+ async canCallNow() {
2280
+ return !!await this.callKind();
2281
+ }
2282
+ /**
2283
+ * Which kind of call this visitor would get: 'live', or null for none.
2284
+ *
2285
+ * const kind = await BugCatch.callKind();
2286
+ * // 'live' — the team's dashboards ring and a colleague picks up
2287
+ *
2288
+ * One kind, and this says whether it is on offer. It is still worth asking
2289
+ * rather than assuming: on most deployments the answer is null, and a button
2290
+ * promising a call nobody can place is worse than no button.
2291
+ *
2292
+ * The answer is remembered for the life of the page. It is a property of the
2293
+ * deployment's configuration rather than of this visitor, and re-asking on
2294
+ * every dialog open is a request per page view for something that changes
2295
+ * about once a year.
2296
+ */
2297
+ async callKind() {
2298
+ return (await this.callKinds()).live ? "live" : null;
2299
+ }
2300
+ /**
2301
+ * The same answer as booleans: `{ agent, live }`.
2302
+ *
2303
+ * const { live } = await BugCatch.callKinds();
2304
+ * if (live) drawCallTheTeam();
2305
+ *
2306
+ * `agent` is always false. It was a triage agent that answered instantly
2307
+ * instead of a person, it is gone, and the key stays only so an integration
2308
+ * that reads it does not get `undefined` and draw a button on it.
2309
+ *
2310
+ * Same round trip and same page-lifetime cache as `callKind()`. False on a
2311
+ * deployment without sphoro.voice, on a workspace that has switched calling
2312
+ * off, and on a visitor this SDK has no address for — a call needs somebody
2313
+ * to attribute it to.
2314
+ */
2315
+ async callKinds() {
2316
+ var n;
2317
+ const e = { agent: !1, live: !1 };
2318
+ return !this.config.voice || !((n = this.config.user) != null && n.email) ? e : this.kinds !== void 0 ? this.kinds : (this.kinds = await nn({
2319
+ apiBase: this.config.apiBase,
2320
+ apiKey: this.config.apiKey,
2321
+ projectId: this.config.projectId
2322
+ }), this.kinds);
2323
+ }
2324
+ /**
2325
+ * Whether a report can be talked through right now.
2326
+ *
2327
+ * if (await BugCatch.canCall(id)) showYourOwnButton();
2328
+ *
2329
+ * False on every deployment without sphoro.voice configured, on every
2330
+ * workspace that has not switched calling on, and on a report that has had
2331
+ * its allowance of calls — so an integration rendering its own affordance can
2332
+ * ask rather than guess.
2333
+ *
2334
+ * The same question as `canCallTeam`, which is the name the widget uses. Both
2335
+ * are kept because both are documented, and there is one kind of call left
2336
+ * for either of them to be asking about.
2337
+ */
2338
+ canCall(e) {
2339
+ return this.config.voice ? Ie({ apiBase: this.config.apiBase, bugId: e }) : Promise.resolve(!1);
2340
+ }
2341
+ /**
2342
+ * Ring the team.
2343
+ *
2344
+ * await BugCatch.startCall(id); // about a report already filed
2345
+ * await BugCatch.startCall(); // instead of filling the form in
2346
+ *
2347
+ * With a report id, it rings about that report. With none, it files one first
2348
+ * — carrying the console, network and environment this SDK is already holding
2349
+ * — and rings about that. The report is written before the phones ring either
2350
+ * way, so a call nobody answers still leaves the context behind; the handle
2351
+ * carries `bugId` so the caller can say what it was filed as.
2352
+ *
2353
+ * Asks for the microphone, places the call, and resolves once it is
2354
+ * *ringing* — not once somebody answers. The handle's `onState` is how the
2355
+ * caller follows what happens after that:
2356
+ *
2357
+ * 'ringing' the team's dashboards are lighting up
2358
+ * 'connecting' somebody answered; the two browsers are finding each other
2359
+ * 'connected' they can hear each other
2360
+ * 'missed' nobody picked up before the ringing stopped
2361
+ * 'ended' it is over
2362
+ *
2363
+ * What comes back is a conversation with a colleague: the audio travels
2364
+ * directly between the two browsers over WebRTC and never reaches bugcatch or
2365
+ * sphoro.voice, so it is not transcribed. The thread gets a note saying the
2366
+ * call happened and who took it.
2367
+ *
2368
+ * Rejects if the microphone is refused, if calling is not available, or if
2369
+ * one is already running. There is deliberately no way to start a second: two
2370
+ * microphones on one page talk over each other. A rejection carrying `.filed`
2371
+ * means the report was written and only the call failed.
2372
+ */
2373
+ async startCall(e) {
2374
+ var n;
2375
+ if (!this.config.voice) throw new Error("[BugCatch] calling is switched off.");
2376
+ if (this.call || this.callStarting)
2377
+ throw new Error("[BugCatch] a call is already running.");
2378
+ this.callStarting = !0;
2379
+ try {
2380
+ if (!await this.callKind())
2381
+ throw new Error((n = this.config.user) != null && n.email ? "[BugCatch] calling is not available on this project." : "[BugCatch] a call needs to know who is calling — call identify() with an email first.");
2382
+ return await x(this, m, ct).call(this, e);
2383
+ } finally {
2384
+ this.callStarting = !1;
2385
+ }
2386
+ }
2387
+ /**
2388
+ * Ring the team, under the name an integration may already be calling.
2389
+ *
2390
+ * await BugCatch.startLiveCall(id);
2391
+ *
2392
+ * Exactly `startCall`, which is now the only call there is. The id is
2393
+ * optional here too: it was required while this was one of two kinds, because
2394
+ * the other one was the only way to call without a report to call about.
2395
+ */
2396
+ startLiveCall(e) {
2397
+ return this.startCall(e);
2398
+ }
2399
+ /**
2400
+ * Whether this report can be talked through with a person.
2401
+ *
2402
+ * For a host application drawing its own button. The widget asks this itself.
2403
+ */
2404
+ canCallTeam(e) {
2405
+ return this.config.voice ? Ie({ apiBase: this.config.apiBase, bugId: e }) : Promise.resolve(!1);
2406
+ }
2407
+ /**
2408
+ * End the call in progress, of either kind. Does nothing when there is not
2409
+ * one — there is a single slot, because there is a single microphone.
2410
+ */
2411
+ endCall() {
2412
+ var e;
2413
+ return (e = this.call) == null || e.hangUp(), this.call = null, this;
2414
+ }
2415
+ /**
2416
+ * Everything this browser has reported, newest first, with the status of
2417
+ * each — the history a reporter normally has no way to see.
2418
+ *
2419
+ * const { reports } = await BugCatch.myReports();
2420
+ * // [{ reference: '3F2A9B01', title, status, priority, createdAt, ... }]
2421
+ *
2422
+ * Resolves to an empty history rather than throwing when nothing has been
2423
+ * filed from this browser, or when identify() supplied no email — there is
2424
+ * nothing to look up in either case.
2425
+ */
2426
+ myReports() {
2427
+ return Qt({ apiBase: this.config.apiBase });
2428
+ }
2429
+ /** True when this browser has filed something, so a "your reports" entry
2430
+ * point can be hidden until it would show anything. */
2431
+ hasReports() {
2432
+ return !!E();
2433
+ }
2434
+ /**
2435
+ * Forget this browser's reports — call on sign-out, or the next person at
2436
+ * this machine inherits the previous one's notifications.
2437
+ */
2438
+ forgetReports() {
2439
+ var e;
2440
+ return (e = this.stopWatching) == null || e.call(this), this.stopWatching = null, this.endCall(), M(), x(this, m, O).call(this), this;
2441
+ }
2442
+ /**
2443
+ * Attach the signed-in user so reports say who hit the bug.
2444
+ *
2445
+ * `email` is the field that matters beyond display: it threads every report
2446
+ * from the same person together in the dashboard, and it is where the team's
2447
+ * "your issue is resolved" email goes. Because your app has already
2448
+ * authenticated this person, an address sent here is treated as reachable —
2449
+ * unlike one typed into a public recording-link form, which stays unusable
2450
+ * for outbound mail until the reporter answers a confirmation code.
2451
+ *
2452
+ * BugCatch.identify({ id: 'u_42', email: 'alex@acme.com', name: 'Alex' });
2453
+ *
2454
+ * Pass null to forget them again on sign-out.
2455
+ */
2456
+ identify(e) {
2457
+ const n = Ee(e, (o) => this.warn(o));
2458
+ return e != null && !n ? this : (this.config.user = n, this.debugLog("identify", n), this);
2459
+ }
2460
+ /**
2461
+ * Say what the reporter is looking at — the loan, application or customer on
2462
+ * this screen — so every report filed from it carries that record.
2463
+ *
2464
+ * BugCatch.setContext({ loanId: 'L26090028459', screen: 'repayment' });
2465
+ * BugCatch.setContext({ loanId: null }); // left the loan screen
2466
+ * BugCatch.setContext(null); // clear everything
2467
+ *
2468
+ * Merged over what was set before and kept until changed — for the life of
2469
+ * the page, not across reloads. `loanId`, `applicationId` and
2470
+ * `customerNumber` are read by bugcatch as identifiers your app asserted, and
2471
+ * outrank the same field on identify(); anything else is readable metadata.
2472
+ */
2473
+ setContext(e) {
2474
+ return this.context = $e(this.context, e, (n) => this.warn(n)), this.debugLog("context", this.context), this;
2475
+ }
2476
+ /** What setContext() currently holds (a copy). */
2477
+ getContext() {
2478
+ return { ...this.context };
2479
+ }
2480
+ /**
2481
+ * Open the report panel, as if the floating button had been pressed — for a
2482
+ * "Report a problem" item in your own menu.
2483
+ *
2484
+ * Needs the built-in widget (`widget: true`, the default). With `widget:
2485
+ * false` there is no panel to open: draw your own form and call report().
2486
+ */
2487
+ open() {
2488
+ return this.started ? this.widgetControls ? (this.widgetControls.open(), this) : (this.warn("open() needs the built-in widget; it was turned off with widget: false."), this) : (this.warn("open() was called before init() — nothing to open yet."), this);
2489
+ }
2490
+ /** Close the report panel if it is open. */
2491
+ close() {
2492
+ var e;
2493
+ return (e = this.widgetControls) == null || e.close(), this;
2494
+ }
2495
+ /** Whether the report panel is open. */
2496
+ isOpen() {
2497
+ var e;
2498
+ return !!((e = this.widgetControls) != null && e.isOpen());
2499
+ }
2500
+ /**
2501
+ * Upload a screenshot, recording or file and get back the storage key that
2502
+ * `report({ attachments })` references.
2503
+ *
2504
+ * The blob goes straight to storage on a presigned URL — it never passes
2505
+ * through the API — so this is also what you call when you drive the SDK
2506
+ * yourself with `widget: false`.
2507
+ */
2508
+ async uploadAttachment(e, n, o) {
2509
+ if (!this.started) throw new Error("[BugCatch] call init() first.");
2510
+ return Ft({
2511
+ apiBase: this.config.apiBase,
2512
+ apiKey: this.config.apiKey,
2513
+ blob: e,
2514
+ mime: n || (e == null ? void 0 : e.type),
2515
+ onProgress: o
2516
+ });
2517
+ }
2518
+ /** File a bug with everything captured so far. */
2519
+ async report({
2520
+ title: e,
2521
+ description: n = "",
2522
+ priority: o = "MEDIUM",
2523
+ metadata: r = {},
2524
+ attachments: i = [],
2525
+ references: a = [],
2526
+ context: s = null,
2527
+ issue: c = null
2528
+ } = {}) {
2529
+ var S, h, I, v, C, w, k, B;
2530
+ if (!this.started) throw new Error("[BugCatch] call init() first.");
2531
+ if (typeof e != "string" || !e.trim()) throw new Error("[BugCatch] a title is required.");
2532
+ ["LOW", "MEDIUM", "HIGH", "CRITICAL"].includes(String(o).toUpperCase()) || (this.warn(`priority "${o}" is not one of LOW, MEDIUM, HIGH, CRITICAL — sent as MEDIUM.`), o = "MEDIUM");
2533
+ const l = (i || []).filter((b) => (b == null ? void 0 : b.storageKey) && ee(b.mime, ke)).map((b) => ({
2534
+ type: b.type === "VIDEO" ? "VIDEO" : "SCREENSHOT",
2535
+ storageKey: b.storageKey,
2536
+ mime: ee(b.mime, ke)
2537
+ })), f = (Array.isArray(a) ? a : []).filter((b) => b && typeof b.key == "string" && b.key && (typeof b.value == "string" || typeof b.value == "number")).map((b) => ({ key: b.key.slice(0, 64), value: String(b.value).trim().slice(0, 120) })).filter((b) => b.value);
2538
+ Array.isArray(a) && f.length < a.length && this.warn("report({ references }) takes [{ key, value }] with string values; malformed entries were dropped.");
2539
+ let p = await x(this, m, z).call(this, {
2540
+ title: e.trim(),
2541
+ description: typeof n == "string" ? n : String(n ?? ""),
2542
+ priority: String(o).toUpperCase(),
2543
+ metadata: r,
2544
+ media: l,
2545
+ references: f,
2546
+ context: s,
2547
+ issue: typeof c == "string" && c ? c.slice(0, 32) : null
2548
+ });
2549
+ const d = x(this, m, ae).call(this, p);
2550
+ if (d === !1) return { cancelled: !0 };
2551
+ p = d, this.debugLog("sending report", {
2552
+ title: p.title,
2553
+ user: (S = p.metadata) == null ? void 0 : S.user,
2554
+ context: (h = p.metadata) == null ? void 0 : h.context,
2555
+ references: p.references,
2556
+ console: (C = (v = (I = p.logs) == null ? void 0 : I.console) == null ? void 0 : v.entries) == null ? void 0 : C.length,
2557
+ network: (B = (k = (w = p.logs) == null ? void 0 : w.network) == null ? void 0 : k.entries) == null ? void 0 : B.length
2558
+ });
2559
+ const u = j(this.config.projectId);
2560
+ let g;
2561
+ try {
2562
+ g = await fetch(`${this.config.apiBase}/ingest/bugs`, {
2563
+ method: "POST",
2564
+ headers: {
2565
+ "Content-Type": "application/json",
2566
+ "X-BugCatch-Key": this.config.apiKey,
2567
+ // Present only where the reporter signed in. The server takes the
2568
+ // address from here rather than from the payload when it is — on a
2569
+ // project that requires sign-in, letting the page name a different
2570
+ // reporter would undo the requirement.
2571
+ ...u != null && u.token ? { "X-BugCatch-Session": u.token } : {}
2572
+ },
2573
+ body: JSON.stringify(to(p))
2574
+ });
2575
+ } catch (b) {
2576
+ const y = new Error(`[BugCatch] could not reach ${this.config.apiBase} — check apiBase, and that this site's origin is allowed by the API.`);
2577
+ throw y.cause = b, x(this, m, H).call(this, "report", y), y;
2578
+ }
2579
+ if (g.status === 401 && u && ne(this.config.projectId), !g.ok) {
2580
+ let b = "";
2581
+ try {
2582
+ const T = await g.json();
2583
+ b = Array.isArray(T.message) ? T.message.join(", ") : T.message || "";
2584
+ } catch {
2585
+ }
2586
+ g.status === 401 && !u && (b || (b = "the apiKey was not accepted")), g.status === 403 && (b || (b = "the apiKey is not allowed to report to this projectId"));
2587
+ const y = g.status === 429 ? new Error(`[BugCatch] ${L(b)}`) : new Error(`[BugCatch] report failed (HTTP ${g.status})${b ? ": " + b : ""}`);
2588
+ throw y.status = g.status, x(this, m, H).call(this, "report", y), y;
2589
+ }
2590
+ const $ = await g.json();
2591
+ return x(this, m, re).call(this, $, { title: p.title ?? e.trim(), via: "form" }), $.watchToken && (te($.watchToken), this.config.updates && this.startWatching()), $;
2592
+ }
2593
+ /**
2594
+ * What this project asks of the person reporting, and who this browser is
2595
+ * signed in as right now.
2596
+ *
2597
+ * const { mode, signedIn } = await BugCatch.loginState();
2598
+ * // mode: 'off' | 'optional' | 'required'
2599
+ *
2600
+ * The widget calls this itself every time it opens. It is here for
2601
+ * integrations running with `widget: false`, which have to draw their own
2602
+ * sign-in and need the same answer to know whether to draw one at all.
2603
+ *
2604
+ * The mode is the project's setting, changed in BugCatch under Project
2605
+ * Settings → General, not in this config. A widget that could opt out of a
2606
+ * required sign-in would not be a requirement.
2607
+ */
2608
+ loginState() {
2609
+ var e;
2610
+ return xn({
2611
+ apiBase: this.config.apiBase,
2612
+ apiKey: this.config.apiKey,
2613
+ projectId: this.config.projectId,
2614
+ token: (e = j(this.config.projectId)) == null ? void 0 : e.token
2615
+ });
2616
+ }
2617
+ /**
2618
+ * What this project needs in order to look a report up, and how it asks.
2619
+ *
2620
+ * const { policy, fields } = await BugCatch.referenceFields();
2621
+ * // policy: 'off' | 'optional' | 'required'
2622
+ *
2623
+ * The widget calls this itself when it opens, and caches the answer for the
2624
+ * life of the page: unlike the sign-in state, this cannot change underneath a
2625
+ * reporter in a way that matters — an admin editing the field list mid-report
2626
+ * costs one stale question, not a failed send.
2627
+ *
2628
+ * Here for the same reason loginState() is: an integration running with
2629
+ * `widget: false` draws its own form and needs the same answer to know
2630
+ * whether that form should have an identifier box on it.
2631
+ */
2632
+ referenceFields() {
2633
+ return this.refFields ? this.refFields : (this.refFields = jn({
2634
+ apiBase: this.config.apiBase,
2635
+ apiKey: this.config.apiKey,
2636
+ projectId: this.config.projectId
2637
+ }), this.refFields);
2638
+ }
2639
+ /**
2640
+ * The questions this project's assistant can answer and close by itself —
2641
+ * "When is my next EMI due?", "Is my KYC complete?" — for a form to offer
2642
+ * as one-tap choices.
2643
+ *
2644
+ * const issues = await BugCatch.agentIssues();
2645
+ * // [{ key: 'emi', title: 'When is my next EMI due?', hint: 'The date and the amount' }]
2646
+ * BugCatch.report({ title: issues[0].title, issue: issues[0].key });
2647
+ *
2648
+ * The assistant looks the reporter up (identify() makes that instant),
2649
+ * answers from their record, and asks whether that settled it: yes resolves
2650
+ * the report, no hands it to the team. Empty unless the project has turned
2651
+ * this on and connected a data source that can answer. The widget calls it
2652
+ * itself; cached for the life of the page.
2653
+ */
2654
+ agentIssues() {
2655
+ return this.agentIssueList ? this.agentIssueList : (this.agentIssueList = On({
2656
+ apiBase: this.config.apiBase,
2657
+ apiKey: this.config.apiKey,
2658
+ projectId: this.config.projectId
2659
+ }), this.agentIssueList);
2660
+ }
2661
+ /**
2662
+ * What this project's team is telling the people using its widget, narrowed
2663
+ * to the visitor's own day and to what this browser has not closed.
2664
+ *
2665
+ * const notices = await BugCatch.announcements();
2666
+ * // [{ id, title, body, tone: 'info' | 'success' | 'warning' | 'critical',
2667
+ * // ctaLabel, ctaHref, placement: 'panel' | 'launcher', dismissible }]
2668
+ * BugCatch.dismissAnnouncement(notices[0]);
2669
+ *
2670
+ * Written and changed on the project's settings page, with no redeploy here;
2671
+ * a browser asks at most every five minutes, so a change reaches visitors
2672
+ * within that. The widget draws them itself — this is for a site running
2673
+ * `widget: false` or `announcements: false`. Empty on every failure.
2674
+ */
2675
+ async announcements() {
2676
+ const e = await Kn({
2677
+ apiBase: this.config.apiBase,
2678
+ apiKey: this.config.apiKey,
2679
+ projectId: this.config.projectId
2680
+ });
2681
+ return Un(e);
2682
+ }
2683
+ /**
2684
+ * Stop showing an announcement in this browser, until the team uses "Show it
2685
+ * again". Takes one of announcements()' results; one that may not be closed
2686
+ * is left alone.
2687
+ */
2688
+ dismissAnnouncement(e) {
2689
+ Nn(e);
2690
+ }
2691
+ /**
2692
+ * Who this browser is signed in as on this project, from storage alone.
2693
+ *
2694
+ * BugCatch.signedInAs() // { email, name } | null
2695
+ *
2696
+ * Synchronous, and therefore only as current as the last round trip: a
2697
+ * session ended elsewhere still reads as signed in here until something asks
2698
+ * the server. Use loginState() where that matters.
2699
+ */
2700
+ signedInAs() {
2701
+ const e = j(this.config.projectId);
2702
+ return e ? { email: e.email, name: e.name } : null;
2703
+ }
2704
+ /**
2705
+ * Step one of signing in: mail a code to any address.
2706
+ *
2707
+ * await BugCatch.signIn('someone@example.com');
2708
+ * await BugCatch.confirmSignIn('someone@example.com', '481920');
2709
+ *
2710
+ * Any address, deliberately — there is no list of people who may report a
2711
+ * bug in somebody's product, and asking for one would be asking the customer
2712
+ * to maintain it. What the code proves is that whoever typed it can read the
2713
+ * address, which is all a reply-to has ever needed to be worth.
2714
+ */
2715
+ signIn(e, { name: n } = {}) {
2716
+ var o;
2717
+ return In({
2718
+ apiBase: this.config.apiBase,
2719
+ apiKey: this.config.apiKey,
2720
+ projectId: this.config.projectId,
2721
+ email: e,
2722
+ // The name the host app already knows beats asking for one again.
2723
+ name: n ?? ((o = this.config.user) == null ? void 0 : o.name) ?? null
2724
+ });
2725
+ }
2726
+ /**
2727
+ * Step two: spend the code and hold the session.
2728
+ *
2729
+ * Signing in signs this address out of this project in every other browser —
2730
+ * one live session per address per project. Somebody who signs in on their
2731
+ * phone has signed their laptop out, on purpose.
2732
+ */
2733
+ async confirmSignIn(e, n) {
2734
+ var r;
2735
+ const o = await Sn({
2736
+ apiBase: this.config.apiBase,
2737
+ apiKey: this.config.apiKey,
2738
+ projectId: this.config.projectId,
2739
+ email: e,
2740
+ code: n
2741
+ });
2742
+ return (r = this.config.user) != null && r.email || (this.config.user = { email: o.email, name: o.name || void 0 }), this.emit("signin", { email: o.email, name: o.name }), { email: o.email, name: o.name };
2743
+ }
2744
+ /**
2745
+ * Sign out of this project.
2746
+ *
2747
+ * Ends the session on the server too, so the token cannot be replayed from a
2748
+ * copy of this browser's storage. Local state goes whatever the network
2749
+ * says — sign-out on a shared machine has to mean signed out on it.
2750
+ */
2751
+ async signOut() {
2752
+ var o;
2753
+ const e = j(this.config.projectId), n = await Cn({
2754
+ apiBase: this.config.apiBase,
2755
+ apiKey: this.config.apiKey,
2756
+ projectId: this.config.projectId,
2757
+ token: e == null ? void 0 : e.token
2758
+ });
2759
+ return e != null && e.email && ((o = this.config.user) == null ? void 0 : o.email) === e.email && (this.config.user = null), this.emit("signout", { email: (e == null ? void 0 : e.email) || null }), n;
2760
+ }
2761
+ /**
2762
+ * What this project is already known to be having trouble with.
2763
+ *
2764
+ * The widget asks this itself when it opens; it is public for a host
2765
+ * application drawing its own help surface with `widget: false`. Resolves to
2766
+ * an empty list on every project that has not switched the feature on, which
2767
+ * is the default — draw nothing rather than an empty heading.
2768
+ */
2769
+ knownIssues() {
2770
+ var e;
2771
+ return gn({
2772
+ apiBase: this.config.apiBase,
2773
+ apiKey: this.config.apiKey,
2774
+ projectId: this.config.projectId,
2775
+ sessionToken: (e = j(this.config.projectId)) == null ? void 0 : e.token
2776
+ });
2777
+ }
2778
+ /**
2779
+ * The known issues that look like a description somebody is writing.
2780
+ *
2781
+ * Pass an AbortSignal when calling this on a keystroke, or a fast typist gets
2782
+ * the results of whichever request happens to return last.
2783
+ */
2784
+ matchKnownIssues(e, { signal: n } = {}) {
2785
+ var o;
2786
+ return mn({
2787
+ apiBase: this.config.apiBase,
2788
+ apiKey: this.config.apiKey,
2789
+ projectId: this.config.projectId,
2790
+ q: e || "",
2791
+ sessionToken: (o = j(this.config.projectId)) == null ? void 0 : o.token,
2792
+ signal: n
2793
+ });
2794
+ }
2795
+ /**
2796
+ * Say a known issue is happening to you too, instead of filing a second
2797
+ * report about it.
2798
+ *
2799
+ * Sends the same capture a report would — this browser's console, network and
2800
+ * environment — because that is what the team gets out of somebody joining
2801
+ * rather than filing: one report with everybody's environment on it.
2802
+ *
2803
+ * Needs an address. A signed-in widget has one, and identify() supplies one
2804
+ * otherwise; pass `email` for the case where neither did. Joining means "tell
2805
+ * me when this is fixed", and there is no way to keep that promise to
2806
+ * somebody anonymous.
2807
+ */
2808
+ async meToo(e, { email: n, name: o } = {}) {
2809
+ var a, s;
2810
+ const r = j(this.config.projectId), i = await me();
2811
+ return wn({
2812
+ apiBase: this.config.apiBase,
2813
+ apiKey: this.config.apiKey,
2814
+ projectId: this.config.projectId,
2815
+ bugId: e,
2816
+ reporterEmail: n || ((a = this.config.user) == null ? void 0 : a.email),
2817
+ reporterName: o || ((s = this.config.user) == null ? void 0 : s.name),
2818
+ metadata: {
2819
+ ...i,
2820
+ user: this.config.user || void 0,
2821
+ ...Object.keys(this.context).length ? { context: this.context } : {},
2822
+ sdk: K
2823
+ },
2824
+ logs: {
2825
+ console: { entries: this.consoleBuffer.all() },
2826
+ network: { entries: this.networkBuffer.all() }
2827
+ },
2828
+ sessionToken: r == null ? void 0 : r.token
2829
+ });
2830
+ }
2831
+ /** What would be sent right now — useful for debugging integrations. */
2832
+ snapshot() {
2833
+ return {
2834
+ user: this.config.user || null,
2835
+ context: { ...this.context },
2836
+ console: this.consoleBuffer.all(),
2837
+ network: this.networkBuffer.all(),
2838
+ environment: Le()
2839
+ };
2840
+ }
2841
+ clear() {
2842
+ this.consoleBuffer.clear(), this.networkBuffer.clear();
2843
+ }
2844
+ /** Restore every patched global and remove the widget. */
2845
+ destroy() {
2846
+ this.endCall(), this.teardown.forEach((e) => {
2847
+ try {
2848
+ e();
2849
+ } catch {
2850
+ }
2851
+ }), this.teardown = [], this.notifier = null, this.started = !1;
2852
+ }
2853
+ }
2854
+ m = new WeakSet(), /** Call a host callback without letting it break the SDK. */
2855
+ P = function(e, n) {
2856
+ const o = this.config[e];
2857
+ if (typeof o == "function")
2858
+ try {
2859
+ o(n);
2860
+ } catch (r) {
2861
+ console.error(`[BugCatch] your ${e} callback threw:`, r);
2862
+ }
2863
+ }, /**
2864
+ * Tell the project this site has the SDK — once a day per browser, a few
2865
+ * seconds after init so identify() and setContext() have had their chance.
2866
+ *
2867
+ * What the Web SDK settings page shows under "Installed on": which site,
2868
+ * which version, whether identify() is wired. Nothing personal goes: the
2869
+ * origin comes from the browser's own Origin header, and the rest is a
2870
+ * version string and four booleans. Every failure is silent — this must
2871
+ * never cost a page anything but one small request a day.
2872
+ */
2873
+ rt = function() {
2874
+ if (typeof window > "u" || typeof fetch != "function") return;
2875
+ const e = `bugcatch:hello:${this.config.projectId}`, n = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2876
+ try {
2877
+ if (window.localStorage.getItem(e) === n) return;
2878
+ } catch {
2879
+ }
2880
+ const o = setTimeout(() => {
2881
+ try {
2882
+ window.localStorage.setItem(e, n);
2883
+ } catch {
2884
+ }
2885
+ fetch(`${this.config.apiBase}/ingest/hello`, {
2886
+ method: "POST",
2887
+ keepalive: !0,
2888
+ headers: { "Content-Type": "application/json", "X-BugCatch-Key": this.config.apiKey },
2889
+ body: JSON.stringify({
2890
+ projectId: this.config.projectId,
2891
+ version: K.version,
2892
+ identify: !!this.config.user,
2893
+ context: !!(this.context && Object.keys(this.context).length),
2894
+ widget: !!this.config.widget,
2895
+ environment: this.config.environment || null,
2896
+ release: this.config.release || null
2897
+ })
2898
+ }).catch(() => {
2899
+ });
2900
+ }, 5e3);
2901
+ this.teardown.push(() => clearTimeout(o));
2902
+ }, /**
2903
+ * Redraw the built-in notifications, and tell anyone counting.
2904
+ *
2905
+ * The single path out of the unread store: every add and every mark-read ends
2906
+ * here, so the cards on screen and a host app's own badge cannot disagree
2907
+ * about what is outstanding.
2908
+ */
2909
+ O = function() {
2910
+ const e = N();
2911
+ if (this.emit("unread", { count: e.length, updates: e }), !!this.config.updateToast) {
2912
+ if (!this.notifier) {
2913
+ if (!e.length) return;
2914
+ this.notifier = pn({
2915
+ accent: this.config.accent,
2916
+ position: this.config.widgetPosition,
2917
+ onRead: (n) => this.markUpdateRead(n),
2918
+ onReadAll: () => this.markUpdatesRead(),
2919
+ // The call button appears only where the API said the offer stands and
2920
+ // this page has calling switched on. Pressing it reads the update —
2921
+ // acting on a notification is the clearest way there is of having seen
2922
+ // it — and `startCall` does the rest.
2923
+ onCall: this.config.voice ? (n) => {
2924
+ this.markUpdateRead(n.id), this.startCall(n.bugId);
2925
+ } : null
2926
+ }), this.teardown.push(() => {
2927
+ var n;
2928
+ return (n = this.notifier) == null ? void 0 : n.destroy();
2929
+ });
2930
+ }
2931
+ this.notifier.render(e);
2932
+ }
2933
+ }, /**
2934
+ * Tell the host app the assistant moved, once per change.
2935
+ *
2936
+ * The phase is only known when a report is fetched, which the widget does
2937
+ * every few seconds while its thread is open and an integration does through
2938
+ * myReport(). The first sighting counts as a change: a host that has just
2939
+ * opened a report wants to know it is being analysed.
2940
+ */
2941
+ at = function(e, n) {
2942
+ const o = n == null ? void 0 : n.phase;
2943
+ if (!e || typeof o != "string" || this.phases.get(e) === o) return;
2944
+ this.phases.set(e, o);
2945
+ const r = { bugId: e, phase: o, title: n.title ?? null, detail: n.detail ?? null };
2946
+ this.debugLog("assistant", r), x(this, m, P).call(this, "onAssistantPhase", r), this.emit("assistant", r);
2947
+ }, /** A report was accepted: tell the host app, however it was filed. */
2948
+ re = function(e, { title: n, via: o }) {
2949
+ if (!(e != null && e.id)) return;
2950
+ const r = { bugId: e.id, title: n ?? null, via: o, result: e };
2951
+ this.debugLog("report accepted", r), x(this, m, P).call(this, "onReportSubmitted", r), this.emit("report", r);
2952
+ }, /** Something done on the page's behalf failed. */
2953
+ H = function(e, n) {
2954
+ x(this, m, P).call(this, "onError", { action: e, error: n }), this.emit("error", { action: e, error: n });
2955
+ }, z = async function({ title: e, description: n = "", priority: o = "MEDIUM", metadata: r = {}, media: i = [], references: a = [], context: s = null, issue: c = null }) {
2956
+ var f, p;
2957
+ const l = s ? $e(this.context, s, (d) => this.warn(d.replace("setContext()", "report({ context })"))) : this.context;
2958
+ return {
2959
+ projectId: this.config.projectId,
2960
+ ...e ? { title: e } : {},
2961
+ description: n,
2962
+ priority: o,
2963
+ url: window.location.href,
2964
+ reporterEmail: (f = this.config.user) == null ? void 0 : f.email,
2965
+ // Sent alongside the address so the dashboard can show a person rather
2966
+ // than a mailbox, and so the resolution email can open with their name.
2967
+ reporterName: (p = this.config.user) == null ? void 0 : p.name,
2968
+ metadata: {
2969
+ ...await me(),
2970
+ release: this.config.release,
2971
+ environment: this.config.environment,
2972
+ buildId: this.config.buildId,
2973
+ ...r,
2974
+ // Who is signed in, as identify() shaped it: `id`, `email` and `phone`
2975
+ // are where the API and the ownership check read them, and `loanId`,
2976
+ // `applicationId`, `customerNumber` become trusted lookup keys.
2977
+ user: this.config.user || void 0,
2978
+ // The screen the report came from — setContext(). Its identifiers
2979
+ // outrank the same kind on `user`.
2980
+ ...Object.keys(l).length ? { context: l } : {},
2981
+ sdk: K
2982
+ },
2983
+ logs: {
2984
+ console: { entries: this.consoleBuffer.all() },
2985
+ network: { entries: this.networkBuffer.all() }
2986
+ },
2987
+ // Omitted rather than sent empty: the field is optional server-side, and
2988
+ // an empty array in every payload makes beforeSend hooks harder to read.
2989
+ ...i.length ? { attachments: i } : {},
2990
+ // What the reporter answered when asked which record this is about. Only
2991
+ // ever what they typed into the box — never what this browser matched in
2992
+ // their own text, which the API reads for itself out of the title and
2993
+ // description it is being sent anyway. A payload is a claim, and there is
2994
+ // no reason to make the join key one when the source text is right here.
2995
+ ...a.length ? { references: a } : {},
2996
+ // A predefined issue the reporter picked (agentIssues()). The API keeps
2997
+ // it only if this project offers it.
2998
+ ...c ? { issue: c } : {}
2999
+ };
3000
+ }, /**
3001
+ * File a report because somebody pressed Call instead of filling the form in.
3002
+ *
3003
+ * Filing is the half a call shares with the form, and it is the half that has
3004
+ * to say who is filing. Held in one place after a session header went missing
3005
+ * from this route while `report()` kept sending it: nothing looked wrong until
3006
+ * a project switched sign-in on, and then a signed-in reporter pressing Call
3007
+ * was told to sign in.
3008
+ */
3009
+ /**
3010
+ * The host's `beforeSend`, applied to anything about to leave the browser.
3011
+ *
3012
+ * Its own method because it was written into `report()` alone, and a
3013
+ * reporter pressing Call files a report by another route — carrying the same
3014
+ * console buffer, the same network buffer and the same identify() fields,
3015
+ * past a hook whose whole purpose is to strip them. A host that set this to
3016
+ * scrub PII was scrubbed on one button and not the other.
3017
+ *
3018
+ * Returns the payload to send, or false when the host cancelled.
3019
+ */
3020
+ ae = function(e) {
3021
+ if (typeof this.config.beforeSend != "function") return e;
3022
+ let n;
3023
+ try {
3024
+ n = this.config.beforeSend(e);
3025
+ } catch (o) {
3026
+ return this.warn(`beforeSend threw, so nothing was sent: ${(o == null ? void 0 : o.message) || o}`), !1;
3027
+ }
3028
+ return n === !1 ? !1 : n && typeof n == "object" ? n : e;
3029
+ }, st = async function() {
3030
+ const e = j(this.config.projectId), n = x(this, m, ae).call(this, await x(this, m, z).call(this, {}));
3031
+ if (n === !1)
3032
+ return this.debugLog("call cancelled by beforeSend"), { cancelled: !0 };
3033
+ let o;
3034
+ try {
3035
+ o = await on({
3036
+ apiBase: this.config.apiBase,
3037
+ apiKey: this.config.apiKey,
3038
+ token: e == null ? void 0 : e.token,
3039
+ payload: n,
3040
+ // See report(): the token this browser holds, so the one that comes
3041
+ // back still covers what it covered.
3042
+ watchToken: E()
3043
+ });
3044
+ } catch (r) {
3045
+ throw r.status === 401 && e && ne(this.config.projectId), x(this, m, H).call(this, "call", r), r;
3046
+ }
3047
+ return x(this, m, re).call(this, o, { title: null, via: "call" }), o.watchToken && (te(o.watchToken), this.config.updates && this.startWatching()), o;
3048
+ }, ct = async function(e) {
3049
+ const { joinLiveCall: n, liveCallSupported: o, openMicrophone: r } = await _(this.config.chunkBase);
3050
+ if (!o())
3051
+ throw new Error("[BugCatch] this browser cannot make live calls.");
3052
+ const i = await r();
3053
+ let a, s = null;
3054
+ try {
3055
+ if (e)
3056
+ a = await rn({ apiBase: this.config.apiBase, bugId: e });
3057
+ else if (s = await x(this, m, st).call(this), e = s.id, a = s.live_call, !(a != null && a.realtime_url)) {
3058
+ const l = new Error(`[BugCatch] the report was filed, but the team could not be rung${s.call_error ? `: ${s.call_error}` : "."}`);
3059
+ throw l.filed = s, l.reason = s.call_error || null, l;
3060
+ }
3061
+ } catch (l) {
3062
+ for (const f of i.getTracks()) f.stop();
3063
+ throw l;
3064
+ }
3065
+ const c = await n({
3066
+ realtimeUrl: a.realtime_url,
3067
+ stream: i,
3068
+ ringsUntil: a.rings_until,
3069
+ onState: (l, f) => {
3070
+ (l === "ended" || l === "failed" || l === "missed") && (this.call = null), l === "missed" && an({ apiBase: this.config.apiBase, bugId: e, callId: a.call_id }), typeof this.config.onCallState == "function" && this.config.onCallState(l, { bugId: e, live: !0, ...f }), this.emit("call", { bugId: e, live: !0, state: l, ...f });
3071
+ }
3072
+ });
3073
+ return this.call = c, Object.assign(c, {
3074
+ bugId: e,
3075
+ callId: a.call_id,
3076
+ live: !0,
3077
+ filed: s,
3078
+ recorded: !!a.recorded
3079
+ });
3080
+ }, // --- being called ------------------------------------------------------------
3081
+ /**
3082
+ * Somebody on the team is ringing this page.
3083
+ *
3084
+ * The only thing in this SDK that starts without the person on the page
3085
+ * asking for it, which is why what it does is so much narrower than what the
3086
+ * outgoing calls do: it draws a card and makes a soft noise. No microphone is
3087
+ * opened and nothing is connected until Answer is pressed, and if nothing is
3088
+ * pressed the card takes itself away when the ringing stops.
3089
+ *
3090
+ * Silently ignored when calling is switched off in this integration's config
3091
+ * or the browser cannot hold a peer connection. A host application that has
3092
+ * said `voice: false` has said its customers are not to be rung, and a card
3093
+ * offering a call this page could not join would be worse than none.
3094
+ */
3095
+ lt = function(e) {
3096
+ var r;
3097
+ const n = e.incomingCall;
3098
+ if (!this.config.voice || ((r = this.ringing) == null ? void 0 : r.callId) === n.callId || this.call || this.callStarting) return;
3099
+ x(this, m, F).call(this);
3100
+ const o = { callId: n.callId, dismiss: () => {
3101
+ } };
3102
+ this.ringing = o, _(this.config.chunkBase).then(({ liveCallSupported: i, showIncomingCall: a }) => {
3103
+ if (this.ringing !== o || (this.ringing = null, !i()) || this.call || this.callStarting) return;
3104
+ const s = a({
3105
+ call: n,
3106
+ accent: this.config.accent,
3107
+ position: this.config.widgetPosition,
3108
+ // Whether the page has been touched, which is when the card may ring
3109
+ // out loud — tracked since load, which the card's own code cannot be.
3110
+ touched: Bn,
3111
+ onAnswer: () => {
3112
+ x(this, m, ut).call(this, e.bugId, n);
3113
+ },
3114
+ onDecline: () => {
3115
+ this.ringing = null, Se({
3116
+ apiBase: this.config.apiBase,
3117
+ bugId: e.bugId,
3118
+ callId: n.callId
3119
+ }), this.emit("incoming-call", { bugId: e.bugId, callId: n.callId, state: "declined" });
3120
+ },
3121
+ // Nothing is reported here. The member who placed it is watching it ring
3122
+ // on their own screen and their browser tells the API when it stops —
3123
+ // the same rule as the other direction, where whoever placed the call is
3124
+ // the one that reports nobody answered.
3125
+ onExpire: () => {
3126
+ this.ringing = null, this.emit("incoming-call", { bugId: e.bugId, callId: n.callId, state: "missed" });
3127
+ }
3128
+ });
3129
+ if (this.ringing = { callId: n.callId, dismiss: s }, typeof this.config.onIncomingCall == "function")
3130
+ try {
3131
+ this.config.onIncomingCall({ bugId: e.bugId, ...n });
3132
+ } catch {
3133
+ }
3134
+ this.emit("incoming-call", { bugId: e.bugId, callId: n.callId, state: "ringing" });
3135
+ }, () => {
3136
+ this.ringing === o && (this.ringing = null);
3137
+ });
3138
+ }, /** Take the card away, whatever it is showing. */
3139
+ F = function(e) {
3140
+ if (this.ringing && !(e && this.ringing.callId !== e)) {
3141
+ try {
3142
+ this.ringing.dismiss();
3143
+ } catch {
3144
+ }
3145
+ this.ringing = null;
3146
+ }
3147
+ }, ut = async function(e, n) {
3148
+ if (this.ringing = null, this.call || this.callStarting) return;
3149
+ this.callStarting = !0;
3150
+ let o, r;
3151
+ try {
3152
+ const a = await _(this.config.chunkBase);
3153
+ r = a.joinLiveCall, o = await a.openMicrophone();
3154
+ } catch (a) {
3155
+ this.callStarting = !1, Se({ apiBase: this.config.apiBase, bugId: e, callId: n.callId }), this.emit("incoming-call", { bugId: e, callId: n.callId, state: "failed", error: a });
3156
+ return;
3157
+ }
3158
+ let i;
3159
+ try {
3160
+ i = await sn({
3161
+ apiBase: this.config.apiBase,
3162
+ bugId: e,
3163
+ callId: n.callId
3164
+ });
3165
+ } catch (a) {
3166
+ for (const s of o.getTracks()) s.stop();
3167
+ this.callStarting = !1, this.emit("incoming-call", { bugId: e, callId: n.callId, state: "missed", error: a });
3168
+ return;
3169
+ }
3170
+ try {
3171
+ const a = await r({
3172
+ realtimeUrl: i.realtime_url,
3173
+ stream: o,
3174
+ // No deadline on this side. The ringing is over — it ended when this
3175
+ // browser claimed the call — and a countdown here would hang up on a
3176
+ // conversation that has already started.
3177
+ onState: (s, c) => {
3178
+ (s === "ended" || s === "failed" || s === "missed") && (this.call = null), typeof this.config.onCallState == "function" && this.config.onCallState(s, { bugId: e, live: !0, incoming: !0, ...c }), this.emit("call", { bugId: e, live: !0, incoming: !0, state: s, ...c });
3179
+ }
3180
+ });
3181
+ this.call = a, this.emit("incoming-call", { bugId: e, callId: n.callId, state: "answered" });
3182
+ } catch (a) {
3183
+ for (const s of o.getTracks()) s.stop();
3184
+ this.emit("incoming-call", { bugId: e, callId: n.callId, state: "failed", error: a });
3185
+ } finally {
3186
+ this.callStarting = !1;
3187
+ }
3188
+ };
3189
+ const dt = new it();
3190
+ dt.Client = it;
3191
+ dt.version = K.version;
3192
+ export {
3193
+ dt as B,
3194
+ ke as R,
3195
+ fo as S,
3196
+ zt as U,
3197
+ io as a,
3198
+ ao as b,
3199
+ Ue as c,
3200
+ so as d,
3201
+ uo as e,
3202
+ Ke as f,
3203
+ At as g,
3204
+ lo as h,
3205
+ Mt as i,
3206
+ oo as j,
3207
+ co as k,
3208
+ Ct as l,
3209
+ ee as n,
3210
+ ho as r,
3211
+ ro as s,
3212
+ An as v
3213
+ };
3214
+ //# sourceMappingURL=bugcatch-core.es.js.map