@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 +390 -115
- package/dist/bugcatch.es.js +2798 -1797
- package/dist/bugcatch.es.js.map +1 -1
- package/dist/bugcatch.umd.js +804 -450
- package/dist/bugcatch.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/identity.js +277 -0
- package/src/index.d.ts +145 -7
- package/src/index.js +326 -36
- package/src/references.js +155 -3
- package/src/updates.js +36 -6
- package/src/widget.js +1170 -456
package/package.json
CHANGED
package/src/identity.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is reporting, what they are looking at, and whether init() was given
|
|
3
|
+
* something that can work — shaped here, before anything is sent.
|
|
4
|
+
*
|
|
5
|
+
* Pure and import-free so the unit tests load it directly. Every function takes
|
|
6
|
+
* a `warn(message)` callback instead of calling console itself: the client
|
|
7
|
+
* decides whether (and how often) a warning is printed, and the tests collect
|
|
8
|
+
* them.
|
|
9
|
+
*
|
|
10
|
+
* WHY THE BUSINESS IDS HAVE NAMES. A lending app knows exactly which customer is
|
|
11
|
+
* signed in and which loan is on screen. Passed as `loanId`, `applicationId` or
|
|
12
|
+
* `customerNumber`, the API reads them as identifiers the app itself asserted
|
|
13
|
+
* (reference source `identify`), which is what lets bugcatch look the customer
|
|
14
|
+
* up in the lender's own system over MCP without asking the reporter — and lets
|
|
15
|
+
* that lookup count as a trusted join. Passed under any other name they are
|
|
16
|
+
* still sent, but only as metadata a person can read.
|
|
17
|
+
*
|
|
18
|
+
* WHERE THEY GO. The signed-in user's fields travel as `metadata.user`, which is
|
|
19
|
+
* where the API has always read `id` and where the ownership check reads `phone`
|
|
20
|
+
* and `email`. The current screen travels as `metadata.context`. Both shapes are
|
|
21
|
+
* mirrored by bugcatch-api/src/settings/reference-extract.ts identifyReferences.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Longest identifier the API keeps; anything longer is dropped there too. */
|
|
25
|
+
export const MAX_ID_CHARS = 120;
|
|
26
|
+
/** Longest free-form string value kept on user or context. */
|
|
27
|
+
export const MAX_TEXT_CHARS = 500;
|
|
28
|
+
/** Most extra (non-standard) keys kept on user or context. */
|
|
29
|
+
export const MAX_EXTRA_KEYS = 30;
|
|
30
|
+
/** Largest nested object or array kept, as JSON. */
|
|
31
|
+
const MAX_NESTED_JSON = 1000;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The identifier fields bugcatch understands, and the names each accepts.
|
|
35
|
+
* The first name is canonical; the rest are accepted and rewritten to it so the
|
|
36
|
+
* API sees one spelling.
|
|
37
|
+
*/
|
|
38
|
+
export const ID_FIELDS = {
|
|
39
|
+
loanId: ['loanId', 'loanNumber'],
|
|
40
|
+
applicationId: ['applicationId'],
|
|
41
|
+
customerNumber: ['customerNumber', 'customerId'],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const CONTROL = /[\u0000-\u001f\u007f]/;
|
|
45
|
+
|
|
46
|
+
/** An identifier: a non-empty string or finite number, as a trimmed string, or null. */
|
|
47
|
+
export function idValue(raw) {
|
|
48
|
+
if (typeof raw === 'number') return Number.isFinite(raw) ? String(raw) : null;
|
|
49
|
+
if (typeof raw !== 'string') return null;
|
|
50
|
+
const v = raw.trim();
|
|
51
|
+
if (!v || v.length > MAX_ID_CHARS || CONTROL.test(v)) return null;
|
|
52
|
+
return v;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function describe(v) {
|
|
56
|
+
if (v === null) return 'null';
|
|
57
|
+
if (Array.isArray(v)) return 'an array';
|
|
58
|
+
return typeof v === 'object' ? 'an object' : `a ${typeof v}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Read one identifier field (under any of its names) off `src`, warning on a bad value. */
|
|
62
|
+
function readId(src, canonical, names, where, warn) {
|
|
63
|
+
for (const name of names) {
|
|
64
|
+
if (!(name in src) || src[name] === undefined || src[name] === null || src[name] === '') continue;
|
|
65
|
+
const v = idValue(src[name]);
|
|
66
|
+
if (v) return v;
|
|
67
|
+
const raw = src[name];
|
|
68
|
+
warn(typeof raw === 'string' && raw.trim().length > MAX_ID_CHARS
|
|
69
|
+
? `${where}.${name} is longer than ${MAX_ID_CHARS} characters and was dropped — pass the identifier itself, not a sentence or a URL.`
|
|
70
|
+
: `${where}.${name} should be a string or number, got ${describe(raw)} — it was dropped.`);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** An extra field worth keeping: a primitive, or a small plain object / array. */
|
|
76
|
+
function extraValue(v) {
|
|
77
|
+
if (v === null || typeof v === 'boolean') return { ok: true, value: v };
|
|
78
|
+
if (typeof v === 'number') return Number.isFinite(v) ? { ok: true, value: v } : { ok: false };
|
|
79
|
+
if (typeof v === 'string') return { ok: true, value: v.slice(0, MAX_TEXT_CHARS) };
|
|
80
|
+
if (typeof v === 'object') {
|
|
81
|
+
try {
|
|
82
|
+
const json = JSON.stringify(v);
|
|
83
|
+
if (json && json.length <= MAX_NESTED_JSON) return { ok: true, value: JSON.parse(json) };
|
|
84
|
+
} catch { /* cyclic or otherwise unserialisable */ }
|
|
85
|
+
}
|
|
86
|
+
return { ok: false };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const USER_TEXT = ['email', 'name', 'phone'];
|
|
90
|
+
const RESERVED_USER = new Set(['id', 'userId', ...USER_TEXT, ...Object.values(ID_FIELDS).flat()]);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* identify()'s argument, as it will be sent under `metadata.user`.
|
|
94
|
+
*
|
|
95
|
+
* Returns null for null/undefined (sign-out) and for anything that is not a
|
|
96
|
+
* plain object, with a warning for the latter. Never throws.
|
|
97
|
+
*
|
|
98
|
+
* id string | number the customer's own account id in your system
|
|
99
|
+
* email string threads their reports; where "resolved" mail goes
|
|
100
|
+
* name string
|
|
101
|
+
* phone string | number used, with email, to confirm the reporter owns
|
|
102
|
+
* the record bugcatch looked up
|
|
103
|
+
* loanId string | number (alias loanNumber)
|
|
104
|
+
* applicationId string | number
|
|
105
|
+
* customerNumber string | number (alias customerId)
|
|
106
|
+
*
|
|
107
|
+
* Anything else is kept as readable metadata, within limits.
|
|
108
|
+
*/
|
|
109
|
+
export function normalizeUser(user, warn = () => {}) {
|
|
110
|
+
if (user === null || user === undefined) return null;
|
|
111
|
+
if (typeof user !== 'object' || Array.isArray(user)) {
|
|
112
|
+
warn(`identify() takes an object like { id, email, name } or null, got ${describe(user)} — ignored.`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const out = {};
|
|
116
|
+
try {
|
|
117
|
+
const id = readId(user, 'id', ['id', 'userId'], 'identify()', warn);
|
|
118
|
+
if (id) out.id = id;
|
|
119
|
+
for (const field of USER_TEXT) {
|
|
120
|
+
const raw = user[field];
|
|
121
|
+
if (raw === undefined || raw === null || raw === '') continue;
|
|
122
|
+
if (field === 'phone' && typeof raw === 'number' && Number.isFinite(raw)) { out.phone = String(raw); continue; }
|
|
123
|
+
if (typeof raw !== 'string') {
|
|
124
|
+
warn(`identify().${field} should be a string, got ${describe(raw)} — it was dropped.`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const v = raw.trim().slice(0, field === 'name' ? 200 : MAX_ID_CHARS);
|
|
128
|
+
if (!v) continue;
|
|
129
|
+
if (field === 'email' && !v.includes('@')) {
|
|
130
|
+
warn(`identify().email "${v}" does not look like an email address — reports cannot be threaded or followed up without one.`);
|
|
131
|
+
}
|
|
132
|
+
out[field] = v;
|
|
133
|
+
}
|
|
134
|
+
for (const [canonical, names] of Object.entries(ID_FIELDS)) {
|
|
135
|
+
const v = readId(user, canonical, names, 'identify()', warn);
|
|
136
|
+
if (v) out[canonical] = v;
|
|
137
|
+
}
|
|
138
|
+
let extras = 0;
|
|
139
|
+
for (const [key, raw] of Object.entries(user)) {
|
|
140
|
+
if (RESERVED_USER.has(key) || raw === undefined || typeof raw === 'function') continue;
|
|
141
|
+
if (extras >= MAX_EXTRA_KEYS) { warn(`identify() was given more than ${MAX_EXTRA_KEYS} extra fields; the rest were dropped.`); break; }
|
|
142
|
+
const kept = extraValue(raw);
|
|
143
|
+
if (!kept.ok) { warn(`identify().${key} could not be sent (${describe(raw)} too large or not serialisable) — dropped.`); continue; }
|
|
144
|
+
out[key] = kept.value;
|
|
145
|
+
extras += 1;
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// A getter that throws, a proxy — whatever it was, the report goes out with
|
|
149
|
+
// what was read before it.
|
|
150
|
+
}
|
|
151
|
+
if (!out.id && !out.email) {
|
|
152
|
+
warn('identify() was called without an id or email — reports will not be attributed to anyone, and nothing can be looked up by user.');
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* setContext()'s argument merged over what was already set, as it will be sent
|
|
159
|
+
* under `metadata.context`.
|
|
160
|
+
*
|
|
161
|
+
* `null` for the patch clears everything; a key set to null/undefined removes
|
|
162
|
+
* that key. Identifier fields are validated exactly as identify()'s are and
|
|
163
|
+
* rewritten to their canonical names; everything else (screen, route, feature
|
|
164
|
+
* flag…) is kept within limits. Never throws.
|
|
165
|
+
*/
|
|
166
|
+
export function mergeContext(prev, patch, warn = () => {}) {
|
|
167
|
+
if (patch === null) return {};
|
|
168
|
+
const out = { ...(prev || {}) };
|
|
169
|
+
if (patch === undefined) return out;
|
|
170
|
+
if (typeof patch !== 'object' || Array.isArray(patch)) {
|
|
171
|
+
warn(`setContext() takes an object like { loanId, screen } or null, got ${describe(patch)} — ignored.`);
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const aliases = new Map();
|
|
176
|
+
for (const [canonical, names] of Object.entries(ID_FIELDS)) for (const n of names) aliases.set(n, canonical);
|
|
177
|
+
for (const [key, raw] of Object.entries(patch)) {
|
|
178
|
+
const canonical = aliases.get(key);
|
|
179
|
+
if (raw === null || raw === undefined) { delete out[canonical || key]; continue; }
|
|
180
|
+
if (canonical) {
|
|
181
|
+
const v = readId({ [key]: raw }, canonical, [key], 'setContext()', warn);
|
|
182
|
+
if (v) out[canonical] = v; else delete out[canonical];
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (typeof raw === 'function') continue;
|
|
186
|
+
if (!(key in out) && Object.keys(out).length >= MAX_EXTRA_KEYS) {
|
|
187
|
+
warn(`setContext() holds at most ${MAX_EXTRA_KEYS} keys; "${key}" was dropped.`);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const kept = extraValue(raw);
|
|
191
|
+
if (!kept.ok) { warn(`setContext().${key} could not be sent (${describe(raw)} too large or not serialisable) — dropped.`); continue; }
|
|
192
|
+
out[key] = kept.value;
|
|
193
|
+
}
|
|
194
|
+
} catch { /* keep what was merged so far */ }
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Every init() option this SDK reads. Anything else is probably a typo. */
|
|
199
|
+
export const KNOWN_OPTIONS = [
|
|
200
|
+
'apiBase', 'apiKey', 'projectId', 'consoleLimit', 'networkLimit', 'captureConsole',
|
|
201
|
+
'captureNetwork', 'captureErrors', 'widget', 'widgetPosition', 'theme', 'accent',
|
|
202
|
+
'attachments', 'maxAttachments', 'maxAttachmentBytes', 'maxRecordingMs', 'beforeSend',
|
|
203
|
+
'user', 'context', 'updates', 'updateToast', 'onUpdate', 'voice', 'onCallState',
|
|
204
|
+
'onIncomingCall', 'onReportSubmitted', 'onAssistantPhase', 'onError', 'debug',
|
|
205
|
+
'drafts', 'knownIssues', 'release', 'environment', 'buildId',
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const CALLBACK_OPTIONS = [
|
|
209
|
+
'beforeSend', 'onUpdate', 'onCallState', 'onIncomingCall', 'onReportSubmitted',
|
|
210
|
+
'onAssistantPhase', 'onError',
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
/** The closest known option to a misspelt one, when it is close enough to suggest. */
|
|
214
|
+
function suggest(name) {
|
|
215
|
+
const lower = name.toLowerCase().replace(/[-_]/g, '');
|
|
216
|
+
return KNOWN_OPTIONS.find((k) => k.toLowerCase() === lower) || null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const PLACEHOLDER = /^(your[_-]|<|xxx|todo|changeme)/i;
|
|
220
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* What is wrong with init()'s options, as two lists: `errors` stop init (it
|
|
224
|
+
* throws the first), `warnings` are printed and init carries on.
|
|
225
|
+
*
|
|
226
|
+
* `pageHost` is the page's hostname, passed in so this stays pure; it is what
|
|
227
|
+
* tells a production page still pointed at the localhost default apart from a
|
|
228
|
+
* developer's machine.
|
|
229
|
+
*/
|
|
230
|
+
export function checkOptions(options, { pageHost = '' } = {}) {
|
|
231
|
+
const errors = [];
|
|
232
|
+
const warnings = [];
|
|
233
|
+
if (!options || typeof options !== 'object') {
|
|
234
|
+
errors.push('init() takes an options object: BugCatch.init({ apiBase, apiKey, projectId }).');
|
|
235
|
+
return { errors, warnings };
|
|
236
|
+
}
|
|
237
|
+
const { apiKey, projectId, apiBase } = options;
|
|
238
|
+
|
|
239
|
+
if (!apiKey) errors.push('apiKey is required — create one under Workspace Settings → API Keys.');
|
|
240
|
+
else if (typeof apiKey !== 'string') errors.push(`apiKey must be a string, got ${describe(apiKey)}.`);
|
|
241
|
+
else if (PLACEHOLDER.test(apiKey)) errors.push(`apiKey is still the placeholder "${apiKey}" — paste the key you saved when you created it.`);
|
|
242
|
+
else if (UUID.test(apiKey)) warnings.push('apiKey looks like a project id — did you swap apiKey and projectId?');
|
|
243
|
+
else if (!apiKey.startsWith('bc_')) warnings.push('apiKey does not look like a BugCatch key (they start with "bc_live_") — reports will be refused with HTTP 401.');
|
|
244
|
+
|
|
245
|
+
if (!projectId) errors.push('projectId is required — copy it from Project Settings → Web SDK.');
|
|
246
|
+
else if (typeof projectId !== 'string') errors.push(`projectId must be a string, got ${describe(projectId)}.`);
|
|
247
|
+
else if (PLACEHOLDER.test(projectId)) errors.push(`projectId is still the placeholder "${projectId}".`);
|
|
248
|
+
else if (projectId.startsWith('bc_')) warnings.push('projectId looks like an API key — did you swap apiKey and projectId?');
|
|
249
|
+
else if (/\s/.test(projectId)) warnings.push('projectId contains whitespace — check it was copied cleanly.');
|
|
250
|
+
|
|
251
|
+
if (apiBase === undefined || apiBase === null) {
|
|
252
|
+
const local = !pageHost || /^(localhost|127\.|0\.0\.0\.0|\[::1\])/.test(pageHost);
|
|
253
|
+
if (!local) warnings.push('apiBase is not set, so reports go to http://localhost:3000 — set apiBase to your BugCatch API URL.');
|
|
254
|
+
} else if (typeof apiBase !== 'string' || !/^https?:\/\//.test(apiBase)) {
|
|
255
|
+
warnings.push(`apiBase should be an absolute http(s) URL, got ${JSON.stringify(apiBase)}.`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const key of Object.keys(options)) {
|
|
259
|
+
if (KNOWN_OPTIONS.includes(key)) continue;
|
|
260
|
+
const near = suggest(key);
|
|
261
|
+
warnings.push(near
|
|
262
|
+
? `unknown option "${key}" — did you mean "${near}"?`
|
|
263
|
+
: `unknown option "${key}" is ignored.`);
|
|
264
|
+
}
|
|
265
|
+
for (const key of CALLBACK_OPTIONS) {
|
|
266
|
+
const v = options[key];
|
|
267
|
+
if (v !== undefined && v !== null && typeof v !== 'function') {
|
|
268
|
+
warnings.push(`${key} should be a function, got ${describe(v)} — it will not be called.`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { errors, warnings };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Drop a trailing slash so `${apiBase}/ingest/bugs` never has two. */
|
|
275
|
+
export function cleanApiBase(apiBase) {
|
|
276
|
+
return typeof apiBase === 'string' ? apiBase.replace(/\/+$/, '') : apiBase;
|
|
277
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,8 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Who is using your app
|
|
3
|
-
*
|
|
2
|
+
* Who is using your app, as identify() takes it.
|
|
3
|
+
*
|
|
4
|
+
* `id`, `email` and `phone` have behaviour attached; so do the three business
|
|
5
|
+
* identifiers, which bugcatch reads as identifiers *your app* asserted — trusted
|
|
6
|
+
* join keys it uses to look the customer up in your own system (over MCP)
|
|
7
|
+
* without asking the reporter. Anything else is kept as readable metadata.
|
|
8
|
+
*/
|
|
9
|
+
export interface BugCatchUser {
|
|
10
|
+
/** The customer's own account id in your system — never a staff or agent id. */
|
|
11
|
+
id?: string | number;
|
|
12
|
+
/** Threads their reports together; where "your issue is resolved" mail goes. */
|
|
13
|
+
email?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
/**
|
|
16
|
+
* With `email`, what bugcatch compares against the record it looked up to
|
|
17
|
+
* confirm the reporter owns it. Include the country code.
|
|
18
|
+
*/
|
|
19
|
+
phone?: string | number;
|
|
20
|
+
/** A loan this customer holds. Looked up as a loan. Alias: `loanNumber`. */
|
|
21
|
+
loanId?: string | number;
|
|
22
|
+
/** A loan application. Looked up as a loan. */
|
|
23
|
+
applicationId?: string | number;
|
|
24
|
+
/** Your customer / client number. Looked up as a customer. Alias: `customerId`. */
|
|
25
|
+
customerNumber?: string | number;
|
|
26
|
+
/** @deprecated alias of `customerNumber`. */
|
|
27
|
+
customerId?: string | number;
|
|
28
|
+
/** @deprecated alias of `loanId`. */
|
|
29
|
+
loanNumber?: string | number;
|
|
30
|
+
/** Extra fields: strings, numbers, booleans or small objects, sent as metadata. */
|
|
31
|
+
[extra: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What the reporter is looking at, as setContext() takes it. The three
|
|
36
|
+
* identifier fields are read exactly as on identify() and outrank them; set one
|
|
37
|
+
* to null to remove it.
|
|
4
38
|
*/
|
|
5
|
-
export interface
|
|
39
|
+
export interface BugCatchContext {
|
|
40
|
+
loanId?: string | number | null;
|
|
41
|
+
applicationId?: string | number | null;
|
|
42
|
+
customerNumber?: string | number | null;
|
|
43
|
+
/** Anything else — screen, route, step, a feature flag. Readable metadata only. */
|
|
44
|
+
[extra: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Where the AI assistant working a report has got to. */
|
|
48
|
+
export type AssistantPhase =
|
|
49
|
+
| 'analyzing' | 'needs_info' | 'confirming' | 'reviewing' | 'resolving' | 'resolved';
|
|
50
|
+
|
|
51
|
+
export interface AssistantPhaseEvent {
|
|
52
|
+
bugId: string;
|
|
53
|
+
phase: AssistantPhase;
|
|
54
|
+
/** A short heading written for the reporter, e.g. "Root cause found". */
|
|
55
|
+
title: string | null;
|
|
56
|
+
detail: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ReportSubmittedEvent {
|
|
60
|
+
bugId: string;
|
|
61
|
+
/** Null for a report filed by pressing Call — it is titled from the call. */
|
|
62
|
+
title: string | null;
|
|
63
|
+
via: 'form' | 'call';
|
|
64
|
+
result: ReportResult;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface BugCatchErrorEvent {
|
|
68
|
+
action: 'report' | 'call';
|
|
69
|
+
error: Error & { status?: number };
|
|
70
|
+
}
|
|
6
71
|
|
|
7
72
|
/**
|
|
8
73
|
* What a bug *report* may carry. Narrower than what may be uploaded: a report's
|
|
@@ -53,13 +118,39 @@ export interface JoinedIssue {
|
|
|
53
118
|
}
|
|
54
119
|
|
|
55
120
|
export interface BugCatchConfig {
|
|
121
|
+
/**
|
|
122
|
+
* Log what the SDK does — identify, context, each report's shape — to the
|
|
123
|
+
* console. Default false. Misconfiguration warnings print either way.
|
|
124
|
+
*/
|
|
125
|
+
debug?: boolean;
|
|
126
|
+
/** Context for the first screen; the same as calling setContext() after init. */
|
|
127
|
+
context?: BugCatchContext | null;
|
|
128
|
+
/** A report was accepted (widget, report() or Call). Also the 'report' event. */
|
|
129
|
+
onReportSubmitted?: ((event: ReportSubmittedEvent) => void) | null;
|
|
130
|
+
/**
|
|
131
|
+
* The AI assistant working a report moved phase. Seen whenever the report is
|
|
132
|
+
* fetched: by the widget while its thread is open, or by myReport(). Also the
|
|
133
|
+
* 'assistant' event.
|
|
134
|
+
*/
|
|
135
|
+
onAssistantPhase?: ((event: AssistantPhaseEvent) => void) | null;
|
|
136
|
+
/** A report or call the SDK filed failed. Also the 'error' event. */
|
|
137
|
+
onError?: ((event: BugCatchErrorEvent) => void) | null;
|
|
56
138
|
/** Recover unfinished widget reports on this device for seven days. Default true. */
|
|
57
139
|
drafts?: boolean;
|
|
140
|
+
/** Your app's version, attached to every report. */
|
|
58
141
|
release?: string;
|
|
142
|
+
/** e.g. 'production' / 'staging', attached to every report. */
|
|
59
143
|
environment?: string;
|
|
60
144
|
buildId?: string;
|
|
145
|
+
/**
|
|
146
|
+
* Your BugCatch API URL, e.g. 'https://api.bugcatch.example'. Default
|
|
147
|
+
* 'http://localhost:3000' — set it anywhere but local development. A trailing
|
|
148
|
+
* slash is removed.
|
|
149
|
+
*/
|
|
61
150
|
apiBase?: string;
|
|
151
|
+
/** A workspace API key (bc_live_…), from Workspace Settings → API Keys. */
|
|
62
152
|
apiKey: string;
|
|
153
|
+
/** The project reports land in, from Project Settings → Web SDK. */
|
|
63
154
|
projectId: string;
|
|
64
155
|
consoleLimit?: number;
|
|
65
156
|
networkLimit?: number;
|
|
@@ -235,6 +326,11 @@ export interface ReportInput {
|
|
|
235
326
|
* form and must ask their own version of that question.
|
|
236
327
|
*/
|
|
237
328
|
references?: ReportReference[];
|
|
329
|
+
/**
|
|
330
|
+
* Context for this report only, merged over setContext()'s without changing
|
|
331
|
+
* it — e.g. the loan a "report a problem with this loan" button belongs to.
|
|
332
|
+
*/
|
|
333
|
+
context?: BugCatchContext;
|
|
238
334
|
}
|
|
239
335
|
|
|
240
336
|
export interface ReportReference {
|
|
@@ -426,6 +522,17 @@ export interface ReportDetail {
|
|
|
426
522
|
* whose send button would fail.
|
|
427
523
|
*/
|
|
428
524
|
canReply: boolean;
|
|
525
|
+
/**
|
|
526
|
+
* Where the AI assistant is on this report, written for the reporter. Null
|
|
527
|
+
* when no assistant is working it. Fetching a report also fires
|
|
528
|
+
* onAssistantPhase / the 'assistant' event when the phase has changed.
|
|
529
|
+
*/
|
|
530
|
+
assistant?: {
|
|
531
|
+
phase: AssistantPhase;
|
|
532
|
+
title: string;
|
|
533
|
+
detail: string;
|
|
534
|
+
steps: { label: string; state: string }[];
|
|
535
|
+
} | null;
|
|
429
536
|
}
|
|
430
537
|
|
|
431
538
|
/**
|
|
@@ -461,6 +568,12 @@ export declare class BugCatchClient {
|
|
|
461
568
|
* Several listeners can coexist, unlike the single `onUpdate` config hook.
|
|
462
569
|
*/
|
|
463
570
|
on(event: 'update', handler: (update: ReportUpdate) => void): () => void;
|
|
571
|
+
/** A report was accepted — from the widget, report(), or pressing Call. */
|
|
572
|
+
on(event: 'report', handler: (event: ReportSubmittedEvent) => void): () => void;
|
|
573
|
+
/** The AI assistant working a report moved phase (seen when the report is fetched). */
|
|
574
|
+
on(event: 'assistant', handler: (event: AssistantPhaseEvent) => void): () => void;
|
|
575
|
+
/** A report or call the SDK filed failed. */
|
|
576
|
+
on(event: 'error', handler: (event: BugCatchErrorEvent) => void): () => void;
|
|
464
577
|
on(
|
|
465
578
|
event: 'call',
|
|
466
579
|
handler: (detail: {
|
|
@@ -495,7 +608,7 @@ export declare class BugCatchClient {
|
|
|
495
608
|
on(event: 'signout', handler: (who: { email: string | null }) => void): () => void;
|
|
496
609
|
off(event: 'update', handler: (update: ReportUpdate) => void): this;
|
|
497
610
|
off(
|
|
498
|
-
event: 'call' | 'unread' | 'incoming-call' | 'signin' | 'signout',
|
|
611
|
+
event: 'call' | 'unread' | 'incoming-call' | 'signin' | 'signout' | 'report' | 'assistant' | 'error',
|
|
499
612
|
handler: (payload: any) => void,
|
|
500
613
|
): this;
|
|
501
614
|
/**
|
|
@@ -630,8 +743,9 @@ export declare class BugCatchClient {
|
|
|
630
743
|
* single slot, because there is a single microphone.
|
|
631
744
|
*/
|
|
632
745
|
endCall(): this;
|
|
633
|
-
/**
|
|
746
|
+
/** Tell the team whether their fix worked. STILL_HAPPENING re-sends this browser's context. */
|
|
634
747
|
verifyFix(bugId: string, feedback: { result: 'CONFIRMED' | 'STILL_HAPPENING'; resolvedAt: string; note?: string }): Promise<{ verification: string; status: string }>;
|
|
748
|
+
/** One report with its full timeline, or null if this browser cannot prove it filed it. */
|
|
635
749
|
myReport(bugId: string): Promise<ReportDetail | null>;
|
|
636
750
|
/**
|
|
637
751
|
* Write back on a report this browser filed. The widget's own conversation
|
|
@@ -664,6 +778,24 @@ export declare class BugCatchClient {
|
|
|
664
778
|
* Pass null on sign-out.
|
|
665
779
|
*/
|
|
666
780
|
identify(user: BugCatchUser | null): this;
|
|
781
|
+
/**
|
|
782
|
+
* Say what the reporter is looking at — the loan, application or customer on
|
|
783
|
+
* this screen. Merged over what was set before and kept for the life of the
|
|
784
|
+
* page; a key set to null is removed, and `setContext(null)` clears it all.
|
|
785
|
+
*/
|
|
786
|
+
setContext(context: BugCatchContext | null): this;
|
|
787
|
+
/** A copy of what setContext() holds. */
|
|
788
|
+
getContext(): BugCatchContext;
|
|
789
|
+
/** Open the built-in report panel (needs `widget: true`). */
|
|
790
|
+
open(): this;
|
|
791
|
+
/** Close the report panel if it is open. */
|
|
792
|
+
close(): this;
|
|
793
|
+
isOpen(): boolean;
|
|
794
|
+
/**
|
|
795
|
+
* File a report with everything captured so far. Rejects with an Error
|
|
796
|
+
* (carrying `.status` for an HTTP refusal) and emits 'error'; resolves to
|
|
797
|
+
* `{ cancelled: true }` when beforeSend returned false.
|
|
798
|
+
*/
|
|
667
799
|
report(input: ReportInput): Promise<ReportResult | { cancelled: true }>;
|
|
668
800
|
/** Uploads straight to storage and resolves the key a report references. */
|
|
669
801
|
uploadAttachment(
|
|
@@ -703,7 +835,13 @@ export declare class BugCatchClient {
|
|
|
703
835
|
* neither did. Rejects when there is none.
|
|
704
836
|
*/
|
|
705
837
|
meToo(bugId: string, who?: { email?: string; name?: string }): Promise<JoinedIssue>;
|
|
706
|
-
snapshot(): {
|
|
838
|
+
snapshot(): {
|
|
839
|
+
user: BugCatchUser | null;
|
|
840
|
+
context: BugCatchContext;
|
|
841
|
+
console: any[];
|
|
842
|
+
network: any[];
|
|
843
|
+
environment: Record<string, any>;
|
|
844
|
+
};
|
|
707
845
|
/**
|
|
708
846
|
* Everything this browser has reported, newest first, with each one's
|
|
709
847
|
* status. Resolves to an empty history when nothing has been filed here.
|
|
@@ -719,5 +857,5 @@ export declare class BugCatchClient {
|
|
|
719
857
|
destroy(): void;
|
|
720
858
|
}
|
|
721
859
|
|
|
722
|
-
declare const BugCatch: BugCatchClient & { Client: typeof BugCatchClient };
|
|
860
|
+
declare const BugCatch: BugCatchClient & { Client: typeof BugCatchClient; version: string };
|
|
723
861
|
export default BugCatch;
|