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