@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
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sphoro/bugcatch-web-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Drop-in browser SDK — report bugs with console, network and environment context, without the extension.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/bugcatch.es.js",
|
|
7
7
|
"module": "dist/bugcatch.es.js",
|
|
8
|
-
"browser": "dist/bugcatch.umd.js",
|
|
8
|
+
"browser": "dist/bugcatch.standalone.umd.js",
|
|
9
9
|
"unpkg": "dist/bugcatch.umd.js",
|
|
10
10
|
"jsdelivr": "dist/bugcatch.umd.js",
|
|
11
11
|
"types": "src/index.d.ts",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"import": "./dist/bugcatch.es.js",
|
|
16
16
|
"default": "./dist/bugcatch.es.js"
|
|
17
17
|
},
|
|
18
|
-
"./dist
|
|
18
|
+
"./dist/*": "./dist/*",
|
|
19
19
|
"./package.json": "./package.json"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
],
|
|
25
25
|
"sideEffects": false,
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "
|
|
28
|
-
"dev": "
|
|
29
|
-
"prepublishOnly": "
|
|
27
|
+
"build": "node scripts/build.mjs",
|
|
28
|
+
"dev": "node scripts/build.mjs --watch",
|
|
29
|
+
"prepublishOnly": "node scripts/build.mjs"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"bug-report",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The predefined issues: questions the project's assistant can answer from the
|
|
3
|
+
* reporter's own account and close, offered in the widget as one-tap choices.
|
|
4
|
+
*
|
|
5
|
+
* The API decides the list (GET /ingest/agent-issues) — it is empty unless the
|
|
6
|
+
* project has the Sphoro Agent answering questions and a data source that can
|
|
7
|
+
* answer them. Every failure is an empty list: the tiles are a shortcut, and a
|
|
8
|
+
* widget that could not load them is still a widget that files reports.
|
|
9
|
+
*
|
|
10
|
+
* The agent ends its answer with SOLVED_QUESTION's wording; the widget draws
|
|
11
|
+
* Yes / No under it (widget.js messageHtml). Keep this pattern in step with
|
|
12
|
+
* agent-issues.ts SOLVED_QUESTION in the API and workers.
|
|
13
|
+
*/
|
|
14
|
+
export const SOLVED_PROMPT = /Did this answer your question\?/i;
|
|
15
|
+
|
|
16
|
+
export async function fetchAgentIssues({ apiBase, apiKey, projectId }) {
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(
|
|
19
|
+
`${apiBase}/ingest/agent-issues?projectId=${encodeURIComponent(projectId)}`,
|
|
20
|
+
{ headers: { 'X-BugCatch-Key': apiKey } },
|
|
21
|
+
);
|
|
22
|
+
if (!res.ok) return [];
|
|
23
|
+
const body = await res.json();
|
|
24
|
+
return (Array.isArray(body?.issues) ? body.issues : [])
|
|
25
|
+
.filter((i) => i && typeof i.key === 'string' && typeof i.title === 'string')
|
|
26
|
+
.map((i) => ({ key: i.key, title: i.title, hint: typeof i.hint === 'string' ? i.hint : '' }));
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the project's team is telling the people using its widget — "UPI
|
|
3
|
+
* payments are delayed today, no need to report it" — written on the Widget
|
|
4
|
+
* announcements settings page and changed there whenever it needs to be,
|
|
5
|
+
* without the site redeploying anything.
|
|
6
|
+
*
|
|
7
|
+
* The API decides what is live (GET /ingest/announcements): published, and in
|
|
8
|
+
* date for somebody on earth. This file decides the two things only the browser
|
|
9
|
+
* can — whether it is in date on the visitor's own day, and whether this
|
|
10
|
+
* visitor has closed it — and keeps the answer for a few minutes.
|
|
11
|
+
*
|
|
12
|
+
* Kept, unlike the other things the widget asks about, because this is the one
|
|
13
|
+
* asked on page load: a card beside the button has to be known before anybody
|
|
14
|
+
* presses it. Without the cache that is one request per page view on every site
|
|
15
|
+
* running the SDK, for an answer that is an empty list on nearly all of them.
|
|
16
|
+
* With it, a browser asks at most once every CACHE_MS, and a notice written now
|
|
17
|
+
* reaches a visitor within that.
|
|
18
|
+
*
|
|
19
|
+
* Every failure is an empty list. A notice is never worth a broken widget.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const CACHE_MS = 5 * 60_000;
|
|
23
|
+
const SEEN_KEY = 'bugcatch:announcements:seen';
|
|
24
|
+
// More than can ever be live at once; the entry that falls off is one whose
|
|
25
|
+
// notice ended long ago, and the cost of being wrong is closing it twice.
|
|
26
|
+
const SEEN_LIMIT = 40;
|
|
27
|
+
|
|
28
|
+
const TONES = ['info', 'success', 'warning', 'critical'];
|
|
29
|
+
|
|
30
|
+
const cacheKey = (projectId) => `bugcatch:announcements:${projectId}`;
|
|
31
|
+
|
|
32
|
+
export async function fetchAnnouncements({
|
|
33
|
+
apiBase, apiKey, projectId, fresh = false,
|
|
34
|
+
}) {
|
|
35
|
+
if (!fresh) {
|
|
36
|
+
const held = readCache(projectId);
|
|
37
|
+
if (held) return held;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(
|
|
41
|
+
`${apiBase}/ingest/announcements?projectId=${encodeURIComponent(projectId)}`,
|
|
42
|
+
{ headers: { 'X-BugCatch-Key': apiKey } },
|
|
43
|
+
);
|
|
44
|
+
// Kept even when refused. An API from before this route answers 404 on
|
|
45
|
+
// every page, and asking it again on the next one changes nothing.
|
|
46
|
+
const list = res.ok ? cleanList((await res.json())?.announcements) : [];
|
|
47
|
+
writeCache(projectId, list);
|
|
48
|
+
return list;
|
|
49
|
+
} catch {
|
|
50
|
+
// Offline, or blocked by a CSP — not kept, so the next page tries again.
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What to draw: in date where the visitor is, and not closed by them. */
|
|
56
|
+
export function visibleAnnouncements(list, { on = localDay(), seen = readSeen() } = {}) {
|
|
57
|
+
return (list || []).filter((a) => inDate(a, on) && !(a.dismissible && seen.includes(seenKey(a))));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Remember that this visitor closed it, until a "show it again" bumps the revision. */
|
|
61
|
+
export function dismissAnnouncement(a) {
|
|
62
|
+
if (!a?.id || !a.dismissible) return;
|
|
63
|
+
try {
|
|
64
|
+
const key = seenKey(a);
|
|
65
|
+
const next = [...readSeen().filter((k) => k !== key), key];
|
|
66
|
+
localStorage.setItem(SEEN_KEY, JSON.stringify(next.slice(-SEEN_LIMIT)));
|
|
67
|
+
} catch { /* storage denied — it still goes away for this page */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether a link is one to draw at all. The API refuses anything else, and this
|
|
72
|
+
* is the second lock on the same door: the widget renders it as a link on the
|
|
73
|
+
* customer's site, and `javascript:` there is a script run by whoever wrote it.
|
|
74
|
+
*/
|
|
75
|
+
export function safeHref(href) {
|
|
76
|
+
if (typeof href !== 'string' || !href) return null;
|
|
77
|
+
if (href.startsWith('//')) return null;
|
|
78
|
+
if (href.startsWith('/')) return href;
|
|
79
|
+
return /^(https?:\/\/|mailto:)./i.test(href) ? href : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Today where the visitor is, as YYYY-MM-DD — never UTC; see the API's dayIn(). */
|
|
83
|
+
export function localDay(now = new Date()) {
|
|
84
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
85
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function inDate(a, on) {
|
|
89
|
+
if (a.startsOn && on < a.startsOn) return false;
|
|
90
|
+
if (a.endsOn && on > a.endsOn) return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function seenKey(a) {
|
|
95
|
+
return `${a.id}#${a.revision || 0}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The shape the widget draws, from whatever came over the wire. */
|
|
99
|
+
function cleanList(rows) {
|
|
100
|
+
return (Array.isArray(rows) ? rows : [])
|
|
101
|
+
.filter((a) => a && typeof a.id === 'string' && typeof a.title === 'string' && a.title.trim())
|
|
102
|
+
.map((a) => {
|
|
103
|
+
const placement = a.placement === 'launcher' ? 'launcher' : 'panel';
|
|
104
|
+
const href = safeHref(a.ctaHref);
|
|
105
|
+
return {
|
|
106
|
+
id: a.id,
|
|
107
|
+
revision: Number.isInteger(a.revision) ? a.revision : 0,
|
|
108
|
+
title: a.title,
|
|
109
|
+
body: typeof a.body === 'string' ? a.body : '',
|
|
110
|
+
tone: TONES.includes(a.tone) ? a.tone : 'info',
|
|
111
|
+
// Both halves, or neither, as the API saves them.
|
|
112
|
+
ctaLabel: href && typeof a.ctaLabel === 'string' ? a.ctaLabel : '',
|
|
113
|
+
ctaHref: href && typeof a.ctaLabel === 'string' ? href : '',
|
|
114
|
+
placement,
|
|
115
|
+
dismissible: placement === 'launcher' || a.dismissible !== false,
|
|
116
|
+
startsOn: typeof a.startsOn === 'string' ? a.startsOn : '',
|
|
117
|
+
endsOn: typeof a.endsOn === 'string' ? a.endsOn : '',
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readSeen() {
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(localStorage.getItem(SEEN_KEY) || '[]');
|
|
125
|
+
return Array.isArray(parsed) ? parsed.filter((k) => typeof k === 'string') : [];
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readCache(projectId) {
|
|
132
|
+
try {
|
|
133
|
+
const held = JSON.parse(localStorage.getItem(cacheKey(projectId)) || 'null');
|
|
134
|
+
if (!held || !Array.isArray(held.list) || typeof held.at !== 'number') return null;
|
|
135
|
+
// A clock moved backwards reads as fresh forever without the second half.
|
|
136
|
+
const age = Date.now() - held.at;
|
|
137
|
+
return age >= 0 && age < CACHE_MS ? held.list : null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function writeCache(projectId, list) {
|
|
144
|
+
try {
|
|
145
|
+
localStorage.setItem(cacheKey(projectId), JSON.stringify({ at: Date.now(), list }));
|
|
146
|
+
} catch { /* not kept — the next page asks again */ }
|
|
147
|
+
}
|
package/src/busy.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What somebody is told when the API refuses a request for coming too fast
|
|
3
|
+
* (HTTP 429).
|
|
4
|
+
*
|
|
5
|
+
* The API's own refusal is already written for a stranger — "Too many reports
|
|
6
|
+
* are being sent from this site at the moment. Please try again in a minute."
|
|
7
|
+
* — and is used wherever it arrived. This is for when it did not, such as a
|
|
8
|
+
* proxy answering 429 with a page of HTML: the person still reads a sentence
|
|
9
|
+
* saying that waiting will fix it, rather than "HTTP 429", which reads as
|
|
10
|
+
* something they broke.
|
|
11
|
+
*/
|
|
12
|
+
export const TOO_MANY_REQUESTS =
|
|
13
|
+
'Too many requests are being sent from here at the moment. Please try again in a minute.';
|
|
14
|
+
|
|
15
|
+
/** The sentence for a 429: the server's own when there was one. */
|
|
16
|
+
export function tooManyMessage(detail) {
|
|
17
|
+
return detail || TOO_MANY_REQUESTS;
|
|
18
|
+
}
|
package/src/call.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Everything a call needs, as one part that is fetched only when a call is
|
|
2
|
+
// offered, placed, or rings (lazy.js). Nothing is added here: the files it
|
|
3
|
+
// gathers stay where they are, because the dashboard imports live-call.js by
|
|
4
|
+
// that path to take the other end of the same call.
|
|
5
|
+
|
|
6
|
+
export { joinLiveCall, liveCallSupported, openMicrophone } from './live-call.js';
|
|
7
|
+
export { showIncomingCall } from './incoming-call.js';
|
package/src/capture.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// purpose: it would need a heavy dependency and would still silently miss
|
|
7
7
|
// cross-origin iframes, canvas and video, which is worse than not offering it.
|
|
8
8
|
|
|
9
|
+
import { tooManyMessage } from './busy.js';
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Pristine references, taken at module-evaluation time — which happens on
|
|
11
13
|
* import, before init() patches anything.
|
|
@@ -252,6 +254,13 @@ export async function uploadAttachment({ apiBase, apiKey, blob, mime, onProgress
|
|
|
252
254
|
headers: { 'Content-Type': 'application/json', 'X-BugCatch-Key': apiKey },
|
|
253
255
|
body: JSON.stringify({ mime: type }),
|
|
254
256
|
});
|
|
257
|
+
if (res.status === 429) {
|
|
258
|
+
// Uploading faster than the API allows. The server's sentence when it sent
|
|
259
|
+
// one; either way words, not a status code, because the widget shows this.
|
|
260
|
+
let detail = '';
|
|
261
|
+
try { detail = (await res.json())?.message || ''; } catch { /* not JSON */ }
|
|
262
|
+
throw new Error(`[BugCatch] ${tooManyMessage(typeof detail === 'string' ? detail : '')}`);
|
|
263
|
+
}
|
|
255
264
|
if (!res.ok) throw new Error(`[BugCatch] could not start the upload (HTTP ${res.status}).`);
|
|
256
265
|
const { key, uploadUrl } = await res.json();
|
|
257
266
|
|
package/src/chunks.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// What the two loaders (lazy.js, umd/lazy.js) share: how a part of the
|
|
2
|
+
// script-tag build that arrived on its own is found, and what a part that
|
|
3
|
+
// never arrived is called.
|
|
4
|
+
//
|
|
5
|
+
// The ES build does not use the registry: a bundler splits it at its import()
|
|
6
|
+
// calls and wires the pieces back together itself. A <script> tag has no such
|
|
7
|
+
// machinery, so each part of that build is a plain script that, when it runs,
|
|
8
|
+
// leaves its exports under a name on `window` — and umd/lazy.js, having added
|
|
9
|
+
// the script, reads them from there once it has loaded.
|
|
10
|
+
//
|
|
11
|
+
// The name carries the version, so a part left in a cache by an older release
|
|
12
|
+
// is never mistaken for this one's: it registers under its own version, this
|
|
13
|
+
// release finds nothing under its, and says so rather than running code that
|
|
14
|
+
// expects a different client.
|
|
15
|
+
|
|
16
|
+
// Replaced at build time from package.json; see vite.config.js.
|
|
17
|
+
const VERSION = typeof __SDK_VERSION__ === 'string' ? __SDK_VERSION__ : '0.0.0-dev';
|
|
18
|
+
|
|
19
|
+
const REGISTRY = '__bugcatchChunks';
|
|
20
|
+
|
|
21
|
+
const key = (name) => `${name}@${VERSION}`;
|
|
22
|
+
|
|
23
|
+
/** Called by a part as its script runs: here is what I export. */
|
|
24
|
+
export function registerChunk(name, exports) {
|
|
25
|
+
const registry = window[REGISTRY] || (window[REGISTRY] = {});
|
|
26
|
+
registry[key(name)] = exports;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What a part registered, once its script has run; undefined until then. */
|
|
30
|
+
export function registeredChunk(name) {
|
|
31
|
+
return typeof window !== 'undefined' ? window[REGISTRY]?.[key(name)] : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A part that could not be loaded, said twice: the details for whoever is
|
|
36
|
+
* debugging the page, in the console, and a sentence for the person using it,
|
|
37
|
+
* as the error — which is what the panel shows when a call cannot start.
|
|
38
|
+
*/
|
|
39
|
+
export function chunkUnavailable(detail, cause) {
|
|
40
|
+
// eslint-disable-next-line no-console
|
|
41
|
+
console.warn(`[BugCatch] ${detail}`, ...(cause ? [cause] : []));
|
|
42
|
+
const err = new Error('[BugCatch] part of the widget could not be loaded — check the connection and try again.');
|
|
43
|
+
if (cause) err.cause = cause;
|
|
44
|
+
return err;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** One fetch per part at a time; a failure is forgotten, so the next ask retries. */
|
|
48
|
+
export function once(load) {
|
|
49
|
+
let pending = null;
|
|
50
|
+
return (...args) => {
|
|
51
|
+
pending ||= load(...args).catch((err) => { pending = null; throw err; });
|
|
52
|
+
return pending;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { VERSION as CHUNK_VERSION };
|
package/src/gesture.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Whether anybody has touched this page yet — for the incoming-call card,
|
|
2
|
+
// which may only make a sound once somebody has.
|
|
3
|
+
//
|
|
4
|
+
// Tracked rather than asked, because there is nothing to ask: a browser will
|
|
5
|
+
// tell you an AudioContext is suspended, but only after you have built one —
|
|
6
|
+
// and building one on a page with no interaction is what writes
|
|
7
|
+
//
|
|
8
|
+
// The AudioContext was not allowed to start. It must be resumed (or
|
|
9
|
+
// created) after a user gesture on the page.
|
|
10
|
+
//
|
|
11
|
+
// into the console. This SDK runs on somebody else's site, and that console
|
|
12
|
+
// belongs to whoever is trying to debug their own product. A warning we cannot
|
|
13
|
+
// act on, for a sound that was never going to play, is not ours to put there.
|
|
14
|
+
//
|
|
15
|
+
// Here, in the script every page loads, rather than beside the card: the card's
|
|
16
|
+
// code is fetched when a call rings (lazy.js), and listeners added then would
|
|
17
|
+
// have missed every click that came before it.
|
|
18
|
+
|
|
19
|
+
let touched = false;
|
|
20
|
+
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
21
|
+
for (const evt of ['pointerdown', 'keydown', 'touchstart']) {
|
|
22
|
+
// Capture, and it matters more here than it would in our own app: this runs
|
|
23
|
+
// on somebody else's page, whose handlers we have never seen and one of
|
|
24
|
+
// which may well stop a click before it reaches the window.
|
|
25
|
+
window.addEventListener(evt, () => { touched = true; }, {
|
|
26
|
+
once: true, capture: true, passive: true,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function pageTouched() {
|
|
32
|
+
return touched;
|
|
33
|
+
}
|
package/src/icons.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The handful of glyphs the launcher draws before the panel exists: its own
|
|
2
|
+
// two, the theme switch's three, what the load-failure card needs, and the
|
|
3
|
+
// tones of an announcement beside the button (notice.js). Every other icon
|
|
4
|
+
// lives with the panel (panel/icons.js), which re-exports these.
|
|
5
|
+
//
|
|
6
|
+
// One export per glyph rather than one object, so a bundle carries only the
|
|
7
|
+
// ones something draws — imported as `* as ICONS` and read as ICONS.chat.
|
|
8
|
+
|
|
9
|
+
export const chat = '<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>';
|
|
10
|
+
export const chevronDown = '<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>';
|
|
11
|
+
export const sun = '<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>';
|
|
12
|
+
export const moon = '<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>';
|
|
13
|
+
export const auto = '<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>';
|
|
14
|
+
export const alert = '<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>';
|
|
15
|
+
export const close = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>';
|
|
16
|
+
export const check = '<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>';
|
|
17
|
+
export const info = '<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>';
|
|
18
|
+
export const warn = '<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>';
|
package/src/identity.js
CHANGED
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
* signed in and which loan is on screen. Passed as `loanId`, `applicationId` or
|
|
12
12
|
* `customerNumber`, the API reads them as identifiers the app itself asserted
|
|
13
13
|
* (reference source `identify`), which is what lets bugcatch look the customer
|
|
14
|
-
* up in the lender's own system over MCP without asking the reporter
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* up in the lender's own system over MCP without asking the reporter. They are
|
|
15
|
+
* a lookup, not proof: the widget key is public, so the Sphoro Agent treats the
|
|
16
|
+
* customer as proved only when the host's server signed the `id` (`userHash`,
|
|
17
|
+
* checked by the API against the project's identity secret) or the reporter
|
|
18
|
+
* verified their email with a code. Passed under any other name they are still
|
|
19
|
+
* sent, but only as metadata a person can read.
|
|
17
20
|
*
|
|
18
21
|
* WHERE THEY GO. The signed-in user's fields travel as `metadata.user`, which is
|
|
19
|
-
* where the API has always read `id` and
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* where the API has always read `id` (and `userHash`). The current screen
|
|
23
|
+
* travels as `metadata.context`. Both shapes are mirrored by
|
|
24
|
+
* bugcatch-api/src/settings/reference-extract.ts identifyReferences.
|
|
22
25
|
*/
|
|
23
26
|
|
|
24
27
|
/** Longest identifier the API keeps; anything longer is dropped there too. */
|
|
@@ -98,8 +101,9 @@ const RESERVED_USER = new Set(['id', 'userId', ...USER_TEXT, ...Object.values(ID
|
|
|
98
101
|
* id string | number the customer's own account id in your system
|
|
99
102
|
* email string threads their reports; where "resolved" mail goes
|
|
100
103
|
* name string
|
|
101
|
-
* phone string | number
|
|
102
|
-
*
|
|
104
|
+
* phone string | number for your team to read; never proof of who they are
|
|
105
|
+
* userHash string HMAC-SHA256(identity secret, id), made on your
|
|
106
|
+
* server — what makes `id` proof rather than a claim
|
|
103
107
|
* loanId string | number (alias loanNumber)
|
|
104
108
|
* applicationId string | number
|
|
105
109
|
* customerNumber string | number (alias customerId)
|
|
@@ -202,7 +206,8 @@ export const KNOWN_OPTIONS = [
|
|
|
202
206
|
'attachments', 'maxAttachments', 'maxAttachmentBytes', 'maxRecordingMs', 'beforeSend',
|
|
203
207
|
'user', 'context', 'updates', 'updateToast', 'onUpdate', 'voice', 'onCallState',
|
|
204
208
|
'onIncomingCall', 'onReportSubmitted', 'onAssistantPhase', 'onError', 'debug',
|
|
205
|
-
'drafts', 'knownIssues', 'release', 'environment', 'buildId',
|
|
209
|
+
'drafts', 'knownIssues', 'agentIssues', 'release', 'environment', 'buildId',
|
|
210
|
+
'chunkBase', 'announcements',
|
|
206
211
|
];
|
|
207
212
|
|
|
208
213
|
const CALLBACK_OPTIONS = [
|
package/src/incoming-call.js
CHANGED
|
@@ -53,32 +53,6 @@ const RING = {
|
|
|
53
53
|
],
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
/**
|
|
57
|
-
* Whether anybody has touched this page yet.
|
|
58
|
-
*
|
|
59
|
-
* Tracked rather than asked, because there is nothing to ask: a browser will
|
|
60
|
-
* tell you an AudioContext is suspended, but only after you have built one —
|
|
61
|
-
* and building one on a page with no interaction is what writes
|
|
62
|
-
*
|
|
63
|
-
* The AudioContext was not allowed to start. It must be resumed (or
|
|
64
|
-
* created) after a user gesture on the page.
|
|
65
|
-
*
|
|
66
|
-
* into the console. This SDK runs on somebody else's site, and that console
|
|
67
|
-
* belongs to whoever is trying to debug their own product. A warning we cannot
|
|
68
|
-
* act on, for a sound that was never going to play, is not ours to put there.
|
|
69
|
-
*/
|
|
70
|
-
let touched = false;
|
|
71
|
-
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
72
|
-
for (const evt of ['pointerdown', 'keydown', 'touchstart']) {
|
|
73
|
-
// Capture, and it matters more here than it would in our own app: this runs
|
|
74
|
-
// on somebody else's page, whose handlers we have never seen and one of
|
|
75
|
-
// which may well stop a click before it reaches the window.
|
|
76
|
-
window.addEventListener(evt, () => { touched = true; }, {
|
|
77
|
-
once: true, capture: true, passive: true,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
56
|
/**
|
|
83
57
|
* The same question asked of the browser rather than of ourselves.
|
|
84
58
|
*
|
|
@@ -104,8 +78,8 @@ function activatedByBrowser() {
|
|
|
104
78
|
* permission, and the correct behaviour there is a card that appears in silence
|
|
105
79
|
* rather than no card.
|
|
106
80
|
*/
|
|
107
|
-
function startRinging() {
|
|
108
|
-
if (!touched && !activatedByBrowser()) return () => {};
|
|
81
|
+
function startRinging(touched) {
|
|
82
|
+
if (!touched() && !activatedByBrowser()) return () => {};
|
|
109
83
|
|
|
110
84
|
const Ctx = typeof window !== 'undefined' && (window.AudioContext || window.webkitAudioContext);
|
|
111
85
|
if (!Ctx) return () => {};
|
|
@@ -199,12 +173,16 @@ const BUTTON = [
|
|
|
199
173
|
* @param {() => void} opts.onAnswer Pressed Answer.
|
|
200
174
|
* @param {() => void} opts.onDecline Pressed Decline.
|
|
201
175
|
* @param {() => void} [opts.onExpire] Nobody pressed anything in time.
|
|
176
|
+
* @param {() => boolean} [opts.touched] Whether anybody has touched the page
|
|
177
|
+
* yet, which is when a sound may be made (gesture.js). Asked of the caller
|
|
178
|
+
* because this file is fetched when a call rings, and could not have been
|
|
179
|
+
* listening for the clicks that came before.
|
|
202
180
|
* @returns {(reason?: string) => void} Take it away. Called with no reason on
|
|
203
181
|
* an ordinary dismissal; the caller passes one only for its own logging.
|
|
204
182
|
*/
|
|
205
183
|
export function showIncomingCall({
|
|
206
184
|
call, accent = '#5b6ef5', position = 'bottom-right',
|
|
207
|
-
onAnswer, onDecline, onExpire,
|
|
185
|
+
onAnswer, onDecline, onExpire, touched = () => false,
|
|
208
186
|
}) {
|
|
209
187
|
if (typeof document === 'undefined' || !document.body) return () => {};
|
|
210
188
|
|
|
@@ -273,7 +251,7 @@ export function showIncomingCall({
|
|
|
273
251
|
el.append(actions);
|
|
274
252
|
document.body.appendChild(el);
|
|
275
253
|
|
|
276
|
-
const stopRinging = startRinging();
|
|
254
|
+
const stopRinging = startRinging(touched);
|
|
277
255
|
let closed = false;
|
|
278
256
|
let ticker = null;
|
|
279
257
|
|
package/src/index.d.ts
CHANGED
|
@@ -117,6 +117,31 @@ export interface JoinedIssue {
|
|
|
117
117
|
watchToken?: string | null;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Something the project's team is telling the people using its widget, written
|
|
122
|
+
* on the Widget announcements settings page. Already narrowed to the visitor's
|
|
123
|
+
* own day and to what this browser has not closed.
|
|
124
|
+
*/
|
|
125
|
+
export interface Announcement {
|
|
126
|
+
id: string;
|
|
127
|
+
/** Bumped by "Show it again", which is what reopens it for people who closed it. */
|
|
128
|
+
revision: number;
|
|
129
|
+
title: string;
|
|
130
|
+
/** '' when there is none. */
|
|
131
|
+
body: string;
|
|
132
|
+
/** Picks the colour and the icon; the site's accent is 'info'. */
|
|
133
|
+
tone: 'info' | 'success' | 'warning' | 'critical';
|
|
134
|
+
/** Both, or both ''. A path is a page on your own site. */
|
|
135
|
+
ctaLabel: string;
|
|
136
|
+
ctaHref: string;
|
|
137
|
+
/** 'launcher' is also shown beside the button, and is always dismissible. */
|
|
138
|
+
placement: 'panel' | 'launcher';
|
|
139
|
+
dismissible: boolean;
|
|
140
|
+
/** YYYY-MM-DD, inclusive, or ''. */
|
|
141
|
+
startsOn: string;
|
|
142
|
+
endsOn: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
120
145
|
export interface BugCatchConfig {
|
|
121
146
|
/**
|
|
122
147
|
* Log what the SDK does — identify, context, each report's shape — to the
|
|
@@ -137,6 +162,16 @@ export interface BugCatchConfig {
|
|
|
137
162
|
onError?: ((event: BugCatchErrorEvent) => void) | null;
|
|
138
163
|
/** Recover unfinished widget reports on this device for seven days. Default true. */
|
|
139
164
|
drafts?: boolean;
|
|
165
|
+
/**
|
|
166
|
+
* Where the script-tag build fetches the rest of itself from: the folder
|
|
167
|
+
* holding bugcatch-panel.umd.js and bugcatch-call.umd.js, which it loads the
|
|
168
|
+
* first time the report panel opens or a call is placed. By default the
|
|
169
|
+
* folder bugcatch.umd.js itself was loaded from — set this only when that
|
|
170
|
+
* cannot be told (the bundle inlined into the page, or its dist/ files served
|
|
171
|
+
* from somewhere else). A relative URL is read against the page. Ignored by
|
|
172
|
+
* the ES build, whose chunks are the bundler's to place.
|
|
173
|
+
*/
|
|
174
|
+
chunkBase?: string | null;
|
|
140
175
|
/** Your app's version, attached to every report. */
|
|
141
176
|
release?: string;
|
|
142
177
|
/** e.g. 'production' / 'staging', attached to every report. */
|
|
@@ -172,6 +207,13 @@ export interface BugCatchConfig {
|
|
|
172
207
|
* the feature on and somebody lists an individual report.
|
|
173
208
|
*/
|
|
174
209
|
knownIssues?: boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Draw what the project's team is telling visitors: at the top of the panel,
|
|
212
|
+
* and beside the button for the ones written for that. Default true — and
|
|
213
|
+
* nothing is drawn until the project publishes one. false draws nothing;
|
|
214
|
+
* announcements() still answers, for a site that shows them its own way.
|
|
215
|
+
*/
|
|
216
|
+
announcements?: boolean;
|
|
175
217
|
/** Default 5. */
|
|
176
218
|
maxAttachments?: number;
|
|
177
219
|
/** Default 25 MB. */
|
|
@@ -813,6 +855,14 @@ export declare class BugCatchClient {
|
|
|
813
855
|
* draw nothing rather than an empty heading.
|
|
814
856
|
*/
|
|
815
857
|
knownIssues(): Promise<KnownIssue[]>;
|
|
858
|
+
/**
|
|
859
|
+
* What this project's team is telling the people using its widget, for a
|
|
860
|
+
* site drawing its own UI (`widget: false` or `announcements: false`). A
|
|
861
|
+
* browser asks at most every five minutes. Empty on every failure.
|
|
862
|
+
*/
|
|
863
|
+
announcements(): Promise<Announcement[]>;
|
|
864
|
+
/** Stop showing one in this browser until the team uses "Show it again". */
|
|
865
|
+
dismissAnnouncement(announcement: Announcement): void;
|
|
816
866
|
/**
|
|
817
867
|
* The known issues that look like what somebody is describing.
|
|
818
868
|
*
|