@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 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
- 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 EndSessionOptions, type EnhancedBugContext, ErrorMonitor, 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, generateFingerprint, isBugCategory, isNetworkError, scrubUrl };
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
- 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 EndSessionOptions, type EnhancedBugContext, ErrorMonitor, 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, generateFingerprint, isBugCategory, isNetworkError, scrubUrl };
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) return null;
2672
- return this.transformSession(data);
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) return null;
2627
- return this.transformSession(data);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/core",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Core utilities and types for BugBear QA platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",