@sphoro/bugcatch-web-sdk 0.13.1 → 0.14.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sphoro/bugcatch-web-sdk",
3
- "version": "0.13.1",
3
+ "version": "0.14.1",
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",
package/src/drafts.js ADDED
@@ -0,0 +1,32 @@
1
+ // Drafts are scoped to the API, project and reporter. Never store credentials.
2
+ const MAX_AGE = 7 * 24 * 60 * 60 * 1000;
3
+ let database;
4
+ function open() {
5
+ if (!database) database = new Promise((resolve, reject) => {
6
+ const request = indexedDB.open('bugcatch-drafts', 1);
7
+ request.onupgradeneeded = () => request.result.createObjectStore('drafts');
8
+ request.onsuccess = () => resolve(request.result);
9
+ request.onerror = () => reject(request.error);
10
+ request.onblocked = () => reject(new Error('Draft storage is blocked.'));
11
+ }).catch((err) => { database = null; throw err; });
12
+ return database;
13
+ }
14
+ export function draftKey(config, email) {
15
+ return JSON.stringify([config.apiBase, config.projectId, email || config.user?.email || config.user?.id || 'anonymous']);
16
+ }
17
+ export async function draftOperation(key, value) {
18
+ const db = await open();
19
+ return new Promise((resolve, reject) => {
20
+ const tx = db.transaction('drafts', value === undefined ? 'readonly' : 'readwrite');
21
+ const store = tx.objectStore('drafts');
22
+ const request = value === undefined ? store.get(key) : value === null ? store.delete(key) : store.put({ ...value, savedAt: Date.now() }, key);
23
+ tx.oncomplete = () => resolve(request.result);
24
+ tx.onerror = () => reject(tx.error);
25
+ tx.onabort = () => reject(tx.error || new Error('Draft was not saved.'));
26
+ });
27
+ }
28
+ export async function readDraft(key) {
29
+ const value = await draftOperation(key);
30
+ if (value && Date.now() - value.savedAt > MAX_AGE) { await draftOperation(key, null); return null; }
31
+ return value || null;
32
+ }
package/src/index.d.ts CHANGED
@@ -53,6 +53,11 @@ export interface JoinedIssue {
53
53
  }
54
54
 
55
55
  export interface BugCatchConfig {
56
+ /** Recover unfinished widget reports on this device for seven days. Default true. */
57
+ drafts?: boolean;
58
+ release?: string;
59
+ environment?: string;
60
+ buildId?: string;
56
61
  apiBase?: string;
57
62
  apiKey: string;
58
63
  projectId: string;
@@ -116,15 +121,13 @@ export interface BugCatchConfig {
116
121
  */
117
122
  voice?: boolean;
118
123
  /**
119
- * Called as a call of either kind moves between states. `detail.live` marks
120
- * the ones about a call to a person; their states come from LiveCallState.
124
+ * Called as a call moves between states. `detail.incoming` marks a call the
125
+ * team placed to this page rather than one started from it.
121
126
  */
122
127
  onCallState?: ((
123
- state: CallState | LiveCallState,
124
- detail: { bugId: string; live?: boolean; reason?: string },
128
+ state: LiveCallState,
129
+ detail: { bugId: string; live?: boolean; incoming?: boolean; reason?: string },
125
130
  ) => void) | null;
126
- /** Called with each line of a call, as it is recognised. */
127
- onCallTranscript?: ((line: CallTranscriptLine) => void) | null;
128
131
  /**
129
132
  * Called when somebody on the team rings this page about a report filed from
130
133
  * it — the one call this SDK does not start.
@@ -171,43 +174,18 @@ export type IncomingCallState =
171
174
  'ringing' | 'answered' | 'declined' | 'missed' | 'failed';
172
175
 
173
176
  /**
174
- * Where a call is. `listening` means the reporter's turn, `speaking` the
175
- * agent's; `ended` and `failed` are both final and release the microphone.
176
- */
177
- export type CallState = 'connecting' | 'listening' | 'speaking' | 'ended' | 'failed';
178
-
179
- /**
180
- * Where a *live* call is — one to a person on the team rather than to the
181
- * agent. A separate vocabulary because the two have almost nothing in common:
182
- * an agent call is never `ringing` because a model does not keep anybody
183
- * waiting, and a live call is never `listening` or `speaking`, because nobody
184
- * is transcribing it to know whose turn it is.
177
+ * Where a call is.
185
178
  *
186
- * `missed` is final and ordinary: the ringing stopped before anybody picked up.
179
+ * It starts `ringing`, because a call reaches a room of people rather than
180
+ * something that answers instantly. `missed` means the ringing stopped before
181
+ * anybody picked up — final, and ordinary rather than a failure. `ended`,
182
+ * `missed` and `failed` all release the microphone.
187
183
  */
188
184
  export type LiveCallState =
189
185
  'ringing' | 'connecting' | 'connected' | 'ended' | 'missed' | 'failed';
190
186
 
191
- export interface CallTranscriptLine {
192
- bugId: string;
193
- /** Who said it. `agent` lines arrive as deltas; `final` marks the end of a turn. */
194
- role: 'reporter' | 'agent';
195
- text: string;
196
- final: boolean;
197
- }
198
-
199
- /** A call in progress. */
200
- /** Which kind of call a deployment offers. One agent has one mode, so never both. */
201
- export type CallKind = 'agent' | 'live';
202
-
203
- export interface CallHandle {
204
- /** End it. Safe to call twice, and after it has already ended. */
205
- hangUp(): void;
206
- /** The report this call is about, whether it was named or filed on the way in. */
207
- bugId: string;
208
- /** What was filed, when the call is the reason the report exists. */
209
- filed?: { id: string; watchToken?: string } | null;
210
- }
187
+ /** Which kind of call a deployment offers. One kind: a person on the team. */
188
+ export type CallKind = 'live';
211
189
 
212
190
  export interface LiveCallHandle {
213
191
  /** End it. Safe to call twice, and after it has already ended. */
@@ -221,11 +199,13 @@ export interface LiveCallHandle {
221
199
  */
222
200
  setMuted(muted: boolean): boolean;
223
201
  isMuted(): boolean;
224
- /** The report this call is about. */
202
+ /** The report this call is about, whether it was named or filed on the way in. */
225
203
  bugId: string;
226
204
  /** sphoro.voice's id for the call, as the team's dashboard also knows it. */
227
205
  callId: string;
228
206
  live: true;
207
+ /** What was filed, when the call is the reason the report exists. */
208
+ filed?: { id: string; watchToken?: string } | null;
229
209
  }
230
210
 
231
211
  export interface Attachment {
@@ -241,6 +221,56 @@ export interface ReportInput {
241
221
  metadata?: Record<string, any>;
242
222
  /** Already uploaded via uploadAttachment(). Entries without a key are dropped. */
243
223
  attachments?: Attachment[];
224
+ /**
225
+ * What identifies the record this report is about — a loan number, a customer
226
+ * id, the mobile number the account is registered under.
227
+ *
228
+ * `key` names one of the kinds this project has configured; see
229
+ * `referenceFields()`. Only supply what the person actually told you: the API
230
+ * reads the title and description for identifiers itself, and does so on its
231
+ * own side rather than trusting what a page claims to have found.
232
+ *
233
+ * The built-in widget fills this in from the question it asks at Send. It is
234
+ * here for integrations running with `widget: false`, which draw their own
235
+ * form and must ask their own version of that question.
236
+ */
237
+ references?: ReportReference[];
238
+ }
239
+
240
+ export interface ReportReference {
241
+ key: string;
242
+ value: string;
243
+ }
244
+
245
+ /** How a project asks reporters to identify the record a report is about. */
246
+ export type ReferencePolicy = 'off' | 'optional' | 'required';
247
+
248
+ /**
249
+ * One kind of identifier this project recognises.
250
+ *
251
+ * `pattern` is a regex source with no delimiters and no flags, matched
252
+ * case-insensitively. Where it has a capture group, group 1 is the value; where
253
+ * it has none, the whole match is. `valuePattern`, when present, is the shape of
254
+ * the value on its own — used to check something typed into a box rather than
255
+ * found in a sentence.
256
+ */
257
+ export interface ReferenceField {
258
+ key: string;
259
+ label: string;
260
+ hint: string;
261
+ placeholder: string;
262
+ inputMode: 'text' | 'email' | 'tel' | 'numeric';
263
+ pattern: string;
264
+ valuePattern: string | null;
265
+ /** Whether this kind may be read out of the report's own text. */
266
+ scan: boolean;
267
+ /** Whether this is one of the kinds that satisfies a `required` policy. */
268
+ required: boolean;
269
+ }
270
+
271
+ export interface ReferenceConfig {
272
+ policy: ReferencePolicy;
273
+ fields: ReferenceField[];
244
274
  }
245
275
 
246
276
  /**
@@ -434,10 +464,9 @@ export declare class BugCatchClient {
434
464
  on(
435
465
  event: 'call',
436
466
  handler: (detail: {
437
- bugId: string; state: CallState | LiveCallState; live?: boolean; reason?: string;
467
+ bugId: string; state: LiveCallState; live?: boolean; incoming?: boolean; reason?: string;
438
468
  }) => void,
439
469
  ): () => void;
440
- on(event: 'call-transcript', handler: (line: CallTranscriptLine) => void): () => void;
441
470
  /**
442
471
  * A call the team placed to this page, as it moves. Separate from `call`,
443
472
  * which is the conversation once one is connected — this is the telephone
@@ -466,7 +495,7 @@ export declare class BugCatchClient {
466
495
  on(event: 'signout', handler: (who: { email: string | null }) => void): () => void;
467
496
  off(event: 'update', handler: (update: ReportUpdate) => void): this;
468
497
  off(
469
- event: 'call' | 'call-transcript' | 'unread' | 'incoming-call' | 'signin' | 'signout',
498
+ event: 'call' | 'unread' | 'incoming-call' | 'signin' | 'signout',
470
499
  handler: (payload: any) => void,
471
500
  ): this;
472
501
  /**
@@ -475,6 +504,17 @@ export declare class BugCatchClient {
475
504
  * for integrations running with `widget: false`.
476
505
  */
477
506
  loginState(): Promise<SdkLoginState>;
507
+ /**
508
+ * What this project needs in order to look a report up, and how it asks.
509
+ *
510
+ * `{ policy: 'off', fields: [] }` on every project that has not configured
511
+ * this, and on every failure to reach the API — a widget that cannot ask the
512
+ * question must never be a widget that refuses to send the report.
513
+ *
514
+ * Cached for the life of the page. The built-in widget calls this itself; it
515
+ * is here for integrations running with `widget: false`.
516
+ */
517
+ referenceFields(): Promise<ReferenceConfig>;
478
518
  /**
479
519
  * Who this browser is signed in as, from storage alone — no request, and
480
520
  * therefore only as current as the last one. A session ended elsewhere still
@@ -533,66 +573,65 @@ export declare class BugCatchClient {
533
573
  /**
534
574
  * Which kind of call this visitor would get, or null for none.
535
575
  *
536
- * `'agent'` is the triage agent: a model answers instantly and asks the
537
- * questions the form did not. `'live'` rings the team and a colleague picks
538
- * up. A deployment offers one or the other and never both — the agent behind
539
- * it has exactly one mode — so this is what to word a button from.
576
+ * `'live'` rings the team and a colleague picks up, which is the only kind
577
+ * there is. Null on most deployments, which is what to hide a button on.
540
578
  *
541
579
  * Remembered for the life of the page: it describes the deployment's
542
580
  * configuration, not this visitor.
543
581
  */
544
582
  callKind(): Promise<CallKind | null>;
545
583
  /**
546
- * Talk a report through. Asks for the microphone, places the call, and
547
- * resolves once it is up.
584
+ * The same answer as booleans.
585
+ *
586
+ * `agent` is always false: it was a triage agent that answered instead of a
587
+ * person, and it is gone. The key stays so an integration reading it gets
588
+ * `false` rather than `undefined`.
589
+ *
590
+ * Same round trip and same page-lifetime cache as `callKind()`.
591
+ */
592
+ callKinds(): Promise<{ agent: false; live: boolean }>;
593
+ /**
594
+ * Ring the team about a report. Asks for the microphone, places the call, and
595
+ * resolves once it is *ringing* — not once somebody answers. Follow the rest
596
+ * through `onCallState` or the `call` event.
548
597
  *
549
- * Lands on whichever kind of call this deployment offers, which is decided by
550
- * the mode of its agent rather than by the method you called: against a
551
- * person-to-person agent this rings the team, and the handle comes back with
552
- * `live: true`. Ask `callKind()` first if the wording around your own button
553
- * has to say which.
598
+ * The audio goes directly between the two browsers over WebRTC, so a call is
599
+ * not transcribed; the report's thread gets a note saying it happened.
554
600
  *
555
601
  * Rejects if the microphone is refused, if calling is not available on this
556
602
  * report, or if a call is already running — there is deliberately no way to
557
603
  * start a second.
558
604
  */
559
- startCall(bugId: string): Promise<CallHandle | LiveCallHandle>;
605
+ startCall(bugId: string): Promise<LiveCallHandle>;
560
606
  /**
561
- * The same, with no report yet: files one from the captured context and calls
562
- * about it. The handle carries the id it was filed as. A rejection with
563
- * `.filed` set means the report was written and only the call failed.
607
+ * The same, with no report yet: files one from the captured context and rings
608
+ * the team about it. The handle carries the id it was filed as. A rejection
609
+ * with `.filed` set means the report was written and only the call failed.
564
610
  */
565
- startCall(): Promise<CallHandle | LiveCallHandle>;
611
+ startCall(): Promise<LiveCallHandle>;
566
612
  /**
567
613
  * Whether this report can be talked through with a person on the team.
568
614
  *
569
- * Never true at the same time as `canCall`: one agent has one mode, so a
570
- * deployment answers for the triage agent or for a live call, not both.
615
+ * The same question as `canCall`, under the name the widget uses: there is
616
+ * one kind of call, so there is one answer.
571
617
  */
572
618
  canCallTeam(bugId: string): Promise<boolean>;
573
619
  /**
574
- * Ring the team about a report, and hand back the call.
620
+ * Ring the team — exactly `startCall`, under the name an integration written
621
+ * when there were two kinds of call is already using.
575
622
  *
576
- * Resolves once it is *ringing*, not once somebody answers — follow the rest
577
- * through `onCallState` or the `call` event. The audio goes directly between
578
- * the two browsers over WebRTC, so a live call is not recorded and not
579
- * transcribed; the report's thread gets a note saying it happened.
580
- *
581
- * A report id is required, unlike `startCall`: ringing a room about a report
582
- * nobody can read would ask somebody to answer a call they know nothing
623
+ * The id is optional here too. It was required while this was one of two
624
+ * kinds, because the other was the only way to call with no report to call
583
625
  * about.
584
- *
585
- * Rejects if the microphone is refused, if live calling is not available on
586
- * this report, or if a call of either kind is already running — this page has
587
- * one microphone.
588
626
  */
589
- startLiveCall(bugId: string): Promise<LiveCallHandle>;
627
+ startLiveCall(bugId?: string): Promise<LiveCallHandle>;
590
628
  /**
591
- * End the call in progress, of either kind. Does nothing when there is not
592
- * one — there is a single slot, because there is a single microphone.
629
+ * End the call in progress. Does nothing when there is not one — there is a
630
+ * single slot, because there is a single microphone.
593
631
  */
594
632
  endCall(): this;
595
633
  /** One report with its full timeline, or null if this browser cannot prove it filed it. */
634
+ verifyFix(bugId: string, feedback: { result: 'CONFIRMED' | 'STILL_HAPPENING'; resolvedAt: string; note?: string }): Promise<{ verification: string; status: string }>;
596
635
  myReport(bugId: string): Promise<ReportDetail | null>;
597
636
  /**
598
637
  * Write back on a report this browser filed. The widget's own conversation