@bbearai/core 0.6.0 → 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 +256 -3
- package/dist/index.d.ts +256 -3
- package/dist/index.js +540 -10
- package/dist/index.mjs +528 -9
- 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
|
|
|
@@ -449,6 +460,433 @@ function isNetworkError(error) {
|
|
|
449
460
|
msg.includes("the internet connection appears to be offline") || msg.includes("a]server with the specified hostname could not be found");
|
|
450
461
|
}
|
|
451
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
|
+
|
|
452
890
|
// src/client.ts
|
|
453
891
|
var formatPgError = (e) => {
|
|
454
892
|
if (!e || typeof e !== "object") return { raw: e };
|
|
@@ -467,6 +905,8 @@ var BugBearClient = class {
|
|
|
467
905
|
/** Active Realtime channel references for cleanup. */
|
|
468
906
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
469
907
|
this.realtimeChannels = [];
|
|
908
|
+
/** Error monitor instance — created when config.monitoring is present. */
|
|
909
|
+
this.monitor = null;
|
|
470
910
|
/** Whether the client has been successfully initialized. */
|
|
471
911
|
this.initialized = false;
|
|
472
912
|
/** Initialization error, if any. */
|
|
@@ -484,6 +924,7 @@ var BugBearClient = class {
|
|
|
484
924
|
this.initialized = true;
|
|
485
925
|
this.pendingInit = Promise.resolve();
|
|
486
926
|
this.initOfflineQueue();
|
|
927
|
+
this.initMonitoring();
|
|
487
928
|
} else {
|
|
488
929
|
throw new Error(
|
|
489
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"
|
|
@@ -541,6 +982,7 @@ var BugBearClient = class {
|
|
|
541
982
|
this.supabase = (0, import_supabase_js.createClient)(resolved.supabaseUrl, resolved.supabaseAnonKey);
|
|
542
983
|
this.initialized = true;
|
|
543
984
|
this.initOfflineQueue();
|
|
985
|
+
this.initMonitoring();
|
|
544
986
|
}
|
|
545
987
|
/** Initialize offline queue if configured. Shared by both init paths. */
|
|
546
988
|
initOfflineQueue() {
|
|
@@ -553,6 +995,17 @@ var BugBearClient = class {
|
|
|
553
995
|
this.registerQueueHandlers();
|
|
554
996
|
}
|
|
555
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
|
+
}
|
|
556
1009
|
/** Read cached config from localStorage if available and not expired. */
|
|
557
1010
|
readConfigCache(apiKey) {
|
|
558
1011
|
if (typeof localStorage === "undefined") return null;
|
|
@@ -808,7 +1261,9 @@ var BugBearClient = class {
|
|
|
808
1261
|
navigation_history: this.getNavigationHistory(),
|
|
809
1262
|
enhanced_context: report.enhancedContext || contextCapture.getEnhancedContext(),
|
|
810
1263
|
assignment_id: report.assignmentId,
|
|
811
|
-
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
|
|
812
1267
|
};
|
|
813
1268
|
const { data, error } = await this.supabase.from("reports").insert(fullReport).select("id").single();
|
|
814
1269
|
if (error) {
|
|
@@ -890,7 +1345,7 @@ var BugBearClient = class {
|
|
|
890
1345
|
`;
|
|
891
1346
|
const [pendingResult, completedResult] = await Promise.all([
|
|
892
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),
|
|
893
|
-
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)
|
|
894
1349
|
]);
|
|
895
1350
|
if (pendingResult.error) {
|
|
896
1351
|
console.error("BugBear: Failed to fetch assignments", formatPgError(pendingResult.error));
|
|
@@ -1359,7 +1814,8 @@ var BugBearClient = class {
|
|
|
1359
1814
|
avatarUrl: tester.avatar_url || void 0,
|
|
1360
1815
|
platforms: tester.platforms || [],
|
|
1361
1816
|
assignedTests: tester.assigned_count || 0,
|
|
1362
|
-
completedTests: tester.completed_count || 0
|
|
1817
|
+
completedTests: tester.completed_count || 0,
|
|
1818
|
+
role: tester.role || "tester"
|
|
1363
1819
|
};
|
|
1364
1820
|
} catch (err) {
|
|
1365
1821
|
console.error("BugBear: getTesterInfo error", err);
|
|
@@ -1647,15 +2103,78 @@ var BugBearClient = class {
|
|
|
1647
2103
|
}
|
|
1648
2104
|
}
|
|
1649
2105
|
/**
|
|
1650
|
-
* Check if the widget should be visible
|
|
1651
|
-
*
|
|
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
|
|
1652
2111
|
*/
|
|
1653
2112
|
async shouldShowWidget() {
|
|
1654
|
-
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([
|
|
1655
2126
|
this.isQAEnabled(),
|
|
1656
|
-
this.
|
|
2127
|
+
this.getTesterInfo(),
|
|
2128
|
+
this.getCurrentUserInfo()
|
|
1657
2129
|
]);
|
|
1658
|
-
|
|
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([
|
|
2144
|
+
this.isQAEnabled(),
|
|
2145
|
+
this.getTesterInfo()
|
|
2146
|
+
]);
|
|
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
|
+
}
|
|
1659
2178
|
}
|
|
1660
2179
|
/**
|
|
1661
2180
|
* Upload a screenshot (web - uses File/Blob)
|
|
@@ -2286,11 +2805,22 @@ function createBugBear(config) {
|
|
|
2286
2805
|
0 && (module.exports = {
|
|
2287
2806
|
BUG_CATEGORIES,
|
|
2288
2807
|
BugBearClient,
|
|
2808
|
+
DedupWindow,
|
|
2809
|
+
ErrorMonitor,
|
|
2289
2810
|
LocalStorageAdapter,
|
|
2290
2811
|
OfflineQueue,
|
|
2812
|
+
RNApiFailureHandler,
|
|
2813
|
+
RNCrashHandler,
|
|
2814
|
+
RNRageClickHandler,
|
|
2815
|
+
RageClickDetector,
|
|
2816
|
+
WebApiFailureHandler,
|
|
2817
|
+
WebCrashHandler,
|
|
2818
|
+
WebRageClickHandler,
|
|
2291
2819
|
captureError,
|
|
2292
2820
|
contextCapture,
|
|
2293
2821
|
createBugBear,
|
|
2822
|
+
generateFingerprint,
|
|
2294
2823
|
isBugCategory,
|
|
2295
|
-
isNetworkError
|
|
2824
|
+
isNetworkError,
|
|
2825
|
+
scrubUrl
|
|
2296
2826
|
});
|