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