@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.
Files changed (52) hide show
  1. package/README.md +92 -5
  2. package/dist/bugcatch-call.es.js +586 -0
  3. package/dist/bugcatch-call.es.js.map +1 -0
  4. package/dist/bugcatch-call.umd.js +2 -0
  5. package/dist/bugcatch-call.umd.js.map +1 -0
  6. package/dist/bugcatch-core.es.js +3214 -0
  7. package/dist/bugcatch-core.es.js.map +1 -0
  8. package/dist/bugcatch-panel.es.js +2402 -0
  9. package/dist/bugcatch-panel.es.js.map +1 -0
  10. package/dist/bugcatch-panel.umd.js +1240 -0
  11. package/dist/bugcatch-panel.umd.js.map +1 -0
  12. package/dist/bugcatch.es.js +2 -5522
  13. package/dist/bugcatch.es.js.map +1 -1
  14. package/dist/bugcatch.standalone.umd.js +1536 -0
  15. package/dist/bugcatch.standalone.umd.js.map +1 -0
  16. package/dist/bugcatch.umd.js +89 -1183
  17. package/dist/bugcatch.umd.js.map +1 -1
  18. package/package.json +6 -6
  19. package/src/agent-issues.js +30 -0
  20. package/src/announcements.js +147 -0
  21. package/src/busy.js +18 -0
  22. package/src/call.js +7 -0
  23. package/src/capture.js +9 -0
  24. package/src/chunks.js +56 -0
  25. package/src/gesture.js +33 -0
  26. package/src/icons.js +18 -0
  27. package/src/identity.js +14 -9
  28. package/src/incoming-call.js +8 -30
  29. package/src/index.d.ts +50 -0
  30. package/src/index.js +170 -37
  31. package/src/lazy.js +23 -0
  32. package/src/notice.js +68 -0
  33. package/src/panel/call.js +242 -0
  34. package/src/panel/compose.js +416 -0
  35. package/src/panel/composer.js +235 -0
  36. package/src/panel/icons.js +34 -0
  37. package/src/panel/index.js +251 -0
  38. package/src/panel/known.js +130 -0
  39. package/src/panel/messages.js +176 -0
  40. package/src/panel/notices.js +34 -0
  41. package/src/panel/picks.js +61 -0
  42. package/src/panel/refs.js +321 -0
  43. package/src/panel/render.js +124 -0
  44. package/src/panel/signin.js +228 -0
  45. package/src/panel/styles.js +960 -0
  46. package/src/panel/thread.js +384 -0
  47. package/src/session.js +8 -3
  48. package/src/umd/call.js +6 -0
  49. package/src/umd/lazy.js +98 -0
  50. package/src/umd/panel.js +6 -0
  51. package/src/updates.js +20 -7
  52. 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.16.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/bugcatch.umd.js": "./dist/bugcatch.umd.js",
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": "vite build",
28
- "dev": "vite build --watch",
29
- "prepublishOnly": "vite build"
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 — and lets
15
- * that lookup count as a trusted join. Passed under any other name they are
16
- * still sent, but only as metadata a person can read.
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 where the ownership check reads `phone`
20
- * and `email`. The current screen travels as `metadata.context`. Both shapes are
21
- * mirrored by bugcatch-api/src/settings/reference-extract.ts identifyReferences.
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 used, with email, to confirm the reporter owns
102
- * the record bugcatch looked up
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 = [
@@ -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
  *