@sphoro/bugcatch-web-sdk 0.14.4 → 0.16.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 CHANGED
@@ -1,11 +1,38 @@
1
1
  # @sphoro/bugcatch-web-sdk
2
2
 
3
- A ~4 kB browser SDK that reports bugs from your site with the context you'd otherwise
4
- have to ask for: console output, network activity, unhandled errors and environment.
3
+ A browser SDK that lets your customers report a problem from inside your app, and
4
+ attaches what you'd otherwise have to ask them for: console output, network
5
+ activity, unhandled errors, the environment, and — when you tell it — who the
6
+ customer is and which loan, application or account they were looking at.
7
+
8
+ No extension, no dependencies. The widget renders in a shadow root, so its CSS and
9
+ yours never collide.
10
+
11
+ - [Quick start](#quick-start)
12
+ - [Tell BugCatch who the customer is](#tell-bugcatch-who-the-customer-is) — with loan / customer IDs
13
+ - [Context for the current screen](#context-for-the-current-screen)
14
+ - [A lending app, end to end](#a-lending-app-end-to-end)
15
+ - [Events](#events)
16
+ - [Reporting and opening from your own UI](#reporting-and-opening-from-your-own-ui)
17
+ - [The AI assistant](#the-ai-assistant)
18
+ - [Privacy: what is captured and what is not](#privacy-what-is-captured-and-what-is-not)
19
+ - [Options](#options)
20
+ - [API at a glance](#api-at-a-glance)
21
+ - [Troubleshooting](#troubleshooting)
22
+ - [Reference](#reference) — attachments, sign-in, live updates, replies, known issues, calls
23
+
24
+ ## Quick start
25
+
26
+ You need two values from BugCatch:
27
+
28
+ | Value | Where to find it |
29
+ |---|---|
30
+ | `apiKey` | Workspace Settings → API Keys (`bc_live_…`; shown once when created) |
31
+ | `projectId` | Project Settings → Web SDK (the snippet there is pre-filled) |
5
32
 
6
- No extension required. No dependencies.
33
+ and the URL of your BugCatch API (`apiBase`).
7
34
 
8
- ## Install
35
+ **npm / bundler**
9
36
 
10
37
  ```bash
11
38
  npm install @sphoro/bugcatch-web-sdk
@@ -15,55 +42,380 @@ npm install @sphoro/bugcatch-web-sdk
15
42
  import BugCatch from '@sphoro/bugcatch-web-sdk';
16
43
 
17
44
  BugCatch.init({
18
- apiBase: 'https://api.yourdomain.com',
19
- apiKey: 'bc_live_…', // Workspace Settings → API Keys
20
- projectId: '…', // Project Settings → General
45
+ apiBase: 'https://api.bugcatch.yourdomain.com',
46
+ apiKey: 'bc_live_…',
47
+ projectId: '…',
48
+ release: '4.12.0', // optional, shown on every report
49
+ environment: 'production', // optional
21
50
  });
22
51
  ```
23
52
 
24
- Or with a script tag:
53
+ **Script tag**
25
54
 
26
55
  ```html
27
56
  <!-- Pin the version. Without one the CDN serves whatever is latest, so a
28
57
  future release changes behaviour on a site nobody redeployed. -->
29
- <script src="https://unpkg.com/@sphoro/bugcatch-web-sdk@0.13.1/dist/bugcatch.umd.js"></script>
58
+ <script src="https://unpkg.com/@sphoro/bugcatch-web-sdk@0.16.0/dist/bugcatch.umd.js"></script>
30
59
  <script>
31
- BugCatch.init({ apiBase: '…', apiKey: '…', projectId: '…' });
60
+ BugCatch.init({ apiBase: '…', apiKey: 'bc_live_…', projectId: '…' });
32
61
  </script>
33
62
  ```
34
63
 
35
- That's it — a floating **Report a bug** button appears, and whatever the page was
36
- doing gets attached to the report.
64
+ A floating **Report a bug** button appears, and whatever the page was doing is
65
+ attached to each report. Types ship with the package (`src/index.d.ts`), so
66
+ editors autocomplete every option and method below.
67
+
68
+ Call `init()` once, as early as you can — console and network capture start at
69
+ `init()`, so anything before it is not recorded. `identify()` and `setContext()`
70
+ may be called before or after it.
71
+
72
+ ## Tell BugCatch who the customer is
73
+
74
+ ```js
75
+ BugCatch.identify({
76
+ id: customer.id, // your customer's own account id
77
+ email: customer.email,
78
+ name: customer.fullName,
79
+ phone: customer.mobile, // include the country code: '+919876543210'
80
+ customerNumber: customer.number, // your customer / client number
81
+ loanId: customer.activeLoanId, // a loan they hold
82
+ applicationId: customer.appId, // a loan application
83
+ });
84
+
85
+ // On sign-out
86
+ BugCatch.identify(null);
87
+ BugCatch.setContext(null);
88
+ BugCatch.forgetReports(); // so the next person on this device doesn't see their updates
89
+ ```
90
+
91
+ Every field is optional; send what you have. What each one does:
92
+
93
+ | Field | Type | What BugCatch does with it |
94
+ |---|---|---|
95
+ | `id` | string \| number | The customer's account id in **your** system. Used to look them up in your connected data source (as `userId`). Never pass a staff or agent id here. |
96
+ | `email` | string | Threads their reports together in the dashboard, and is where the "your issue is resolved" email goes. Treated as reachable because your app authenticated them. Needed for live updates and calls. |
97
+ | `name` | string | Shown instead of a bare address. |
98
+ | `phone` | string \| number | With `email`, compared against the record BugCatch looked up, to confirm the reporter really is that customer before anything is done on their behalf. |
99
+ | `loanId` | string \| number | Looked up as a **loan** (`loanId`). Alias: `loanNumber`. |
100
+ | `applicationId` | string \| number | Looked up as a **loan application** (`loanId`). |
101
+ | `customerNumber` | string \| number | Looked up as a **customer** (`customerNumber`). Alias: `customerId`. |
102
+ | anything else | primitive or small object | Kept as readable metadata on the report. Not used for lookups. |
103
+
104
+ **Why the IDs matter.** When your project has a data source connected (MCP), the
105
+ AI assistant looks the customer up *before* it reasons about the report. IDs your
106
+ app passes are **trusted identifiers**: your application asserted them, so the
107
+ assistant uses them directly instead of asking the customer "what is your loan
108
+ number?", and a record found by one of them counts as belonging to the reporter.
109
+ IDs a customer types into the report are still read — but as hints, not facts.
110
+
111
+ Rules the SDK enforces (with a console warning, never an exception):
112
+
113
+ - Identifiers must be strings or numbers, at most 120 characters, with no control
114
+ characters. Anything else is dropped — a truncated loan number is a different loan.
115
+ - An object, array or function passed as an identifier is dropped.
116
+ - `identify('u_42')` (not an object) is ignored and keeps the previous user.
117
+ Only `identify(null)` signs out.
118
+ - Extra fields: up to 30, strings capped at 500 characters, nested objects at 1 kB of JSON.
119
+
120
+ ## Context for the current screen
121
+
122
+ `identify()` says who the customer is. `setContext()` says what they are looking
123
+ at — so a report filed from the repayment screen of one loan carries that loan,
124
+ even when the customer holds three.
125
+
126
+ ```js
127
+ // Entering a loan's screen
128
+ BugCatch.setContext({ loanId: loan.id, screen: 'repayment' });
129
+
130
+ // Leaving it — null removes one key
131
+ BugCatch.setContext({ loanId: null });
132
+
133
+ // Clear everything
134
+ BugCatch.setContext(null);
135
+
136
+ BugCatch.getContext(); // { screen: 'repayment' }
137
+ ```
138
+
139
+ - Merged over what was set before, and kept until changed, for the life of the
140
+ page (not across reloads — call it again when the screen mounts).
141
+ - `loanId`, `applicationId` and `customerNumber` are validated and trusted exactly
142
+ as on `identify()`, and **outrank** the same field there: if the customer holds
143
+ loan A but files from loan B's screen, B is what gets looked up.
144
+ - Anything else (`screen`, `step`, `route`, a feature flag) is readable metadata.
145
+ - For a single report, pass `context` to `report()` instead — it is merged for
146
+ that report only and does not change what `setContext()` holds.
147
+
148
+ In a single-page app, the natural place is your router:
149
+
150
+ ```js
151
+ router.afterEach((to) => {
152
+ BugCatch.setContext({
153
+ screen: to.name,
154
+ loanId: to.params.loanId ?? null,
155
+ applicationId: to.params.applicationId ?? null,
156
+ });
157
+ });
158
+ ```
159
+
160
+ ## A lending app, end to end
161
+
162
+ ```js
163
+ import BugCatch from '@sphoro/bugcatch-web-sdk';
164
+
165
+ BugCatch.init({
166
+ apiBase: 'https://bugcatch-api.example.com',
167
+ apiKey: 'bc_live_…',
168
+ projectId: '…',
169
+ release: APP_VERSION,
170
+ environment: 'production',
171
+ onReportSubmitted: ({ bugId }) => analytics.track('bug_reported', { bugId }),
172
+ onAssistantPhase: ({ phase }) => {
173
+ if (phase === 'needs_info') showBadge('We need one more detail from you');
174
+ },
175
+ });
176
+
177
+ auth.onSignIn((customer) => {
178
+ BugCatch.identify({
179
+ id: customer.userId, // e.g. 'dc046b84-3314-46ae-9b8f-526d3e50bc50'
180
+ email: customer.email,
181
+ name: customer.name,
182
+ phone: customer.mobile, // '+919876543210'
183
+ customerNumber: customer.customerNumber, // e.g. 'C1234567'
184
+ });
185
+ });
186
+
187
+ auth.onSignOut(() => {
188
+ BugCatch.identify(null);
189
+ BugCatch.setContext(null);
190
+ BugCatch.forgetReports();
191
+ });
192
+
193
+ // Loan details / repayment / EMI screens
194
+ function onLoanScreen(loan) {
195
+ BugCatch.setContext({ loanId: loan.formattedLoanId, screen: 'loan-details' }); // 'L26090028459'
196
+ }
197
+ ```
198
+
199
+ **How the IDs travel.**
200
+
201
+ 1. The SDK sends `identify()` fields as `metadata.user` and `setContext()` as
202
+ `metadata.context` on every report (and on "me too" joins).
203
+ 2. The API reads them — only on the SDK's authenticated route, never from a public
204
+ recording link — and stores them as references with source `identify`:
205
+ `id` → `identify_user_id`, `loanId` → `auto_loan`, `applicationId` →
206
+ `auto_application`, `customerNumber` → `auto_customer`.
207
+ 3. The analysis looks each one up as what it is (`loanId`, `customerNumber`,
208
+ `userId`, loans first) through your data source's tools, and marks the join
209
+ trusted only when the record it found came from one of these IDs.
210
+ 4. `phone` and `email` from `identify()` are what the ownership check compares
211
+ with the record, before the assistant acts for the customer.
212
+
213
+ ## Events
214
+
215
+ Every event is available two ways: as a single callback in `init()`, or through
216
+ `on()`, which several parts of your app can use independently. `on()` returns an
217
+ unsubscribe function. A handler that throws is logged and never breaks the SDK.
218
+
219
+ | `on(…)` event | `init` option | Payload | When |
220
+ |---|---|---|---|
221
+ | `'report'` | `onReportSubmitted` | `{ bugId, title, via: 'form' \| 'call', result }` | A report was accepted — from the widget, `report()`, or pressing Call. |
222
+ | `'assistant'` | `onAssistantPhase` | `{ bugId, phase, title, detail }` | The AI assistant working a report changed phase — see [The AI assistant](#the-ai-assistant). |
223
+ | `'error'` | `onError` | `{ action: 'report' \| 'call', error }` | A report or call failed. `error.status` is set for an HTTP refusal. |
224
+ | `'update'` | `onUpdate` | `ReportUpdate` | A report filed from this browser changed (status, reply, …). |
225
+ | `'unread'` | — | `{ count, updates }` | The unread count changed — for your own bell. |
226
+ | `'call'` | `onCallState` | `{ bugId, state, … }` | A call moved: `ringing`, `connecting`, `connected`, `missed`, `ended`, `failed`. |
227
+ | `'incoming-call'` | `onIncomingCall` | `{ bugId, callId, state }` | The team is ringing this page. |
228
+ | `'signin'` / `'signout'` | — | `{ email, name }` | The reporter signed in or out through the widget. |
229
+
230
+ ```js
231
+ const off = BugCatch.on('report', ({ bugId, via }) => {
232
+ console.log(`filed ${bugId} via ${via}`);
233
+ });
234
+ off(); // stop listening
235
+ ```
236
+
237
+ ## Reporting and opening from your own UI
238
+
239
+ Open the built-in panel from your own menu item:
240
+
241
+ ```js
242
+ helpMenu.onReportProblem = () => BugCatch.open();
243
+ BugCatch.close();
244
+ BugCatch.isOpen();
245
+ ```
246
+
247
+ Or turn the widget off and file reports yourself — everything captured is still
248
+ attached:
249
+
250
+ ```js
251
+ BugCatch.init({ apiKey, projectId, apiBase, widget: false });
252
+
253
+ const result = await BugCatch.report({
254
+ title: 'EMI paid but still showing due', // required
255
+ description: 'Paid ₹4,210 by UPI this morning',
256
+ priority: 'HIGH', // LOW | MEDIUM | HIGH | CRITICAL
257
+ context: { loanId: 'L26090028459' }, // this report only
258
+ references: [{ key: 'utr', value: '4321…' }], // what the customer typed, if you asked
259
+ metadata: { paymentMethod: 'upi' },
260
+ });
261
+ // → { id, redactionsApplied, watchToken?, estimate? } or { cancelled: true } from beforeSend
262
+ ```
263
+
264
+ `report()` rejects with an `Error` whose message says what went wrong
265
+ (`[BugCatch] report failed (HTTP 401): the apiKey was not accepted`), and emits
266
+ `'error'`. Malformed `references` entries are dropped rather than failing the report.
267
+
268
+ ## The AI assistant
269
+
270
+ When your workspace has AI analysis on, every report is investigated as soon as
271
+ it arrives: the assistant reads the report, the captured logs and — with a
272
+ connected data source — the customer's own records, looked up by the IDs above.
273
+ After a report is filed, the widget opens its conversation, where the reporter
274
+ watches progress and answers questions.
275
+
276
+ | Phase | What the reporter sees |
277
+ |---|---|
278
+ | `analyzing` | "Analyzing the issue…" — reading the report, the account and the logs. |
279
+ | `needs_info` | It asked a question (often: which loan?) and is waiting for the reporter's reply. |
280
+ | `confirming` | It found the likely cause and asks the reporter to confirm. |
281
+ | `resolving` | The cause is confirmed, or it is being handled. |
282
+ | `reviewing` | Handed to your team, who will reply in the thread. |
283
+ | `resolved` | Settled. |
284
+
285
+ The phase is known when a report is fetched: the widget does this every few
286
+ seconds while its thread is open, and `BugCatch.myReport(bugId)` does it for your
287
+ own view (it also returns `report.assistant`). Each change fires
288
+ `onAssistantPhase` / `'assistant'` once.
289
+
290
+ **Passing IDs is the biggest thing you can do for it.** Without them, the first
291
+ thing the assistant usually has to do is ask the customer which account or loan
292
+ they mean — the `needs_info` step — and wait for an answer.
293
+
294
+ ## Privacy: what is captured and what is not
295
+
296
+ Captured, and only after `init()`:
297
+
298
+ - **Console** — the last 100 entries (level, message, time), plus uncaught errors
299
+ and unhandled promise rejections. Whatever your app logs is captured, so do not
300
+ log tokens or personal data — or drop those entries in `beforeSend`.
301
+ - **Network** — the last 50 `fetch`/XHR calls: URL, method, status, duration.
302
+ **Request and response bodies are never read.** Credentials in URLs (API keys,
303
+ tokens, signatures, JWTs, `user:password@`) are replaced with `[REDACTED]` before
304
+ the entry is even buffered.
305
+ - **Environment** — browser, OS, language, viewport and screen, timezone,
306
+ connection type, page title and referrer.
307
+ - **The page URL** when the report is filed (credential-scrubbed the same way).
308
+ - **Only what you pass** to `identify()`, `setContext()` and `report({ metadata })`.
309
+ The SDK never reads your cookies, your app's storage or what is typed into your forms.
310
+ - **Screenshots and recordings** only when the reporter chooses to take one; the
311
+ browser always shows its own permission prompt.
312
+
313
+ On top of that:
314
+
315
+ - `beforeSend(payload)` runs in the browser before anything leaves it — mutate the
316
+ payload or return `false` to cancel.
317
+ - Your workspace's redaction rules (Workspace Settings → Security) run again on the
318
+ server before anything is stored; encrypted workspaces seal it at rest.
319
+ - `captureConsole`, `captureNetwork` and `captureErrors` can each be switched off.
320
+ - The SDK stores in the reporter's own browser only: a watch token for their reports,
321
+ unread update notices, unfinished drafts (7 days), and a widget sign-in if used.
322
+ `forgetReports()` clears the first two.
37
323
 
38
324
  ## Options
39
325
 
40
326
  | Option | Default | What it does |
41
327
  |---|---|---|
42
- | `apiKey` | *required* | Workspace API key. |
43
- | `projectId` | *required* | Project the bugs land in. |
44
- | `apiBase` | `http://localhost:3000` | Where the API lives. |
45
- | `widget` | `true` | Floating launcher. Set `false` to drive it from your own UI. |
328
+ | `apiKey` | *required* | Workspace API key (`bc_live_…`). |
329
+ | `projectId` | *required* | Project the reports land in. |
330
+ | `apiBase` | `http://localhost:3000` | Your BugCatch API URL. **Set it everywhere except local development.** A trailing slash is removed. |
331
+ | `user` | `null` | Same as calling `identify()` right after `init()`. |
332
+ | `context` | `null` | Same as calling `setContext()` right after `init()`. |
333
+ | `release` / `environment` / `buildId` | — | Attached to every report. |
334
+ | `debug` | `false` | Log identify, context and each report's shape to the console. |
335
+ | `onReportSubmitted` | — | See [Events](#events). |
336
+ | `onAssistantPhase` | — | See [Events](#events). |
337
+ | `onError` | — | See [Events](#events). |
338
+ | `widget` | `true` | Floating launcher and panel. `false` to drive everything from your own UI. |
46
339
  | `widgetPosition` | `'bottom-right'` | `bottom-left`, `top-right`, `top-left`. |
47
- | `captureConsole` | `true` | Wrap `console.*`. |
48
- | `captureNetwork` | `true` | Wrap `fetch` and `XMLHttpRequest`. |
49
- | `captureErrors` | `true` | Listen for `error` and `unhandledrejection`. |
50
- | `consoleLimit` | `100` | Console entries retained. |
51
- | `networkLimit` | `50` | Network entries retained. |
52
- | `theme` | `'auto'` | Starting theme: `auto` follows the visitor's system setting; `light` / `dark` pin it. A light/dark/auto switch sits above the launcher, and once the visitor uses it their choice is remembered in `localStorage` and wins over this value on later visits. |
53
- | `accent` | `'#4F46E5'` | Any CSS colour. Used for the launcher, focus rings and the primary button. |
54
- | `attachments` | `true` | Offer screenshot, screen recording and file attach. |
55
- | `knownIssues` | `true` | Show what this project already knows is broken above the form, and offer "this is happening to me too" instead of a second report. Draws nothing until the project switches the feature on and somebody lists an individual report — see [What is already known](#what-is-already-known). |
56
- | `maxAttachments` | `5` | Per report. |
57
- | `maxAttachmentBytes` | `25 MB` | Per file. |
58
- | `maxRecordingMs` | `120000` | Hard cap on a single recording. |
340
+ | `theme` | `'auto'` | `auto` follows the system; `light` / `dark` pin it. A visitor's own choice in the widget wins on later visits. |
341
+ | `accent` | `'#4F46E5'` | Any CSS colour. |
342
+ | `captureConsole` / `captureNetwork` / `captureErrors` | `true` | Each capture can be switched off. |
343
+ | `consoleLimit` / `networkLimit` | `100` / `50` | Entries retained. |
344
+ | `attachments` | `true` | Offer screenshot, recording and file attach. |
345
+ | `maxAttachments` / `maxAttachmentBytes` / `maxRecordingMs` | `5` / `25 MB` / `120000` | Attachment limits. |
346
+ | `drafts` | `true` | Recover an unfinished report on this device for seven days. |
347
+ | `knownIssues` | `true` | Show what this project already knows is broken, with "me too". Draws nothing until the project turns it on. |
59
348
  | `beforeSend` | — | Mutate the payload, or return `false` to cancel. |
60
- | `updates` | `true` | Tell the reporter, live, when a bug they filed moves. Connects only once this browser has filed something. |
61
- | `updateToast` | `true` | Show the built-in notification. It stays until the reporter marks it read — a reload included. Set `false` to render your own. |
62
- | `onUpdate` | — | `{ id, bugId, title, kind, summary, at }` on every update, whether or not the built-in notification is shown. |
63
- | `voice` | `true` | Offer a call to the team when a report is picked up, where the workspace has calling configured. Nothing is dialled and no microphone is asked for until the reporter presses the button — see [Talking a report through](#talking-a-report-through). |
64
- | `onCallState` | — | `('ringing' \| 'connecting' \| 'connected' \| 'missed' \| 'ended' \| 'failed', { bugId })` as a call moves. |
349
+ | `updates` | `true` | Tell the reporter live when a report they filed moves. |
350
+ | `updateToast` | `true` | Show the built-in notification; `false` to render your own from `'update'` / `'unread'`. |
351
+ | `onUpdate` | — | Called on every update. |
352
+ | `voice` | `true` | Offer calls where the workspace has calling configured. `false` switches calls off in both directions. |
353
+ | `onCallState` / `onIncomingCall` | — | See [Talking a report through](#talking-a-report-through). |
354
+
355
+ An option the SDK does not know is reported in the console with a suggestion
356
+ (`unknown option "projectID" — did you mean "projectId"?`).
357
+
358
+ ## API at a glance
359
+
360
+ ```js
361
+ BugCatch.init(options); // once
362
+ BugCatch.identify({ id, email, name, phone, loanId, applicationId, customerNumber });
363
+ BugCatch.identify(null); // sign-out
364
+ BugCatch.setContext({ loanId, screen }); // merge; null value removes a key
365
+ BugCatch.setContext(null); // clear
366
+ BugCatch.getContext();
367
+
368
+ BugCatch.open(); BugCatch.close(); BugCatch.isOpen();
369
+ await BugCatch.report({ title, description, priority, context, references, metadata, attachments });
370
+ await BugCatch.uploadAttachment(blob, mime, onProgress);
371
+
372
+ const off = BugCatch.on('report' | 'assistant' | 'error' | 'update' | 'unread' | 'call'
373
+ | 'incoming-call' | 'signin' | 'signout', handler);
374
+ BugCatch.off(event, handler);
375
+
376
+ await BugCatch.myReports(); // everything filed from this browser
377
+ await BugCatch.myReport(bugId); // one report, with timeline and assistant phase
378
+ await BugCatch.replyToReport(bugId, { body, attachments });
379
+ BugCatch.unreadUpdates(); BugCatch.markUpdateRead(id); BugCatch.markReportRead(bugId); BugCatch.markUpdatesRead();
380
+ BugCatch.hasReports(); BugCatch.forgetReports();
381
+
382
+ await BugCatch.loginState(); BugCatch.signedInAs();
383
+ await BugCatch.signIn(email); await BugCatch.confirmSignIn(email, code); await BugCatch.signOut();
384
+ await BugCatch.referenceFields(); // the identifier question this project asks
385
+
386
+ await BugCatch.knownIssues(); await BugCatch.matchKnownIssues(q, { signal }); await BugCatch.meToo(bugId);
387
+
388
+ await BugCatch.canCallNow(); await BugCatch.startCall(bugId?); BugCatch.endCall();
65
389
 
66
- ## Screenshots, recordings and files
390
+ BugCatch.snapshot(); // what would be sent right now (user, context, logs)
391
+ BugCatch.clear(); // empty the capture buffers
392
+ BugCatch.destroy(); // restore patched globals, remove the widget
393
+ BugCatch.version; // the SDK version, for support tickets
394
+ ```
395
+
396
+ ## Troubleshooting
397
+
398
+ Turn on `debug: true` first: the console then shows what `init()` received, every
399
+ `identify()` / `setContext()` and the shape of each report as it is sent.
400
+
401
+ | Symptom | Likely cause |
402
+ |---|---|
403
+ | `[BugCatch] apiKey is required` / `projectId is required` thrown | The option is missing or still the snippet placeholder. |
404
+ | Warning: *apiBase is not set, so reports go to http://localhost:3000* | Set `apiBase` to your BugCatch API URL. |
405
+ | Warning: *apiKey looks like a project id* | `apiKey` and `projectId` are swapped. |
406
+ | `report failed (HTTP 401): the apiKey was not accepted` | Revoked or mistyped key — keys are shown once; create a new one. |
407
+ | `report failed (HTTP 403)` | The key belongs to a different workspace than `projectId`. |
408
+ | `could not reach https://…` | Wrong `apiBase`, the API is down, or the API's CORS does not allow your site's origin. |
409
+ | `This project needs something to look the report up by — …` | The project requires an identifier. Pass `loanId` / `customerNumber` via `identify()` or `setContext()`, or let the widget ask. |
410
+ | The assistant keeps asking customers for their loan number | Pass the IDs through `identify()` / `setContext()`; check `BugCatch.snapshot().user` and `.context` show them. |
411
+ | `identify()` "did nothing" | It was called with a non-object (ignored with a warning), or an identifier was an object/array/too long. See the console. |
412
+ | Reports are not attributed / no resolution email / no live updates / no Call button | `identify()` has no `email`. |
413
+ | The panel will not open with `open()` | `widget: false` — there is no built-in panel; use `report()`. |
414
+ | Nothing is captured from before a certain point | Capture starts at `init()`. Call it as early as possible. |
415
+
416
+ ## Reference
417
+
418
+ ### Screenshots, recordings and files
67
419
 
68
420
  The widget can attach a screenshot, a screen recording, or a file the reporter
69
421
  picks. Capture goes through `getDisplayMedia`, so **the browser always shows its
@@ -98,68 +450,6 @@ off.
98
450
  > "MaxAgeSeconds":86400}]}'
99
451
  > ```
100
452
 
101
- ## API
102
-
103
- ```js
104
- BugCatch.identify({ id, email, name }); // attach the signed-in user
105
- await BugCatch.report({ title, description, priority, metadata, attachments });
106
- await BugCatch.uploadAttachment(blob, mime, onProgress);
107
- BugCatch.snapshot(); // what would be sent right now
108
- BugCatch.clear(); // empty the buffers
109
- BugCatch.destroy(); // restore every patched global
110
-
111
- const off = BugCatch.on('update', fn); // live updates on this browser's reports
112
- await BugCatch.myReports(); // everything filed here
113
- await BugCatch.myReport(bugId); // one report, with its full timeline
114
- await BugCatch.replyToReport(bugId, { body, attachments }); // write back
115
- BugCatch.hasReports(); // has this browser filed anything?
116
- BugCatch.forgetReports(); // drop the subscription (call on sign-out)
117
-
118
- await BugCatch.loginState(); // what this project asks, and who is signed in
119
- BugCatch.signedInAs(); // { email, name } | null, from storage
120
- await BugCatch.signIn(email); // step one: mail a code to any address
121
- await BugCatch.confirmSignIn(email, code);// step two: spend it
122
- await BugCatch.signOut(); // end the session, here and on the server
123
-
124
- await BugCatch.knownIssues(); // what this project says is already broken
125
- await BugCatch.matchKnownIssues(q, { signal }); // the ones matching what they are typing
126
- await BugCatch.meToo(bugId, { email }); // join a report instead of filing a second
127
-
128
- await BugCatch.canCallNow(); // may this visitor call instead of typing?
129
- await BugCatch.startCall(); // file a report and ring the team about it
130
- await BugCatch.canCall(bugId); // may this report be talked through?
131
- await BugCatch.startCall(bugId); // ring the team about an existing report
132
- await BugCatch.canCallTeam(bugId); // the same question, under the widget's name
133
- await BugCatch.startLiveCall(bugId); // the same call, under its older name
134
- BugCatch.on('incoming-call', fn); // the team ringing *them* — the SDK draws the card
135
- BugCatch.endCall(); // hang up
136
- ```
137
-
138
- ### Telling BugCatch who is reporting
139
-
140
- ```js
141
- BugCatch.identify({ id: 'u_42', email: 'alex@example.com', name: 'Alex' });
142
-
143
- // On sign-out
144
- BugCatch.identify(null);
145
- ```
146
-
147
- `email` is the field with behaviour attached. It does three things:
148
-
149
- - **Threads their reports together.** Every report from that address belongs to
150
- one person in the dashboard, with a count and a last-seen — so "this user
151
- keeps hitting things" is visible rather than something you have to notice.
152
- - **Closes the loop.** When someone marks the bug resolved, that person gets
153
- the project's resolution email. Nobody has to remember to tell them.
154
- - **Is trusted, because you authenticated them.** An address typed into a
155
- public recording-link form is unusable for outbound mail until the reporter
156
- answers a confirmation code. One that arrives through `identify()` is not,
157
- because your app already knows who they are. Only send addresses you have
158
- actually signed in.
159
-
160
- Calling `identify()` is optional; without it reports are filed anonymously and
161
- nobody gets a resolution email.
162
-
163
453
  ### Letting them sign in themselves
164
454
 
165
455
  `identify()` is your application vouching for its own user. Some projects want
@@ -457,7 +747,7 @@ parameters — `?page=2`, `?sort=created_at`, `?code=US` — are left alone.
457
747
  The API applies the same scrubbing on intake and again when a report is read back, so
458
748
  reports captured by older SDK versions are covered too.
459
749
 
460
- ## What is already known
750
+ ### What is already known
461
751
 
462
752
  The widget opens with the reports this project has chosen to say are already
463
753
  broken, above the form rather than after it, and offers **Me too** beside each
@@ -503,7 +793,7 @@ this on, which is the default — draw nothing rather than an empty heading. Wha
503
793
  comes back is a title, a state and a count, and never a severity, an assignee or
504
794
  a description: the question it answers is "is this the thing I came here about".
505
795
 
506
- ## Talking a report through
796
+ ### Talking a report through
507
797
 
508
798
  Where the workspace has sphoro.voice configured, a reporter can talk to the team
509
799
  instead of typing. There are two ways in.
@@ -576,7 +866,7 @@ Four things are worth being explicit about, because they are what people ask:
576
866
  - **One call at a time**, and `destroy()` ends it. Two microphones on one page
577
867
  talk over each other.
578
868
 
579
- ### Being called
869
+ #### Being called
580
870
 
581
871
  The same live call, placed from the other end. Somebody on the team reads a
582
872
  report an hour after it was filed, has one question whose answer is thirty
@@ -652,22 +942,7 @@ supplied an email: a call with nobody to attribute has no way to tell the
652
942
  reporter what happened next. Set `voice: false` to opt out entirely; the code is
653
943
  still in the bundle, but nothing runs.
654
944
 
655
- ## What is captured
656
-
657
- - **Console** — last N entries with level and timestamp, plus uncaught errors and
658
- unhandled promise rejections.
659
- - **Network** — last N `fetch`/XHR calls: URL, method, status, duration, and flags for
660
- failed or slow. **Request and response bodies are never read**, and **credentials are
661
- stripped out of the URL before it is buffered** — see below.
662
- - **Environment** — user agent, platform, language(s), viewport, screen (size, available
663
- size, colour depth, orientation), DPR, timezone and offset, connection type, CPU cores,
664
- device memory, touch points, battery level and charging state, colour-scheme and
665
- reduced-motion preferences, Do Not Track, online state, referrer, page title.
666
- - **Page URL** at the moment of reporting.
667
-
668
- Both buffers are ring buffers, so a long-lived page won't grow memory without bound.
669
-
670
- ## Notes
945
+ ### Notes
671
946
 
672
947
  - The widget renders in a shadow root, so your CSS and its CSS can't collide.
673
948
  - Capture never throws into your page: every hook is wrapped, and originals are always