@sphoro/bugcatch-web-sdk 0.14.3 → 0.15.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 +2633 -1796
- package/dist/bugcatch.es.js.map +1 -1
- package/dist/bugcatch.umd.js +786 -426
- 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 +243 -28
- package/src/widget.js +1019 -394
package/src/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { joinLiveCall, liveCallSupported, openMicrophone } from './live-call.js';
|
|
20
20
|
import { showIncomingCall } from './incoming-call.js';
|
|
21
21
|
import { referenceFields } from './references.js';
|
|
22
|
+
import { normalizeUser, mergeContext, checkOptions, cleanApiBase } from './identity.js';
|
|
22
23
|
|
|
23
24
|
// Replaced at build time from package.json. The guard is for anyone importing
|
|
24
25
|
// src/ directly, where the define never runs.
|
|
@@ -78,6 +79,25 @@ const DEFAULTS = {
|
|
|
78
79
|
// that can suppress a call the person can see ringing. Set `voice: false` to
|
|
79
80
|
// switch the whole thing off.
|
|
80
81
|
onIncomingCall: null,
|
|
82
|
+
// Called with { bugId, title, via: 'form' | 'call', result } once a report
|
|
83
|
+
// has been accepted — from the widget, from report(), or from pressing Call.
|
|
84
|
+
// The same thing is emitted as the 'report' event.
|
|
85
|
+
onReportSubmitted: null,
|
|
86
|
+
// Called with { bugId, phase, title, detail } when the AI assistant working
|
|
87
|
+
// a report moves: 'analyzing' → 'needs_info' | 'confirming' | 'reviewing' |
|
|
88
|
+
// 'resolving' | 'resolved'. Seen whenever the report is fetched — the widget
|
|
89
|
+
// does while it is open; myReport(id) does for an integration's own view.
|
|
90
|
+
onAssistantPhase: null,
|
|
91
|
+
// Called with { action, error } when something the SDK did on the page's
|
|
92
|
+
// behalf failed (a report that was refused, a listener that threw). Also the
|
|
93
|
+
// 'error' event.
|
|
94
|
+
onError: null,
|
|
95
|
+
// Business context for the screen the SDK starts on — see setContext().
|
|
96
|
+
context: null,
|
|
97
|
+
// Log what the SDK is doing (identify, context, each report's shape) to the
|
|
98
|
+
// console. Off in production; the warnings about misconfiguration are
|
|
99
|
+
// printed either way.
|
|
100
|
+
debug: false,
|
|
81
101
|
};
|
|
82
102
|
|
|
83
103
|
class BugCatchClient {
|
|
@@ -105,6 +125,40 @@ class BugCatchClient {
|
|
|
105
125
|
// event -> Set<handler>. Several parts of a host app can listen without
|
|
106
126
|
// having to share the single `onUpdate` config callback between them.
|
|
107
127
|
this.listeners = new Map();
|
|
128
|
+
// What setContext() has said about the screen the reporter is on. Its own
|
|
129
|
+
// field rather than part of config, so init() — which rebuilds config —
|
|
130
|
+
// cannot wipe a context set before it ran.
|
|
131
|
+
this.context = {};
|
|
132
|
+
// Warnings already printed, so a misconfiguration in a render loop says so
|
|
133
|
+
// once rather than a thousand times.
|
|
134
|
+
this.warned = new Set();
|
|
135
|
+
// The last assistant phase seen per report, so onAssistantPhase fires on a
|
|
136
|
+
// change rather than on every poll.
|
|
137
|
+
this.phases = new Map();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A misconfiguration, said once. Always printed — it is what saves a day of guessing. */
|
|
141
|
+
warn(message) {
|
|
142
|
+
if (this.warned.has(message)) return;
|
|
143
|
+
this.warned.add(message);
|
|
144
|
+
// eslint-disable-next-line no-console
|
|
145
|
+
console.warn(`[BugCatch] ${message}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** What the SDK is doing, for `debug: true`. */
|
|
149
|
+
debugLog(...args) {
|
|
150
|
+
// eslint-disable-next-line no-console
|
|
151
|
+
if (this.config.debug) console.info('[BugCatch]', ...args);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Call a host callback without letting it break the SDK. */
|
|
155
|
+
#callback(name, payload) {
|
|
156
|
+
const fn = this.config[name];
|
|
157
|
+
if (typeof fn !== 'function') return;
|
|
158
|
+
try { fn(payload); } catch (err) {
|
|
159
|
+
// eslint-disable-next-line no-console
|
|
160
|
+
console.error(`[BugCatch] your ${name} callback threw:`, err);
|
|
161
|
+
}
|
|
108
162
|
}
|
|
109
163
|
|
|
110
164
|
init(options = {}) {
|
|
@@ -112,10 +166,21 @@ class BugCatchClient {
|
|
|
112
166
|
console.warn('[BugCatch] init() called twice — ignoring the second call.');
|
|
113
167
|
return this;
|
|
114
168
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
169
|
+
const { errors, warnings } = checkOptions(options, {
|
|
170
|
+
pageHost: typeof location !== 'undefined' ? location.hostname : '',
|
|
171
|
+
});
|
|
172
|
+
if (errors.length) throw new Error(`[BugCatch] ${errors[0]}`);
|
|
173
|
+
for (const w of warnings) this.warn(w);
|
|
174
|
+
|
|
175
|
+
// identify() and setContext() may run before init() — an app that knows its
|
|
176
|
+
// user before it has loaded its config is ordinary. Rebuilding config here
|
|
177
|
+
// used to throw that user away; it is carried over unless init names one.
|
|
178
|
+
const earlier = this.config.user;
|
|
179
|
+
this.config = { ...DEFAULTS, ...options, apiBase: cleanApiBase(options.apiBase ?? DEFAULTS.apiBase) };
|
|
180
|
+
this.config.user = options.user !== undefined
|
|
181
|
+
? normalizeUser(options.user, (m) => this.warn(m.replace('identify()', 'init({ user })')))
|
|
182
|
+
: earlier || null;
|
|
183
|
+
if (options.context) this.setContext(options.context);
|
|
119
184
|
|
|
120
185
|
this.consoleBuffer = createRingBuffer(this.config.consoleLimit);
|
|
121
186
|
this.networkBuffer = createRingBuffer(this.config.networkLimit);
|
|
@@ -141,6 +206,10 @@ class BugCatchClient {
|
|
|
141
206
|
}
|
|
142
207
|
|
|
143
208
|
this.started = true;
|
|
209
|
+
this.debugLog('initialised', {
|
|
210
|
+
version: SDK_IDENTITY.version, apiBase: this.config.apiBase, projectId: this.config.projectId,
|
|
211
|
+
user: this.config.user, context: this.context,
|
|
212
|
+
});
|
|
144
213
|
return this;
|
|
145
214
|
}
|
|
146
215
|
|
|
@@ -223,7 +292,7 @@ class BugCatchClient {
|
|
|
223
292
|
} catch (err) {
|
|
224
293
|
// Someone else's handler, on someone else's page.
|
|
225
294
|
// eslint-disable-next-line no-console
|
|
226
|
-
console.error(
|
|
295
|
+
console.error(`[BugCatch] a '${event}' listener threw:`, err);
|
|
227
296
|
}
|
|
228
297
|
}
|
|
229
298
|
}
|
|
@@ -327,8 +396,44 @@ class BugCatchClient {
|
|
|
327
396
|
return verifyFix({ apiBase: this.config.apiBase, bugId, result, resolvedAt, note, metadata: context.metadata, logs: context.logs });
|
|
328
397
|
}
|
|
329
398
|
|
|
330
|
-
myReport(bugId) {
|
|
331
|
-
|
|
399
|
+
async myReport(bugId) {
|
|
400
|
+
const report = await fetchReport({ apiBase: this.config.apiBase, bugId });
|
|
401
|
+
this.#notePhase(bugId, report?.assistant);
|
|
402
|
+
return report;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Tell the host app the assistant moved, once per change.
|
|
407
|
+
*
|
|
408
|
+
* The phase is only known when a report is fetched, which the widget does
|
|
409
|
+
* every few seconds while its thread is open and an integration does through
|
|
410
|
+
* myReport(). The first sighting counts as a change: a host that has just
|
|
411
|
+
* opened a report wants to know it is being analysed.
|
|
412
|
+
*/
|
|
413
|
+
#notePhase(bugId, assistant) {
|
|
414
|
+
const phase = assistant?.phase;
|
|
415
|
+
if (!bugId || typeof phase !== 'string') return;
|
|
416
|
+
if (this.phases.get(bugId) === phase) return;
|
|
417
|
+
this.phases.set(bugId, phase);
|
|
418
|
+
const detail = { bugId, phase, title: assistant.title ?? null, detail: assistant.detail ?? null };
|
|
419
|
+
this.debugLog('assistant', detail);
|
|
420
|
+
this.#callback('onAssistantPhase', detail);
|
|
421
|
+
this.emit('assistant', detail);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** A report was accepted: tell the host app, however it was filed. */
|
|
425
|
+
#submitted(result, { title, via }) {
|
|
426
|
+
if (!result?.id) return;
|
|
427
|
+
const detail = { bugId: result.id, title: title ?? null, via, result };
|
|
428
|
+
this.debugLog('report accepted', detail);
|
|
429
|
+
this.#callback('onReportSubmitted', detail);
|
|
430
|
+
this.emit('report', detail);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Something done on the page's behalf failed. */
|
|
434
|
+
#failed(action, error) {
|
|
435
|
+
this.#callback('onError', { action, error });
|
|
436
|
+
this.emit('error', { action, error });
|
|
332
437
|
}
|
|
333
438
|
|
|
334
439
|
/**
|
|
@@ -356,7 +461,9 @@ class BugCatchClient {
|
|
|
356
461
|
* copies of this would drift, and the half that drifted would be the one that
|
|
357
462
|
* stopped attaching the console.
|
|
358
463
|
*/
|
|
359
|
-
async #payload({ title, description = '', priority = 'MEDIUM', metadata = {}, media = [], references = [] }) {
|
|
464
|
+
async #payload({ title, description = '', priority = 'MEDIUM', metadata = {}, media = [], references = [], context = null }) {
|
|
465
|
+
// This report's own context over the screen's, without keeping it.
|
|
466
|
+
const ctx = context ? mergeContext(this.context, context, (m) => this.warn(m.replace('setContext()', 'report({ context })'))) : this.context;
|
|
360
467
|
return {
|
|
361
468
|
projectId: this.config.projectId,
|
|
362
469
|
...(title ? { title } : {}),
|
|
@@ -373,7 +480,13 @@ class BugCatchClient {
|
|
|
373
480
|
environment: this.config.environment,
|
|
374
481
|
buildId: this.config.buildId,
|
|
375
482
|
...metadata,
|
|
483
|
+
// Who is signed in, as identify() shaped it: `id`, `email` and `phone`
|
|
484
|
+
// are where the API and the ownership check read them, and `loanId`,
|
|
485
|
+
// `applicationId`, `customerNumber` become trusted lookup keys.
|
|
376
486
|
user: this.config.user || undefined,
|
|
487
|
+
// The screen the report came from — setContext(). Its identifiers
|
|
488
|
+
// outrank the same kind on `user`.
|
|
489
|
+
...(Object.keys(ctx).length ? { context: ctx } : {}),
|
|
377
490
|
sdk: SDK_IDENTITY,
|
|
378
491
|
},
|
|
379
492
|
logs: {
|
|
@@ -417,8 +530,10 @@ class BugCatchClient {
|
|
|
417
530
|
// `report()` drops it: the widget re-reads state every time it opens, so
|
|
418
531
|
// forgetting it now is what makes it draw the sign-in step instead.
|
|
419
532
|
if (err.status === 401 && session) clearSession(this.config.projectId);
|
|
533
|
+
this.#failed('call', err);
|
|
420
534
|
throw err;
|
|
421
535
|
}
|
|
536
|
+
this.#submitted(filed, { title: null, via: 'call' });
|
|
422
537
|
|
|
423
538
|
// The same token `report()` keeps, and for the same reason: it is what lets
|
|
424
539
|
// this browser hear what happens to the report afterwards.
|
|
@@ -864,10 +979,64 @@ class BugCatchClient {
|
|
|
864
979
|
* Pass null to forget them again on sign-out.
|
|
865
980
|
*/
|
|
866
981
|
identify(user) {
|
|
867
|
-
|
|
982
|
+
const shaped = normalizeUser(user, (m) => this.warn(m));
|
|
983
|
+
// Anything but null/undefined that yielded nothing usable is a mistake, not
|
|
984
|
+
// a sign-out: keep who was there rather than silently forgetting them.
|
|
985
|
+
if (user !== null && user !== undefined && !shaped) return this;
|
|
986
|
+
this.config.user = shaped;
|
|
987
|
+
this.debugLog('identify', shaped);
|
|
988
|
+
return this;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Say what the reporter is looking at — the loan, application or customer on
|
|
993
|
+
* this screen — so every report filed from it carries that record.
|
|
994
|
+
*
|
|
995
|
+
* BugCatch.setContext({ loanId: 'L26090028459', screen: 'repayment' });
|
|
996
|
+
* BugCatch.setContext({ loanId: null }); // left the loan screen
|
|
997
|
+
* BugCatch.setContext(null); // clear everything
|
|
998
|
+
*
|
|
999
|
+
* Merged over what was set before and kept until changed — for the life of
|
|
1000
|
+
* the page, not across reloads. `loanId`, `applicationId` and
|
|
1001
|
+
* `customerNumber` are read by bugcatch as identifiers your app asserted, and
|
|
1002
|
+
* outrank the same field on identify(); anything else is readable metadata.
|
|
1003
|
+
*/
|
|
1004
|
+
setContext(context) {
|
|
1005
|
+
this.context = mergeContext(this.context, context, (m) => this.warn(m));
|
|
1006
|
+
this.debugLog('context', this.context);
|
|
868
1007
|
return this;
|
|
869
1008
|
}
|
|
870
1009
|
|
|
1010
|
+
/** What setContext() currently holds (a copy). */
|
|
1011
|
+
getContext() {
|
|
1012
|
+
return { ...this.context };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Open the report panel, as if the floating button had been pressed — for a
|
|
1017
|
+
* "Report a problem" item in your own menu.
|
|
1018
|
+
*
|
|
1019
|
+
* Needs the built-in widget (`widget: true`, the default). With `widget:
|
|
1020
|
+
* false` there is no panel to open: draw your own form and call report().
|
|
1021
|
+
*/
|
|
1022
|
+
open() {
|
|
1023
|
+
if (!this.started) { this.warn('open() was called before init() — nothing to open yet.'); return this; }
|
|
1024
|
+
if (!this.widgetControls) { this.warn('open() needs the built-in widget; it was turned off with widget: false.'); return this; }
|
|
1025
|
+
this.widgetControls.open();
|
|
1026
|
+
return this;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/** Close the report panel if it is open. */
|
|
1030
|
+
close() {
|
|
1031
|
+
this.widgetControls?.close();
|
|
1032
|
+
return this;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/** Whether the report panel is open. */
|
|
1036
|
+
isOpen() {
|
|
1037
|
+
return Boolean(this.widgetControls?.isOpen());
|
|
1038
|
+
}
|
|
1039
|
+
|
|
871
1040
|
/**
|
|
872
1041
|
* Upload a screenshot, recording or file and get back the storage key that
|
|
873
1042
|
* `report({ attachments })` references.
|
|
@@ -890,10 +1059,14 @@ class BugCatchClient {
|
|
|
890
1059
|
/** File a bug with everything captured so far. */
|
|
891
1060
|
async report({
|
|
892
1061
|
title, description = '', priority = 'MEDIUM', metadata = {}, attachments = [],
|
|
893
|
-
references = [],
|
|
1062
|
+
references = [], context = null,
|
|
894
1063
|
} = {}) {
|
|
895
1064
|
if (!this.started) throw new Error('[BugCatch] call init() first.');
|
|
896
|
-
if (
|
|
1065
|
+
if (typeof title !== 'string' || !title.trim()) throw new Error('[BugCatch] a title is required.');
|
|
1066
|
+
if (!['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'].includes(String(priority).toUpperCase())) {
|
|
1067
|
+
this.warn(`priority "${priority}" is not one of LOW, MEDIUM, HIGH, CRITICAL — sent as MEDIUM.`);
|
|
1068
|
+
priority = 'MEDIUM';
|
|
1069
|
+
}
|
|
897
1070
|
|
|
898
1071
|
// Drop anything the API would reject outright, so one bad entry can't cost
|
|
899
1072
|
// the reporter the whole report.
|
|
@@ -908,8 +1081,19 @@ class BugCatchClient {
|
|
|
908
1081
|
mime: normalizeMime(a.mime, REPORT_MIMES),
|
|
909
1082
|
}));
|
|
910
1083
|
|
|
1084
|
+
// Only well-formed entries: one malformed reference must not cost the
|
|
1085
|
+
// reporter the report (the API refuses the whole body on a bad one).
|
|
1086
|
+
const refs = (Array.isArray(references) ? references : [])
|
|
1087
|
+
.filter((r) => r && typeof r.key === 'string' && r.key && (typeof r.value === 'string' || typeof r.value === 'number'))
|
|
1088
|
+
.map((r) => ({ key: r.key.slice(0, 64), value: String(r.value).trim().slice(0, 120) }))
|
|
1089
|
+
.filter((r) => r.value);
|
|
1090
|
+
if (Array.isArray(references) && refs.length < references.length) {
|
|
1091
|
+
this.warn('report({ references }) takes [{ key, value }] with string values; malformed entries were dropped.');
|
|
1092
|
+
}
|
|
1093
|
+
|
|
911
1094
|
let payload = await this.#payload({
|
|
912
|
-
title: title.trim(), description
|
|
1095
|
+
title: title.trim(), description: typeof description === 'string' ? description : String(description ?? ''),
|
|
1096
|
+
priority: String(priority).toUpperCase(), metadata, media, references: refs, context,
|
|
913
1097
|
});
|
|
914
1098
|
|
|
915
1099
|
if (typeof this.config.beforeSend === 'function') {
|
|
@@ -918,20 +1102,35 @@ class BugCatchClient {
|
|
|
918
1102
|
if (result && typeof result === 'object') payload = result;
|
|
919
1103
|
}
|
|
920
1104
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
'Content-Type': 'application/json',
|
|
926
|
-
'X-BugCatch-Key': this.config.apiKey,
|
|
927
|
-
// Present only where the reporter signed in. The server takes the
|
|
928
|
-
// address from here rather than from the payload when it is — on a
|
|
929
|
-
// project that requires sign-in, letting the page name a different
|
|
930
|
-
// reporter would undo the requirement.
|
|
931
|
-
...(session?.token ? { 'X-BugCatch-Session': session.token } : {}),
|
|
932
|
-
},
|
|
933
|
-
body: JSON.stringify(payload),
|
|
1105
|
+
this.debugLog('sending report', {
|
|
1106
|
+
title: payload.title, user: payload.metadata?.user, context: payload.metadata?.context,
|
|
1107
|
+
references: payload.references, console: payload.logs?.console?.entries?.length,
|
|
1108
|
+
network: payload.logs?.network?.entries?.length,
|
|
934
1109
|
});
|
|
1110
|
+
const session = readSession(this.config.projectId);
|
|
1111
|
+
let res;
|
|
1112
|
+
try {
|
|
1113
|
+
res = await fetch(`${this.config.apiBase}/ingest/bugs`, {
|
|
1114
|
+
method: 'POST',
|
|
1115
|
+
headers: {
|
|
1116
|
+
'Content-Type': 'application/json',
|
|
1117
|
+
'X-BugCatch-Key': this.config.apiKey,
|
|
1118
|
+
// Present only where the reporter signed in. The server takes the
|
|
1119
|
+
// address from here rather than from the payload when it is — on a
|
|
1120
|
+
// project that requires sign-in, letting the page name a different
|
|
1121
|
+
// reporter would undo the requirement.
|
|
1122
|
+
...(session?.token ? { 'X-BugCatch-Session': session.token } : {}),
|
|
1123
|
+
},
|
|
1124
|
+
body: JSON.stringify(payload),
|
|
1125
|
+
});
|
|
1126
|
+
} catch (cause) {
|
|
1127
|
+
// No response at all: the wrong apiBase, the API down, or CORS refusing
|
|
1128
|
+
// this origin. The browser's own "Failed to fetch" names none of them.
|
|
1129
|
+
const err = new Error(`[BugCatch] could not reach ${this.config.apiBase} — check apiBase, and that this site's origin is allowed by the API.`);
|
|
1130
|
+
err.cause = cause;
|
|
1131
|
+
this.#failed('report', err);
|
|
1132
|
+
throw err;
|
|
1133
|
+
}
|
|
935
1134
|
|
|
936
1135
|
// A session the server no longer recognises — expired, or ended by a
|
|
937
1136
|
// sign-in somewhere else. Dropped here rather than left to fail again on
|
|
@@ -945,10 +1144,17 @@ class BugCatchClient {
|
|
|
945
1144
|
const body = await res.json();
|
|
946
1145
|
detail = Array.isArray(body.message) ? body.message.join(', ') : body.message || '';
|
|
947
1146
|
} catch { /* non-JSON error */ }
|
|
948
|
-
|
|
1147
|
+
// The two refusals an integrator hits first, said in their terms.
|
|
1148
|
+
if (res.status === 401 && !session) detail ||= 'the apiKey was not accepted';
|
|
1149
|
+
if (res.status === 403) detail ||= 'the apiKey is not allowed to report to this projectId';
|
|
1150
|
+
const err = new Error(`[BugCatch] report failed (HTTP ${res.status})${detail ? ': ' + detail : ''}`);
|
|
1151
|
+
err.status = res.status;
|
|
1152
|
+
this.#failed('report', err);
|
|
1153
|
+
throw err;
|
|
949
1154
|
}
|
|
950
1155
|
|
|
951
1156
|
const result = await res.json();
|
|
1157
|
+
this.#submitted(result, { title: payload.title ?? title.trim(), via: 'form' });
|
|
952
1158
|
|
|
953
1159
|
// The API returns a watch token only when it could attribute the report to
|
|
954
1160
|
// someone — which means identify() was called with an email. Without one
|
|
@@ -1095,7 +1301,6 @@ class BugCatchClient {
|
|
|
1095
1301
|
return result;
|
|
1096
1302
|
}
|
|
1097
1303
|
|
|
1098
|
-
/** What would be sent right now — useful for debugging integrations. */
|
|
1099
1304
|
/**
|
|
1100
1305
|
* What this project is already known to be having trouble with.
|
|
1101
1306
|
*
|
|
@@ -1153,7 +1358,12 @@ class BugCatchClient {
|
|
|
1153
1358
|
bugId,
|
|
1154
1359
|
reporterEmail: email || this.config.user?.email,
|
|
1155
1360
|
reporterName: name || this.config.user?.name,
|
|
1156
|
-
metadata: {
|
|
1361
|
+
metadata: {
|
|
1362
|
+
...environment,
|
|
1363
|
+
user: this.config.user || undefined,
|
|
1364
|
+
...(Object.keys(this.context).length ? { context: this.context } : {}),
|
|
1365
|
+
sdk: SDK_IDENTITY,
|
|
1366
|
+
},
|
|
1157
1367
|
logs: {
|
|
1158
1368
|
console: { entries: this.consoleBuffer.all() },
|
|
1159
1369
|
network: { entries: this.networkBuffer.all() },
|
|
@@ -1162,8 +1372,11 @@ class BugCatchClient {
|
|
|
1162
1372
|
});
|
|
1163
1373
|
}
|
|
1164
1374
|
|
|
1375
|
+
/** What would be sent right now — useful for debugging integrations. */
|
|
1165
1376
|
snapshot() {
|
|
1166
1377
|
return {
|
|
1378
|
+
user: this.config.user || null,
|
|
1379
|
+
context: { ...this.context },
|
|
1167
1380
|
console: this.consoleBuffer.all(),
|
|
1168
1381
|
network: this.networkBuffer.all(),
|
|
1169
1382
|
environment: collectEnvironmentSync(),
|
|
@@ -1196,5 +1409,7 @@ const BugCatch = new BugCatchClient();
|
|
|
1196
1409
|
// build, a named export alongside the default turns `window.BugCatch` into a
|
|
1197
1410
|
// namespace object, so `BugCatch.init(...)` from a <script> tag would break.
|
|
1198
1411
|
BugCatch.Client = BugCatchClient;
|
|
1412
|
+
// For support tickets and `console.log(BugCatch.version)`.
|
|
1413
|
+
BugCatch.version = SDK_IDENTITY.version;
|
|
1199
1414
|
|
|
1200
1415
|
export default BugCatch;
|