@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/README.md +48 -78
- package/dist/bugcatch.es.js +1752 -1685
- package/dist/bugcatch.es.js.map +1 -1
- package/dist/bugcatch.umd.js +154 -110
- package/dist/bugcatch.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/drafts.js +32 -0
- package/src/index.d.ts +114 -75
- package/src/index.js +147 -164
- package/src/references.js +172 -0
- package/src/updates.js +45 -74
- package/src/widget.js +320 -105
- package/src/voice.js +0 -372
package/package.json
CHANGED
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
|
|
120
|
-
*
|
|
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:
|
|
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.
|
|
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
|
-
* `
|
|
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
|
-
|
|
192
|
-
|
|
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:
|
|
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' | '
|
|
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
|
-
* `'
|
|
537
|
-
*
|
|
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
|
-
*
|
|
547
|
-
*
|
|
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
|
-
*
|
|
550
|
-
*
|
|
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<
|
|
605
|
+
startCall(bugId: string): Promise<LiveCallHandle>;
|
|
560
606
|
/**
|
|
561
|
-
* The same, with no report yet: files one from the captured context and
|
|
562
|
-
* about it. The handle carries the id it was filed as. A rejection
|
|
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<
|
|
611
|
+
startCall(): Promise<LiveCallHandle>;
|
|
566
612
|
/**
|
|
567
613
|
* Whether this report can be talked through with a person on the team.
|
|
568
614
|
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
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
|
|
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
|
-
*
|
|
577
|
-
*
|
|
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
|
|
627
|
+
startLiveCall(bugId?: string): Promise<LiveCallHandle>;
|
|
590
628
|
/**
|
|
591
|
-
* End the call in progress
|
|
592
|
-
*
|
|
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
|