@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.mjs
CHANGED
|
@@ -18,6 +18,7 @@ var ContextCaptureManager = class {
|
|
|
18
18
|
this.networkRequests = [];
|
|
19
19
|
this.navigationHistory = [];
|
|
20
20
|
this.originalConsole = {};
|
|
21
|
+
this.fetchHost = null;
|
|
21
22
|
this.isCapturing = false;
|
|
22
23
|
}
|
|
23
24
|
/**
|
|
@@ -40,8 +41,9 @@ var ContextCaptureManager = class {
|
|
|
40
41
|
if (this.originalConsole.warn) console.warn = this.originalConsole.warn;
|
|
41
42
|
if (this.originalConsole.error) console.error = this.originalConsole.error;
|
|
42
43
|
if (this.originalConsole.info) console.info = this.originalConsole.info;
|
|
43
|
-
if (this.originalFetch &&
|
|
44
|
-
|
|
44
|
+
if (this.originalFetch && this.fetchHost) {
|
|
45
|
+
this.fetchHost.fetch = this.originalFetch;
|
|
46
|
+
this.fetchHost = null;
|
|
45
47
|
}
|
|
46
48
|
if (typeof window !== "undefined" && typeof history !== "undefined") {
|
|
47
49
|
if (this.originalPushState) {
|
|
@@ -150,15 +152,19 @@ var ContextCaptureManager = class {
|
|
|
150
152
|
});
|
|
151
153
|
}
|
|
152
154
|
captureFetch() {
|
|
153
|
-
if (typeof
|
|
154
|
-
|
|
155
|
+
if (typeof fetch === "undefined") return;
|
|
156
|
+
const host = typeof window !== "undefined" && typeof window.fetch === "function" ? window : typeof globalThis !== "undefined" && typeof globalThis.fetch === "function" ? globalThis : null;
|
|
157
|
+
if (!host) return;
|
|
158
|
+
const canCloneResponse = typeof document !== "undefined";
|
|
159
|
+
this.fetchHost = host;
|
|
160
|
+
this.originalFetch = host.fetch;
|
|
155
161
|
const self = this;
|
|
156
|
-
|
|
162
|
+
host.fetch = async function(input, init) {
|
|
157
163
|
const startTime = Date.now();
|
|
158
164
|
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
159
165
|
const method = init?.method || "GET";
|
|
160
166
|
try {
|
|
161
|
-
const response = await self.originalFetch.call(
|
|
167
|
+
const response = await self.originalFetch.call(host, input, init);
|
|
162
168
|
const requestEntry = {
|
|
163
169
|
method,
|
|
164
170
|
url: url.slice(0, 200),
|
|
@@ -167,7 +173,7 @@ var ContextCaptureManager = class {
|
|
|
167
173
|
duration: Date.now() - startTime,
|
|
168
174
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
169
175
|
};
|
|
170
|
-
if (response.status >= 400) {
|
|
176
|
+
if (canCloneResponse && response.status >= 400) {
|
|
171
177
|
try {
|
|
172
178
|
const cloned = response.clone();
|
|
173
179
|
const body = await cloned.text();
|
|
@@ -409,12 +415,442 @@ function isNetworkError(error) {
|
|
|
409
415
|
msg.includes("the internet connection appears to be offline") || msg.includes("a]server with the specified hostname could not be found");
|
|
410
416
|
}
|
|
411
417
|
|
|
418
|
+
// src/monitoring/fingerprint.ts
|
|
419
|
+
function generateFingerprint(source, message, route) {
|
|
420
|
+
const input = `${source}|${message}|${route}`;
|
|
421
|
+
let hash = 5381;
|
|
422
|
+
for (let i = 0; i < input.length; i++) {
|
|
423
|
+
hash = (hash << 5) + hash + input.charCodeAt(i) & 4294967295;
|
|
424
|
+
}
|
|
425
|
+
return `bb_${(hash >>> 0).toString(36)}`;
|
|
426
|
+
}
|
|
427
|
+
var DedupWindow = class {
|
|
428
|
+
constructor(windowMs) {
|
|
429
|
+
this.windowMs = windowMs;
|
|
430
|
+
this.seen = /* @__PURE__ */ new Map();
|
|
431
|
+
}
|
|
432
|
+
/** Returns true if this fingerprint should be reported (not a recent duplicate). */
|
|
433
|
+
shouldReport(fingerprint) {
|
|
434
|
+
const now = Date.now();
|
|
435
|
+
const lastSeen = this.seen.get(fingerprint);
|
|
436
|
+
if (lastSeen !== void 0 && now - lastSeen < this.windowMs) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
this.seen.set(fingerprint, now);
|
|
440
|
+
this.cleanup(now);
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
/** Remove expired entries to prevent memory leaks in long sessions. */
|
|
444
|
+
cleanup(now) {
|
|
445
|
+
if (this.seen.size > 100) {
|
|
446
|
+
for (const [fp, ts] of this.seen) {
|
|
447
|
+
if (now - ts >= this.windowMs) {
|
|
448
|
+
this.seen.delete(fp);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// src/monitoring/scrub.ts
|
|
456
|
+
var SENSITIVE_PARAMS = /^(token|key|password|secret|auth|credential|api_key|access_token|secret_key|apikey|session)$/i;
|
|
457
|
+
function scrubUrl(url) {
|
|
458
|
+
try {
|
|
459
|
+
const parsed = new URL(url);
|
|
460
|
+
let changed = false;
|
|
461
|
+
for (const [key] of parsed.searchParams) {
|
|
462
|
+
if (SENSITIVE_PARAMS.test(key)) {
|
|
463
|
+
parsed.searchParams.set(key, "[REDACTED]");
|
|
464
|
+
changed = true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return changed ? parsed.toString() : url;
|
|
468
|
+
} catch {
|
|
469
|
+
return url;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/monitoring/error-monitor.ts
|
|
474
|
+
var SOURCE_TO_REPORT_SOURCE = {
|
|
475
|
+
crash: "auto_crash",
|
|
476
|
+
api_failure: "auto_api",
|
|
477
|
+
rage_click: "auto_rage_click"
|
|
478
|
+
};
|
|
479
|
+
var SEVERITY_ORDER = ["low", "medium", "high", "critical"];
|
|
480
|
+
var ErrorMonitor = class {
|
|
481
|
+
constructor(config, deps) {
|
|
482
|
+
this.deps = deps;
|
|
483
|
+
this.sessionReportCount = 0;
|
|
484
|
+
this.pendingSentryData = null;
|
|
485
|
+
this.config = {
|
|
486
|
+
sampleRate: 1,
|
|
487
|
+
maxReportsPerSession: 10,
|
|
488
|
+
dedupWindowMs: 3e5,
|
|
489
|
+
severityThreshold: "low",
|
|
490
|
+
scrubUrls: true,
|
|
491
|
+
...config
|
|
492
|
+
};
|
|
493
|
+
this.dedup = new DedupWindow(this.config.dedupWindowMs);
|
|
494
|
+
}
|
|
495
|
+
async handleEvent(event) {
|
|
496
|
+
if (!this.dedup.shouldReport(event.fingerprint)) return;
|
|
497
|
+
if (Math.random() >= this.config.sampleRate) return;
|
|
498
|
+
if (this.sessionReportCount >= this.config.maxReportsPerSession) return;
|
|
499
|
+
const severity = this.getSeverity(event);
|
|
500
|
+
if (SEVERITY_ORDER.indexOf(severity) < SEVERITY_ORDER.indexOf(this.config.severityThreshold)) return;
|
|
501
|
+
if (this.config.beforeCapture && !this.config.beforeCapture(event)) return;
|
|
502
|
+
const scrubbedUrl = this.config.scrubUrls && event.requestUrl ? scrubUrl(event.requestUrl) : event.requestUrl;
|
|
503
|
+
if (this.pendingSentryData) {
|
|
504
|
+
event.sentryEventId = this.pendingSentryData.sentryEventId;
|
|
505
|
+
event.sentryBreadcrumbs = this.pendingSentryData.sentryBreadcrumbs;
|
|
506
|
+
this.pendingSentryData = null;
|
|
507
|
+
}
|
|
508
|
+
this.sessionReportCount++;
|
|
509
|
+
await this.deps.submitReport({
|
|
510
|
+
type: "bug",
|
|
511
|
+
reportSource: SOURCE_TO_REPORT_SOURCE[event.source],
|
|
512
|
+
title: this.generateTitle(event),
|
|
513
|
+
description: event.message,
|
|
514
|
+
severity,
|
|
515
|
+
category: event.source === "crash" ? "crash" : "functional",
|
|
516
|
+
appContext: {
|
|
517
|
+
currentRoute: event.route,
|
|
518
|
+
errorMessage: event.error?.message,
|
|
519
|
+
errorStack: event.error?.stack,
|
|
520
|
+
custom: {
|
|
521
|
+
monitoringSource: event.source,
|
|
522
|
+
fingerprint: event.fingerprint,
|
|
523
|
+
...event.statusCode ? { statusCode: event.statusCode } : {},
|
|
524
|
+
...scrubbedUrl ? { requestUrl: scrubbedUrl } : {},
|
|
525
|
+
...event.requestMethod ? { requestMethod: event.requestMethod } : {},
|
|
526
|
+
...event.clickCount ? { clickCount: event.clickCount } : {},
|
|
527
|
+
...event.targetSelector ? { targetSelector: event.targetSelector } : {},
|
|
528
|
+
...event.sentryEventId ? { sentryEventId: event.sentryEventId } : {}
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
enhancedContext: this.deps.getEnhancedContext(),
|
|
532
|
+
deviceInfo: this.deps.getDeviceInfo()
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
enrichWithSentry(data) {
|
|
536
|
+
this.pendingSentryData = data;
|
|
537
|
+
}
|
|
538
|
+
getSeverity(event) {
|
|
539
|
+
if (event.source === "crash") return "critical";
|
|
540
|
+
if (event.source === "api_failure") return event.statusCode && event.statusCode >= 500 ? "high" : "medium";
|
|
541
|
+
return "medium";
|
|
542
|
+
}
|
|
543
|
+
generateTitle(event) {
|
|
544
|
+
switch (event.source) {
|
|
545
|
+
case "crash":
|
|
546
|
+
return `[Auto] Crash on ${event.route}: ${event.message.slice(0, 80)}`;
|
|
547
|
+
case "api_failure":
|
|
548
|
+
return `[Auto] ${event.requestMethod || "API"} ${event.statusCode || "error"} on ${event.route}`;
|
|
549
|
+
case "rage_click":
|
|
550
|
+
return `[Auto] Rage click on ${event.route} (${event.targetSelector || "unknown"})`;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
destroy() {
|
|
554
|
+
this.sessionReportCount = 0;
|
|
555
|
+
this.pendingSentryData = null;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
// src/monitoring/rage-click-detector.ts
|
|
560
|
+
var RageClickDetector = class {
|
|
561
|
+
constructor(onRageClick) {
|
|
562
|
+
this.onRageClick = onRageClick;
|
|
563
|
+
this.clicks = [];
|
|
564
|
+
this.threshold = 3;
|
|
565
|
+
this.windowMs = 2e3;
|
|
566
|
+
this.destroyed = false;
|
|
567
|
+
}
|
|
568
|
+
recordClick(x, y, targetSelector) {
|
|
569
|
+
if (this.destroyed) return;
|
|
570
|
+
const now = Date.now();
|
|
571
|
+
this.clicks = this.clicks.filter((c) => now - c.time < this.windowMs);
|
|
572
|
+
this.clicks.push({ time: now, target: targetSelector, x, y });
|
|
573
|
+
const sameTarget = this.clicks.filter((c) => c.target === targetSelector);
|
|
574
|
+
if (sameTarget.length >= this.threshold) {
|
|
575
|
+
this.onRageClick({
|
|
576
|
+
clickCount: sameTarget.length,
|
|
577
|
+
targetSelector,
|
|
578
|
+
x,
|
|
579
|
+
y
|
|
580
|
+
});
|
|
581
|
+
this.clicks = [];
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
destroy() {
|
|
585
|
+
this.destroyed = true;
|
|
586
|
+
this.clicks = [];
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// src/monitoring/web-handlers.ts
|
|
591
|
+
function getSelector(el) {
|
|
592
|
+
if (el.id) return `#${el.id}`;
|
|
593
|
+
const tag = el.tagName.toLowerCase();
|
|
594
|
+
const classes = Array.from(el.classList).slice(0, 2);
|
|
595
|
+
return classes.length > 0 ? `${tag}.${classes.join(".")}` : tag;
|
|
596
|
+
}
|
|
597
|
+
function currentRoute() {
|
|
598
|
+
try {
|
|
599
|
+
return typeof window !== "undefined" && window.location ? window.location.pathname : "unknown";
|
|
600
|
+
} catch {
|
|
601
|
+
return "unknown";
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
var WebCrashHandler = class {
|
|
605
|
+
constructor(onEvent) {
|
|
606
|
+
this.onEvent = onEvent;
|
|
607
|
+
this.prevOnError = null;
|
|
608
|
+
this.rejectionHandler = null;
|
|
609
|
+
}
|
|
610
|
+
start() {
|
|
611
|
+
this.prevOnError = window.onerror;
|
|
612
|
+
window.onerror = (message, source, lineno, colno, error) => {
|
|
613
|
+
const msg = typeof message === "string" ? message : "Unknown error";
|
|
614
|
+
const route = currentRoute();
|
|
615
|
+
this.onEvent({
|
|
616
|
+
source: "crash",
|
|
617
|
+
fingerprint: generateFingerprint("crash", msg, route),
|
|
618
|
+
message: msg,
|
|
619
|
+
route,
|
|
620
|
+
timestamp: Date.now(),
|
|
621
|
+
error: error ?? new Error(msg)
|
|
622
|
+
});
|
|
623
|
+
if (typeof this.prevOnError === "function") {
|
|
624
|
+
return this.prevOnError(message, source, lineno, colno, error);
|
|
625
|
+
}
|
|
626
|
+
return false;
|
|
627
|
+
};
|
|
628
|
+
this.rejectionHandler = (e) => {
|
|
629
|
+
const reason = e.reason;
|
|
630
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
631
|
+
const msg = error.message || "Unhandled promise rejection";
|
|
632
|
+
const route = currentRoute();
|
|
633
|
+
this.onEvent({
|
|
634
|
+
source: "crash",
|
|
635
|
+
fingerprint: generateFingerprint("crash", msg, route),
|
|
636
|
+
message: msg,
|
|
637
|
+
route,
|
|
638
|
+
timestamp: Date.now(),
|
|
639
|
+
error
|
|
640
|
+
});
|
|
641
|
+
};
|
|
642
|
+
window.addEventListener("unhandledrejection", this.rejectionHandler);
|
|
643
|
+
}
|
|
644
|
+
destroy() {
|
|
645
|
+
window.onerror = this.prevOnError;
|
|
646
|
+
if (this.rejectionHandler) {
|
|
647
|
+
window.removeEventListener("unhandledrejection", this.rejectionHandler);
|
|
648
|
+
this.rejectionHandler = null;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
var WebApiFailureHandler = class {
|
|
653
|
+
constructor(onEvent) {
|
|
654
|
+
this.onEvent = onEvent;
|
|
655
|
+
this.originalFetch = null;
|
|
656
|
+
}
|
|
657
|
+
start() {
|
|
658
|
+
this.originalFetch = window.fetch;
|
|
659
|
+
const original = this.originalFetch;
|
|
660
|
+
const onEvent = this.onEvent;
|
|
661
|
+
window.fetch = async function patchedFetch(input, init) {
|
|
662
|
+
const response = await original.call(window, input, init);
|
|
663
|
+
if (response.status >= 400) {
|
|
664
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
665
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
666
|
+
const route = currentRoute();
|
|
667
|
+
const msg = `${method} ${url} failed with ${response.status}`;
|
|
668
|
+
onEvent({
|
|
669
|
+
source: "api_failure",
|
|
670
|
+
fingerprint: generateFingerprint("api_failure", msg, route),
|
|
671
|
+
message: msg,
|
|
672
|
+
route,
|
|
673
|
+
timestamp: Date.now(),
|
|
674
|
+
requestUrl: url,
|
|
675
|
+
requestMethod: method,
|
|
676
|
+
statusCode: response.status
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
return response;
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
destroy() {
|
|
683
|
+
if (this.originalFetch) {
|
|
684
|
+
window.fetch = this.originalFetch;
|
|
685
|
+
this.originalFetch = null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
var WebRageClickHandler = class {
|
|
690
|
+
constructor(onEvent) {
|
|
691
|
+
this.onEvent = onEvent;
|
|
692
|
+
this.clickHandler = null;
|
|
693
|
+
this.detector = new RageClickDetector((rageEvent) => {
|
|
694
|
+
const route = currentRoute();
|
|
695
|
+
this.onEvent({
|
|
696
|
+
source: "rage_click",
|
|
697
|
+
fingerprint: generateFingerprint("rage_click", rageEvent.targetSelector, route),
|
|
698
|
+
message: `Rage click detected on ${rageEvent.targetSelector}`,
|
|
699
|
+
route,
|
|
700
|
+
timestamp: Date.now(),
|
|
701
|
+
clickCount: rageEvent.clickCount,
|
|
702
|
+
targetSelector: rageEvent.targetSelector
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
start() {
|
|
707
|
+
this.clickHandler = (e) => {
|
|
708
|
+
const target = e.target;
|
|
709
|
+
if (!(target instanceof Element)) return;
|
|
710
|
+
const selector = getSelector(target);
|
|
711
|
+
this.detector.recordClick(e.clientX, e.clientY, selector);
|
|
712
|
+
};
|
|
713
|
+
document.addEventListener("click", this.clickHandler, true);
|
|
714
|
+
}
|
|
715
|
+
destroy() {
|
|
716
|
+
if (this.clickHandler) {
|
|
717
|
+
document.removeEventListener("click", this.clickHandler, true);
|
|
718
|
+
this.clickHandler = null;
|
|
719
|
+
}
|
|
720
|
+
this.detector.destroy();
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// src/monitoring/rn-handlers.ts
|
|
725
|
+
var RNCrashHandler = class {
|
|
726
|
+
constructor(onEvent, getCurrentRoute) {
|
|
727
|
+
this.onEvent = onEvent;
|
|
728
|
+
this.getCurrentRoute = getCurrentRoute;
|
|
729
|
+
this.originalHandler = null;
|
|
730
|
+
this.started = false;
|
|
731
|
+
}
|
|
732
|
+
start() {
|
|
733
|
+
if (this.started) return;
|
|
734
|
+
this.started = true;
|
|
735
|
+
const errorUtils = globalThis.ErrorUtils;
|
|
736
|
+
if (!errorUtils) return;
|
|
737
|
+
this.originalHandler = errorUtils.getGlobalHandler();
|
|
738
|
+
const self = this;
|
|
739
|
+
errorUtils.setGlobalHandler((error, isFatal) => {
|
|
740
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
741
|
+
const route = self.getCurrentRoute();
|
|
742
|
+
self.onEvent({
|
|
743
|
+
source: "crash",
|
|
744
|
+
fingerprint: generateFingerprint("crash", normalizedError.message, route),
|
|
745
|
+
message: normalizedError.message,
|
|
746
|
+
route,
|
|
747
|
+
timestamp: Date.now(),
|
|
748
|
+
error: normalizedError
|
|
749
|
+
});
|
|
750
|
+
if (self.originalHandler) {
|
|
751
|
+
self.originalHandler(error, isFatal);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
destroy() {
|
|
756
|
+
if (!this.started) return;
|
|
757
|
+
this.started = false;
|
|
758
|
+
const errorUtils = globalThis.ErrorUtils;
|
|
759
|
+
if (errorUtils && this.originalHandler) {
|
|
760
|
+
errorUtils.setGlobalHandler(this.originalHandler);
|
|
761
|
+
}
|
|
762
|
+
this.originalHandler = null;
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
var RNApiFailureHandler = class {
|
|
766
|
+
constructor(onEvent, getCurrentRoute) {
|
|
767
|
+
this.onEvent = onEvent;
|
|
768
|
+
this.getCurrentRoute = getCurrentRoute;
|
|
769
|
+
this.originalFetch = null;
|
|
770
|
+
this.started = false;
|
|
771
|
+
}
|
|
772
|
+
start() {
|
|
773
|
+
if (this.started) return;
|
|
774
|
+
this.started = true;
|
|
775
|
+
this.originalFetch = globalThis.fetch;
|
|
776
|
+
const self = this;
|
|
777
|
+
globalThis.fetch = async function patchedFetch(input, init) {
|
|
778
|
+
const response = await self.originalFetch.call(globalThis, input, init);
|
|
779
|
+
if (response.status >= 400) {
|
|
780
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
781
|
+
const method = init?.method ?? "GET";
|
|
782
|
+
const route = self.getCurrentRoute();
|
|
783
|
+
self.onEvent({
|
|
784
|
+
source: "api_failure",
|
|
785
|
+
fingerprint: generateFingerprint("api_failure", `${method} ${response.status}`, route),
|
|
786
|
+
message: `${method} ${url} failed with ${response.status}`,
|
|
787
|
+
route,
|
|
788
|
+
timestamp: Date.now(),
|
|
789
|
+
requestUrl: url,
|
|
790
|
+
requestMethod: method,
|
|
791
|
+
statusCode: response.status
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return response;
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
destroy() {
|
|
798
|
+
if (!this.started) return;
|
|
799
|
+
this.started = false;
|
|
800
|
+
if (this.originalFetch) {
|
|
801
|
+
globalThis.fetch = this.originalFetch;
|
|
802
|
+
this.originalFetch = null;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
var RNRageClickHandler = class {
|
|
807
|
+
constructor(onEvent, getCurrentRoute) {
|
|
808
|
+
this.onEvent = onEvent;
|
|
809
|
+
this.getCurrentRoute = getCurrentRoute;
|
|
810
|
+
this.detector = null;
|
|
811
|
+
}
|
|
812
|
+
start() {
|
|
813
|
+
if (this.detector) return;
|
|
814
|
+
const self = this;
|
|
815
|
+
this.detector = new RageClickDetector((rageEvent) => {
|
|
816
|
+
const route = self.getCurrentRoute();
|
|
817
|
+
self.onEvent({
|
|
818
|
+
source: "rage_click",
|
|
819
|
+
fingerprint: generateFingerprint("rage_click", rageEvent.targetSelector, route),
|
|
820
|
+
message: `Rage click detected on ${rageEvent.targetSelector}`,
|
|
821
|
+
route,
|
|
822
|
+
timestamp: Date.now(),
|
|
823
|
+
clickCount: rageEvent.clickCount,
|
|
824
|
+
targetSelector: rageEvent.targetSelector
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Record a touch event. Call this from the BugBearProvider's onTouchEnd handler.
|
|
830
|
+
* @param x - Touch x coordinate
|
|
831
|
+
* @param y - Touch y coordinate
|
|
832
|
+
* @param viewId - Identifier for the touched view (e.g., testID, accessibilityLabel, or nativeID)
|
|
833
|
+
*/
|
|
834
|
+
recordTouch(x, y, viewId) {
|
|
835
|
+
this.detector?.recordClick(x, y, viewId);
|
|
836
|
+
}
|
|
837
|
+
destroy() {
|
|
838
|
+
if (this.detector) {
|
|
839
|
+
this.detector.destroy();
|
|
840
|
+
this.detector = null;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
|
|
412
845
|
// src/client.ts
|
|
413
846
|
var formatPgError = (e) => {
|
|
414
847
|
if (!e || typeof e !== "object") return { raw: e };
|
|
415
848
|
const { message, code, details, hint } = e;
|
|
416
849
|
return { message, code, details, hint };
|
|
417
850
|
};
|
|
851
|
+
var DEFAULT_API_BASE_URL = "https://app.bugbear.ai";
|
|
852
|
+
var CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
853
|
+
var CONFIG_CACHE_PREFIX = "bugbear_config_";
|
|
418
854
|
var BugBearClient = class {
|
|
419
855
|
constructor(config) {
|
|
420
856
|
this.navigationHistory = [];
|
|
@@ -424,23 +860,148 @@ var BugBearClient = class {
|
|
|
424
860
|
/** Active Realtime channel references for cleanup. */
|
|
425
861
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
426
862
|
this.realtimeChannels = [];
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
863
|
+
/** Error monitor instance — created when config.monitoring is present. */
|
|
864
|
+
this.monitor = null;
|
|
865
|
+
/** Whether the client has been successfully initialized. */
|
|
866
|
+
this.initialized = false;
|
|
867
|
+
/** Initialization error, if any. */
|
|
868
|
+
this.initError = null;
|
|
433
869
|
this.config = config;
|
|
434
|
-
|
|
435
|
-
|
|
870
|
+
if (config.apiKey) {
|
|
871
|
+
this.pendingInit = this.resolveFromApiKey(config.apiKey);
|
|
872
|
+
} else if (config.supabaseUrl && config.supabaseAnonKey) {
|
|
873
|
+
if (!config.projectId) {
|
|
874
|
+
throw new Error(
|
|
875
|
+
"BugBear: projectId is required when using explicit Supabase credentials. Tip: Use apiKey instead for simpler setup \u2014 it resolves everything automatically."
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
this.supabase = createClient(config.supabaseUrl, config.supabaseAnonKey);
|
|
879
|
+
this.initialized = true;
|
|
880
|
+
this.pendingInit = Promise.resolve();
|
|
881
|
+
this.initOfflineQueue();
|
|
882
|
+
this.initMonitoring();
|
|
883
|
+
} else {
|
|
884
|
+
throw new Error(
|
|
885
|
+
"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"
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/** Whether the client is ready for requests. */
|
|
890
|
+
get isReady() {
|
|
891
|
+
return this.initialized;
|
|
892
|
+
}
|
|
893
|
+
/** Wait until the client is ready. Throws if initialization failed. */
|
|
894
|
+
async ready() {
|
|
895
|
+
await this.pendingInit;
|
|
896
|
+
if (this.initError) throw this.initError;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Resolve Supabase credentials from a BugBear API key.
|
|
900
|
+
* Checks localStorage cache first, falls back to /api/v1/config.
|
|
901
|
+
*/
|
|
902
|
+
async resolveFromApiKey(apiKey) {
|
|
903
|
+
try {
|
|
904
|
+
const cached = this.readConfigCache(apiKey);
|
|
905
|
+
if (cached) {
|
|
906
|
+
this.applyResolvedConfig(cached);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
|
|
910
|
+
const response = await fetch(`${baseUrl}/api/v1/config`, {
|
|
911
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
912
|
+
});
|
|
913
|
+
if (!response.ok) {
|
|
914
|
+
const body = await response.json().catch(() => ({}));
|
|
915
|
+
const message = body.error || `HTTP ${response.status}`;
|
|
916
|
+
throw new Error(
|
|
917
|
+
`BugBear: Invalid API key \u2014 ${message}. Get yours at https://app.bugbear.ai/settings/projects`
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
const data = await response.json();
|
|
921
|
+
this.writeConfigCache(apiKey, data);
|
|
922
|
+
this.applyResolvedConfig(data);
|
|
923
|
+
} catch (err) {
|
|
924
|
+
this.initError = err instanceof Error ? err : new Error(String(err));
|
|
925
|
+
this.config.onError?.(this.initError, { context: "apikey_resolution_failed" });
|
|
926
|
+
throw this.initError;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
/** Apply resolved credentials and create the Supabase client. */
|
|
930
|
+
applyResolvedConfig(resolved) {
|
|
931
|
+
this.config = {
|
|
932
|
+
...this.config,
|
|
933
|
+
projectId: resolved.projectId,
|
|
934
|
+
supabaseUrl: resolved.supabaseUrl,
|
|
935
|
+
supabaseAnonKey: resolved.supabaseAnonKey
|
|
936
|
+
};
|
|
937
|
+
this.supabase = createClient(resolved.supabaseUrl, resolved.supabaseAnonKey);
|
|
938
|
+
this.initialized = true;
|
|
939
|
+
this.initOfflineQueue();
|
|
940
|
+
this.initMonitoring();
|
|
941
|
+
}
|
|
942
|
+
/** Initialize offline queue if configured. Shared by both init paths. */
|
|
943
|
+
initOfflineQueue() {
|
|
944
|
+
if (this.config.offlineQueue?.enabled) {
|
|
436
945
|
this._queue = new OfflineQueue({
|
|
437
946
|
enabled: true,
|
|
438
|
-
maxItems: config.offlineQueue.maxItems,
|
|
439
|
-
maxRetries: config.offlineQueue.maxRetries
|
|
947
|
+
maxItems: this.config.offlineQueue.maxItems,
|
|
948
|
+
maxRetries: this.config.offlineQueue.maxRetries
|
|
440
949
|
});
|
|
441
950
|
this.registerQueueHandlers();
|
|
442
951
|
}
|
|
443
952
|
}
|
|
953
|
+
/** Initialize error monitoring if configured. Shared by both init paths. */
|
|
954
|
+
initMonitoring() {
|
|
955
|
+
const mc = this.config.monitoring;
|
|
956
|
+
if (!mc || !mc.crashes && !mc.apiFailures && !mc.rageClicks) return;
|
|
957
|
+
this.monitor = new ErrorMonitor(mc, {
|
|
958
|
+
submitReport: (report) => this.submitReport(report),
|
|
959
|
+
getCurrentRoute: () => contextCapture.getCurrentRoute(),
|
|
960
|
+
getEnhancedContext: () => contextCapture.getEnhancedContext(),
|
|
961
|
+
getDeviceInfo: () => this.getDeviceInfo()
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
/** Read cached config from localStorage if available and not expired. */
|
|
965
|
+
readConfigCache(apiKey) {
|
|
966
|
+
if (typeof localStorage === "undefined") return null;
|
|
967
|
+
try {
|
|
968
|
+
const key = CONFIG_CACHE_PREFIX + this.hashKey(apiKey);
|
|
969
|
+
const raw = localStorage.getItem(key);
|
|
970
|
+
if (!raw) return null;
|
|
971
|
+
const cached = JSON.parse(raw);
|
|
972
|
+
if (Date.now() - cached.cachedAt > CONFIG_CACHE_TTL_MS) {
|
|
973
|
+
localStorage.removeItem(key);
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
return cached;
|
|
977
|
+
} catch {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/** Write resolved config to localStorage cache. */
|
|
982
|
+
writeConfigCache(apiKey, data) {
|
|
983
|
+
if (typeof localStorage === "undefined") return;
|
|
984
|
+
try {
|
|
985
|
+
const key = CONFIG_CACHE_PREFIX + this.hashKey(apiKey);
|
|
986
|
+
const cached = { ...data, cachedAt: Date.now() };
|
|
987
|
+
localStorage.setItem(key, JSON.stringify(cached));
|
|
988
|
+
} catch {
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
/** Simple string hash for cache keys — avoids storing raw API keys. */
|
|
992
|
+
hashKey(apiKey) {
|
|
993
|
+
let hash = 0;
|
|
994
|
+
for (let i = 0; i < apiKey.length; i++) {
|
|
995
|
+
hash = (hash << 5) - hash + apiKey.charCodeAt(i) | 0;
|
|
996
|
+
}
|
|
997
|
+
return hash.toString(36);
|
|
998
|
+
}
|
|
999
|
+
/** Ensure the client is initialized before making requests. */
|
|
1000
|
+
async ensureReady() {
|
|
1001
|
+
if (this.initialized) return;
|
|
1002
|
+
await this.pendingInit;
|
|
1003
|
+
if (this.initError) throw this.initError;
|
|
1004
|
+
}
|
|
444
1005
|
// ── Offline Queue ─────────────────────────────────────────
|
|
445
1006
|
/**
|
|
446
1007
|
* Access the offline queue (if enabled).
|
|
@@ -596,6 +1157,7 @@ var BugBearClient = class {
|
|
|
596
1157
|
* Get current user info from host app or BugBear's own auth
|
|
597
1158
|
*/
|
|
598
1159
|
async getCurrentUserInfo() {
|
|
1160
|
+
await this.ensureReady();
|
|
599
1161
|
if (this.config.getCurrentUser) {
|
|
600
1162
|
return await this.config.getCurrentUser();
|
|
601
1163
|
}
|
|
@@ -654,7 +1216,9 @@ var BugBearClient = class {
|
|
|
654
1216
|
navigation_history: this.getNavigationHistory(),
|
|
655
1217
|
enhanced_context: report.enhancedContext || contextCapture.getEnhancedContext(),
|
|
656
1218
|
assignment_id: report.assignmentId,
|
|
657
|
-
test_case_id: report.testCaseId
|
|
1219
|
+
test_case_id: report.testCaseId,
|
|
1220
|
+
report_source: report.reportSource || "manual",
|
|
1221
|
+
error_fingerprint: report.appContext?.custom?.fingerprint || null
|
|
658
1222
|
};
|
|
659
1223
|
const { data, error } = await this.supabase.from("reports").insert(fullReport).select("id").single();
|
|
660
1224
|
if (error) {
|
|
@@ -736,7 +1300,7 @@ var BugBearClient = class {
|
|
|
736
1300
|
`;
|
|
737
1301
|
const [pendingResult, completedResult] = await Promise.all([
|
|
738
1302
|
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),
|
|
739
|
-
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)
|
|
1303
|
+
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)
|
|
740
1304
|
]);
|
|
741
1305
|
if (pendingResult.error) {
|
|
742
1306
|
console.error("BugBear: Failed to fetch assignments", formatPgError(pendingResult.error));
|
|
@@ -815,6 +1379,7 @@ var BugBearClient = class {
|
|
|
815
1379
|
*/
|
|
816
1380
|
async getAssignment(assignmentId) {
|
|
817
1381
|
try {
|
|
1382
|
+
await this.ensureReady();
|
|
818
1383
|
const { data, error } = await this.supabase.from("test_assignments").select(`
|
|
819
1384
|
id,
|
|
820
1385
|
status,
|
|
@@ -886,6 +1451,7 @@ var BugBearClient = class {
|
|
|
886
1451
|
*/
|
|
887
1452
|
async updateAssignmentStatus(assignmentId, status, options) {
|
|
888
1453
|
try {
|
|
1454
|
+
await this.ensureReady();
|
|
889
1455
|
const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at").eq("id", assignmentId).single();
|
|
890
1456
|
if (fetchError || !currentAssignment) {
|
|
891
1457
|
console.error("BugBear: Assignment not found", {
|
|
@@ -972,6 +1538,7 @@ var BugBearClient = class {
|
|
|
972
1538
|
*/
|
|
973
1539
|
async reopenAssignment(assignmentId) {
|
|
974
1540
|
try {
|
|
1541
|
+
await this.ensureReady();
|
|
975
1542
|
const { data: current, error: fetchError } = await this.supabase.from("test_assignments").select("status").eq("id", assignmentId).single();
|
|
976
1543
|
if (fetchError || !current) {
|
|
977
1544
|
return { success: false, error: "Assignment not found" };
|
|
@@ -1011,6 +1578,7 @@ var BugBearClient = class {
|
|
|
1011
1578
|
actualNotes = notes;
|
|
1012
1579
|
}
|
|
1013
1580
|
try {
|
|
1581
|
+
await this.ensureReady();
|
|
1014
1582
|
const updateData = {
|
|
1015
1583
|
status: "skipped",
|
|
1016
1584
|
skip_reason: actualReason,
|
|
@@ -1180,6 +1748,7 @@ var BugBearClient = class {
|
|
|
1180
1748
|
*/
|
|
1181
1749
|
async getTesterInfo() {
|
|
1182
1750
|
try {
|
|
1751
|
+
await this.ensureReady();
|
|
1183
1752
|
const userInfo = await this.getCurrentUserInfo();
|
|
1184
1753
|
if (!userInfo?.email) return null;
|
|
1185
1754
|
if (!this.isValidEmail(userInfo.email)) {
|
|
@@ -1200,7 +1769,8 @@ var BugBearClient = class {
|
|
|
1200
1769
|
avatarUrl: tester.avatar_url || void 0,
|
|
1201
1770
|
platforms: tester.platforms || [],
|
|
1202
1771
|
assignedTests: tester.assigned_count || 0,
|
|
1203
|
-
completedTests: tester.completed_count || 0
|
|
1772
|
+
completedTests: tester.completed_count || 0,
|
|
1773
|
+
role: tester.role || "tester"
|
|
1204
1774
|
};
|
|
1205
1775
|
} catch (err) {
|
|
1206
1776
|
console.error("BugBear: getTesterInfo error", err);
|
|
@@ -1471,6 +2041,7 @@ var BugBearClient = class {
|
|
|
1471
2041
|
*/
|
|
1472
2042
|
async isQAEnabled() {
|
|
1473
2043
|
try {
|
|
2044
|
+
await this.ensureReady();
|
|
1474
2045
|
const { data, error } = await this.supabase.rpc("check_qa_enabled", {
|
|
1475
2046
|
p_project_id: this.config.projectId
|
|
1476
2047
|
});
|
|
@@ -1487,21 +2058,85 @@ var BugBearClient = class {
|
|
|
1487
2058
|
}
|
|
1488
2059
|
}
|
|
1489
2060
|
/**
|
|
1490
|
-
* Check if the widget should be visible
|
|
1491
|
-
*
|
|
2061
|
+
* Check if the widget should be visible.
|
|
2062
|
+
* Behavior depends on the configured mode:
|
|
2063
|
+
* - 'qa': QA enabled AND user is a registered tester
|
|
2064
|
+
* - 'feedback': Any authenticated user
|
|
2065
|
+
* - 'auto': Either QA tester OR authenticated non-tester
|
|
1492
2066
|
*/
|
|
1493
2067
|
async shouldShowWidget() {
|
|
1494
|
-
const
|
|
2068
|
+
const mode = this.config.mode || "qa";
|
|
2069
|
+
if (mode === "qa") {
|
|
2070
|
+
const [qaEnabled2, tester] = await Promise.all([
|
|
2071
|
+
this.isQAEnabled(),
|
|
2072
|
+
this.isTester()
|
|
2073
|
+
]);
|
|
2074
|
+
return qaEnabled2 && tester;
|
|
2075
|
+
}
|
|
2076
|
+
if (mode === "feedback") {
|
|
2077
|
+
const userInfo2 = await this.getCurrentUserInfo();
|
|
2078
|
+
return userInfo2 !== null;
|
|
2079
|
+
}
|
|
2080
|
+
const [qaEnabled, testerInfo, userInfo] = await Promise.all([
|
|
2081
|
+
this.isQAEnabled(),
|
|
2082
|
+
this.getTesterInfo(),
|
|
2083
|
+
this.getCurrentUserInfo()
|
|
2084
|
+
]);
|
|
2085
|
+
if (qaEnabled && testerInfo && testerInfo.role !== "feedback") return true;
|
|
2086
|
+
if (userInfo) return true;
|
|
2087
|
+
return false;
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Resolve the effective widget mode for the current user.
|
|
2091
|
+
* - 'qa' or 'feedback' config → returned as-is
|
|
2092
|
+
* - 'auto' → checks if user is a QA tester (role='tester') → 'qa', otherwise 'feedback'
|
|
2093
|
+
*/
|
|
2094
|
+
async getEffectiveMode() {
|
|
2095
|
+
const mode = this.config.mode || "qa";
|
|
2096
|
+
if (mode === "qa") return "qa";
|
|
2097
|
+
if (mode === "feedback") return "feedback";
|
|
2098
|
+
const [qaEnabled, testerInfo] = await Promise.all([
|
|
1495
2099
|
this.isQAEnabled(),
|
|
1496
|
-
this.
|
|
2100
|
+
this.getTesterInfo()
|
|
1497
2101
|
]);
|
|
1498
|
-
|
|
2102
|
+
if (qaEnabled && testerInfo && testerInfo.role !== "feedback") return "qa";
|
|
2103
|
+
return "feedback";
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Auto-provision a feedback user record in the testers table.
|
|
2107
|
+
* Called during initialization when mode is 'feedback' or 'auto' for non-testers.
|
|
2108
|
+
* Idempotent — safe to call multiple times. Returns the tester info for the user.
|
|
2109
|
+
*/
|
|
2110
|
+
async ensureFeedbackUser() {
|
|
2111
|
+
try {
|
|
2112
|
+
await this.ensureReady();
|
|
2113
|
+
const userInfo = await this.getCurrentUserInfo();
|
|
2114
|
+
if (!userInfo?.email) return null;
|
|
2115
|
+
const { error } = await this.supabase.rpc("ensure_feedback_user", {
|
|
2116
|
+
p_project_id: this.config.projectId,
|
|
2117
|
+
p_user_id: userInfo.id,
|
|
2118
|
+
p_email: userInfo.email,
|
|
2119
|
+
p_name: userInfo.name || null
|
|
2120
|
+
});
|
|
2121
|
+
if (error) {
|
|
2122
|
+
console.error("BugBear: Failed to ensure feedback user", error.message);
|
|
2123
|
+
this.config.onError?.(new Error(`ensure_feedback_user failed: ${error.message}`), {
|
|
2124
|
+
context: "feedback_user_provisioning"
|
|
2125
|
+
});
|
|
2126
|
+
return null;
|
|
2127
|
+
}
|
|
2128
|
+
return this.getTesterInfo();
|
|
2129
|
+
} catch (err) {
|
|
2130
|
+
console.error("BugBear: Error ensuring feedback user", err);
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
1499
2133
|
}
|
|
1500
2134
|
/**
|
|
1501
2135
|
* Upload a screenshot (web - uses File/Blob)
|
|
1502
2136
|
*/
|
|
1503
2137
|
async uploadScreenshot(file, filename, bucket = "screenshots") {
|
|
1504
2138
|
try {
|
|
2139
|
+
await this.ensureReady();
|
|
1505
2140
|
const contentType = file.type || "image/png";
|
|
1506
2141
|
const ext = contentType.includes("png") ? "png" : "jpg";
|
|
1507
2142
|
const name = filename || `screenshot-${Date.now()}.${ext}`;
|
|
@@ -1542,6 +2177,7 @@ var BugBearClient = class {
|
|
|
1542
2177
|
*/
|
|
1543
2178
|
async uploadImageFromUri(uri, filename, bucket = "screenshots") {
|
|
1544
2179
|
try {
|
|
2180
|
+
await this.ensureReady();
|
|
1545
2181
|
const response = await fetch(uri);
|
|
1546
2182
|
const blob = await response.blob();
|
|
1547
2183
|
const contentType = blob.type || "image/jpeg";
|
|
@@ -1636,6 +2272,7 @@ var BugBearClient = class {
|
|
|
1636
2272
|
*/
|
|
1637
2273
|
async getFixRequests(options) {
|
|
1638
2274
|
try {
|
|
2275
|
+
await this.ensureReady();
|
|
1639
2276
|
let query = this.supabase.from("fix_requests").select("*").eq("project_id", this.config.projectId).order("created_at", { ascending: false }).limit(options?.limit || 20);
|
|
1640
2277
|
if (options?.status) {
|
|
1641
2278
|
query = query.eq("status", options.status);
|
|
@@ -1707,6 +2344,7 @@ var BugBearClient = class {
|
|
|
1707
2344
|
*/
|
|
1708
2345
|
async getThreadMessages(threadId) {
|
|
1709
2346
|
try {
|
|
2347
|
+
await this.ensureReady();
|
|
1710
2348
|
const { data, error } = await this.supabase.from("discussion_messages").select(`
|
|
1711
2349
|
id,
|
|
1712
2350
|
thread_id,
|
|
@@ -1909,6 +2547,7 @@ var BugBearClient = class {
|
|
|
1909
2547
|
*/
|
|
1910
2548
|
async endSession(sessionId, options = {}) {
|
|
1911
2549
|
try {
|
|
2550
|
+
await this.ensureReady();
|
|
1912
2551
|
const { data, error } = await this.supabase.rpc("end_qa_session", {
|
|
1913
2552
|
p_session_id: sessionId,
|
|
1914
2553
|
p_notes: options.notes || null,
|
|
@@ -1946,6 +2585,7 @@ var BugBearClient = class {
|
|
|
1946
2585
|
*/
|
|
1947
2586
|
async getSession(sessionId) {
|
|
1948
2587
|
try {
|
|
2588
|
+
await this.ensureReady();
|
|
1949
2589
|
const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("id", sessionId).single();
|
|
1950
2590
|
if (error || !data) return null;
|
|
1951
2591
|
return this.transformSession(data);
|
|
@@ -1977,6 +2617,7 @@ var BugBearClient = class {
|
|
|
1977
2617
|
*/
|
|
1978
2618
|
async addFinding(sessionId, options) {
|
|
1979
2619
|
try {
|
|
2620
|
+
await this.ensureReady();
|
|
1980
2621
|
const { data, error } = await this.supabase.rpc("add_session_finding", {
|
|
1981
2622
|
p_session_id: sessionId,
|
|
1982
2623
|
p_type: options.type,
|
|
@@ -2007,6 +2648,7 @@ var BugBearClient = class {
|
|
|
2007
2648
|
*/
|
|
2008
2649
|
async getSessionFindings(sessionId) {
|
|
2009
2650
|
try {
|
|
2651
|
+
await this.ensureReady();
|
|
2010
2652
|
const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
|
|
2011
2653
|
if (error) {
|
|
2012
2654
|
console.error("BugBear: Failed to fetch findings", formatPgError(error));
|
|
@@ -2023,6 +2665,7 @@ var BugBearClient = class {
|
|
|
2023
2665
|
*/
|
|
2024
2666
|
async convertFindingToBug(findingId) {
|
|
2025
2667
|
try {
|
|
2668
|
+
await this.ensureReady();
|
|
2026
2669
|
const { data, error } = await this.supabase.rpc("convert_finding_to_bug", {
|
|
2027
2670
|
p_finding_id: findingId
|
|
2028
2671
|
});
|
|
@@ -2042,6 +2685,7 @@ var BugBearClient = class {
|
|
|
2042
2685
|
*/
|
|
2043
2686
|
async dismissFinding(findingId, reason) {
|
|
2044
2687
|
try {
|
|
2688
|
+
await this.ensureReady();
|
|
2045
2689
|
const { error } = await this.supabase.from("qa_findings").update({
|
|
2046
2690
|
dismissed: true,
|
|
2047
2691
|
dismissed_reason: reason || null,
|
|
@@ -2115,11 +2759,22 @@ function createBugBear(config) {
|
|
|
2115
2759
|
export {
|
|
2116
2760
|
BUG_CATEGORIES,
|
|
2117
2761
|
BugBearClient,
|
|
2762
|
+
DedupWindow,
|
|
2763
|
+
ErrorMonitor,
|
|
2118
2764
|
LocalStorageAdapter,
|
|
2119
2765
|
OfflineQueue,
|
|
2766
|
+
RNApiFailureHandler,
|
|
2767
|
+
RNCrashHandler,
|
|
2768
|
+
RNRageClickHandler,
|
|
2769
|
+
RageClickDetector,
|
|
2770
|
+
WebApiFailureHandler,
|
|
2771
|
+
WebCrashHandler,
|
|
2772
|
+
WebRageClickHandler,
|
|
2120
2773
|
captureError,
|
|
2121
2774
|
contextCapture,
|
|
2122
2775
|
createBugBear,
|
|
2776
|
+
generateFingerprint,
|
|
2123
2777
|
isBugCategory,
|
|
2124
|
-
isNetworkError
|
|
2778
|
+
isNetworkError,
|
|
2779
|
+
scrubUrl
|
|
2125
2780
|
};
|