@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.mjs
CHANGED
|
@@ -415,6 +415,433 @@ function isNetworkError(error) {
|
|
|
415
415
|
msg.includes("the internet connection appears to be offline") || msg.includes("a]server with the specified hostname could not be found");
|
|
416
416
|
}
|
|
417
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
|
+
|
|
418
845
|
// src/client.ts
|
|
419
846
|
var formatPgError = (e) => {
|
|
420
847
|
if (!e || typeof e !== "object") return { raw: e };
|
|
@@ -433,6 +860,8 @@ var BugBearClient = class {
|
|
|
433
860
|
/** Active Realtime channel references for cleanup. */
|
|
434
861
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
435
862
|
this.realtimeChannels = [];
|
|
863
|
+
/** Error monitor instance — created when config.monitoring is present. */
|
|
864
|
+
this.monitor = null;
|
|
436
865
|
/** Whether the client has been successfully initialized. */
|
|
437
866
|
this.initialized = false;
|
|
438
867
|
/** Initialization error, if any. */
|
|
@@ -450,6 +879,7 @@ var BugBearClient = class {
|
|
|
450
879
|
this.initialized = true;
|
|
451
880
|
this.pendingInit = Promise.resolve();
|
|
452
881
|
this.initOfflineQueue();
|
|
882
|
+
this.initMonitoring();
|
|
453
883
|
} else {
|
|
454
884
|
throw new Error(
|
|
455
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"
|
|
@@ -507,6 +937,7 @@ var BugBearClient = class {
|
|
|
507
937
|
this.supabase = createClient(resolved.supabaseUrl, resolved.supabaseAnonKey);
|
|
508
938
|
this.initialized = true;
|
|
509
939
|
this.initOfflineQueue();
|
|
940
|
+
this.initMonitoring();
|
|
510
941
|
}
|
|
511
942
|
/** Initialize offline queue if configured. Shared by both init paths. */
|
|
512
943
|
initOfflineQueue() {
|
|
@@ -519,6 +950,17 @@ var BugBearClient = class {
|
|
|
519
950
|
this.registerQueueHandlers();
|
|
520
951
|
}
|
|
521
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
|
+
}
|
|
522
964
|
/** Read cached config from localStorage if available and not expired. */
|
|
523
965
|
readConfigCache(apiKey) {
|
|
524
966
|
if (typeof localStorage === "undefined") return null;
|
|
@@ -774,7 +1216,9 @@ var BugBearClient = class {
|
|
|
774
1216
|
navigation_history: this.getNavigationHistory(),
|
|
775
1217
|
enhanced_context: report.enhancedContext || contextCapture.getEnhancedContext(),
|
|
776
1218
|
assignment_id: report.assignmentId,
|
|
777
|
-
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
|
|
778
1222
|
};
|
|
779
1223
|
const { data, error } = await this.supabase.from("reports").insert(fullReport).select("id").single();
|
|
780
1224
|
if (error) {
|
|
@@ -856,7 +1300,7 @@ var BugBearClient = class {
|
|
|
856
1300
|
`;
|
|
857
1301
|
const [pendingResult, completedResult] = await Promise.all([
|
|
858
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),
|
|
859
|
-
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)
|
|
860
1304
|
]);
|
|
861
1305
|
if (pendingResult.error) {
|
|
862
1306
|
console.error("BugBear: Failed to fetch assignments", formatPgError(pendingResult.error));
|
|
@@ -1325,7 +1769,8 @@ var BugBearClient = class {
|
|
|
1325
1769
|
avatarUrl: tester.avatar_url || void 0,
|
|
1326
1770
|
platforms: tester.platforms || [],
|
|
1327
1771
|
assignedTests: tester.assigned_count || 0,
|
|
1328
|
-
completedTests: tester.completed_count || 0
|
|
1772
|
+
completedTests: tester.completed_count || 0,
|
|
1773
|
+
role: tester.role || "tester"
|
|
1329
1774
|
};
|
|
1330
1775
|
} catch (err) {
|
|
1331
1776
|
console.error("BugBear: getTesterInfo error", err);
|
|
@@ -1613,15 +2058,78 @@ var BugBearClient = class {
|
|
|
1613
2058
|
}
|
|
1614
2059
|
}
|
|
1615
2060
|
/**
|
|
1616
|
-
* Check if the widget should be visible
|
|
1617
|
-
*
|
|
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
|
|
1618
2066
|
*/
|
|
1619
2067
|
async shouldShowWidget() {
|
|
1620
|
-
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([
|
|
1621
2081
|
this.isQAEnabled(),
|
|
1622
|
-
this.
|
|
2082
|
+
this.getTesterInfo(),
|
|
2083
|
+
this.getCurrentUserInfo()
|
|
1623
2084
|
]);
|
|
1624
|
-
|
|
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([
|
|
2099
|
+
this.isQAEnabled(),
|
|
2100
|
+
this.getTesterInfo()
|
|
2101
|
+
]);
|
|
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
|
+
}
|
|
1625
2133
|
}
|
|
1626
2134
|
/**
|
|
1627
2135
|
* Upload a screenshot (web - uses File/Blob)
|
|
@@ -2251,11 +2759,22 @@ function createBugBear(config) {
|
|
|
2251
2759
|
export {
|
|
2252
2760
|
BUG_CATEGORIES,
|
|
2253
2761
|
BugBearClient,
|
|
2762
|
+
DedupWindow,
|
|
2763
|
+
ErrorMonitor,
|
|
2254
2764
|
LocalStorageAdapter,
|
|
2255
2765
|
OfflineQueue,
|
|
2766
|
+
RNApiFailureHandler,
|
|
2767
|
+
RNCrashHandler,
|
|
2768
|
+
RNRageClickHandler,
|
|
2769
|
+
RageClickDetector,
|
|
2770
|
+
WebApiFailureHandler,
|
|
2771
|
+
WebCrashHandler,
|
|
2772
|
+
WebRageClickHandler,
|
|
2256
2773
|
captureError,
|
|
2257
2774
|
contextCapture,
|
|
2258
2775
|
createBugBear,
|
|
2776
|
+
generateFingerprint,
|
|
2259
2777
|
isBugCategory,
|
|
2260
|
-
isNetworkError
|
|
2778
|
+
isNetworkError,
|
|
2779
|
+
scrubUrl
|
|
2261
2780
|
};
|