@bbearai/core 0.5.4 → 0.7.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/dist/index.d.mts +321 -9
- package/dist/index.d.ts +321 -9
- package/dist/index.js +693 -27
- package/dist/index.mjs +681 -26
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,13 +22,24 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
BUG_CATEGORIES: () => BUG_CATEGORIES,
|
|
24
24
|
BugBearClient: () => BugBearClient,
|
|
25
|
+
DedupWindow: () => DedupWindow,
|
|
26
|
+
ErrorMonitor: () => ErrorMonitor,
|
|
25
27
|
LocalStorageAdapter: () => LocalStorageAdapter,
|
|
26
28
|
OfflineQueue: () => OfflineQueue,
|
|
29
|
+
RNApiFailureHandler: () => RNApiFailureHandler,
|
|
30
|
+
RNCrashHandler: () => RNCrashHandler,
|
|
31
|
+
RNRageClickHandler: () => RNRageClickHandler,
|
|
32
|
+
RageClickDetector: () => RageClickDetector,
|
|
33
|
+
WebApiFailureHandler: () => WebApiFailureHandler,
|
|
34
|
+
WebCrashHandler: () => WebCrashHandler,
|
|
35
|
+
WebRageClickHandler: () => WebRageClickHandler,
|
|
27
36
|
captureError: () => captureError,
|
|
28
37
|
contextCapture: () => contextCapture,
|
|
29
38
|
createBugBear: () => createBugBear,
|
|
39
|
+
generateFingerprint: () => generateFingerprint,
|
|
30
40
|
isBugCategory: () => isBugCategory,
|
|
31
|
-
isNetworkError: () => isNetworkError
|
|
41
|
+
isNetworkError: () => isNetworkError,
|
|
42
|
+
scrubUrl: () => scrubUrl
|
|
32
43
|
});
|
|
33
44
|
module.exports = __toCommonJS(index_exports);
|
|
34
45
|
|
|
@@ -52,6 +63,7 @@ var ContextCaptureManager = class {
|
|
|
52
63
|
this.networkRequests = [];
|
|
53
64
|
this.navigationHistory = [];
|
|
54
65
|
this.originalConsole = {};
|
|
66
|
+
this.fetchHost = null;
|
|
55
67
|
this.isCapturing = false;
|
|
56
68
|
}
|
|
57
69
|
/**
|
|
@@ -74,8 +86,9 @@ var ContextCaptureManager = class {
|
|
|
74
86
|
if (this.originalConsole.warn) console.warn = this.originalConsole.warn;
|
|
75
87
|
if (this.originalConsole.error) console.error = this.originalConsole.error;
|
|
76
88
|
if (this.originalConsole.info) console.info = this.originalConsole.info;
|
|
77
|
-
if (this.originalFetch &&
|
|
78
|
-
|
|
89
|
+
if (this.originalFetch && this.fetchHost) {
|
|
90
|
+
this.fetchHost.fetch = this.originalFetch;
|
|
91
|
+
this.fetchHost = null;
|
|
79
92
|
}
|
|
80
93
|
if (typeof window !== "undefined" && typeof history !== "undefined") {
|
|
81
94
|
if (this.originalPushState) {
|
|
@@ -184,15 +197,19 @@ var ContextCaptureManager = class {
|
|
|
184
197
|
});
|
|
185
198
|
}
|
|
186
199
|
captureFetch() {
|
|
187
|
-
if (typeof
|
|
188
|
-
|
|
200
|
+
if (typeof fetch === "undefined") return;
|
|
201
|
+
const host = typeof window !== "undefined" && typeof window.fetch === "function" ? window : typeof globalThis !== "undefined" && typeof globalThis.fetch === "function" ? globalThis : null;
|
|
202
|
+
if (!host) return;
|
|
203
|
+
const canCloneResponse = typeof document !== "undefined";
|
|
204
|
+
this.fetchHost = host;
|
|
205
|
+
this.originalFetch = host.fetch;
|
|
189
206
|
const self = this;
|
|
190
|
-
|
|
207
|
+
host.fetch = async function(input, init) {
|
|
191
208
|
const startTime = Date.now();
|
|
192
209
|
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
193
210
|
const method = init?.method || "GET";
|
|
194
211
|
try {
|
|
195
|
-
const response = await self.originalFetch.call(
|
|
212
|
+
const response = await self.originalFetch.call(host, input, init);
|
|
196
213
|
const requestEntry = {
|
|
197
214
|
method,
|
|
198
215
|
url: url.slice(0, 200),
|
|
@@ -201,7 +218,7 @@ var ContextCaptureManager = class {
|
|
|
201
218
|
duration: Date.now() - startTime,
|
|
202
219
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
203
220
|
};
|
|
204
|
-
if (response.status >= 400) {
|
|
221
|
+
if (canCloneResponse && response.status >= 400) {
|
|
205
222
|
try {
|
|
206
223
|
const cloned = response.clone();
|
|
207
224
|
const body = await cloned.text();
|
|
@@ -443,12 +460,442 @@ function isNetworkError(error) {
|
|
|
443
460
|
msg.includes("the internet connection appears to be offline") || msg.includes("a]server with the specified hostname could not be found");
|
|
444
461
|
}
|
|
445
462
|
|
|
463
|
+
// src/monitoring/fingerprint.ts
|
|
464
|
+
function generateFingerprint(source, message, route) {
|
|
465
|
+
const input = `${source}|${message}|${route}`;
|
|
466
|
+
let hash = 5381;
|
|
467
|
+
for (let i = 0; i < input.length; i++) {
|
|
468
|
+
hash = (hash << 5) + hash + input.charCodeAt(i) & 4294967295;
|
|
469
|
+
}
|
|
470
|
+
return `bb_${(hash >>> 0).toString(36)}`;
|
|
471
|
+
}
|
|
472
|
+
var DedupWindow = class {
|
|
473
|
+
constructor(windowMs) {
|
|
474
|
+
this.windowMs = windowMs;
|
|
475
|
+
this.seen = /* @__PURE__ */ new Map();
|
|
476
|
+
}
|
|
477
|
+
/** Returns true if this fingerprint should be reported (not a recent duplicate). */
|
|
478
|
+
shouldReport(fingerprint) {
|
|
479
|
+
const now = Date.now();
|
|
480
|
+
const lastSeen = this.seen.get(fingerprint);
|
|
481
|
+
if (lastSeen !== void 0 && now - lastSeen < this.windowMs) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
this.seen.set(fingerprint, now);
|
|
485
|
+
this.cleanup(now);
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
/** Remove expired entries to prevent memory leaks in long sessions. */
|
|
489
|
+
cleanup(now) {
|
|
490
|
+
if (this.seen.size > 100) {
|
|
491
|
+
for (const [fp, ts] of this.seen) {
|
|
492
|
+
if (now - ts >= this.windowMs) {
|
|
493
|
+
this.seen.delete(fp);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// src/monitoring/scrub.ts
|
|
501
|
+
var SENSITIVE_PARAMS = /^(token|key|password|secret|auth|credential|api_key|access_token|secret_key|apikey|session)$/i;
|
|
502
|
+
function scrubUrl(url) {
|
|
503
|
+
try {
|
|
504
|
+
const parsed = new URL(url);
|
|
505
|
+
let changed = false;
|
|
506
|
+
for (const [key] of parsed.searchParams) {
|
|
507
|
+
if (SENSITIVE_PARAMS.test(key)) {
|
|
508
|
+
parsed.searchParams.set(key, "[REDACTED]");
|
|
509
|
+
changed = true;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return changed ? parsed.toString() : url;
|
|
513
|
+
} catch {
|
|
514
|
+
return url;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// src/monitoring/error-monitor.ts
|
|
519
|
+
var SOURCE_TO_REPORT_SOURCE = {
|
|
520
|
+
crash: "auto_crash",
|
|
521
|
+
api_failure: "auto_api",
|
|
522
|
+
rage_click: "auto_rage_click"
|
|
523
|
+
};
|
|
524
|
+
var SEVERITY_ORDER = ["low", "medium", "high", "critical"];
|
|
525
|
+
var ErrorMonitor = class {
|
|
526
|
+
constructor(config, deps) {
|
|
527
|
+
this.deps = deps;
|
|
528
|
+
this.sessionReportCount = 0;
|
|
529
|
+
this.pendingSentryData = null;
|
|
530
|
+
this.config = {
|
|
531
|
+
sampleRate: 1,
|
|
532
|
+
maxReportsPerSession: 10,
|
|
533
|
+
dedupWindowMs: 3e5,
|
|
534
|
+
severityThreshold: "low",
|
|
535
|
+
scrubUrls: true,
|
|
536
|
+
...config
|
|
537
|
+
};
|
|
538
|
+
this.dedup = new DedupWindow(this.config.dedupWindowMs);
|
|
539
|
+
}
|
|
540
|
+
async handleEvent(event) {
|
|
541
|
+
if (!this.dedup.shouldReport(event.fingerprint)) return;
|
|
542
|
+
if (Math.random() >= this.config.sampleRate) return;
|
|
543
|
+
if (this.sessionReportCount >= this.config.maxReportsPerSession) return;
|
|
544
|
+
const severity = this.getSeverity(event);
|
|
545
|
+
if (SEVERITY_ORDER.indexOf(severity) < SEVERITY_ORDER.indexOf(this.config.severityThreshold)) return;
|
|
546
|
+
if (this.config.beforeCapture && !this.config.beforeCapture(event)) return;
|
|
547
|
+
const scrubbedUrl = this.config.scrubUrls && event.requestUrl ? scrubUrl(event.requestUrl) : event.requestUrl;
|
|
548
|
+
if (this.pendingSentryData) {
|
|
549
|
+
event.sentryEventId = this.pendingSentryData.sentryEventId;
|
|
550
|
+
event.sentryBreadcrumbs = this.pendingSentryData.sentryBreadcrumbs;
|
|
551
|
+
this.pendingSentryData = null;
|
|
552
|
+
}
|
|
553
|
+
this.sessionReportCount++;
|
|
554
|
+
await this.deps.submitReport({
|
|
555
|
+
type: "bug",
|
|
556
|
+
reportSource: SOURCE_TO_REPORT_SOURCE[event.source],
|
|
557
|
+
title: this.generateTitle(event),
|
|
558
|
+
description: event.message,
|
|
559
|
+
severity,
|
|
560
|
+
category: event.source === "crash" ? "crash" : "functional",
|
|
561
|
+
appContext: {
|
|
562
|
+
currentRoute: event.route,
|
|
563
|
+
errorMessage: event.error?.message,
|
|
564
|
+
errorStack: event.error?.stack,
|
|
565
|
+
custom: {
|
|
566
|
+
monitoringSource: event.source,
|
|
567
|
+
fingerprint: event.fingerprint,
|
|
568
|
+
...event.statusCode ? { statusCode: event.statusCode } : {},
|
|
569
|
+
...scrubbedUrl ? { requestUrl: scrubbedUrl } : {},
|
|
570
|
+
...event.requestMethod ? { requestMethod: event.requestMethod } : {},
|
|
571
|
+
...event.clickCount ? { clickCount: event.clickCount } : {},
|
|
572
|
+
...event.targetSelector ? { targetSelector: event.targetSelector } : {},
|
|
573
|
+
...event.sentryEventId ? { sentryEventId: event.sentryEventId } : {}
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
enhancedContext: this.deps.getEnhancedContext(),
|
|
577
|
+
deviceInfo: this.deps.getDeviceInfo()
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
enrichWithSentry(data) {
|
|
581
|
+
this.pendingSentryData = data;
|
|
582
|
+
}
|
|
583
|
+
getSeverity(event) {
|
|
584
|
+
if (event.source === "crash") return "critical";
|
|
585
|
+
if (event.source === "api_failure") return event.statusCode && event.statusCode >= 500 ? "high" : "medium";
|
|
586
|
+
return "medium";
|
|
587
|
+
}
|
|
588
|
+
generateTitle(event) {
|
|
589
|
+
switch (event.source) {
|
|
590
|
+
case "crash":
|
|
591
|
+
return `[Auto] Crash on ${event.route}: ${event.message.slice(0, 80)}`;
|
|
592
|
+
case "api_failure":
|
|
593
|
+
return `[Auto] ${event.requestMethod || "API"} ${event.statusCode || "error"} on ${event.route}`;
|
|
594
|
+
case "rage_click":
|
|
595
|
+
return `[Auto] Rage click on ${event.route} (${event.targetSelector || "unknown"})`;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
destroy() {
|
|
599
|
+
this.sessionReportCount = 0;
|
|
600
|
+
this.pendingSentryData = null;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// src/monitoring/rage-click-detector.ts
|
|
605
|
+
var RageClickDetector = class {
|
|
606
|
+
constructor(onRageClick) {
|
|
607
|
+
this.onRageClick = onRageClick;
|
|
608
|
+
this.clicks = [];
|
|
609
|
+
this.threshold = 3;
|
|
610
|
+
this.windowMs = 2e3;
|
|
611
|
+
this.destroyed = false;
|
|
612
|
+
}
|
|
613
|
+
recordClick(x, y, targetSelector) {
|
|
614
|
+
if (this.destroyed) return;
|
|
615
|
+
const now = Date.now();
|
|
616
|
+
this.clicks = this.clicks.filter((c) => now - c.time < this.windowMs);
|
|
617
|
+
this.clicks.push({ time: now, target: targetSelector, x, y });
|
|
618
|
+
const sameTarget = this.clicks.filter((c) => c.target === targetSelector);
|
|
619
|
+
if (sameTarget.length >= this.threshold) {
|
|
620
|
+
this.onRageClick({
|
|
621
|
+
clickCount: sameTarget.length,
|
|
622
|
+
targetSelector,
|
|
623
|
+
x,
|
|
624
|
+
y
|
|
625
|
+
});
|
|
626
|
+
this.clicks = [];
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
destroy() {
|
|
630
|
+
this.destroyed = true;
|
|
631
|
+
this.clicks = [];
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
// src/monitoring/web-handlers.ts
|
|
636
|
+
function getSelector(el) {
|
|
637
|
+
if (el.id) return `#${el.id}`;
|
|
638
|
+
const tag = el.tagName.toLowerCase();
|
|
639
|
+
const classes = Array.from(el.classList).slice(0, 2);
|
|
640
|
+
return classes.length > 0 ? `${tag}.${classes.join(".")}` : tag;
|
|
641
|
+
}
|
|
642
|
+
function currentRoute() {
|
|
643
|
+
try {
|
|
644
|
+
return typeof window !== "undefined" && window.location ? window.location.pathname : "unknown";
|
|
645
|
+
} catch {
|
|
646
|
+
return "unknown";
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
var WebCrashHandler = class {
|
|
650
|
+
constructor(onEvent) {
|
|
651
|
+
this.onEvent = onEvent;
|
|
652
|
+
this.prevOnError = null;
|
|
653
|
+
this.rejectionHandler = null;
|
|
654
|
+
}
|
|
655
|
+
start() {
|
|
656
|
+
this.prevOnError = window.onerror;
|
|
657
|
+
window.onerror = (message, source, lineno, colno, error) => {
|
|
658
|
+
const msg = typeof message === "string" ? message : "Unknown error";
|
|
659
|
+
const route = currentRoute();
|
|
660
|
+
this.onEvent({
|
|
661
|
+
source: "crash",
|
|
662
|
+
fingerprint: generateFingerprint("crash", msg, route),
|
|
663
|
+
message: msg,
|
|
664
|
+
route,
|
|
665
|
+
timestamp: Date.now(),
|
|
666
|
+
error: error ?? new Error(msg)
|
|
667
|
+
});
|
|
668
|
+
if (typeof this.prevOnError === "function") {
|
|
669
|
+
return this.prevOnError(message, source, lineno, colno, error);
|
|
670
|
+
}
|
|
671
|
+
return false;
|
|
672
|
+
};
|
|
673
|
+
this.rejectionHandler = (e) => {
|
|
674
|
+
const reason = e.reason;
|
|
675
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
676
|
+
const msg = error.message || "Unhandled promise rejection";
|
|
677
|
+
const route = currentRoute();
|
|
678
|
+
this.onEvent({
|
|
679
|
+
source: "crash",
|
|
680
|
+
fingerprint: generateFingerprint("crash", msg, route),
|
|
681
|
+
message: msg,
|
|
682
|
+
route,
|
|
683
|
+
timestamp: Date.now(),
|
|
684
|
+
error
|
|
685
|
+
});
|
|
686
|
+
};
|
|
687
|
+
window.addEventListener("unhandledrejection", this.rejectionHandler);
|
|
688
|
+
}
|
|
689
|
+
destroy() {
|
|
690
|
+
window.onerror = this.prevOnError;
|
|
691
|
+
if (this.rejectionHandler) {
|
|
692
|
+
window.removeEventListener("unhandledrejection", this.rejectionHandler);
|
|
693
|
+
this.rejectionHandler = null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
var WebApiFailureHandler = class {
|
|
698
|
+
constructor(onEvent) {
|
|
699
|
+
this.onEvent = onEvent;
|
|
700
|
+
this.originalFetch = null;
|
|
701
|
+
}
|
|
702
|
+
start() {
|
|
703
|
+
this.originalFetch = window.fetch;
|
|
704
|
+
const original = this.originalFetch;
|
|
705
|
+
const onEvent = this.onEvent;
|
|
706
|
+
window.fetch = async function patchedFetch(input, init) {
|
|
707
|
+
const response = await original.call(window, input, init);
|
|
708
|
+
if (response.status >= 400) {
|
|
709
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
710
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
711
|
+
const route = currentRoute();
|
|
712
|
+
const msg = `${method} ${url} failed with ${response.status}`;
|
|
713
|
+
onEvent({
|
|
714
|
+
source: "api_failure",
|
|
715
|
+
fingerprint: generateFingerprint("api_failure", msg, route),
|
|
716
|
+
message: msg,
|
|
717
|
+
route,
|
|
718
|
+
timestamp: Date.now(),
|
|
719
|
+
requestUrl: url,
|
|
720
|
+
requestMethod: method,
|
|
721
|
+
statusCode: response.status
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
return response;
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
destroy() {
|
|
728
|
+
if (this.originalFetch) {
|
|
729
|
+
window.fetch = this.originalFetch;
|
|
730
|
+
this.originalFetch = null;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
var WebRageClickHandler = class {
|
|
735
|
+
constructor(onEvent) {
|
|
736
|
+
this.onEvent = onEvent;
|
|
737
|
+
this.clickHandler = null;
|
|
738
|
+
this.detector = new RageClickDetector((rageEvent) => {
|
|
739
|
+
const route = currentRoute();
|
|
740
|
+
this.onEvent({
|
|
741
|
+
source: "rage_click",
|
|
742
|
+
fingerprint: generateFingerprint("rage_click", rageEvent.targetSelector, route),
|
|
743
|
+
message: `Rage click detected on ${rageEvent.targetSelector}`,
|
|
744
|
+
route,
|
|
745
|
+
timestamp: Date.now(),
|
|
746
|
+
clickCount: rageEvent.clickCount,
|
|
747
|
+
targetSelector: rageEvent.targetSelector
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
start() {
|
|
752
|
+
this.clickHandler = (e) => {
|
|
753
|
+
const target = e.target;
|
|
754
|
+
if (!(target instanceof Element)) return;
|
|
755
|
+
const selector = getSelector(target);
|
|
756
|
+
this.detector.recordClick(e.clientX, e.clientY, selector);
|
|
757
|
+
};
|
|
758
|
+
document.addEventListener("click", this.clickHandler, true);
|
|
759
|
+
}
|
|
760
|
+
destroy() {
|
|
761
|
+
if (this.clickHandler) {
|
|
762
|
+
document.removeEventListener("click", this.clickHandler, true);
|
|
763
|
+
this.clickHandler = null;
|
|
764
|
+
}
|
|
765
|
+
this.detector.destroy();
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
// src/monitoring/rn-handlers.ts
|
|
770
|
+
var RNCrashHandler = class {
|
|
771
|
+
constructor(onEvent, getCurrentRoute) {
|
|
772
|
+
this.onEvent = onEvent;
|
|
773
|
+
this.getCurrentRoute = getCurrentRoute;
|
|
774
|
+
this.originalHandler = null;
|
|
775
|
+
this.started = false;
|
|
776
|
+
}
|
|
777
|
+
start() {
|
|
778
|
+
if (this.started) return;
|
|
779
|
+
this.started = true;
|
|
780
|
+
const errorUtils = globalThis.ErrorUtils;
|
|
781
|
+
if (!errorUtils) return;
|
|
782
|
+
this.originalHandler = errorUtils.getGlobalHandler();
|
|
783
|
+
const self = this;
|
|
784
|
+
errorUtils.setGlobalHandler((error, isFatal) => {
|
|
785
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
786
|
+
const route = self.getCurrentRoute();
|
|
787
|
+
self.onEvent({
|
|
788
|
+
source: "crash",
|
|
789
|
+
fingerprint: generateFingerprint("crash", normalizedError.message, route),
|
|
790
|
+
message: normalizedError.message,
|
|
791
|
+
route,
|
|
792
|
+
timestamp: Date.now(),
|
|
793
|
+
error: normalizedError
|
|
794
|
+
});
|
|
795
|
+
if (self.originalHandler) {
|
|
796
|
+
self.originalHandler(error, isFatal);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
destroy() {
|
|
801
|
+
if (!this.started) return;
|
|
802
|
+
this.started = false;
|
|
803
|
+
const errorUtils = globalThis.ErrorUtils;
|
|
804
|
+
if (errorUtils && this.originalHandler) {
|
|
805
|
+
errorUtils.setGlobalHandler(this.originalHandler);
|
|
806
|
+
}
|
|
807
|
+
this.originalHandler = null;
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
var RNApiFailureHandler = class {
|
|
811
|
+
constructor(onEvent, getCurrentRoute) {
|
|
812
|
+
this.onEvent = onEvent;
|
|
813
|
+
this.getCurrentRoute = getCurrentRoute;
|
|
814
|
+
this.originalFetch = null;
|
|
815
|
+
this.started = false;
|
|
816
|
+
}
|
|
817
|
+
start() {
|
|
818
|
+
if (this.started) return;
|
|
819
|
+
this.started = true;
|
|
820
|
+
this.originalFetch = globalThis.fetch;
|
|
821
|
+
const self = this;
|
|
822
|
+
globalThis.fetch = async function patchedFetch(input, init) {
|
|
823
|
+
const response = await self.originalFetch.call(globalThis, input, init);
|
|
824
|
+
if (response.status >= 400) {
|
|
825
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
826
|
+
const method = init?.method ?? "GET";
|
|
827
|
+
const route = self.getCurrentRoute();
|
|
828
|
+
self.onEvent({
|
|
829
|
+
source: "api_failure",
|
|
830
|
+
fingerprint: generateFingerprint("api_failure", `${method} ${response.status}`, route),
|
|
831
|
+
message: `${method} ${url} failed with ${response.status}`,
|
|
832
|
+
route,
|
|
833
|
+
timestamp: Date.now(),
|
|
834
|
+
requestUrl: url,
|
|
835
|
+
requestMethod: method,
|
|
836
|
+
statusCode: response.status
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
return response;
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
destroy() {
|
|
843
|
+
if (!this.started) return;
|
|
844
|
+
this.started = false;
|
|
845
|
+
if (this.originalFetch) {
|
|
846
|
+
globalThis.fetch = this.originalFetch;
|
|
847
|
+
this.originalFetch = null;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
var RNRageClickHandler = class {
|
|
852
|
+
constructor(onEvent, getCurrentRoute) {
|
|
853
|
+
this.onEvent = onEvent;
|
|
854
|
+
this.getCurrentRoute = getCurrentRoute;
|
|
855
|
+
this.detector = null;
|
|
856
|
+
}
|
|
857
|
+
start() {
|
|
858
|
+
if (this.detector) return;
|
|
859
|
+
const self = this;
|
|
860
|
+
this.detector = new RageClickDetector((rageEvent) => {
|
|
861
|
+
const route = self.getCurrentRoute();
|
|
862
|
+
self.onEvent({
|
|
863
|
+
source: "rage_click",
|
|
864
|
+
fingerprint: generateFingerprint("rage_click", rageEvent.targetSelector, route),
|
|
865
|
+
message: `Rage click detected on ${rageEvent.targetSelector}`,
|
|
866
|
+
route,
|
|
867
|
+
timestamp: Date.now(),
|
|
868
|
+
clickCount: rageEvent.clickCount,
|
|
869
|
+
targetSelector: rageEvent.targetSelector
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Record a touch event. Call this from the BugBearProvider's onTouchEnd handler.
|
|
875
|
+
* @param x - Touch x coordinate
|
|
876
|
+
* @param y - Touch y coordinate
|
|
877
|
+
* @param viewId - Identifier for the touched view (e.g., testID, accessibilityLabel, or nativeID)
|
|
878
|
+
*/
|
|
879
|
+
recordTouch(x, y, viewId) {
|
|
880
|
+
this.detector?.recordClick(x, y, viewId);
|
|
881
|
+
}
|
|
882
|
+
destroy() {
|
|
883
|
+
if (this.detector) {
|
|
884
|
+
this.detector.destroy();
|
|
885
|
+
this.detector = null;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
|
|
446
890
|
// src/client.ts
|
|
447
891
|
var formatPgError = (e) => {
|
|
448
892
|
if (!e || typeof e !== "object") return { raw: e };
|
|
449
893
|
const { message, code, details, hint } = e;
|
|
450
894
|
return { message, code, details, hint };
|
|
451
895
|
};
|
|
896
|
+
var DEFAULT_API_BASE_URL = "https://app.bugbear.ai";
|
|
897
|
+
var CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
898
|
+
var CONFIG_CACHE_PREFIX = "bugbear_config_";
|
|
452
899
|
var BugBearClient = class {
|
|
453
900
|
constructor(config) {
|
|
454
901
|
this.navigationHistory = [];
|
|
@@ -458,23 +905,148 @@ var BugBearClient = class {
|
|
|
458
905
|
/** Active Realtime channel references for cleanup. */
|
|
459
906
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
460
907
|
this.realtimeChannels = [];
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
908
|
+
/** Error monitor instance — created when config.monitoring is present. */
|
|
909
|
+
this.monitor = null;
|
|
910
|
+
/** Whether the client has been successfully initialized. */
|
|
911
|
+
this.initialized = false;
|
|
912
|
+
/** Initialization error, if any. */
|
|
913
|
+
this.initError = null;
|
|
467
914
|
this.config = config;
|
|
468
|
-
|
|
469
|
-
|
|
915
|
+
if (config.apiKey) {
|
|
916
|
+
this.pendingInit = this.resolveFromApiKey(config.apiKey);
|
|
917
|
+
} else if (config.supabaseUrl && config.supabaseAnonKey) {
|
|
918
|
+
if (!config.projectId) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
"BugBear: projectId is required when using explicit Supabase credentials. Tip: Use apiKey instead for simpler setup \u2014 it resolves everything automatically."
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
this.supabase = (0, import_supabase_js.createClient)(config.supabaseUrl, config.supabaseAnonKey);
|
|
924
|
+
this.initialized = true;
|
|
925
|
+
this.pendingInit = Promise.resolve();
|
|
926
|
+
this.initOfflineQueue();
|
|
927
|
+
this.initMonitoring();
|
|
928
|
+
} else {
|
|
929
|
+
throw new Error(
|
|
930
|
+
"BugBear: Missing configuration. Provide either:\n \u2022 apiKey (recommended) \u2014 resolves everything automatically\n \u2022 projectId + supabaseUrl + supabaseAnonKey \u2014 explicit credentials\n\nGet your API key at https://app.bugbear.ai/settings/projects"
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/** Whether the client is ready for requests. */
|
|
935
|
+
get isReady() {
|
|
936
|
+
return this.initialized;
|
|
937
|
+
}
|
|
938
|
+
/** Wait until the client is ready. Throws if initialization failed. */
|
|
939
|
+
async ready() {
|
|
940
|
+
await this.pendingInit;
|
|
941
|
+
if (this.initError) throw this.initError;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Resolve Supabase credentials from a BugBear API key.
|
|
945
|
+
* Checks localStorage cache first, falls back to /api/v1/config.
|
|
946
|
+
*/
|
|
947
|
+
async resolveFromApiKey(apiKey) {
|
|
948
|
+
try {
|
|
949
|
+
const cached = this.readConfigCache(apiKey);
|
|
950
|
+
if (cached) {
|
|
951
|
+
this.applyResolvedConfig(cached);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
|
|
955
|
+
const response = await fetch(`${baseUrl}/api/v1/config`, {
|
|
956
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
957
|
+
});
|
|
958
|
+
if (!response.ok) {
|
|
959
|
+
const body = await response.json().catch(() => ({}));
|
|
960
|
+
const message = body.error || `HTTP ${response.status}`;
|
|
961
|
+
throw new Error(
|
|
962
|
+
`BugBear: Invalid API key \u2014 ${message}. Get yours at https://app.bugbear.ai/settings/projects`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
const data = await response.json();
|
|
966
|
+
this.writeConfigCache(apiKey, data);
|
|
967
|
+
this.applyResolvedConfig(data);
|
|
968
|
+
} catch (err) {
|
|
969
|
+
this.initError = err instanceof Error ? err : new Error(String(err));
|
|
970
|
+
this.config.onError?.(this.initError, { context: "apikey_resolution_failed" });
|
|
971
|
+
throw this.initError;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
/** Apply resolved credentials and create the Supabase client. */
|
|
975
|
+
applyResolvedConfig(resolved) {
|
|
976
|
+
this.config = {
|
|
977
|
+
...this.config,
|
|
978
|
+
projectId: resolved.projectId,
|
|
979
|
+
supabaseUrl: resolved.supabaseUrl,
|
|
980
|
+
supabaseAnonKey: resolved.supabaseAnonKey
|
|
981
|
+
};
|
|
982
|
+
this.supabase = (0, import_supabase_js.createClient)(resolved.supabaseUrl, resolved.supabaseAnonKey);
|
|
983
|
+
this.initialized = true;
|
|
984
|
+
this.initOfflineQueue();
|
|
985
|
+
this.initMonitoring();
|
|
986
|
+
}
|
|
987
|
+
/** Initialize offline queue if configured. Shared by both init paths. */
|
|
988
|
+
initOfflineQueue() {
|
|
989
|
+
if (this.config.offlineQueue?.enabled) {
|
|
470
990
|
this._queue = new OfflineQueue({
|
|
471
991
|
enabled: true,
|
|
472
|
-
maxItems: config.offlineQueue.maxItems,
|
|
473
|
-
maxRetries: config.offlineQueue.maxRetries
|
|
992
|
+
maxItems: this.config.offlineQueue.maxItems,
|
|
993
|
+
maxRetries: this.config.offlineQueue.maxRetries
|
|
474
994
|
});
|
|
475
995
|
this.registerQueueHandlers();
|
|
476
996
|
}
|
|
477
997
|
}
|
|
998
|
+
/** Initialize error monitoring if configured. Shared by both init paths. */
|
|
999
|
+
initMonitoring() {
|
|
1000
|
+
const mc = this.config.monitoring;
|
|
1001
|
+
if (!mc || !mc.crashes && !mc.apiFailures && !mc.rageClicks) return;
|
|
1002
|
+
this.monitor = new ErrorMonitor(mc, {
|
|
1003
|
+
submitReport: (report) => this.submitReport(report),
|
|
1004
|
+
getCurrentRoute: () => contextCapture.getCurrentRoute(),
|
|
1005
|
+
getEnhancedContext: () => contextCapture.getEnhancedContext(),
|
|
1006
|
+
getDeviceInfo: () => this.getDeviceInfo()
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
/** Read cached config from localStorage if available and not expired. */
|
|
1010
|
+
readConfigCache(apiKey) {
|
|
1011
|
+
if (typeof localStorage === "undefined") return null;
|
|
1012
|
+
try {
|
|
1013
|
+
const key = CONFIG_CACHE_PREFIX + this.hashKey(apiKey);
|
|
1014
|
+
const raw = localStorage.getItem(key);
|
|
1015
|
+
if (!raw) return null;
|
|
1016
|
+
const cached = JSON.parse(raw);
|
|
1017
|
+
if (Date.now() - cached.cachedAt > CONFIG_CACHE_TTL_MS) {
|
|
1018
|
+
localStorage.removeItem(key);
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
return cached;
|
|
1022
|
+
} catch {
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/** Write resolved config to localStorage cache. */
|
|
1027
|
+
writeConfigCache(apiKey, data) {
|
|
1028
|
+
if (typeof localStorage === "undefined") return;
|
|
1029
|
+
try {
|
|
1030
|
+
const key = CONFIG_CACHE_PREFIX + this.hashKey(apiKey);
|
|
1031
|
+
const cached = { ...data, cachedAt: Date.now() };
|
|
1032
|
+
localStorage.setItem(key, JSON.stringify(cached));
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
/** Simple string hash for cache keys — avoids storing raw API keys. */
|
|
1037
|
+
hashKey(apiKey) {
|
|
1038
|
+
let hash = 0;
|
|
1039
|
+
for (let i = 0; i < apiKey.length; i++) {
|
|
1040
|
+
hash = (hash << 5) - hash + apiKey.charCodeAt(i) | 0;
|
|
1041
|
+
}
|
|
1042
|
+
return hash.toString(36);
|
|
1043
|
+
}
|
|
1044
|
+
/** Ensure the client is initialized before making requests. */
|
|
1045
|
+
async ensureReady() {
|
|
1046
|
+
if (this.initialized) return;
|
|
1047
|
+
await this.pendingInit;
|
|
1048
|
+
if (this.initError) throw this.initError;
|
|
1049
|
+
}
|
|
478
1050
|
// ── Offline Queue ─────────────────────────────────────────
|
|
479
1051
|
/**
|
|
480
1052
|
* Access the offline queue (if enabled).
|
|
@@ -630,6 +1202,7 @@ var BugBearClient = class {
|
|
|
630
1202
|
* Get current user info from host app or BugBear's own auth
|
|
631
1203
|
*/
|
|
632
1204
|
async getCurrentUserInfo() {
|
|
1205
|
+
await this.ensureReady();
|
|
633
1206
|
if (this.config.getCurrentUser) {
|
|
634
1207
|
return await this.config.getCurrentUser();
|
|
635
1208
|
}
|
|
@@ -688,7 +1261,9 @@ var BugBearClient = class {
|
|
|
688
1261
|
navigation_history: this.getNavigationHistory(),
|
|
689
1262
|
enhanced_context: report.enhancedContext || contextCapture.getEnhancedContext(),
|
|
690
1263
|
assignment_id: report.assignmentId,
|
|
691
|
-
test_case_id: report.testCaseId
|
|
1264
|
+
test_case_id: report.testCaseId,
|
|
1265
|
+
report_source: report.reportSource || "manual",
|
|
1266
|
+
error_fingerprint: report.appContext?.custom?.fingerprint || null
|
|
692
1267
|
};
|
|
693
1268
|
const { data, error } = await this.supabase.from("reports").insert(fullReport).select("id").single();
|
|
694
1269
|
if (error) {
|
|
@@ -770,7 +1345,7 @@ var BugBearClient = class {
|
|
|
770
1345
|
`;
|
|
771
1346
|
const [pendingResult, completedResult] = await Promise.all([
|
|
772
1347
|
this.supabase.from("test_assignments").select(selectFields).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).range(from, to),
|
|
773
|
-
this.supabase.from("test_assignments").select(selectFields).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["passed", "failed", "skipped", "blocked"]).order("completed_at", { ascending: false }).limit(100)
|
|
1348
|
+
this.supabase.from("test_assignments").select(selectFields).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["passed", "failed", "skipped", "blocked"]).or("is_verification.eq.false,is_verification.is.null").order("completed_at", { ascending: false }).limit(100)
|
|
774
1349
|
]);
|
|
775
1350
|
if (pendingResult.error) {
|
|
776
1351
|
console.error("BugBear: Failed to fetch assignments", formatPgError(pendingResult.error));
|
|
@@ -849,6 +1424,7 @@ var BugBearClient = class {
|
|
|
849
1424
|
*/
|
|
850
1425
|
async getAssignment(assignmentId) {
|
|
851
1426
|
try {
|
|
1427
|
+
await this.ensureReady();
|
|
852
1428
|
const { data, error } = await this.supabase.from("test_assignments").select(`
|
|
853
1429
|
id,
|
|
854
1430
|
status,
|
|
@@ -920,6 +1496,7 @@ var BugBearClient = class {
|
|
|
920
1496
|
*/
|
|
921
1497
|
async updateAssignmentStatus(assignmentId, status, options) {
|
|
922
1498
|
try {
|
|
1499
|
+
await this.ensureReady();
|
|
923
1500
|
const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at").eq("id", assignmentId).single();
|
|
924
1501
|
if (fetchError || !currentAssignment) {
|
|
925
1502
|
console.error("BugBear: Assignment not found", {
|
|
@@ -1006,6 +1583,7 @@ var BugBearClient = class {
|
|
|
1006
1583
|
*/
|
|
1007
1584
|
async reopenAssignment(assignmentId) {
|
|
1008
1585
|
try {
|
|
1586
|
+
await this.ensureReady();
|
|
1009
1587
|
const { data: current, error: fetchError } = await this.supabase.from("test_assignments").select("status").eq("id", assignmentId).single();
|
|
1010
1588
|
if (fetchError || !current) {
|
|
1011
1589
|
return { success: false, error: "Assignment not found" };
|
|
@@ -1045,6 +1623,7 @@ var BugBearClient = class {
|
|
|
1045
1623
|
actualNotes = notes;
|
|
1046
1624
|
}
|
|
1047
1625
|
try {
|
|
1626
|
+
await this.ensureReady();
|
|
1048
1627
|
const updateData = {
|
|
1049
1628
|
status: "skipped",
|
|
1050
1629
|
skip_reason: actualReason,
|
|
@@ -1214,6 +1793,7 @@ var BugBearClient = class {
|
|
|
1214
1793
|
*/
|
|
1215
1794
|
async getTesterInfo() {
|
|
1216
1795
|
try {
|
|
1796
|
+
await this.ensureReady();
|
|
1217
1797
|
const userInfo = await this.getCurrentUserInfo();
|
|
1218
1798
|
if (!userInfo?.email) return null;
|
|
1219
1799
|
if (!this.isValidEmail(userInfo.email)) {
|
|
@@ -1234,7 +1814,8 @@ var BugBearClient = class {
|
|
|
1234
1814
|
avatarUrl: tester.avatar_url || void 0,
|
|
1235
1815
|
platforms: tester.platforms || [],
|
|
1236
1816
|
assignedTests: tester.assigned_count || 0,
|
|
1237
|
-
completedTests: tester.completed_count || 0
|
|
1817
|
+
completedTests: tester.completed_count || 0,
|
|
1818
|
+
role: tester.role || "tester"
|
|
1238
1819
|
};
|
|
1239
1820
|
} catch (err) {
|
|
1240
1821
|
console.error("BugBear: getTesterInfo error", err);
|
|
@@ -1505,6 +2086,7 @@ var BugBearClient = class {
|
|
|
1505
2086
|
*/
|
|
1506
2087
|
async isQAEnabled() {
|
|
1507
2088
|
try {
|
|
2089
|
+
await this.ensureReady();
|
|
1508
2090
|
const { data, error } = await this.supabase.rpc("check_qa_enabled", {
|
|
1509
2091
|
p_project_id: this.config.projectId
|
|
1510
2092
|
});
|
|
@@ -1521,21 +2103,85 @@ var BugBearClient = class {
|
|
|
1521
2103
|
}
|
|
1522
2104
|
}
|
|
1523
2105
|
/**
|
|
1524
|
-
* Check if the widget should be visible
|
|
1525
|
-
*
|
|
2106
|
+
* Check if the widget should be visible.
|
|
2107
|
+
* Behavior depends on the configured mode:
|
|
2108
|
+
* - 'qa': QA enabled AND user is a registered tester
|
|
2109
|
+
* - 'feedback': Any authenticated user
|
|
2110
|
+
* - 'auto': Either QA tester OR authenticated non-tester
|
|
1526
2111
|
*/
|
|
1527
2112
|
async shouldShowWidget() {
|
|
1528
|
-
const
|
|
2113
|
+
const mode = this.config.mode || "qa";
|
|
2114
|
+
if (mode === "qa") {
|
|
2115
|
+
const [qaEnabled2, tester] = await Promise.all([
|
|
2116
|
+
this.isQAEnabled(),
|
|
2117
|
+
this.isTester()
|
|
2118
|
+
]);
|
|
2119
|
+
return qaEnabled2 && tester;
|
|
2120
|
+
}
|
|
2121
|
+
if (mode === "feedback") {
|
|
2122
|
+
const userInfo2 = await this.getCurrentUserInfo();
|
|
2123
|
+
return userInfo2 !== null;
|
|
2124
|
+
}
|
|
2125
|
+
const [qaEnabled, testerInfo, userInfo] = await Promise.all([
|
|
2126
|
+
this.isQAEnabled(),
|
|
2127
|
+
this.getTesterInfo(),
|
|
2128
|
+
this.getCurrentUserInfo()
|
|
2129
|
+
]);
|
|
2130
|
+
if (qaEnabled && testerInfo && testerInfo.role !== "feedback") return true;
|
|
2131
|
+
if (userInfo) return true;
|
|
2132
|
+
return false;
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Resolve the effective widget mode for the current user.
|
|
2136
|
+
* - 'qa' or 'feedback' config → returned as-is
|
|
2137
|
+
* - 'auto' → checks if user is a QA tester (role='tester') → 'qa', otherwise 'feedback'
|
|
2138
|
+
*/
|
|
2139
|
+
async getEffectiveMode() {
|
|
2140
|
+
const mode = this.config.mode || "qa";
|
|
2141
|
+
if (mode === "qa") return "qa";
|
|
2142
|
+
if (mode === "feedback") return "feedback";
|
|
2143
|
+
const [qaEnabled, testerInfo] = await Promise.all([
|
|
1529
2144
|
this.isQAEnabled(),
|
|
1530
|
-
this.
|
|
2145
|
+
this.getTesterInfo()
|
|
1531
2146
|
]);
|
|
1532
|
-
|
|
2147
|
+
if (qaEnabled && testerInfo && testerInfo.role !== "feedback") return "qa";
|
|
2148
|
+
return "feedback";
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Auto-provision a feedback user record in the testers table.
|
|
2152
|
+
* Called during initialization when mode is 'feedback' or 'auto' for non-testers.
|
|
2153
|
+
* Idempotent — safe to call multiple times. Returns the tester info for the user.
|
|
2154
|
+
*/
|
|
2155
|
+
async ensureFeedbackUser() {
|
|
2156
|
+
try {
|
|
2157
|
+
await this.ensureReady();
|
|
2158
|
+
const userInfo = await this.getCurrentUserInfo();
|
|
2159
|
+
if (!userInfo?.email) return null;
|
|
2160
|
+
const { error } = await this.supabase.rpc("ensure_feedback_user", {
|
|
2161
|
+
p_project_id: this.config.projectId,
|
|
2162
|
+
p_user_id: userInfo.id,
|
|
2163
|
+
p_email: userInfo.email,
|
|
2164
|
+
p_name: userInfo.name || null
|
|
2165
|
+
});
|
|
2166
|
+
if (error) {
|
|
2167
|
+
console.error("BugBear: Failed to ensure feedback user", error.message);
|
|
2168
|
+
this.config.onError?.(new Error(`ensure_feedback_user failed: ${error.message}`), {
|
|
2169
|
+
context: "feedback_user_provisioning"
|
|
2170
|
+
});
|
|
2171
|
+
return null;
|
|
2172
|
+
}
|
|
2173
|
+
return this.getTesterInfo();
|
|
2174
|
+
} catch (err) {
|
|
2175
|
+
console.error("BugBear: Error ensuring feedback user", err);
|
|
2176
|
+
return null;
|
|
2177
|
+
}
|
|
1533
2178
|
}
|
|
1534
2179
|
/**
|
|
1535
2180
|
* Upload a screenshot (web - uses File/Blob)
|
|
1536
2181
|
*/
|
|
1537
2182
|
async uploadScreenshot(file, filename, bucket = "screenshots") {
|
|
1538
2183
|
try {
|
|
2184
|
+
await this.ensureReady();
|
|
1539
2185
|
const contentType = file.type || "image/png";
|
|
1540
2186
|
const ext = contentType.includes("png") ? "png" : "jpg";
|
|
1541
2187
|
const name = filename || `screenshot-${Date.now()}.${ext}`;
|
|
@@ -1576,6 +2222,7 @@ var BugBearClient = class {
|
|
|
1576
2222
|
*/
|
|
1577
2223
|
async uploadImageFromUri(uri, filename, bucket = "screenshots") {
|
|
1578
2224
|
try {
|
|
2225
|
+
await this.ensureReady();
|
|
1579
2226
|
const response = await fetch(uri);
|
|
1580
2227
|
const blob = await response.blob();
|
|
1581
2228
|
const contentType = blob.type || "image/jpeg";
|
|
@@ -1670,6 +2317,7 @@ var BugBearClient = class {
|
|
|
1670
2317
|
*/
|
|
1671
2318
|
async getFixRequests(options) {
|
|
1672
2319
|
try {
|
|
2320
|
+
await this.ensureReady();
|
|
1673
2321
|
let query = this.supabase.from("fix_requests").select("*").eq("project_id", this.config.projectId).order("created_at", { ascending: false }).limit(options?.limit || 20);
|
|
1674
2322
|
if (options?.status) {
|
|
1675
2323
|
query = query.eq("status", options.status);
|
|
@@ -1741,6 +2389,7 @@ var BugBearClient = class {
|
|
|
1741
2389
|
*/
|
|
1742
2390
|
async getThreadMessages(threadId) {
|
|
1743
2391
|
try {
|
|
2392
|
+
await this.ensureReady();
|
|
1744
2393
|
const { data, error } = await this.supabase.from("discussion_messages").select(`
|
|
1745
2394
|
id,
|
|
1746
2395
|
thread_id,
|
|
@@ -1943,6 +2592,7 @@ var BugBearClient = class {
|
|
|
1943
2592
|
*/
|
|
1944
2593
|
async endSession(sessionId, options = {}) {
|
|
1945
2594
|
try {
|
|
2595
|
+
await this.ensureReady();
|
|
1946
2596
|
const { data, error } = await this.supabase.rpc("end_qa_session", {
|
|
1947
2597
|
p_session_id: sessionId,
|
|
1948
2598
|
p_notes: options.notes || null,
|
|
@@ -1980,6 +2630,7 @@ var BugBearClient = class {
|
|
|
1980
2630
|
*/
|
|
1981
2631
|
async getSession(sessionId) {
|
|
1982
2632
|
try {
|
|
2633
|
+
await this.ensureReady();
|
|
1983
2634
|
const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("id", sessionId).single();
|
|
1984
2635
|
if (error || !data) return null;
|
|
1985
2636
|
return this.transformSession(data);
|
|
@@ -2011,6 +2662,7 @@ var BugBearClient = class {
|
|
|
2011
2662
|
*/
|
|
2012
2663
|
async addFinding(sessionId, options) {
|
|
2013
2664
|
try {
|
|
2665
|
+
await this.ensureReady();
|
|
2014
2666
|
const { data, error } = await this.supabase.rpc("add_session_finding", {
|
|
2015
2667
|
p_session_id: sessionId,
|
|
2016
2668
|
p_type: options.type,
|
|
@@ -2041,6 +2693,7 @@ var BugBearClient = class {
|
|
|
2041
2693
|
*/
|
|
2042
2694
|
async getSessionFindings(sessionId) {
|
|
2043
2695
|
try {
|
|
2696
|
+
await this.ensureReady();
|
|
2044
2697
|
const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
|
|
2045
2698
|
if (error) {
|
|
2046
2699
|
console.error("BugBear: Failed to fetch findings", formatPgError(error));
|
|
@@ -2057,6 +2710,7 @@ var BugBearClient = class {
|
|
|
2057
2710
|
*/
|
|
2058
2711
|
async convertFindingToBug(findingId) {
|
|
2059
2712
|
try {
|
|
2713
|
+
await this.ensureReady();
|
|
2060
2714
|
const { data, error } = await this.supabase.rpc("convert_finding_to_bug", {
|
|
2061
2715
|
p_finding_id: findingId
|
|
2062
2716
|
});
|
|
@@ -2076,6 +2730,7 @@ var BugBearClient = class {
|
|
|
2076
2730
|
*/
|
|
2077
2731
|
async dismissFinding(findingId, reason) {
|
|
2078
2732
|
try {
|
|
2733
|
+
await this.ensureReady();
|
|
2079
2734
|
const { error } = await this.supabase.from("qa_findings").update({
|
|
2080
2735
|
dismissed: true,
|
|
2081
2736
|
dismissed_reason: reason || null,
|
|
@@ -2150,11 +2805,22 @@ function createBugBear(config) {
|
|
|
2150
2805
|
0 && (module.exports = {
|
|
2151
2806
|
BUG_CATEGORIES,
|
|
2152
2807
|
BugBearClient,
|
|
2808
|
+
DedupWindow,
|
|
2809
|
+
ErrorMonitor,
|
|
2153
2810
|
LocalStorageAdapter,
|
|
2154
2811
|
OfflineQueue,
|
|
2812
|
+
RNApiFailureHandler,
|
|
2813
|
+
RNCrashHandler,
|
|
2814
|
+
RNRageClickHandler,
|
|
2815
|
+
RageClickDetector,
|
|
2816
|
+
WebApiFailureHandler,
|
|
2817
|
+
WebCrashHandler,
|
|
2818
|
+
WebRageClickHandler,
|
|
2155
2819
|
captureError,
|
|
2156
2820
|
contextCapture,
|
|
2157
2821
|
createBugBear,
|
|
2822
|
+
generateFingerprint,
|
|
2158
2823
|
isBugCategory,
|
|
2159
|
-
isNetworkError
|
|
2824
|
+
isNetworkError,
|
|
2825
|
+
scrubUrl
|
|
2160
2826
|
});
|