@sphoro/bugcatch-web-sdk 0.17.0 → 0.19.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 +104 -6
- package/dist/bugcatch-call.es.js +587 -0
- package/dist/bugcatch-call.es.js.map +1 -0
- package/dist/bugcatch-call.umd.js +2 -0
- package/dist/bugcatch-call.umd.js.map +1 -0
- package/dist/bugcatch-core.es.js +3292 -0
- package/dist/bugcatch-core.es.js.map +1 -0
- package/dist/bugcatch-panel.es.js +2531 -0
- package/dist/bugcatch-panel.es.js.map +1 -0
- package/dist/bugcatch-panel.umd.js +1311 -0
- package/dist/bugcatch-panel.umd.js.map +1 -0
- package/dist/bugcatch.es.js +2 -5625
- package/dist/bugcatch.es.js.map +1 -1
- package/dist/bugcatch.standalone.umd.js +1619 -0
- package/dist/bugcatch.standalone.umd.js.map +1 -0
- package/dist/bugcatch.umd.js +195 -1318
- package/dist/bugcatch.umd.js.map +1 -1
- package/package.json +6 -6
- package/src/accent.js +38 -0
- package/src/announcements.js +147 -0
- package/src/busy.js +18 -0
- package/src/call.js +7 -0
- package/src/capture.js +9 -0
- package/src/chunks.js +56 -0
- package/src/gesture.js +33 -0
- package/src/icons.js +18 -0
- package/src/identity.js +13 -8
- package/src/incoming-call.js +25 -45
- package/src/index.d.ts +93 -2
- package/src/index.js +164 -42
- package/src/lazy.js +23 -0
- package/src/notice.js +68 -0
- package/src/panel/call.js +242 -0
- package/src/panel/compose.js +416 -0
- package/src/panel/composer.js +235 -0
- package/src/panel/icons.js +36 -0
- package/src/panel/index.js +265 -0
- package/src/panel/known.js +130 -0
- package/src/panel/messages.js +176 -0
- package/src/panel/notices.js +34 -0
- package/src/panel/picks.js +65 -0
- package/src/panel/refs.js +321 -0
- package/src/panel/render.js +132 -0
- package/src/panel/signin.js +228 -0
- package/src/panel/styles.js +1016 -0
- package/src/panel/thread.js +494 -0
- package/src/session.js +8 -3
- package/src/umd/call.js +6 -0
- package/src/umd/lazy.js +98 -0
- package/src/umd/panel.js +6 -0
- package/src/updates.js +97 -34
- package/src/widget.js +385 -3376
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. */
|
|
@@ -161,8 +196,12 @@ export interface BugCatchConfig {
|
|
|
161
196
|
widgetPosition?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
162
197
|
/** 'auto' follows the visitor's system setting. Default 'auto'. */
|
|
163
198
|
theme?: 'auto' | 'light' | 'dark';
|
|
164
|
-
/**
|
|
165
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Any CSS colour, for the launcher, the buttons and the assistant's orb. The
|
|
201
|
+
* words on it are white or near-black, whichever reads. Default null: ink,
|
|
202
|
+
* which is a black button on light and a white one on dark.
|
|
203
|
+
*/
|
|
204
|
+
accent?: string | null;
|
|
166
205
|
/** Offer screenshot / recording / file attach in the widget. Default true. */
|
|
167
206
|
attachments?: boolean;
|
|
168
207
|
/**
|
|
@@ -172,6 +211,13 @@ export interface BugCatchConfig {
|
|
|
172
211
|
* the feature on and somebody lists an individual report.
|
|
173
212
|
*/
|
|
174
213
|
knownIssues?: boolean;
|
|
214
|
+
/**
|
|
215
|
+
* Draw what the project's team is telling visitors: at the top of the panel,
|
|
216
|
+
* and beside the button for the ones written for that. Default true — and
|
|
217
|
+
* nothing is drawn until the project publishes one. false draws nothing;
|
|
218
|
+
* announcements() still answers, for a site that shows them its own way.
|
|
219
|
+
*/
|
|
220
|
+
announcements?: boolean;
|
|
175
221
|
/** Default 5. */
|
|
176
222
|
maxAttachments?: number;
|
|
177
223
|
/** Default 25 MB. */
|
|
@@ -420,6 +466,25 @@ export interface MyReport {
|
|
|
420
466
|
project: string;
|
|
421
467
|
workspace: string;
|
|
422
468
|
attachments: number;
|
|
469
|
+
/** Their score for how it was handled, 1–5, or null when they have not rated it. */
|
|
470
|
+
rating: number | null;
|
|
471
|
+
/** Settled, on a project that asks for ratings — so a list can say "rate it". */
|
|
472
|
+
ratingAsked: boolean;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* A reporter's own rating of how a settled report was handled.
|
|
477
|
+
*
|
|
478
|
+
* `current` is false when the report has been reopened and settled again since
|
|
479
|
+
* it was given — the rating stands, but it was for an earlier fix.
|
|
480
|
+
*/
|
|
481
|
+
export interface ReportRating {
|
|
482
|
+
/** 1–5, five best. */
|
|
483
|
+
score: number;
|
|
484
|
+
comment: string | null;
|
|
485
|
+
ratedAt: string;
|
|
486
|
+
via: 'widget' | 'portal' | 'email';
|
|
487
|
+
current: boolean;
|
|
423
488
|
}
|
|
424
489
|
|
|
425
490
|
/** A report of this browser's changed status. */
|
|
@@ -510,6 +575,18 @@ export interface ReportDetail {
|
|
|
510
575
|
attachments: { type: string; mime: string; createdAt: string; url: string | null }[];
|
|
511
576
|
/** Every step from filing to now, oldest first, both sides' messages included. */
|
|
512
577
|
timeline: ReportTimelineEntry[];
|
|
578
|
+
/** When it was last settled, or null while it is open. */
|
|
579
|
+
resolvedAt: string | null;
|
|
580
|
+
/** Their answer to "does the fix work?", or null when they have not given one. */
|
|
581
|
+
verification: 'CONFIRMED' | 'STILL_HAPPENING' | null;
|
|
582
|
+
/** Their rating of how it was handled, or null. See rate(). */
|
|
583
|
+
rating: ReportRating | null;
|
|
584
|
+
/**
|
|
585
|
+
* Whether a rating is being asked for: the report is settled and the project
|
|
586
|
+
* has not switched ratings off. The widget draws its stars only when this is
|
|
587
|
+
* true; an integration's own view should do the same.
|
|
588
|
+
*/
|
|
589
|
+
ratingAsked: boolean;
|
|
513
590
|
/**
|
|
514
591
|
* The same figure they were given when they filed it, so coming back to check
|
|
515
592
|
* is not a worse experience than not checking. Null on a settled report — the
|
|
@@ -745,6 +822,12 @@ export declare class BugCatchClient {
|
|
|
745
822
|
endCall(): this;
|
|
746
823
|
/** Tell the team whether their fix worked. STILL_HAPPENING re-sends this browser's context. */
|
|
747
824
|
verifyFix(bugId: string, feedback: { result: 'CONFIRMED' | 'STILL_HAPPENING'; resolvedAt: string; note?: string }): Promise<{ verification: string; status: string }>;
|
|
825
|
+
/**
|
|
826
|
+
* Rate how a settled report was handled, 1–5, five best. Rating again
|
|
827
|
+
* replaces the earlier score. Rejects while the report is open, or on a
|
|
828
|
+
* project that does not ask for ratings (`ratingAsked` is false).
|
|
829
|
+
*/
|
|
830
|
+
rate(bugId: string, rating: { score: 1 | 2 | 3 | 4 | 5; comment?: string }): Promise<ReportRating>;
|
|
748
831
|
/** One report with its full timeline, or null if this browser cannot prove it filed it. */
|
|
749
832
|
myReport(bugId: string): Promise<ReportDetail | null>;
|
|
750
833
|
/**
|
|
@@ -813,6 +896,14 @@ export declare class BugCatchClient {
|
|
|
813
896
|
* draw nothing rather than an empty heading.
|
|
814
897
|
*/
|
|
815
898
|
knownIssues(): Promise<KnownIssue[]>;
|
|
899
|
+
/**
|
|
900
|
+
* What this project's team is telling the people using its widget, for a
|
|
901
|
+
* site drawing its own UI (`widget: false` or `announcements: false`). A
|
|
902
|
+
* browser asks at most every five minutes. Empty on every failure.
|
|
903
|
+
*/
|
|
904
|
+
announcements(): Promise<Announcement[]>;
|
|
905
|
+
/** Stop showing one in this browser until the team uses "Show it again". */
|
|
906
|
+
dismissAnnouncement(announcement: Announcement): void;
|
|
816
907
|
/**
|
|
817
908
|
* The known issues that look like what somebody is describing.
|
|
818
909
|
*
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { mountWidget } from './widget.js';
|
|
|
6
6
|
import { uploadAttachment, normalizeMime, REPORT_MIMES } from './capture.js';
|
|
7
7
|
import {
|
|
8
8
|
watchUpdates, createUpdateNotifier, fetchMyReports, fetchReport, writeToken, readToken,
|
|
9
|
-
clearToken, scopeReports, sendMessage, verifyFix, callKindsForProject,
|
|
9
|
+
clearToken, scopeReports, sendMessage, verifyFix, rateReport, callKindsForProject,
|
|
10
10
|
reportByCall, liveCallAvailable, requestLiveCall, giveUpLiveCall,
|
|
11
11
|
answerIncomingCall, declineIncomingCall,
|
|
12
12
|
readUnread, rememberUpdate, forgetUpdate, forgetReportUpdates, forgetAllUpdates,
|
|
@@ -16,11 +16,17 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
loginState, startSignIn, confirmSignIn, endSignIn, readSession, clearSession,
|
|
18
18
|
} from './session.js';
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// Not live-call.js and incoming-call.js: the call code is fetched when a call
|
|
20
|
+
// is placed or rings, not with every page (see lazy.js).
|
|
21
|
+
import { loadCall } from './lazy.js';
|
|
22
|
+
import { pageTouched } from './gesture.js';
|
|
21
23
|
import { referenceFields } from './references.js';
|
|
22
24
|
import { fetchAgentIssues } from './agent-issues.js';
|
|
25
|
+
import {
|
|
26
|
+
fetchAnnouncements, visibleAnnouncements, dismissAnnouncement as rememberDismissed,
|
|
27
|
+
} from './announcements.js';
|
|
23
28
|
import { normalizeUser, mergeContext, checkOptions, cleanApiBase } from './identity.js';
|
|
29
|
+
import { tooManyMessage } from './busy.js';
|
|
24
30
|
|
|
25
31
|
// Replaced at build time from package.json. The guard is for anyone importing
|
|
26
32
|
// src/ directly, where the define never runs.
|
|
@@ -43,7 +49,14 @@ const DEFAULTS = {
|
|
|
43
49
|
widgetPosition: 'bottom-right',
|
|
44
50
|
// 'auto' follows the visitor's system setting; 'light' / 'dark' pin it.
|
|
45
51
|
theme: 'auto',
|
|
46
|
-
|
|
52
|
+
// null is ink: black buttons on light, white on dark (accent.js). Any CSS
|
|
53
|
+
// colour makes the buttons, the launcher and the assistant's orb that colour.
|
|
54
|
+
accent: null,
|
|
55
|
+
// Draw what the project's team is telling visitors — written on the Widget
|
|
56
|
+
// announcements settings page — in the panel, and beside the button for the
|
|
57
|
+
// ones written for that. false draws nothing; announcements() still answers,
|
|
58
|
+
// for a site that shows them its own way.
|
|
59
|
+
announcements: true,
|
|
47
60
|
// Screenshot / screen recording / file attach. Capture needs getDisplayMedia,
|
|
48
61
|
// so it is offered only where the browser has it — file attach always works.
|
|
49
62
|
attachments: true,
|
|
@@ -95,12 +108,34 @@ const DEFAULTS = {
|
|
|
95
108
|
onError: null,
|
|
96
109
|
// Business context for the screen the SDK starts on — see setContext().
|
|
97
110
|
context: null,
|
|
111
|
+
// The script-tag build fetches the report panel and the call code when they
|
|
112
|
+
// are first needed, from the folder it was itself loaded from. Set this to
|
|
113
|
+
// that folder's URL when it cannot tell — a copy of the bundle inlined into
|
|
114
|
+
// the page, or served from somewhere other than beside its dist/ siblings.
|
|
115
|
+
// The ES build ignores it: the bundler decides where its chunks live.
|
|
116
|
+
chunkBase: null,
|
|
98
117
|
// Log what the SDK is doing (identify, context, each report's shape) to the
|
|
99
118
|
// console. Off in production; the warnings about misconfiguration are
|
|
100
119
|
// printed either way.
|
|
101
120
|
debug: false,
|
|
102
121
|
};
|
|
103
122
|
|
|
123
|
+
/**
|
|
124
|
+
* A report's body, plus the watch token this browser already holds for this
|
|
125
|
+
* project.
|
|
126
|
+
*
|
|
127
|
+
* The token that comes back from filing names the reports this browser filed —
|
|
128
|
+
* not every report ever filed under the address it sent, which anybody holding
|
|
129
|
+
* the public key could claim. Sending the held one back is what makes the new
|
|
130
|
+
* token cover the earlier reports too, so "Your reports" keeps its history
|
|
131
|
+
* without anybody signing in. Added after beforeSend, which has no business
|
|
132
|
+
* with it, and absent when there is nothing held.
|
|
133
|
+
*/
|
|
134
|
+
function withHeldToken(payload) {
|
|
135
|
+
const held = readToken();
|
|
136
|
+
return held ? { ...payload, watchToken: held } : payload;
|
|
137
|
+
}
|
|
138
|
+
|
|
104
139
|
class BugCatchClient {
|
|
105
140
|
constructor() {
|
|
106
141
|
this.config = { ...DEFAULTS };
|
|
@@ -423,6 +458,28 @@ class BugCatchClient {
|
|
|
423
458
|
this.notifier.render(unread);
|
|
424
459
|
}
|
|
425
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Tell the team whether their fix worked. STILL_HAPPENING reopens the report
|
|
463
|
+
* and re-sends this browser's context with it.
|
|
464
|
+
*/
|
|
465
|
+
async verifyFix(bugId, { result, resolvedAt, note }) {
|
|
466
|
+
const context = result === 'STILL_HAPPENING' ? await this.#payload({}) : {};
|
|
467
|
+
return verifyFix({ apiBase: this.config.apiBase, bugId, result, resolvedAt, note, metadata: context.metadata, logs: context.logs });
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Rate how a settled report was handled, 1 to 5, five best.
|
|
472
|
+
*
|
|
473
|
+
* await BugCatch.rate(report.id, { score: 4, comment: 'Quick, thanks' });
|
|
474
|
+
*
|
|
475
|
+
* Only once the report is RESOLVED or CLOSED, on a project that asks for
|
|
476
|
+
* ratings — `myReport(id).ratingAsked` says whether it does. Rating again
|
|
477
|
+
* replaces the earlier score rather than adding a second one.
|
|
478
|
+
*/
|
|
479
|
+
async rate(bugId, { score, comment } = {}) {
|
|
480
|
+
return rateReport({ apiBase: this.config.apiBase, bugId, score, comment });
|
|
481
|
+
}
|
|
482
|
+
|
|
426
483
|
/**
|
|
427
484
|
* One report in full, with its timeline — every step from filing to now.
|
|
428
485
|
*
|
|
@@ -431,11 +488,6 @@ class BugCatchClient {
|
|
|
431
488
|
*
|
|
432
489
|
* Resolves to null when this browser cannot prove it filed that report.
|
|
433
490
|
*/
|
|
434
|
-
async verifyFix(bugId, { result, resolvedAt, note }) {
|
|
435
|
-
const context = result === 'STILL_HAPPENING' ? await this.#payload({}) : {};
|
|
436
|
-
return verifyFix({ apiBase: this.config.apiBase, bugId, result, resolvedAt, note, metadata: context.metadata, logs: context.logs });
|
|
437
|
-
}
|
|
438
|
-
|
|
439
491
|
async myReport(bugId) {
|
|
440
492
|
const report = await fetchReport({ apiBase: this.config.apiBase, bugId });
|
|
441
493
|
this.#notePhase(bugId, report?.assistant);
|
|
@@ -600,6 +652,9 @@ class BugCatchClient {
|
|
|
600
652
|
apiKey: this.config.apiKey,
|
|
601
653
|
token: session?.token,
|
|
602
654
|
payload,
|
|
655
|
+
// See report(): the token this browser holds, so the one that comes
|
|
656
|
+
// back still covers what it covered.
|
|
657
|
+
watchToken: readToken(),
|
|
603
658
|
});
|
|
604
659
|
} catch (err) {
|
|
605
660
|
// A session the server no longer recognises, dropped exactly as
|
|
@@ -779,6 +834,11 @@ class BugCatchClient {
|
|
|
779
834
|
|
|
780
835
|
/** The body of startCall, so the in-flight flag clears on every path. */
|
|
781
836
|
async #ringTheTeam(bugId) {
|
|
837
|
+
// The call code first: fetched now, when somebody has asked for a call,
|
|
838
|
+
// rather than on every page load for the few that ever place one. A failure
|
|
839
|
+
// here rejects before anything is asked for or filed.
|
|
840
|
+
const { joinLiveCall, liveCallSupported, openMicrophone } = await loadCall(this.config.chunkBase);
|
|
841
|
+
|
|
782
842
|
// Before the call is placed, not after. A permission prompt takes as long
|
|
783
843
|
// as the person takes to read it, and asking afterwards would have a room
|
|
784
844
|
// full of dashboards ringing about a call its own caller may yet refuse to
|
|
@@ -875,7 +935,7 @@ class BugCatchClient {
|
|
|
875
935
|
*/
|
|
876
936
|
#ring(event) {
|
|
877
937
|
const call = event.incomingCall;
|
|
878
|
-
if (!this.config.voice
|
|
938
|
+
if (!this.config.voice) return;
|
|
879
939
|
// Already ringing about this one — a reconnect replaying the frame, or a
|
|
880
940
|
// second delivery. One card either way.
|
|
881
941
|
if (this.ringing?.callId === call.callId) return;
|
|
@@ -885,36 +945,56 @@ class BugCatchClient {
|
|
|
885
945
|
|
|
886
946
|
this.#stopRinging();
|
|
887
947
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
948
|
+
// The card's code is fetched now, the first time anything rings. The slot
|
|
949
|
+
// is held while it arrives, so a second delivery of this frame finds it,
|
|
950
|
+
// and a withdrawal or a teardown that lands first takes it away before any
|
|
951
|
+
// card is drawn.
|
|
952
|
+
const pending = { callId: call.callId, dismiss: () => {} };
|
|
953
|
+
this.ringing = pending;
|
|
954
|
+
loadCall(this.config.chunkBase).then(({ liveCallSupported, showIncomingCall }) => {
|
|
955
|
+
if (this.ringing !== pending) return;
|
|
956
|
+
this.ringing = null;
|
|
957
|
+
if (!liveCallSupported()) return;
|
|
958
|
+
if (this.call || this.callStarting) return;
|
|
959
|
+
|
|
960
|
+
const dismiss = showIncomingCall({
|
|
961
|
+
call,
|
|
962
|
+
accent: this.config.accent,
|
|
963
|
+
position: this.config.widgetPosition,
|
|
964
|
+
// Whether the page has been touched, which is when the card may ring
|
|
965
|
+
// out loud — tracked since load, which the card's own code cannot be.
|
|
966
|
+
touched: pageTouched,
|
|
967
|
+
onAnswer: () => { void this.#answer(event.bugId, call); },
|
|
968
|
+
onDecline: () => {
|
|
969
|
+
this.ringing = null;
|
|
970
|
+
declineIncomingCall({
|
|
971
|
+
apiBase: this.config.apiBase, bugId: event.bugId, callId: call.callId,
|
|
972
|
+
});
|
|
973
|
+
this.emit('incoming-call', { bugId: event.bugId, callId: call.callId, state: 'declined' });
|
|
974
|
+
},
|
|
975
|
+
// Nothing is reported here. The member who placed it is watching it ring
|
|
976
|
+
// on their own screen and their browser tells the API when it stops —
|
|
977
|
+
// the same rule as the other direction, where whoever placed the call is
|
|
978
|
+
// the one that reports nobody answered.
|
|
979
|
+
onExpire: () => {
|
|
980
|
+
this.ringing = null;
|
|
981
|
+
this.emit('incoming-call', { bugId: event.bugId, callId: call.callId, state: 'missed' });
|
|
982
|
+
},
|
|
983
|
+
});
|
|
909
984
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
985
|
+
this.ringing = { callId: call.callId, dismiss };
|
|
986
|
+
if (typeof this.config.onIncomingCall === 'function') {
|
|
987
|
+
// After the card, not instead of it. This is for a host application that
|
|
988
|
+
// wants to know — to duck its own audio, to pause a video — and not a
|
|
989
|
+
// hook that can suppress a call the person can see ringing.
|
|
990
|
+
try { this.config.onIncomingCall({ bugId: event.bugId, ...call }); } catch { /* theirs */ }
|
|
991
|
+
}
|
|
992
|
+
this.emit('incoming-call', { bugId: event.bugId, callId: call.callId, state: 'ringing' });
|
|
993
|
+
}, () => {
|
|
994
|
+
// No card can be drawn without its code. chunkUnavailable has said why in
|
|
995
|
+
// the console; the member ringing sees it go unanswered.
|
|
996
|
+
if (this.ringing === pending) this.ringing = null;
|
|
997
|
+
});
|
|
918
998
|
}
|
|
919
999
|
|
|
920
1000
|
/** Take the card away, whatever it is showing. */
|
|
@@ -940,8 +1020,12 @@ class BugCatchClient {
|
|
|
940
1020
|
this.callStarting = true;
|
|
941
1021
|
|
|
942
1022
|
let microphone;
|
|
1023
|
+
let joinLiveCall;
|
|
943
1024
|
try {
|
|
944
|
-
|
|
1025
|
+
// Already here: the card that was answered was drawn by this code.
|
|
1026
|
+
const calling = await loadCall(this.config.chunkBase);
|
|
1027
|
+
joinLiveCall = calling.joinLiveCall;
|
|
1028
|
+
microphone = await calling.openMicrophone();
|
|
945
1029
|
} catch (err) {
|
|
946
1030
|
this.callStarting = false;
|
|
947
1031
|
// Refused the prompt, or no microphone on this machine. The call is still
|
|
@@ -1201,7 +1285,7 @@ class BugCatchClient {
|
|
|
1201
1285
|
// reporter would undo the requirement.
|
|
1202
1286
|
...(session?.token ? { 'X-BugCatch-Session': session.token } : {}),
|
|
1203
1287
|
},
|
|
1204
|
-
body: JSON.stringify(payload),
|
|
1288
|
+
body: JSON.stringify(withHeldToken(payload)),
|
|
1205
1289
|
});
|
|
1206
1290
|
} catch (cause) {
|
|
1207
1291
|
// No response at all: the wrong apiBase, the API down, or CORS refusing
|
|
@@ -1227,7 +1311,13 @@ class BugCatchClient {
|
|
|
1227
1311
|
// The two refusals an integrator hits first, said in their terms.
|
|
1228
1312
|
if (res.status === 401 && !session) detail ||= 'the apiKey was not accepted';
|
|
1229
1313
|
if (res.status === 403) detail ||= 'the apiKey is not allowed to report to this projectId';
|
|
1230
|
-
|
|
1314
|
+
// Too many, too fast: the one refusal that is nobody's mistake and that
|
|
1315
|
+
// waiting fixes. Said as a sentence, because the widget puts this string
|
|
1316
|
+
// in front of the person reporting, and "report failed (HTTP 429)" reads
|
|
1317
|
+
// as though they broke something.
|
|
1318
|
+
const err = res.status === 429
|
|
1319
|
+
? new Error(`[BugCatch] ${tooManyMessage(detail)}`)
|
|
1320
|
+
: new Error(`[BugCatch] report failed (HTTP ${res.status})${detail ? ': ' + detail : ''}`);
|
|
1231
1321
|
err.status = res.status;
|
|
1232
1322
|
this.#failed('report', err);
|
|
1233
1323
|
throw err;
|
|
@@ -1321,6 +1411,38 @@ class BugCatchClient {
|
|
|
1321
1411
|
return this.agentIssueList;
|
|
1322
1412
|
}
|
|
1323
1413
|
|
|
1414
|
+
/**
|
|
1415
|
+
* What this project's team is telling the people using its widget, narrowed
|
|
1416
|
+
* to the visitor's own day and to what this browser has not closed.
|
|
1417
|
+
*
|
|
1418
|
+
* const notices = await BugCatch.announcements();
|
|
1419
|
+
* // [{ id, title, body, tone: 'info' | 'success' | 'warning' | 'critical',
|
|
1420
|
+
* // ctaLabel, ctaHref, placement: 'panel' | 'launcher', dismissible }]
|
|
1421
|
+
* BugCatch.dismissAnnouncement(notices[0]);
|
|
1422
|
+
*
|
|
1423
|
+
* Written and changed on the project's settings page, with no redeploy here;
|
|
1424
|
+
* a browser asks at most every five minutes, so a change reaches visitors
|
|
1425
|
+
* within that. The widget draws them itself — this is for a site running
|
|
1426
|
+
* `widget: false` or `announcements: false`. Empty on every failure.
|
|
1427
|
+
*/
|
|
1428
|
+
async announcements() {
|
|
1429
|
+
const list = await fetchAnnouncements({
|
|
1430
|
+
apiBase: this.config.apiBase,
|
|
1431
|
+
apiKey: this.config.apiKey,
|
|
1432
|
+
projectId: this.config.projectId,
|
|
1433
|
+
});
|
|
1434
|
+
return visibleAnnouncements(list);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Stop showing an announcement in this browser, until the team uses "Show it
|
|
1439
|
+
* again". Takes one of announcements()' results; one that may not be closed
|
|
1440
|
+
* is left alone.
|
|
1441
|
+
*/
|
|
1442
|
+
dismissAnnouncement(announcement) {
|
|
1443
|
+
rememberDismissed(announcement);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1324
1446
|
/**
|
|
1325
1447
|
* Who this browser is signed in as on this project, from storage alone.
|
|
1326
1448
|
*
|
package/src/lazy.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Where the parts of the SDK that most pages never use come from.
|
|
2
|
+
//
|
|
3
|
+
// Two parts are left out of the script every page loads: the report panel
|
|
4
|
+
// (panel/), fetched the first time the launcher is pressed — or a little
|
|
5
|
+
// before, see widget.js — and the call code (call.js), fetched when a call is
|
|
6
|
+
// offered, placed, or rings.
|
|
7
|
+
//
|
|
8
|
+
// This is the ES build's way of fetching them: import(), which the host app's
|
|
9
|
+
// bundler turns into chunks of its own and loads from wherever it put them.
|
|
10
|
+
// The script-tag build has no bundler behind it and gets umd/lazy.js in this
|
|
11
|
+
// file's place, with the same exports — see vite.config.js.
|
|
12
|
+
//
|
|
13
|
+
// Each resolves to the part's exports, and to the same promise every time once
|
|
14
|
+
// it has arrived. `chunkBase` is the script-tag build's; a bundler decides for
|
|
15
|
+
// itself where its chunks live.
|
|
16
|
+
|
|
17
|
+
import { chunkUnavailable, once } from './chunks.js';
|
|
18
|
+
|
|
19
|
+
export const loadPanel = once(() => import('./panel/index.js')
|
|
20
|
+
.catch((cause) => { throw chunkUnavailable('the report panel could not be loaded.', cause); }));
|
|
21
|
+
|
|
22
|
+
export const loadCall = once(() => import('./call.js')
|
|
23
|
+
.catch((cause) => { throw chunkUnavailable('the calling code could not be loaded.', cause); }));
|
package/src/notice.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// One announcement as a card: beside the launcher, or at the top of the panel.
|
|
2
|
+
// The rules for it are in the launcher's sheet (widget.js), because the card
|
|
3
|
+
// beside the button is drawn before the panel's code exists.
|
|
4
|
+
//
|
|
5
|
+
// Built from nodes rather than markup. Every word on it was typed into a
|
|
6
|
+
// settings page and lands on somebody else's site, and textContent cannot be
|
|
7
|
+
// talked into being HTML.
|
|
8
|
+
|
|
9
|
+
import * as ICONS from './icons.js';
|
|
10
|
+
|
|
11
|
+
const TONE_ICON = {
|
|
12
|
+
info: ICONS.info, success: ICONS.check, warning: ICONS.warn, critical: ICONS.alert,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `where` is 'launcher' or 'panel'. `onDismiss` is called with the
|
|
17
|
+
* announcement when its × is pressed; a notice that may not be closed has none.
|
|
18
|
+
*/
|
|
19
|
+
export function noticeNode(a, { where, onDismiss }) {
|
|
20
|
+
const card = document.createElement('div');
|
|
21
|
+
card.className = `notice tone-${a.tone} at-${where}`;
|
|
22
|
+
// A status, not an alert: it is news the visitor did not ask for, and a
|
|
23
|
+
// screen reader should get to it when it is done with what they were doing.
|
|
24
|
+
card.setAttribute('role', 'status');
|
|
25
|
+
|
|
26
|
+
const ico = document.createElement('span');
|
|
27
|
+
ico.className = 'notice-ico';
|
|
28
|
+
ico.setAttribute('aria-hidden', 'true');
|
|
29
|
+
ico.innerHTML = TONE_ICON[a.tone] || ICONS.info;
|
|
30
|
+
|
|
31
|
+
const text = document.createElement('div');
|
|
32
|
+
text.className = 'notice-text';
|
|
33
|
+
const title = document.createElement('strong');
|
|
34
|
+
title.textContent = a.title;
|
|
35
|
+
text.append(title);
|
|
36
|
+
if (a.body) {
|
|
37
|
+
const body = document.createElement('p');
|
|
38
|
+
body.textContent = a.body;
|
|
39
|
+
text.append(body);
|
|
40
|
+
}
|
|
41
|
+
if (a.ctaHref && a.ctaLabel) {
|
|
42
|
+
const link = document.createElement('a');
|
|
43
|
+
link.className = 'notice-cta';
|
|
44
|
+
link.href = a.ctaHref;
|
|
45
|
+
link.textContent = a.ctaLabel;
|
|
46
|
+
// A path is a page on this site and opens here, like any other link on
|
|
47
|
+
// it. An address is somewhere else, and leaving the page for it would
|
|
48
|
+
// take a half-written report with it.
|
|
49
|
+
if (/^https?:/i.test(a.ctaHref)) {
|
|
50
|
+
link.target = '_blank';
|
|
51
|
+
link.rel = 'noopener noreferrer';
|
|
52
|
+
}
|
|
53
|
+
text.append(link);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
card.append(ico, text);
|
|
57
|
+
|
|
58
|
+
if (a.dismissible && onDismiss) {
|
|
59
|
+
const close = document.createElement('button');
|
|
60
|
+
close.type = 'button';
|
|
61
|
+
close.className = 'notice-x';
|
|
62
|
+
close.setAttribute('aria-label', 'Dismiss this notice');
|
|
63
|
+
close.innerHTML = ICONS.close;
|
|
64
|
+
close.addEventListener('click', () => onDismiss(a));
|
|
65
|
+
card.append(close);
|
|
66
|
+
}
|
|
67
|
+
return card;
|
|
68
|
+
}
|