@bbearai/core 0.7.1 → 0.8.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 +130 -2
- package/dist/index.d.ts +130 -2
- package/dist/index.js +339 -3
- package/dist/index.mjs +338 -3
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -479,6 +479,7 @@ interface TesterThread {
|
|
|
479
479
|
createdAt: string;
|
|
480
480
|
unreadCount: number;
|
|
481
481
|
lastMessage?: TesterMessage;
|
|
482
|
+
reporterName?: string;
|
|
482
483
|
}
|
|
483
484
|
interface TesterMessage {
|
|
484
485
|
id: string;
|
|
@@ -789,6 +790,50 @@ interface TesterIssue {
|
|
|
789
790
|
/** Original bug title (for reopened/test_fail issues) */
|
|
790
791
|
originalBugTitle?: string;
|
|
791
792
|
}
|
|
793
|
+
/** Delivery status for captured emails */
|
|
794
|
+
type EmailDeliveryStatus = 'pending' | 'sent' | 'delivered' | 'bounced' | 'dropped' | 'deferred';
|
|
795
|
+
/** Email capture mode */
|
|
796
|
+
type EmailCaptureMode = 'capture' | 'intercept';
|
|
797
|
+
/** A captured email payload sent to the BugBear API */
|
|
798
|
+
interface EmailCapturePayload {
|
|
799
|
+
to: string[];
|
|
800
|
+
from?: string;
|
|
801
|
+
subject: string;
|
|
802
|
+
html?: string;
|
|
803
|
+
text?: string;
|
|
804
|
+
templateId?: string;
|
|
805
|
+
captureMode: EmailCaptureMode;
|
|
806
|
+
wasDelivered: boolean;
|
|
807
|
+
metadata?: Record<string, unknown>;
|
|
808
|
+
}
|
|
809
|
+
/** Result of capturing an email */
|
|
810
|
+
interface EmailCaptureResult {
|
|
811
|
+
success: boolean;
|
|
812
|
+
captureId?: string;
|
|
813
|
+
queued?: boolean;
|
|
814
|
+
error?: string;
|
|
815
|
+
}
|
|
816
|
+
/** Options for the email interceptor */
|
|
817
|
+
interface EmailInterceptorOptions {
|
|
818
|
+
/** 'capture' = log + send, 'intercept' = log + block */
|
|
819
|
+
mode: EmailCaptureMode;
|
|
820
|
+
/** Only capture emails sent to these addresses */
|
|
821
|
+
filterTo?: string[];
|
|
822
|
+
/** Replace real recipient addresses with "[redacted]" in captures */
|
|
823
|
+
redactRecipients?: boolean;
|
|
824
|
+
}
|
|
825
|
+
/** Extracted email fields from a send function call */
|
|
826
|
+
interface ExtractedEmailPayload {
|
|
827
|
+
to: string | string[];
|
|
828
|
+
from?: string;
|
|
829
|
+
subject: string;
|
|
830
|
+
html?: string;
|
|
831
|
+
text?: string;
|
|
832
|
+
templateId?: string;
|
|
833
|
+
[key: string]: unknown;
|
|
834
|
+
}
|
|
835
|
+
/** Custom extractor for non-standard email function signatures */
|
|
836
|
+
type EmailPayloadExtractor = (args: unknown[]) => ExtractedEmailPayload;
|
|
792
837
|
|
|
793
838
|
/**
|
|
794
839
|
* BugBear Offline Queue
|
|
@@ -815,7 +860,7 @@ declare class LocalStorageAdapter implements StorageAdapter {
|
|
|
815
860
|
setItem(key: string, value: string): Promise<void>;
|
|
816
861
|
removeItem(key: string): Promise<void>;
|
|
817
862
|
}
|
|
818
|
-
type QueueItemType = 'report' | 'message' | 'feedback';
|
|
863
|
+
type QueueItemType = 'report' | 'message' | 'feedback' | 'email_capture';
|
|
819
864
|
interface QueueItem {
|
|
820
865
|
/** Unique ID for this queued operation. */
|
|
821
866
|
id: string;
|
|
@@ -1042,6 +1087,8 @@ declare class BugBearClient {
|
|
|
1042
1087
|
private reportSubmitInFlight;
|
|
1043
1088
|
/** Offline queue — only created when config.offlineQueue.enabled is true. */
|
|
1044
1089
|
private _queue;
|
|
1090
|
+
/** Session cache storage — defaults to LocalStorageAdapter. */
|
|
1091
|
+
private _sessionStorage;
|
|
1045
1092
|
/** Active Realtime channel references for cleanup. */
|
|
1046
1093
|
private realtimeChannels;
|
|
1047
1094
|
/** Error monitor instance — created when config.monitoring is present. */
|
|
@@ -1056,6 +1103,12 @@ declare class BugBearClient {
|
|
|
1056
1103
|
private initialized;
|
|
1057
1104
|
/** Initialization error, if any. */
|
|
1058
1105
|
private initError;
|
|
1106
|
+
/** Active presence session ID (passive time tracking). */
|
|
1107
|
+
private _presenceSessionId;
|
|
1108
|
+
/** Heartbeat interval for presence tracking. */
|
|
1109
|
+
private _presenceInterval;
|
|
1110
|
+
/** Whether presence is paused (tab hidden / app backgrounded). */
|
|
1111
|
+
private _presencePaused;
|
|
1059
1112
|
constructor(config: BugBearConfig);
|
|
1060
1113
|
/** Whether the client is ready for requests. */
|
|
1061
1114
|
get isReady(): boolean;
|
|
@@ -1068,6 +1121,8 @@ declare class BugBearClient {
|
|
|
1068
1121
|
private resolveFromApiKey;
|
|
1069
1122
|
/** Apply resolved credentials and create the Supabase client. */
|
|
1070
1123
|
private applyResolvedConfig;
|
|
1124
|
+
/** Cache key scoped to the active project. */
|
|
1125
|
+
private get sessionCacheKey();
|
|
1071
1126
|
/** Initialize offline queue if configured. Shared by both init paths. */
|
|
1072
1127
|
private initOfflineQueue;
|
|
1073
1128
|
/** Initialize error monitoring if configured. Shared by both init paths. */
|
|
@@ -1080,6 +1135,12 @@ declare class BugBearClient {
|
|
|
1080
1135
|
private hashKey;
|
|
1081
1136
|
/** Ensure the client is initialized before making requests. */
|
|
1082
1137
|
private ensureReady;
|
|
1138
|
+
/**
|
|
1139
|
+
* Fire-and-forget call to a dashboard notification endpoint.
|
|
1140
|
+
* Only works when apiKey is configured (needed for API auth).
|
|
1141
|
+
* Failures are silently ignored — notifications are best-effort.
|
|
1142
|
+
*/
|
|
1143
|
+
private notifyDashboard;
|
|
1083
1144
|
/**
|
|
1084
1145
|
* Access the offline queue (if enabled).
|
|
1085
1146
|
* Use this to check queue.count, subscribe to changes, or trigger flush.
|
|
@@ -1091,6 +1152,22 @@ declare class BugBearClient {
|
|
|
1091
1152
|
* Web callers can skip this — LocalStorageAdapter is the default.
|
|
1092
1153
|
*/
|
|
1093
1154
|
initQueue(storage?: StorageAdapter): Promise<void>;
|
|
1155
|
+
/**
|
|
1156
|
+
* Swap the session cache storage adapter (for React Native — pass AsyncStorage).
|
|
1157
|
+
* Must be called before getCachedSession() for persistence across app kills.
|
|
1158
|
+
* Web callers don't need this — LocalStorageAdapter is the default.
|
|
1159
|
+
*/
|
|
1160
|
+
setSessionStorage(adapter: StorageAdapter): void;
|
|
1161
|
+
/**
|
|
1162
|
+
* Cache the active QA session locally for instant restore on app restart.
|
|
1163
|
+
* Pass null to clear the cache (e.g. after ending a session).
|
|
1164
|
+
*/
|
|
1165
|
+
cacheSession(session: QASession | null): Promise<void>;
|
|
1166
|
+
/**
|
|
1167
|
+
* Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
|
|
1168
|
+
* or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
|
|
1169
|
+
*/
|
|
1170
|
+
getCachedSession(): Promise<QASession | null>;
|
|
1094
1171
|
private registerQueueHandlers;
|
|
1095
1172
|
/** Whether realtime is enabled in config. */
|
|
1096
1173
|
get realtimeEnabled(): boolean;
|
|
@@ -1137,6 +1214,11 @@ declare class BugBearClient {
|
|
|
1137
1214
|
queued?: boolean;
|
|
1138
1215
|
error?: string;
|
|
1139
1216
|
}>;
|
|
1217
|
+
/**
|
|
1218
|
+
* Capture an email for QA testing.
|
|
1219
|
+
* Called by the email interceptor — not typically called directly.
|
|
1220
|
+
*/
|
|
1221
|
+
captureEmail(payload: EmailCapturePayload): Promise<EmailCaptureResult>;
|
|
1140
1222
|
/**
|
|
1141
1223
|
* Get assigned tests for current user
|
|
1142
1224
|
* First looks up the tester by email, then fetches their assignments
|
|
@@ -1253,6 +1335,14 @@ declare class BugBearClient {
|
|
|
1253
1335
|
* reopened issues include original bug context.
|
|
1254
1336
|
*/
|
|
1255
1337
|
getIssues(category: 'open' | 'done' | 'reopened'): Promise<TesterIssue[]>;
|
|
1338
|
+
/**
|
|
1339
|
+
* Reopen a done issue that the tester believes isn't actually fixed.
|
|
1340
|
+
* Transitions the report from a done status back to 'confirmed'.
|
|
1341
|
+
*/
|
|
1342
|
+
reopenReport(reportId: string, reason: string): Promise<{
|
|
1343
|
+
success: boolean;
|
|
1344
|
+
error?: string;
|
|
1345
|
+
}>;
|
|
1256
1346
|
/**
|
|
1257
1347
|
* Basic email format validation (defense in depth)
|
|
1258
1348
|
*/
|
|
@@ -1478,6 +1568,22 @@ declare class BugBearClient {
|
|
|
1478
1568
|
* Transform database finding to QAFinding type
|
|
1479
1569
|
*/
|
|
1480
1570
|
private transformFinding;
|
|
1571
|
+
/** Current presence session ID (null if not tracking). */
|
|
1572
|
+
get presenceSessionId(): string | null;
|
|
1573
|
+
/**
|
|
1574
|
+
* Start passive presence tracking for this tester.
|
|
1575
|
+
* Idempotent — reuses an existing active session if one exists.
|
|
1576
|
+
*/
|
|
1577
|
+
startPresence(platform: 'web' | 'ios' | 'android'): Promise<string | null>;
|
|
1578
|
+
/** Gracefully end the current presence session. */
|
|
1579
|
+
endPresence(): Promise<void>;
|
|
1580
|
+
/** Pause heartbeat (tab hidden / app backgrounded). Sends one final beat. */
|
|
1581
|
+
pausePresence(): void;
|
|
1582
|
+
/** Resume heartbeat after pause. Restarts if session was cleaned up. */
|
|
1583
|
+
resumePresence(): Promise<void>;
|
|
1584
|
+
private heartbeatPresence;
|
|
1585
|
+
private startPresenceHeartbeat;
|
|
1586
|
+
private stopPresenceHeartbeat;
|
|
1481
1587
|
}
|
|
1482
1588
|
/**
|
|
1483
1589
|
* Create a BugBear client instance
|
|
@@ -1558,4 +1664,26 @@ declare function captureError(error: Error, errorInfo?: {
|
|
|
1558
1664
|
componentStack?: string;
|
|
1559
1665
|
};
|
|
1560
1666
|
|
|
1561
|
-
|
|
1667
|
+
interface WrapOptions {
|
|
1668
|
+
extractPayload?: EmailPayloadExtractor;
|
|
1669
|
+
}
|
|
1670
|
+
interface EmailInterceptor {
|
|
1671
|
+
wrap<T extends (...args: any[]) => Promise<any>>(sendFn: T, options?: WrapOptions): T;
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Create an email interceptor that captures emails sent through a wrapped function.
|
|
1675
|
+
*
|
|
1676
|
+
* Two modes:
|
|
1677
|
+
* - `capture`: emails are sent normally AND captured in BugBear (safe for production)
|
|
1678
|
+
* - `intercept`: emails are NOT sent, only captured in BugBear (for staging/QA)
|
|
1679
|
+
*
|
|
1680
|
+
* @example
|
|
1681
|
+
* ```ts
|
|
1682
|
+
* const interceptor = createEmailInterceptor(bugbearClient, { mode: 'intercept' });
|
|
1683
|
+
* const wrappedSend = interceptor.wrap(originalSendEmail);
|
|
1684
|
+
* await wrappedSend({ to: 'user@example.com', subject: 'Test', html: '<p>Hi</p>' });
|
|
1685
|
+
* ```
|
|
1686
|
+
*/
|
|
1687
|
+
declare function createEmailInterceptor(client: BugBearClient, options?: EmailInterceptorOptions): EmailInterceptor;
|
|
1688
|
+
|
|
1689
|
+
export { type AddFindingOptions, type AppContext, BUG_CATEGORIES, BugBearClient, type BugBearConfig, type BugBearMode, type BugBearReport, type BugBearTheme, type BugCategory, type ChecklistItem, type ChecklistResult, type ConsoleLogEntry, type CoverageGap, type CoverageMatrixCell, type CoverageMatrixRow, DedupWindow, type DeployChecklist, type DeviceInfo, type EmailCaptureMode, type EmailCapturePayload, type EmailCaptureResult, type EmailDeliveryStatus, type EmailInterceptorOptions, type EmailPayloadExtractor, type EndSessionOptions, type EnhancedBugContext, ErrorMonitor, type ExtractedEmailPayload, type FindingSeverity, type FindingType, type HostUserInfo, type IssueCategory, type IssueCounts, LocalStorageAdapter, type MessageSenderType, type MonitorDeps, type MonitoringConfig, type MonitoringEvent, type NetworkRequest, OfflineQueue, type OfflineQueueConfig, type PriorityFactors, type ProjectRole, type QAFinding, type QAHealthMetrics, type QAHealthScore, type QASession, type QASessionStatus, type QATrack, type QueueItem, type QueueItemType, RNApiFailureHandler, RNCrashHandler, RNRageClickHandler, RageClickDetector, type RageClickEvent, type RegressionEvent, type ReportSource, type ReportStatus, type ReportType, type RoutePriority, type RouteTestStats, type RubricMode, type RubricResult, type Severity, type SkipReason, type StartSessionOptions, type StorageAdapter, type SubmitFeedbackOptions, type TestAssignment, type TestFeedback, type TestGroup, type TestResult, type TestStep, type TestTemplate, type TesterInfo, type TesterIssue, type TesterMessage, type TesterProfileUpdate, type TesterThread, type ThreadPriority, type ThreadType, WebApiFailureHandler, WebCrashHandler, WebRageClickHandler, type WidgetColorScheme, type WidgetConfig, captureError, contextCapture, createBugBear, createEmailInterceptor, generateFingerprint, isBugCategory, isNetworkError, scrubUrl };
|
package/dist/index.d.ts
CHANGED
|
@@ -479,6 +479,7 @@ interface TesterThread {
|
|
|
479
479
|
createdAt: string;
|
|
480
480
|
unreadCount: number;
|
|
481
481
|
lastMessage?: TesterMessage;
|
|
482
|
+
reporterName?: string;
|
|
482
483
|
}
|
|
483
484
|
interface TesterMessage {
|
|
484
485
|
id: string;
|
|
@@ -789,6 +790,50 @@ interface TesterIssue {
|
|
|
789
790
|
/** Original bug title (for reopened/test_fail issues) */
|
|
790
791
|
originalBugTitle?: string;
|
|
791
792
|
}
|
|
793
|
+
/** Delivery status for captured emails */
|
|
794
|
+
type EmailDeliveryStatus = 'pending' | 'sent' | 'delivered' | 'bounced' | 'dropped' | 'deferred';
|
|
795
|
+
/** Email capture mode */
|
|
796
|
+
type EmailCaptureMode = 'capture' | 'intercept';
|
|
797
|
+
/** A captured email payload sent to the BugBear API */
|
|
798
|
+
interface EmailCapturePayload {
|
|
799
|
+
to: string[];
|
|
800
|
+
from?: string;
|
|
801
|
+
subject: string;
|
|
802
|
+
html?: string;
|
|
803
|
+
text?: string;
|
|
804
|
+
templateId?: string;
|
|
805
|
+
captureMode: EmailCaptureMode;
|
|
806
|
+
wasDelivered: boolean;
|
|
807
|
+
metadata?: Record<string, unknown>;
|
|
808
|
+
}
|
|
809
|
+
/** Result of capturing an email */
|
|
810
|
+
interface EmailCaptureResult {
|
|
811
|
+
success: boolean;
|
|
812
|
+
captureId?: string;
|
|
813
|
+
queued?: boolean;
|
|
814
|
+
error?: string;
|
|
815
|
+
}
|
|
816
|
+
/** Options for the email interceptor */
|
|
817
|
+
interface EmailInterceptorOptions {
|
|
818
|
+
/** 'capture' = log + send, 'intercept' = log + block */
|
|
819
|
+
mode: EmailCaptureMode;
|
|
820
|
+
/** Only capture emails sent to these addresses */
|
|
821
|
+
filterTo?: string[];
|
|
822
|
+
/** Replace real recipient addresses with "[redacted]" in captures */
|
|
823
|
+
redactRecipients?: boolean;
|
|
824
|
+
}
|
|
825
|
+
/** Extracted email fields from a send function call */
|
|
826
|
+
interface ExtractedEmailPayload {
|
|
827
|
+
to: string | string[];
|
|
828
|
+
from?: string;
|
|
829
|
+
subject: string;
|
|
830
|
+
html?: string;
|
|
831
|
+
text?: string;
|
|
832
|
+
templateId?: string;
|
|
833
|
+
[key: string]: unknown;
|
|
834
|
+
}
|
|
835
|
+
/** Custom extractor for non-standard email function signatures */
|
|
836
|
+
type EmailPayloadExtractor = (args: unknown[]) => ExtractedEmailPayload;
|
|
792
837
|
|
|
793
838
|
/**
|
|
794
839
|
* BugBear Offline Queue
|
|
@@ -815,7 +860,7 @@ declare class LocalStorageAdapter implements StorageAdapter {
|
|
|
815
860
|
setItem(key: string, value: string): Promise<void>;
|
|
816
861
|
removeItem(key: string): Promise<void>;
|
|
817
862
|
}
|
|
818
|
-
type QueueItemType = 'report' | 'message' | 'feedback';
|
|
863
|
+
type QueueItemType = 'report' | 'message' | 'feedback' | 'email_capture';
|
|
819
864
|
interface QueueItem {
|
|
820
865
|
/** Unique ID for this queued operation. */
|
|
821
866
|
id: string;
|
|
@@ -1042,6 +1087,8 @@ declare class BugBearClient {
|
|
|
1042
1087
|
private reportSubmitInFlight;
|
|
1043
1088
|
/** Offline queue — only created when config.offlineQueue.enabled is true. */
|
|
1044
1089
|
private _queue;
|
|
1090
|
+
/** Session cache storage — defaults to LocalStorageAdapter. */
|
|
1091
|
+
private _sessionStorage;
|
|
1045
1092
|
/** Active Realtime channel references for cleanup. */
|
|
1046
1093
|
private realtimeChannels;
|
|
1047
1094
|
/** Error monitor instance — created when config.monitoring is present. */
|
|
@@ -1056,6 +1103,12 @@ declare class BugBearClient {
|
|
|
1056
1103
|
private initialized;
|
|
1057
1104
|
/** Initialization error, if any. */
|
|
1058
1105
|
private initError;
|
|
1106
|
+
/** Active presence session ID (passive time tracking). */
|
|
1107
|
+
private _presenceSessionId;
|
|
1108
|
+
/** Heartbeat interval for presence tracking. */
|
|
1109
|
+
private _presenceInterval;
|
|
1110
|
+
/** Whether presence is paused (tab hidden / app backgrounded). */
|
|
1111
|
+
private _presencePaused;
|
|
1059
1112
|
constructor(config: BugBearConfig);
|
|
1060
1113
|
/** Whether the client is ready for requests. */
|
|
1061
1114
|
get isReady(): boolean;
|
|
@@ -1068,6 +1121,8 @@ declare class BugBearClient {
|
|
|
1068
1121
|
private resolveFromApiKey;
|
|
1069
1122
|
/** Apply resolved credentials and create the Supabase client. */
|
|
1070
1123
|
private applyResolvedConfig;
|
|
1124
|
+
/** Cache key scoped to the active project. */
|
|
1125
|
+
private get sessionCacheKey();
|
|
1071
1126
|
/** Initialize offline queue if configured. Shared by both init paths. */
|
|
1072
1127
|
private initOfflineQueue;
|
|
1073
1128
|
/** Initialize error monitoring if configured. Shared by both init paths. */
|
|
@@ -1080,6 +1135,12 @@ declare class BugBearClient {
|
|
|
1080
1135
|
private hashKey;
|
|
1081
1136
|
/** Ensure the client is initialized before making requests. */
|
|
1082
1137
|
private ensureReady;
|
|
1138
|
+
/**
|
|
1139
|
+
* Fire-and-forget call to a dashboard notification endpoint.
|
|
1140
|
+
* Only works when apiKey is configured (needed for API auth).
|
|
1141
|
+
* Failures are silently ignored — notifications are best-effort.
|
|
1142
|
+
*/
|
|
1143
|
+
private notifyDashboard;
|
|
1083
1144
|
/**
|
|
1084
1145
|
* Access the offline queue (if enabled).
|
|
1085
1146
|
* Use this to check queue.count, subscribe to changes, or trigger flush.
|
|
@@ -1091,6 +1152,22 @@ declare class BugBearClient {
|
|
|
1091
1152
|
* Web callers can skip this — LocalStorageAdapter is the default.
|
|
1092
1153
|
*/
|
|
1093
1154
|
initQueue(storage?: StorageAdapter): Promise<void>;
|
|
1155
|
+
/**
|
|
1156
|
+
* Swap the session cache storage adapter (for React Native — pass AsyncStorage).
|
|
1157
|
+
* Must be called before getCachedSession() for persistence across app kills.
|
|
1158
|
+
* Web callers don't need this — LocalStorageAdapter is the default.
|
|
1159
|
+
*/
|
|
1160
|
+
setSessionStorage(adapter: StorageAdapter): void;
|
|
1161
|
+
/**
|
|
1162
|
+
* Cache the active QA session locally for instant restore on app restart.
|
|
1163
|
+
* Pass null to clear the cache (e.g. after ending a session).
|
|
1164
|
+
*/
|
|
1165
|
+
cacheSession(session: QASession | null): Promise<void>;
|
|
1166
|
+
/**
|
|
1167
|
+
* Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
|
|
1168
|
+
* or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
|
|
1169
|
+
*/
|
|
1170
|
+
getCachedSession(): Promise<QASession | null>;
|
|
1094
1171
|
private registerQueueHandlers;
|
|
1095
1172
|
/** Whether realtime is enabled in config. */
|
|
1096
1173
|
get realtimeEnabled(): boolean;
|
|
@@ -1137,6 +1214,11 @@ declare class BugBearClient {
|
|
|
1137
1214
|
queued?: boolean;
|
|
1138
1215
|
error?: string;
|
|
1139
1216
|
}>;
|
|
1217
|
+
/**
|
|
1218
|
+
* Capture an email for QA testing.
|
|
1219
|
+
* Called by the email interceptor — not typically called directly.
|
|
1220
|
+
*/
|
|
1221
|
+
captureEmail(payload: EmailCapturePayload): Promise<EmailCaptureResult>;
|
|
1140
1222
|
/**
|
|
1141
1223
|
* Get assigned tests for current user
|
|
1142
1224
|
* First looks up the tester by email, then fetches their assignments
|
|
@@ -1253,6 +1335,14 @@ declare class BugBearClient {
|
|
|
1253
1335
|
* reopened issues include original bug context.
|
|
1254
1336
|
*/
|
|
1255
1337
|
getIssues(category: 'open' | 'done' | 'reopened'): Promise<TesterIssue[]>;
|
|
1338
|
+
/**
|
|
1339
|
+
* Reopen a done issue that the tester believes isn't actually fixed.
|
|
1340
|
+
* Transitions the report from a done status back to 'confirmed'.
|
|
1341
|
+
*/
|
|
1342
|
+
reopenReport(reportId: string, reason: string): Promise<{
|
|
1343
|
+
success: boolean;
|
|
1344
|
+
error?: string;
|
|
1345
|
+
}>;
|
|
1256
1346
|
/**
|
|
1257
1347
|
* Basic email format validation (defense in depth)
|
|
1258
1348
|
*/
|
|
@@ -1478,6 +1568,22 @@ declare class BugBearClient {
|
|
|
1478
1568
|
* Transform database finding to QAFinding type
|
|
1479
1569
|
*/
|
|
1480
1570
|
private transformFinding;
|
|
1571
|
+
/** Current presence session ID (null if not tracking). */
|
|
1572
|
+
get presenceSessionId(): string | null;
|
|
1573
|
+
/**
|
|
1574
|
+
* Start passive presence tracking for this tester.
|
|
1575
|
+
* Idempotent — reuses an existing active session if one exists.
|
|
1576
|
+
*/
|
|
1577
|
+
startPresence(platform: 'web' | 'ios' | 'android'): Promise<string | null>;
|
|
1578
|
+
/** Gracefully end the current presence session. */
|
|
1579
|
+
endPresence(): Promise<void>;
|
|
1580
|
+
/** Pause heartbeat (tab hidden / app backgrounded). Sends one final beat. */
|
|
1581
|
+
pausePresence(): void;
|
|
1582
|
+
/** Resume heartbeat after pause. Restarts if session was cleaned up. */
|
|
1583
|
+
resumePresence(): Promise<void>;
|
|
1584
|
+
private heartbeatPresence;
|
|
1585
|
+
private startPresenceHeartbeat;
|
|
1586
|
+
private stopPresenceHeartbeat;
|
|
1481
1587
|
}
|
|
1482
1588
|
/**
|
|
1483
1589
|
* Create a BugBear client instance
|
|
@@ -1558,4 +1664,26 @@ declare function captureError(error: Error, errorInfo?: {
|
|
|
1558
1664
|
componentStack?: string;
|
|
1559
1665
|
};
|
|
1560
1666
|
|
|
1561
|
-
|
|
1667
|
+
interface WrapOptions {
|
|
1668
|
+
extractPayload?: EmailPayloadExtractor;
|
|
1669
|
+
}
|
|
1670
|
+
interface EmailInterceptor {
|
|
1671
|
+
wrap<T extends (...args: any[]) => Promise<any>>(sendFn: T, options?: WrapOptions): T;
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Create an email interceptor that captures emails sent through a wrapped function.
|
|
1675
|
+
*
|
|
1676
|
+
* Two modes:
|
|
1677
|
+
* - `capture`: emails are sent normally AND captured in BugBear (safe for production)
|
|
1678
|
+
* - `intercept`: emails are NOT sent, only captured in BugBear (for staging/QA)
|
|
1679
|
+
*
|
|
1680
|
+
* @example
|
|
1681
|
+
* ```ts
|
|
1682
|
+
* const interceptor = createEmailInterceptor(bugbearClient, { mode: 'intercept' });
|
|
1683
|
+
* const wrappedSend = interceptor.wrap(originalSendEmail);
|
|
1684
|
+
* await wrappedSend({ to: 'user@example.com', subject: 'Test', html: '<p>Hi</p>' });
|
|
1685
|
+
* ```
|
|
1686
|
+
*/
|
|
1687
|
+
declare function createEmailInterceptor(client: BugBearClient, options?: EmailInterceptorOptions): EmailInterceptor;
|
|
1688
|
+
|
|
1689
|
+
export { type AddFindingOptions, type AppContext, BUG_CATEGORIES, BugBearClient, type BugBearConfig, type BugBearMode, type BugBearReport, type BugBearTheme, type BugCategory, type ChecklistItem, type ChecklistResult, type ConsoleLogEntry, type CoverageGap, type CoverageMatrixCell, type CoverageMatrixRow, DedupWindow, type DeployChecklist, type DeviceInfo, type EmailCaptureMode, type EmailCapturePayload, type EmailCaptureResult, type EmailDeliveryStatus, type EmailInterceptorOptions, type EmailPayloadExtractor, type EndSessionOptions, type EnhancedBugContext, ErrorMonitor, type ExtractedEmailPayload, type FindingSeverity, type FindingType, type HostUserInfo, type IssueCategory, type IssueCounts, LocalStorageAdapter, type MessageSenderType, type MonitorDeps, type MonitoringConfig, type MonitoringEvent, type NetworkRequest, OfflineQueue, type OfflineQueueConfig, type PriorityFactors, type ProjectRole, type QAFinding, type QAHealthMetrics, type QAHealthScore, type QASession, type QASessionStatus, type QATrack, type QueueItem, type QueueItemType, RNApiFailureHandler, RNCrashHandler, RNRageClickHandler, RageClickDetector, type RageClickEvent, type RegressionEvent, type ReportSource, type ReportStatus, type ReportType, type RoutePriority, type RouteTestStats, type RubricMode, type RubricResult, type Severity, type SkipReason, type StartSessionOptions, type StorageAdapter, type SubmitFeedbackOptions, type TestAssignment, type TestFeedback, type TestGroup, type TestResult, type TestStep, type TestTemplate, type TesterInfo, type TesterIssue, type TesterMessage, type TesterProfileUpdate, type TesterThread, type ThreadPriority, type ThreadType, WebApiFailureHandler, WebCrashHandler, WebRageClickHandler, type WidgetColorScheme, type WidgetConfig, captureError, contextCapture, createBugBear, createEmailInterceptor, generateFingerprint, isBugCategory, isNetworkError, scrubUrl };
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
captureError: () => captureError,
|
|
37
37
|
contextCapture: () => contextCapture,
|
|
38
38
|
createBugBear: () => createBugBear,
|
|
39
|
+
createEmailInterceptor: () => createEmailInterceptor,
|
|
39
40
|
generateFingerprint: () => generateFingerprint,
|
|
40
41
|
isBugCategory: () => isBugCategory,
|
|
41
42
|
isNetworkError: () => isNetworkError,
|
|
@@ -902,6 +903,8 @@ var BugBearClient = class {
|
|
|
902
903
|
this.reportSubmitInFlight = false;
|
|
903
904
|
/** Offline queue — only created when config.offlineQueue.enabled is true. */
|
|
904
905
|
this._queue = null;
|
|
906
|
+
/** Session cache storage — defaults to LocalStorageAdapter. */
|
|
907
|
+
this._sessionStorage = new LocalStorageAdapter();
|
|
905
908
|
/** Active Realtime channel references for cleanup. */
|
|
906
909
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
907
910
|
this.realtimeChannels = [];
|
|
@@ -911,6 +914,12 @@ var BugBearClient = class {
|
|
|
911
914
|
this.initialized = false;
|
|
912
915
|
/** Initialization error, if any. */
|
|
913
916
|
this.initError = null;
|
|
917
|
+
/** Active presence session ID (passive time tracking). */
|
|
918
|
+
this._presenceSessionId = null;
|
|
919
|
+
/** Heartbeat interval for presence tracking. */
|
|
920
|
+
this._presenceInterval = null;
|
|
921
|
+
/** Whether presence is paused (tab hidden / app backgrounded). */
|
|
922
|
+
this._presencePaused = false;
|
|
914
923
|
this.config = config;
|
|
915
924
|
if (config.apiKey) {
|
|
916
925
|
this.pendingInit = this.resolveFromApiKey(config.apiKey);
|
|
@@ -984,6 +993,10 @@ var BugBearClient = class {
|
|
|
984
993
|
this.initOfflineQueue();
|
|
985
994
|
this.initMonitoring();
|
|
986
995
|
}
|
|
996
|
+
/** Cache key scoped to the active project. */
|
|
997
|
+
get sessionCacheKey() {
|
|
998
|
+
return `bugbear_session_${this.config.projectId ?? "unknown"}`;
|
|
999
|
+
}
|
|
987
1000
|
/** Initialize offline queue if configured. Shared by both init paths. */
|
|
988
1001
|
initOfflineQueue() {
|
|
989
1002
|
if (this.config.offlineQueue?.enabled) {
|
|
@@ -1047,6 +1060,26 @@ var BugBearClient = class {
|
|
|
1047
1060
|
await this.pendingInit;
|
|
1048
1061
|
if (this.initError) throw this.initError;
|
|
1049
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Fire-and-forget call to a dashboard notification endpoint.
|
|
1065
|
+
* Only works when apiKey is configured (needed for API auth).
|
|
1066
|
+
* Failures are silently ignored — notifications are best-effort.
|
|
1067
|
+
*/
|
|
1068
|
+
async notifyDashboard(path, body) {
|
|
1069
|
+
if (!this.config.apiKey) return;
|
|
1070
|
+
try {
|
|
1071
|
+
const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
|
|
1072
|
+
await fetch(`${baseUrl}/api/v1/notifications/${path}`, {
|
|
1073
|
+
method: "POST",
|
|
1074
|
+
headers: {
|
|
1075
|
+
"Content-Type": "application/json",
|
|
1076
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
1077
|
+
},
|
|
1078
|
+
body: JSON.stringify(body)
|
|
1079
|
+
});
|
|
1080
|
+
} catch {
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1050
1083
|
// ── Offline Queue ─────────────────────────────────────────
|
|
1051
1084
|
/**
|
|
1052
1085
|
* Access the offline queue (if enabled).
|
|
@@ -1073,6 +1106,48 @@ var BugBearClient = class {
|
|
|
1073
1106
|
}
|
|
1074
1107
|
await this._queue.load();
|
|
1075
1108
|
}
|
|
1109
|
+
// ── Session Cache ──────────────────────────────────────────
|
|
1110
|
+
/**
|
|
1111
|
+
* Swap the session cache storage adapter (for React Native — pass AsyncStorage).
|
|
1112
|
+
* Must be called before getCachedSession() for persistence across app kills.
|
|
1113
|
+
* Web callers don't need this — LocalStorageAdapter is the default.
|
|
1114
|
+
*/
|
|
1115
|
+
setSessionStorage(adapter) {
|
|
1116
|
+
this._sessionStorage = adapter;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Cache the active QA session locally for instant restore on app restart.
|
|
1120
|
+
* Pass null to clear the cache (e.g. after ending a session).
|
|
1121
|
+
*/
|
|
1122
|
+
async cacheSession(session) {
|
|
1123
|
+
try {
|
|
1124
|
+
if (session) {
|
|
1125
|
+
await this._sessionStorage.setItem(this.sessionCacheKey, JSON.stringify(session));
|
|
1126
|
+
} else {
|
|
1127
|
+
await this._sessionStorage.removeItem(this.sessionCacheKey);
|
|
1128
|
+
}
|
|
1129
|
+
} catch {
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
|
|
1134
|
+
* or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
|
|
1135
|
+
*/
|
|
1136
|
+
async getCachedSession() {
|
|
1137
|
+
try {
|
|
1138
|
+
const raw = await this._sessionStorage.getItem(this.sessionCacheKey);
|
|
1139
|
+
if (!raw) return null;
|
|
1140
|
+
const session = JSON.parse(raw);
|
|
1141
|
+
const age = Date.now() - new Date(session.startedAt).getTime();
|
|
1142
|
+
if (age > 24 * 60 * 60 * 1e3) {
|
|
1143
|
+
await this._sessionStorage.removeItem(this.sessionCacheKey);
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
return session;
|
|
1147
|
+
} catch {
|
|
1148
|
+
return null;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1076
1151
|
registerQueueHandlers() {
|
|
1077
1152
|
if (!this._queue) return;
|
|
1078
1153
|
this._queue.registerHandler("report", async (payload) => {
|
|
@@ -1090,6 +1165,11 @@ var BugBearClient = class {
|
|
|
1090
1165
|
if (error) return { success: false, error: error.message };
|
|
1091
1166
|
return { success: true };
|
|
1092
1167
|
});
|
|
1168
|
+
this._queue.registerHandler("email_capture", async (payload) => {
|
|
1169
|
+
const { error } = await this.supabase.from("email_captures").insert(payload).select("id").single();
|
|
1170
|
+
if (error) return { success: false, error: error.message };
|
|
1171
|
+
return { success: true };
|
|
1172
|
+
});
|
|
1093
1173
|
}
|
|
1094
1174
|
// ── Realtime Subscriptions ─────────────────────────────────
|
|
1095
1175
|
/** Whether realtime is enabled in config. */
|
|
@@ -1277,6 +1357,8 @@ var BugBearClient = class {
|
|
|
1277
1357
|
if (this.config.onReportSubmitted) {
|
|
1278
1358
|
this.config.onReportSubmitted(report);
|
|
1279
1359
|
}
|
|
1360
|
+
this.notifyDashboard("report", { reportId: data.id }).catch(() => {
|
|
1361
|
+
});
|
|
1280
1362
|
return { success: true, reportId: data.id };
|
|
1281
1363
|
} catch (err) {
|
|
1282
1364
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -1289,6 +1371,44 @@ var BugBearClient = class {
|
|
|
1289
1371
|
this.reportSubmitInFlight = false;
|
|
1290
1372
|
}
|
|
1291
1373
|
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Capture an email for QA testing.
|
|
1376
|
+
* Called by the email interceptor — not typically called directly.
|
|
1377
|
+
*/
|
|
1378
|
+
async captureEmail(payload) {
|
|
1379
|
+
try {
|
|
1380
|
+
await this.ready();
|
|
1381
|
+
if (!payload.subject || !payload.to || payload.to.length === 0) {
|
|
1382
|
+
return { success: false, error: "subject and to are required" };
|
|
1383
|
+
}
|
|
1384
|
+
const record = {
|
|
1385
|
+
project_id: this.config.projectId,
|
|
1386
|
+
to_addresses: payload.to,
|
|
1387
|
+
from_address: payload.from || null,
|
|
1388
|
+
subject: payload.subject,
|
|
1389
|
+
html_content: payload.html || null,
|
|
1390
|
+
text_content: payload.text || null,
|
|
1391
|
+
template_id: payload.templateId || null,
|
|
1392
|
+
metadata: payload.metadata || {},
|
|
1393
|
+
capture_mode: payload.captureMode,
|
|
1394
|
+
was_delivered: payload.wasDelivered,
|
|
1395
|
+
delivery_status: payload.wasDelivered ? "sent" : "pending"
|
|
1396
|
+
};
|
|
1397
|
+
const { data, error } = await this.supabase.from("email_captures").insert(record).select("id").single();
|
|
1398
|
+
if (error) {
|
|
1399
|
+
if (this._queue && isNetworkError(error.message)) {
|
|
1400
|
+
await this._queue.enqueue("email_capture", record);
|
|
1401
|
+
return { success: false, queued: true, error: "Queued \u2014 will send when online" };
|
|
1402
|
+
}
|
|
1403
|
+
console.error("BugBear: Failed to capture email", error.message);
|
|
1404
|
+
return { success: false, error: error.message };
|
|
1405
|
+
}
|
|
1406
|
+
return { success: true, captureId: data.id };
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1409
|
+
return { success: false, error: message };
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1292
1412
|
/**
|
|
1293
1413
|
* Get assigned tests for current user
|
|
1294
1414
|
* First looks up the tester by email, then fetches their assignments
|
|
@@ -1910,6 +2030,32 @@ var BugBearClient = class {
|
|
|
1910
2030
|
return [];
|
|
1911
2031
|
}
|
|
1912
2032
|
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Reopen a done issue that the tester believes isn't actually fixed.
|
|
2035
|
+
* Transitions the report from a done status back to 'confirmed'.
|
|
2036
|
+
*/
|
|
2037
|
+
async reopenReport(reportId, reason) {
|
|
2038
|
+
try {
|
|
2039
|
+
const testerInfo = await this.getTesterInfo();
|
|
2040
|
+
if (!testerInfo) return { success: false, error: "Not authenticated as tester" };
|
|
2041
|
+
const { data, error } = await this.supabase.rpc("reopen_report", {
|
|
2042
|
+
p_report_id: reportId,
|
|
2043
|
+
p_tester_id: testerInfo.id,
|
|
2044
|
+
p_reason: reason
|
|
2045
|
+
});
|
|
2046
|
+
if (error) {
|
|
2047
|
+
console.error("BugBear: Failed to reopen report", formatPgError(error));
|
|
2048
|
+
return { success: false, error: error.message };
|
|
2049
|
+
}
|
|
2050
|
+
if (!data?.success) {
|
|
2051
|
+
return { success: false, error: data?.error || "Failed to reopen report" };
|
|
2052
|
+
}
|
|
2053
|
+
return { success: true };
|
|
2054
|
+
} catch (err) {
|
|
2055
|
+
console.error("BugBear: Error reopening report", err);
|
|
2056
|
+
return { success: false, error: "Unexpected error" };
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
1913
2059
|
/**
|
|
1914
2060
|
* Basic email format validation (defense in depth)
|
|
1915
2061
|
*/
|
|
@@ -2419,6 +2565,7 @@ var BugBearClient = class {
|
|
|
2419
2565
|
lastMessageAt: row.last_message_at,
|
|
2420
2566
|
createdAt: row.created_at,
|
|
2421
2567
|
unreadCount: Number(row.unread_count) || 0,
|
|
2568
|
+
reporterName: row.reporter_name || void 0,
|
|
2422
2569
|
lastMessage: row.last_message_preview ? {
|
|
2423
2570
|
id: "",
|
|
2424
2571
|
threadId: row.thread_id,
|
|
@@ -2496,7 +2643,7 @@ var BugBearClient = class {
|
|
|
2496
2643
|
insertData.attachments = safeAttachments;
|
|
2497
2644
|
}
|
|
2498
2645
|
}
|
|
2499
|
-
const { error } = await this.supabase.from("discussion_messages").insert(insertData);
|
|
2646
|
+
const { data: msgData, error } = await this.supabase.from("discussion_messages").insert(insertData).select("id").single();
|
|
2500
2647
|
if (error) {
|
|
2501
2648
|
if (this._queue && isNetworkError(error.message)) {
|
|
2502
2649
|
await this._queue.enqueue("message", insertData);
|
|
@@ -2505,6 +2652,10 @@ var BugBearClient = class {
|
|
|
2505
2652
|
console.error("BugBear: Failed to send message", formatPgError(error));
|
|
2506
2653
|
return false;
|
|
2507
2654
|
}
|
|
2655
|
+
if (msgData?.id) {
|
|
2656
|
+
this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
|
|
2657
|
+
});
|
|
2658
|
+
}
|
|
2508
2659
|
await this.markThreadAsRead(threadId);
|
|
2509
2660
|
return true;
|
|
2510
2661
|
} catch (err) {
|
|
@@ -2630,6 +2781,7 @@ var BugBearClient = class {
|
|
|
2630
2781
|
if (!session) {
|
|
2631
2782
|
return { success: false, error: "Session created but could not be fetched" };
|
|
2632
2783
|
}
|
|
2784
|
+
await this.cacheSession(session);
|
|
2633
2785
|
return { success: true, session };
|
|
2634
2786
|
} catch (err) {
|
|
2635
2787
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -2653,6 +2805,7 @@ var BugBearClient = class {
|
|
|
2653
2805
|
return { success: false, error: error.message };
|
|
2654
2806
|
}
|
|
2655
2807
|
const session = this.transformSession(data);
|
|
2808
|
+
await this.cacheSession(null);
|
|
2656
2809
|
return { success: true, session };
|
|
2657
2810
|
} catch (err) {
|
|
2658
2811
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -2668,8 +2821,13 @@ var BugBearClient = class {
|
|
|
2668
2821
|
const testerInfo = await this.getTesterInfo();
|
|
2669
2822
|
if (!testerInfo) return null;
|
|
2670
2823
|
const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "active").order("started_at", { ascending: false }).limit(1).maybeSingle();
|
|
2671
|
-
if (error || !data)
|
|
2672
|
-
|
|
2824
|
+
if (error || !data) {
|
|
2825
|
+
await this.cacheSession(null);
|
|
2826
|
+
return null;
|
|
2827
|
+
}
|
|
2828
|
+
const session = this.transformSession(data);
|
|
2829
|
+
await this.cacheSession(session);
|
|
2830
|
+
return session;
|
|
2673
2831
|
} catch (err) {
|
|
2674
2832
|
console.error("BugBear: Error fetching active session", err);
|
|
2675
2833
|
return null;
|
|
@@ -2847,10 +3005,187 @@ var BugBearClient = class {
|
|
|
2847
3005
|
updatedAt: data.updated_at
|
|
2848
3006
|
};
|
|
2849
3007
|
}
|
|
3008
|
+
// ─── Passive Presence Tracking ──────────────────────────────
|
|
3009
|
+
/** Current presence session ID (null if not tracking). */
|
|
3010
|
+
get presenceSessionId() {
|
|
3011
|
+
return this._presenceSessionId;
|
|
3012
|
+
}
|
|
3013
|
+
/**
|
|
3014
|
+
* Start passive presence tracking for this tester.
|
|
3015
|
+
* Idempotent — reuses an existing active session if one exists.
|
|
3016
|
+
*/
|
|
3017
|
+
async startPresence(platform) {
|
|
3018
|
+
try {
|
|
3019
|
+
await this.ensureReady();
|
|
3020
|
+
const testerInfo = await this.getTesterInfo();
|
|
3021
|
+
if (!testerInfo) return null;
|
|
3022
|
+
const { data, error } = await this.supabase.rpc("upsert_tester_presence", {
|
|
3023
|
+
p_project_id: this.config.projectId,
|
|
3024
|
+
p_tester_id: testerInfo.id,
|
|
3025
|
+
p_platform: platform
|
|
3026
|
+
});
|
|
3027
|
+
if (error) {
|
|
3028
|
+
console.error("BugBear: Failed to start presence", formatPgError(error));
|
|
3029
|
+
return null;
|
|
3030
|
+
}
|
|
3031
|
+
this._presenceSessionId = data;
|
|
3032
|
+
this._presencePaused = false;
|
|
3033
|
+
this.startPresenceHeartbeat();
|
|
3034
|
+
return data;
|
|
3035
|
+
} catch (err) {
|
|
3036
|
+
console.error("BugBear: Error starting presence", err);
|
|
3037
|
+
return null;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
/** Gracefully end the current presence session. */
|
|
3041
|
+
async endPresence() {
|
|
3042
|
+
this.stopPresenceHeartbeat();
|
|
3043
|
+
if (!this._presenceSessionId) return;
|
|
3044
|
+
try {
|
|
3045
|
+
await this.supabase.rpc("end_tester_presence", {
|
|
3046
|
+
p_session_id: this._presenceSessionId
|
|
3047
|
+
});
|
|
3048
|
+
} catch {
|
|
3049
|
+
}
|
|
3050
|
+
this._presenceSessionId = null;
|
|
3051
|
+
}
|
|
3052
|
+
/** Pause heartbeat (tab hidden / app backgrounded). Sends one final beat. */
|
|
3053
|
+
pausePresence() {
|
|
3054
|
+
this._presencePaused = true;
|
|
3055
|
+
this.heartbeatPresence();
|
|
3056
|
+
}
|
|
3057
|
+
/** Resume heartbeat after pause. Restarts if session was cleaned up. */
|
|
3058
|
+
async resumePresence() {
|
|
3059
|
+
if (!this._presenceSessionId) return;
|
|
3060
|
+
this._presencePaused = false;
|
|
3061
|
+
try {
|
|
3062
|
+
const { data } = await this.supabase.rpc("heartbeat_tester_presence", {
|
|
3063
|
+
p_session_id: this._presenceSessionId
|
|
3064
|
+
});
|
|
3065
|
+
if (!data) {
|
|
3066
|
+
this._presenceSessionId = null;
|
|
3067
|
+
}
|
|
3068
|
+
} catch {
|
|
3069
|
+
this._presenceSessionId = null;
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
async heartbeatPresence() {
|
|
3073
|
+
if (!this._presenceSessionId || this._presencePaused) return;
|
|
3074
|
+
try {
|
|
3075
|
+
const { data, error } = await this.supabase.rpc("heartbeat_tester_presence", {
|
|
3076
|
+
p_session_id: this._presenceSessionId
|
|
3077
|
+
});
|
|
3078
|
+
if (error || data === false) {
|
|
3079
|
+
this.stopPresenceHeartbeat();
|
|
3080
|
+
this._presenceSessionId = null;
|
|
3081
|
+
}
|
|
3082
|
+
} catch {
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
startPresenceHeartbeat() {
|
|
3086
|
+
this.stopPresenceHeartbeat();
|
|
3087
|
+
this._presenceInterval = setInterval(() => this.heartbeatPresence(), 6e4);
|
|
3088
|
+
}
|
|
3089
|
+
stopPresenceHeartbeat() {
|
|
3090
|
+
if (this._presenceInterval) {
|
|
3091
|
+
clearInterval(this._presenceInterval);
|
|
3092
|
+
this._presenceInterval = null;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
2850
3095
|
};
|
|
2851
3096
|
function createBugBear(config) {
|
|
2852
3097
|
return new BugBearClient(config);
|
|
2853
3098
|
}
|
|
3099
|
+
|
|
3100
|
+
// src/email-interceptor.ts
|
|
3101
|
+
function defaultExtract(args) {
|
|
3102
|
+
const first = args[0];
|
|
3103
|
+
if (!first || typeof first !== "object") {
|
|
3104
|
+
throw new Error("BugBear email interceptor: first argument must be an object with { to, subject }");
|
|
3105
|
+
}
|
|
3106
|
+
const obj = first;
|
|
3107
|
+
return {
|
|
3108
|
+
to: obj.to,
|
|
3109
|
+
from: obj.from,
|
|
3110
|
+
subject: obj.subject,
|
|
3111
|
+
html: obj.html,
|
|
3112
|
+
text: obj.text,
|
|
3113
|
+
templateId: obj.templateId
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
function normalizeToArray(to) {
|
|
3117
|
+
if (Array.isArray(to)) return to;
|
|
3118
|
+
return [to];
|
|
3119
|
+
}
|
|
3120
|
+
function createEmailInterceptor(client, options = { mode: "capture" }) {
|
|
3121
|
+
const { mode, filterTo, redactRecipients } = options;
|
|
3122
|
+
return {
|
|
3123
|
+
wrap(sendFn, wrapOpts) {
|
|
3124
|
+
const extract = wrapOpts?.extractPayload || defaultExtract;
|
|
3125
|
+
const wrapped = async (...args) => {
|
|
3126
|
+
let extracted;
|
|
3127
|
+
try {
|
|
3128
|
+
extracted = extract(args);
|
|
3129
|
+
} catch {
|
|
3130
|
+
return sendFn(...args);
|
|
3131
|
+
}
|
|
3132
|
+
const toArray = normalizeToArray(extracted.to);
|
|
3133
|
+
if (filterTo && filterTo.length > 0) {
|
|
3134
|
+
const matches = toArray.some(
|
|
3135
|
+
(addr) => filterTo.includes(addr.toLowerCase())
|
|
3136
|
+
);
|
|
3137
|
+
if (!matches) {
|
|
3138
|
+
return sendFn(...args);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
const capturedTo = redactRecipients ? toArray.map(() => "[redacted]") : toArray;
|
|
3142
|
+
if (mode === "intercept") {
|
|
3143
|
+
client.captureEmail({
|
|
3144
|
+
to: capturedTo,
|
|
3145
|
+
from: extracted.from,
|
|
3146
|
+
subject: extracted.subject,
|
|
3147
|
+
html: extracted.html,
|
|
3148
|
+
text: extracted.text,
|
|
3149
|
+
templateId: extracted.templateId,
|
|
3150
|
+
captureMode: "intercept",
|
|
3151
|
+
wasDelivered: false
|
|
3152
|
+
}).catch(() => {
|
|
3153
|
+
});
|
|
3154
|
+
return { success: true, intercepted: true };
|
|
3155
|
+
}
|
|
3156
|
+
try {
|
|
3157
|
+
const result = await sendFn(...args);
|
|
3158
|
+
client.captureEmail({
|
|
3159
|
+
to: capturedTo,
|
|
3160
|
+
from: extracted.from,
|
|
3161
|
+
subject: extracted.subject,
|
|
3162
|
+
html: extracted.html,
|
|
3163
|
+
text: extracted.text,
|
|
3164
|
+
templateId: extracted.templateId,
|
|
3165
|
+
captureMode: "capture",
|
|
3166
|
+
wasDelivered: true
|
|
3167
|
+
}).catch(() => {
|
|
3168
|
+
});
|
|
3169
|
+
return result;
|
|
3170
|
+
} catch (err) {
|
|
3171
|
+
client.captureEmail({
|
|
3172
|
+
to: capturedTo,
|
|
3173
|
+
from: extracted.from,
|
|
3174
|
+
subject: extracted.subject,
|
|
3175
|
+
html: extracted.html,
|
|
3176
|
+
text: extracted.text,
|
|
3177
|
+
templateId: extracted.templateId,
|
|
3178
|
+
captureMode: "capture",
|
|
3179
|
+
wasDelivered: false
|
|
3180
|
+
}).catch(() => {
|
|
3181
|
+
});
|
|
3182
|
+
throw err;
|
|
3183
|
+
}
|
|
3184
|
+
};
|
|
3185
|
+
return wrapped;
|
|
3186
|
+
}
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
2854
3189
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2855
3190
|
0 && (module.exports = {
|
|
2856
3191
|
BUG_CATEGORIES,
|
|
@@ -2869,6 +3204,7 @@ function createBugBear(config) {
|
|
|
2869
3204
|
captureError,
|
|
2870
3205
|
contextCapture,
|
|
2871
3206
|
createBugBear,
|
|
3207
|
+
createEmailInterceptor,
|
|
2872
3208
|
generateFingerprint,
|
|
2873
3209
|
isBugCategory,
|
|
2874
3210
|
isNetworkError,
|
package/dist/index.mjs
CHANGED
|
@@ -857,6 +857,8 @@ var BugBearClient = class {
|
|
|
857
857
|
this.reportSubmitInFlight = false;
|
|
858
858
|
/** Offline queue — only created when config.offlineQueue.enabled is true. */
|
|
859
859
|
this._queue = null;
|
|
860
|
+
/** Session cache storage — defaults to LocalStorageAdapter. */
|
|
861
|
+
this._sessionStorage = new LocalStorageAdapter();
|
|
860
862
|
/** Active Realtime channel references for cleanup. */
|
|
861
863
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
862
864
|
this.realtimeChannels = [];
|
|
@@ -866,6 +868,12 @@ var BugBearClient = class {
|
|
|
866
868
|
this.initialized = false;
|
|
867
869
|
/** Initialization error, if any. */
|
|
868
870
|
this.initError = null;
|
|
871
|
+
/** Active presence session ID (passive time tracking). */
|
|
872
|
+
this._presenceSessionId = null;
|
|
873
|
+
/** Heartbeat interval for presence tracking. */
|
|
874
|
+
this._presenceInterval = null;
|
|
875
|
+
/** Whether presence is paused (tab hidden / app backgrounded). */
|
|
876
|
+
this._presencePaused = false;
|
|
869
877
|
this.config = config;
|
|
870
878
|
if (config.apiKey) {
|
|
871
879
|
this.pendingInit = this.resolveFromApiKey(config.apiKey);
|
|
@@ -939,6 +947,10 @@ var BugBearClient = class {
|
|
|
939
947
|
this.initOfflineQueue();
|
|
940
948
|
this.initMonitoring();
|
|
941
949
|
}
|
|
950
|
+
/** Cache key scoped to the active project. */
|
|
951
|
+
get sessionCacheKey() {
|
|
952
|
+
return `bugbear_session_${this.config.projectId ?? "unknown"}`;
|
|
953
|
+
}
|
|
942
954
|
/** Initialize offline queue if configured. Shared by both init paths. */
|
|
943
955
|
initOfflineQueue() {
|
|
944
956
|
if (this.config.offlineQueue?.enabled) {
|
|
@@ -1002,6 +1014,26 @@ var BugBearClient = class {
|
|
|
1002
1014
|
await this.pendingInit;
|
|
1003
1015
|
if (this.initError) throw this.initError;
|
|
1004
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Fire-and-forget call to a dashboard notification endpoint.
|
|
1019
|
+
* Only works when apiKey is configured (needed for API auth).
|
|
1020
|
+
* Failures are silently ignored — notifications are best-effort.
|
|
1021
|
+
*/
|
|
1022
|
+
async notifyDashboard(path, body) {
|
|
1023
|
+
if (!this.config.apiKey) return;
|
|
1024
|
+
try {
|
|
1025
|
+
const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
|
|
1026
|
+
await fetch(`${baseUrl}/api/v1/notifications/${path}`, {
|
|
1027
|
+
method: "POST",
|
|
1028
|
+
headers: {
|
|
1029
|
+
"Content-Type": "application/json",
|
|
1030
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
1031
|
+
},
|
|
1032
|
+
body: JSON.stringify(body)
|
|
1033
|
+
});
|
|
1034
|
+
} catch {
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1005
1037
|
// ── Offline Queue ─────────────────────────────────────────
|
|
1006
1038
|
/**
|
|
1007
1039
|
* Access the offline queue (if enabled).
|
|
@@ -1028,6 +1060,48 @@ var BugBearClient = class {
|
|
|
1028
1060
|
}
|
|
1029
1061
|
await this._queue.load();
|
|
1030
1062
|
}
|
|
1063
|
+
// ── Session Cache ──────────────────────────────────────────
|
|
1064
|
+
/**
|
|
1065
|
+
* Swap the session cache storage adapter (for React Native — pass AsyncStorage).
|
|
1066
|
+
* Must be called before getCachedSession() for persistence across app kills.
|
|
1067
|
+
* Web callers don't need this — LocalStorageAdapter is the default.
|
|
1068
|
+
*/
|
|
1069
|
+
setSessionStorage(adapter) {
|
|
1070
|
+
this._sessionStorage = adapter;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Cache the active QA session locally for instant restore on app restart.
|
|
1074
|
+
* Pass null to clear the cache (e.g. after ending a session).
|
|
1075
|
+
*/
|
|
1076
|
+
async cacheSession(session) {
|
|
1077
|
+
try {
|
|
1078
|
+
if (session) {
|
|
1079
|
+
await this._sessionStorage.setItem(this.sessionCacheKey, JSON.stringify(session));
|
|
1080
|
+
} else {
|
|
1081
|
+
await this._sessionStorage.removeItem(this.sessionCacheKey);
|
|
1082
|
+
}
|
|
1083
|
+
} catch {
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
|
|
1088
|
+
* or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
|
|
1089
|
+
*/
|
|
1090
|
+
async getCachedSession() {
|
|
1091
|
+
try {
|
|
1092
|
+
const raw = await this._sessionStorage.getItem(this.sessionCacheKey);
|
|
1093
|
+
if (!raw) return null;
|
|
1094
|
+
const session = JSON.parse(raw);
|
|
1095
|
+
const age = Date.now() - new Date(session.startedAt).getTime();
|
|
1096
|
+
if (age > 24 * 60 * 60 * 1e3) {
|
|
1097
|
+
await this._sessionStorage.removeItem(this.sessionCacheKey);
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
return session;
|
|
1101
|
+
} catch {
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1031
1105
|
registerQueueHandlers() {
|
|
1032
1106
|
if (!this._queue) return;
|
|
1033
1107
|
this._queue.registerHandler("report", async (payload) => {
|
|
@@ -1045,6 +1119,11 @@ var BugBearClient = class {
|
|
|
1045
1119
|
if (error) return { success: false, error: error.message };
|
|
1046
1120
|
return { success: true };
|
|
1047
1121
|
});
|
|
1122
|
+
this._queue.registerHandler("email_capture", async (payload) => {
|
|
1123
|
+
const { error } = await this.supabase.from("email_captures").insert(payload).select("id").single();
|
|
1124
|
+
if (error) return { success: false, error: error.message };
|
|
1125
|
+
return { success: true };
|
|
1126
|
+
});
|
|
1048
1127
|
}
|
|
1049
1128
|
// ── Realtime Subscriptions ─────────────────────────────────
|
|
1050
1129
|
/** Whether realtime is enabled in config. */
|
|
@@ -1232,6 +1311,8 @@ var BugBearClient = class {
|
|
|
1232
1311
|
if (this.config.onReportSubmitted) {
|
|
1233
1312
|
this.config.onReportSubmitted(report);
|
|
1234
1313
|
}
|
|
1314
|
+
this.notifyDashboard("report", { reportId: data.id }).catch(() => {
|
|
1315
|
+
});
|
|
1235
1316
|
return { success: true, reportId: data.id };
|
|
1236
1317
|
} catch (err) {
|
|
1237
1318
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -1244,6 +1325,44 @@ var BugBearClient = class {
|
|
|
1244
1325
|
this.reportSubmitInFlight = false;
|
|
1245
1326
|
}
|
|
1246
1327
|
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Capture an email for QA testing.
|
|
1330
|
+
* Called by the email interceptor — not typically called directly.
|
|
1331
|
+
*/
|
|
1332
|
+
async captureEmail(payload) {
|
|
1333
|
+
try {
|
|
1334
|
+
await this.ready();
|
|
1335
|
+
if (!payload.subject || !payload.to || payload.to.length === 0) {
|
|
1336
|
+
return { success: false, error: "subject and to are required" };
|
|
1337
|
+
}
|
|
1338
|
+
const record = {
|
|
1339
|
+
project_id: this.config.projectId,
|
|
1340
|
+
to_addresses: payload.to,
|
|
1341
|
+
from_address: payload.from || null,
|
|
1342
|
+
subject: payload.subject,
|
|
1343
|
+
html_content: payload.html || null,
|
|
1344
|
+
text_content: payload.text || null,
|
|
1345
|
+
template_id: payload.templateId || null,
|
|
1346
|
+
metadata: payload.metadata || {},
|
|
1347
|
+
capture_mode: payload.captureMode,
|
|
1348
|
+
was_delivered: payload.wasDelivered,
|
|
1349
|
+
delivery_status: payload.wasDelivered ? "sent" : "pending"
|
|
1350
|
+
};
|
|
1351
|
+
const { data, error } = await this.supabase.from("email_captures").insert(record).select("id").single();
|
|
1352
|
+
if (error) {
|
|
1353
|
+
if (this._queue && isNetworkError(error.message)) {
|
|
1354
|
+
await this._queue.enqueue("email_capture", record);
|
|
1355
|
+
return { success: false, queued: true, error: "Queued \u2014 will send when online" };
|
|
1356
|
+
}
|
|
1357
|
+
console.error("BugBear: Failed to capture email", error.message);
|
|
1358
|
+
return { success: false, error: error.message };
|
|
1359
|
+
}
|
|
1360
|
+
return { success: true, captureId: data.id };
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1363
|
+
return { success: false, error: message };
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1247
1366
|
/**
|
|
1248
1367
|
* Get assigned tests for current user
|
|
1249
1368
|
* First looks up the tester by email, then fetches their assignments
|
|
@@ -1865,6 +1984,32 @@ var BugBearClient = class {
|
|
|
1865
1984
|
return [];
|
|
1866
1985
|
}
|
|
1867
1986
|
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Reopen a done issue that the tester believes isn't actually fixed.
|
|
1989
|
+
* Transitions the report from a done status back to 'confirmed'.
|
|
1990
|
+
*/
|
|
1991
|
+
async reopenReport(reportId, reason) {
|
|
1992
|
+
try {
|
|
1993
|
+
const testerInfo = await this.getTesterInfo();
|
|
1994
|
+
if (!testerInfo) return { success: false, error: "Not authenticated as tester" };
|
|
1995
|
+
const { data, error } = await this.supabase.rpc("reopen_report", {
|
|
1996
|
+
p_report_id: reportId,
|
|
1997
|
+
p_tester_id: testerInfo.id,
|
|
1998
|
+
p_reason: reason
|
|
1999
|
+
});
|
|
2000
|
+
if (error) {
|
|
2001
|
+
console.error("BugBear: Failed to reopen report", formatPgError(error));
|
|
2002
|
+
return { success: false, error: error.message };
|
|
2003
|
+
}
|
|
2004
|
+
if (!data?.success) {
|
|
2005
|
+
return { success: false, error: data?.error || "Failed to reopen report" };
|
|
2006
|
+
}
|
|
2007
|
+
return { success: true };
|
|
2008
|
+
} catch (err) {
|
|
2009
|
+
console.error("BugBear: Error reopening report", err);
|
|
2010
|
+
return { success: false, error: "Unexpected error" };
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
1868
2013
|
/**
|
|
1869
2014
|
* Basic email format validation (defense in depth)
|
|
1870
2015
|
*/
|
|
@@ -2374,6 +2519,7 @@ var BugBearClient = class {
|
|
|
2374
2519
|
lastMessageAt: row.last_message_at,
|
|
2375
2520
|
createdAt: row.created_at,
|
|
2376
2521
|
unreadCount: Number(row.unread_count) || 0,
|
|
2522
|
+
reporterName: row.reporter_name || void 0,
|
|
2377
2523
|
lastMessage: row.last_message_preview ? {
|
|
2378
2524
|
id: "",
|
|
2379
2525
|
threadId: row.thread_id,
|
|
@@ -2451,7 +2597,7 @@ var BugBearClient = class {
|
|
|
2451
2597
|
insertData.attachments = safeAttachments;
|
|
2452
2598
|
}
|
|
2453
2599
|
}
|
|
2454
|
-
const { error } = await this.supabase.from("discussion_messages").insert(insertData);
|
|
2600
|
+
const { data: msgData, error } = await this.supabase.from("discussion_messages").insert(insertData).select("id").single();
|
|
2455
2601
|
if (error) {
|
|
2456
2602
|
if (this._queue && isNetworkError(error.message)) {
|
|
2457
2603
|
await this._queue.enqueue("message", insertData);
|
|
@@ -2460,6 +2606,10 @@ var BugBearClient = class {
|
|
|
2460
2606
|
console.error("BugBear: Failed to send message", formatPgError(error));
|
|
2461
2607
|
return false;
|
|
2462
2608
|
}
|
|
2609
|
+
if (msgData?.id) {
|
|
2610
|
+
this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2463
2613
|
await this.markThreadAsRead(threadId);
|
|
2464
2614
|
return true;
|
|
2465
2615
|
} catch (err) {
|
|
@@ -2585,6 +2735,7 @@ var BugBearClient = class {
|
|
|
2585
2735
|
if (!session) {
|
|
2586
2736
|
return { success: false, error: "Session created but could not be fetched" };
|
|
2587
2737
|
}
|
|
2738
|
+
await this.cacheSession(session);
|
|
2588
2739
|
return { success: true, session };
|
|
2589
2740
|
} catch (err) {
|
|
2590
2741
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -2608,6 +2759,7 @@ var BugBearClient = class {
|
|
|
2608
2759
|
return { success: false, error: error.message };
|
|
2609
2760
|
}
|
|
2610
2761
|
const session = this.transformSession(data);
|
|
2762
|
+
await this.cacheSession(null);
|
|
2611
2763
|
return { success: true, session };
|
|
2612
2764
|
} catch (err) {
|
|
2613
2765
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -2623,8 +2775,13 @@ var BugBearClient = class {
|
|
|
2623
2775
|
const testerInfo = await this.getTesterInfo();
|
|
2624
2776
|
if (!testerInfo) return null;
|
|
2625
2777
|
const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "active").order("started_at", { ascending: false }).limit(1).maybeSingle();
|
|
2626
|
-
if (error || !data)
|
|
2627
|
-
|
|
2778
|
+
if (error || !data) {
|
|
2779
|
+
await this.cacheSession(null);
|
|
2780
|
+
return null;
|
|
2781
|
+
}
|
|
2782
|
+
const session = this.transformSession(data);
|
|
2783
|
+
await this.cacheSession(session);
|
|
2784
|
+
return session;
|
|
2628
2785
|
} catch (err) {
|
|
2629
2786
|
console.error("BugBear: Error fetching active session", err);
|
|
2630
2787
|
return null;
|
|
@@ -2802,10 +2959,187 @@ var BugBearClient = class {
|
|
|
2802
2959
|
updatedAt: data.updated_at
|
|
2803
2960
|
};
|
|
2804
2961
|
}
|
|
2962
|
+
// ─── Passive Presence Tracking ──────────────────────────────
|
|
2963
|
+
/** Current presence session ID (null if not tracking). */
|
|
2964
|
+
get presenceSessionId() {
|
|
2965
|
+
return this._presenceSessionId;
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* Start passive presence tracking for this tester.
|
|
2969
|
+
* Idempotent — reuses an existing active session if one exists.
|
|
2970
|
+
*/
|
|
2971
|
+
async startPresence(platform) {
|
|
2972
|
+
try {
|
|
2973
|
+
await this.ensureReady();
|
|
2974
|
+
const testerInfo = await this.getTesterInfo();
|
|
2975
|
+
if (!testerInfo) return null;
|
|
2976
|
+
const { data, error } = await this.supabase.rpc("upsert_tester_presence", {
|
|
2977
|
+
p_project_id: this.config.projectId,
|
|
2978
|
+
p_tester_id: testerInfo.id,
|
|
2979
|
+
p_platform: platform
|
|
2980
|
+
});
|
|
2981
|
+
if (error) {
|
|
2982
|
+
console.error("BugBear: Failed to start presence", formatPgError(error));
|
|
2983
|
+
return null;
|
|
2984
|
+
}
|
|
2985
|
+
this._presenceSessionId = data;
|
|
2986
|
+
this._presencePaused = false;
|
|
2987
|
+
this.startPresenceHeartbeat();
|
|
2988
|
+
return data;
|
|
2989
|
+
} catch (err) {
|
|
2990
|
+
console.error("BugBear: Error starting presence", err);
|
|
2991
|
+
return null;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
/** Gracefully end the current presence session. */
|
|
2995
|
+
async endPresence() {
|
|
2996
|
+
this.stopPresenceHeartbeat();
|
|
2997
|
+
if (!this._presenceSessionId) return;
|
|
2998
|
+
try {
|
|
2999
|
+
await this.supabase.rpc("end_tester_presence", {
|
|
3000
|
+
p_session_id: this._presenceSessionId
|
|
3001
|
+
});
|
|
3002
|
+
} catch {
|
|
3003
|
+
}
|
|
3004
|
+
this._presenceSessionId = null;
|
|
3005
|
+
}
|
|
3006
|
+
/** Pause heartbeat (tab hidden / app backgrounded). Sends one final beat. */
|
|
3007
|
+
pausePresence() {
|
|
3008
|
+
this._presencePaused = true;
|
|
3009
|
+
this.heartbeatPresence();
|
|
3010
|
+
}
|
|
3011
|
+
/** Resume heartbeat after pause. Restarts if session was cleaned up. */
|
|
3012
|
+
async resumePresence() {
|
|
3013
|
+
if (!this._presenceSessionId) return;
|
|
3014
|
+
this._presencePaused = false;
|
|
3015
|
+
try {
|
|
3016
|
+
const { data } = await this.supabase.rpc("heartbeat_tester_presence", {
|
|
3017
|
+
p_session_id: this._presenceSessionId
|
|
3018
|
+
});
|
|
3019
|
+
if (!data) {
|
|
3020
|
+
this._presenceSessionId = null;
|
|
3021
|
+
}
|
|
3022
|
+
} catch {
|
|
3023
|
+
this._presenceSessionId = null;
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
async heartbeatPresence() {
|
|
3027
|
+
if (!this._presenceSessionId || this._presencePaused) return;
|
|
3028
|
+
try {
|
|
3029
|
+
const { data, error } = await this.supabase.rpc("heartbeat_tester_presence", {
|
|
3030
|
+
p_session_id: this._presenceSessionId
|
|
3031
|
+
});
|
|
3032
|
+
if (error || data === false) {
|
|
3033
|
+
this.stopPresenceHeartbeat();
|
|
3034
|
+
this._presenceSessionId = null;
|
|
3035
|
+
}
|
|
3036
|
+
} catch {
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
startPresenceHeartbeat() {
|
|
3040
|
+
this.stopPresenceHeartbeat();
|
|
3041
|
+
this._presenceInterval = setInterval(() => this.heartbeatPresence(), 6e4);
|
|
3042
|
+
}
|
|
3043
|
+
stopPresenceHeartbeat() {
|
|
3044
|
+
if (this._presenceInterval) {
|
|
3045
|
+
clearInterval(this._presenceInterval);
|
|
3046
|
+
this._presenceInterval = null;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
2805
3049
|
};
|
|
2806
3050
|
function createBugBear(config) {
|
|
2807
3051
|
return new BugBearClient(config);
|
|
2808
3052
|
}
|
|
3053
|
+
|
|
3054
|
+
// src/email-interceptor.ts
|
|
3055
|
+
function defaultExtract(args) {
|
|
3056
|
+
const first = args[0];
|
|
3057
|
+
if (!first || typeof first !== "object") {
|
|
3058
|
+
throw new Error("BugBear email interceptor: first argument must be an object with { to, subject }");
|
|
3059
|
+
}
|
|
3060
|
+
const obj = first;
|
|
3061
|
+
return {
|
|
3062
|
+
to: obj.to,
|
|
3063
|
+
from: obj.from,
|
|
3064
|
+
subject: obj.subject,
|
|
3065
|
+
html: obj.html,
|
|
3066
|
+
text: obj.text,
|
|
3067
|
+
templateId: obj.templateId
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
function normalizeToArray(to) {
|
|
3071
|
+
if (Array.isArray(to)) return to;
|
|
3072
|
+
return [to];
|
|
3073
|
+
}
|
|
3074
|
+
function createEmailInterceptor(client, options = { mode: "capture" }) {
|
|
3075
|
+
const { mode, filterTo, redactRecipients } = options;
|
|
3076
|
+
return {
|
|
3077
|
+
wrap(sendFn, wrapOpts) {
|
|
3078
|
+
const extract = wrapOpts?.extractPayload || defaultExtract;
|
|
3079
|
+
const wrapped = async (...args) => {
|
|
3080
|
+
let extracted;
|
|
3081
|
+
try {
|
|
3082
|
+
extracted = extract(args);
|
|
3083
|
+
} catch {
|
|
3084
|
+
return sendFn(...args);
|
|
3085
|
+
}
|
|
3086
|
+
const toArray = normalizeToArray(extracted.to);
|
|
3087
|
+
if (filterTo && filterTo.length > 0) {
|
|
3088
|
+
const matches = toArray.some(
|
|
3089
|
+
(addr) => filterTo.includes(addr.toLowerCase())
|
|
3090
|
+
);
|
|
3091
|
+
if (!matches) {
|
|
3092
|
+
return sendFn(...args);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
const capturedTo = redactRecipients ? toArray.map(() => "[redacted]") : toArray;
|
|
3096
|
+
if (mode === "intercept") {
|
|
3097
|
+
client.captureEmail({
|
|
3098
|
+
to: capturedTo,
|
|
3099
|
+
from: extracted.from,
|
|
3100
|
+
subject: extracted.subject,
|
|
3101
|
+
html: extracted.html,
|
|
3102
|
+
text: extracted.text,
|
|
3103
|
+
templateId: extracted.templateId,
|
|
3104
|
+
captureMode: "intercept",
|
|
3105
|
+
wasDelivered: false
|
|
3106
|
+
}).catch(() => {
|
|
3107
|
+
});
|
|
3108
|
+
return { success: true, intercepted: true };
|
|
3109
|
+
}
|
|
3110
|
+
try {
|
|
3111
|
+
const result = await sendFn(...args);
|
|
3112
|
+
client.captureEmail({
|
|
3113
|
+
to: capturedTo,
|
|
3114
|
+
from: extracted.from,
|
|
3115
|
+
subject: extracted.subject,
|
|
3116
|
+
html: extracted.html,
|
|
3117
|
+
text: extracted.text,
|
|
3118
|
+
templateId: extracted.templateId,
|
|
3119
|
+
captureMode: "capture",
|
|
3120
|
+
wasDelivered: true
|
|
3121
|
+
}).catch(() => {
|
|
3122
|
+
});
|
|
3123
|
+
return result;
|
|
3124
|
+
} catch (err) {
|
|
3125
|
+
client.captureEmail({
|
|
3126
|
+
to: capturedTo,
|
|
3127
|
+
from: extracted.from,
|
|
3128
|
+
subject: extracted.subject,
|
|
3129
|
+
html: extracted.html,
|
|
3130
|
+
text: extracted.text,
|
|
3131
|
+
templateId: extracted.templateId,
|
|
3132
|
+
captureMode: "capture",
|
|
3133
|
+
wasDelivered: false
|
|
3134
|
+
}).catch(() => {
|
|
3135
|
+
});
|
|
3136
|
+
throw err;
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
return wrapped;
|
|
3140
|
+
}
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
2809
3143
|
export {
|
|
2810
3144
|
BUG_CATEGORIES,
|
|
2811
3145
|
BugBearClient,
|
|
@@ -2823,6 +3157,7 @@ export {
|
|
|
2823
3157
|
captureError,
|
|
2824
3158
|
contextCapture,
|
|
2825
3159
|
createBugBear,
|
|
3160
|
+
createEmailInterceptor,
|
|
2826
3161
|
generateFingerprint,
|
|
2827
3162
|
isBugCategory,
|
|
2828
3163
|
isNetworkError,
|