@bbearai/core 0.7.0 → 0.7.2

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
@@ -301,8 +301,13 @@ interface MonitoringEvent {
301
301
  /** Sentry breadcrumbs — populated by @bbearai/sentry adapter */
302
302
  sentryBreadcrumbs?: unknown[];
303
303
  }
304
- /** Widget color scheme: 'dark' (default), 'light', or 'auto' (follows system preference). */
304
+ /** Widget color scheme: 'dark', 'light', or 'auto' (follows system preference, default). */
305
305
  type WidgetColorScheme = 'dark' | 'light' | 'auto';
306
+ /** Server-side widget configuration set by the project admin in the dashboard. */
307
+ interface WidgetConfig {
308
+ /** Admin-forced color scheme. When set, overrides the developer's config and user's OS preference. */
309
+ colorScheme?: WidgetColorScheme;
310
+ }
306
311
  interface BugBearTheme {
307
312
  /** Primary brand color */
308
313
  primaryColor?: string;
@@ -312,7 +317,7 @@ interface BugBearTheme {
312
317
  textColor?: string;
313
318
  /** Border radius */
314
319
  borderRadius?: number;
315
- /** Color scheme for the widget. Defaults to 'dark'. */
320
+ /** Color scheme for the widget. Defaults to 'auto' (follows OS preference). */
316
321
  colorScheme?: WidgetColorScheme;
317
322
  }
318
323
  type TestTemplate = 'steps' | 'checklist' | 'rubric' | 'freeform';
@@ -784,6 +789,50 @@ interface TesterIssue {
784
789
  /** Original bug title (for reopened/test_fail issues) */
785
790
  originalBugTitle?: string;
786
791
  }
792
+ /** Delivery status for captured emails */
793
+ type EmailDeliveryStatus = 'pending' | 'sent' | 'delivered' | 'bounced' | 'dropped' | 'deferred';
794
+ /** Email capture mode */
795
+ type EmailCaptureMode = 'capture' | 'intercept';
796
+ /** A captured email payload sent to the BugBear API */
797
+ interface EmailCapturePayload {
798
+ to: string[];
799
+ from?: string;
800
+ subject: string;
801
+ html?: string;
802
+ text?: string;
803
+ templateId?: string;
804
+ captureMode: EmailCaptureMode;
805
+ wasDelivered: boolean;
806
+ metadata?: Record<string, unknown>;
807
+ }
808
+ /** Result of capturing an email */
809
+ interface EmailCaptureResult {
810
+ success: boolean;
811
+ captureId?: string;
812
+ queued?: boolean;
813
+ error?: string;
814
+ }
815
+ /** Options for the email interceptor */
816
+ interface EmailInterceptorOptions {
817
+ /** 'capture' = log + send, 'intercept' = log + block */
818
+ mode: EmailCaptureMode;
819
+ /** Only capture emails sent to these addresses */
820
+ filterTo?: string[];
821
+ /** Replace real recipient addresses with "[redacted]" in captures */
822
+ redactRecipients?: boolean;
823
+ }
824
+ /** Extracted email fields from a send function call */
825
+ interface ExtractedEmailPayload {
826
+ to: string | string[];
827
+ from?: string;
828
+ subject: string;
829
+ html?: string;
830
+ text?: string;
831
+ templateId?: string;
832
+ [key: string]: unknown;
833
+ }
834
+ /** Custom extractor for non-standard email function signatures */
835
+ type EmailPayloadExtractor = (args: unknown[]) => ExtractedEmailPayload;
787
836
 
788
837
  /**
789
838
  * BugBear Offline Queue
@@ -810,7 +859,7 @@ declare class LocalStorageAdapter implements StorageAdapter {
810
859
  setItem(key: string, value: string): Promise<void>;
811
860
  removeItem(key: string): Promise<void>;
812
861
  }
813
- type QueueItemType = 'report' | 'message' | 'feedback';
862
+ type QueueItemType = 'report' | 'message' | 'feedback' | 'email_capture';
814
863
  interface QueueItem {
815
864
  /** Unique ID for this queued operation. */
816
865
  id: string;
@@ -1037,6 +1086,8 @@ declare class BugBearClient {
1037
1086
  private reportSubmitInFlight;
1038
1087
  /** Offline queue — only created when config.offlineQueue.enabled is true. */
1039
1088
  private _queue;
1089
+ /** Session cache storage — defaults to LocalStorageAdapter. */
1090
+ private _sessionStorage;
1040
1091
  /** Active Realtime channel references for cleanup. */
1041
1092
  private realtimeChannels;
1042
1093
  /** Error monitor instance — created when config.monitoring is present. */
@@ -1063,6 +1114,8 @@ declare class BugBearClient {
1063
1114
  private resolveFromApiKey;
1064
1115
  /** Apply resolved credentials and create the Supabase client. */
1065
1116
  private applyResolvedConfig;
1117
+ /** Cache key scoped to the active project. */
1118
+ private get sessionCacheKey();
1066
1119
  /** Initialize offline queue if configured. Shared by both init paths. */
1067
1120
  private initOfflineQueue;
1068
1121
  /** Initialize error monitoring if configured. Shared by both init paths. */
@@ -1075,6 +1128,12 @@ declare class BugBearClient {
1075
1128
  private hashKey;
1076
1129
  /** Ensure the client is initialized before making requests. */
1077
1130
  private ensureReady;
1131
+ /**
1132
+ * Fire-and-forget call to a dashboard notification endpoint.
1133
+ * Only works when apiKey is configured (needed for API auth).
1134
+ * Failures are silently ignored — notifications are best-effort.
1135
+ */
1136
+ private notifyDashboard;
1078
1137
  /**
1079
1138
  * Access the offline queue (if enabled).
1080
1139
  * Use this to check queue.count, subscribe to changes, or trigger flush.
@@ -1086,6 +1145,22 @@ declare class BugBearClient {
1086
1145
  * Web callers can skip this — LocalStorageAdapter is the default.
1087
1146
  */
1088
1147
  initQueue(storage?: StorageAdapter): Promise<void>;
1148
+ /**
1149
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
1150
+ * Must be called before getCachedSession() for persistence across app kills.
1151
+ * Web callers don't need this — LocalStorageAdapter is the default.
1152
+ */
1153
+ setSessionStorage(adapter: StorageAdapter): void;
1154
+ /**
1155
+ * Cache the active QA session locally for instant restore on app restart.
1156
+ * Pass null to clear the cache (e.g. after ending a session).
1157
+ */
1158
+ cacheSession(session: QASession | null): Promise<void>;
1159
+ /**
1160
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
1161
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
1162
+ */
1163
+ getCachedSession(): Promise<QASession | null>;
1089
1164
  private registerQueueHandlers;
1090
1165
  /** Whether realtime is enabled in config. */
1091
1166
  get realtimeEnabled(): boolean;
@@ -1132,6 +1207,11 @@ declare class BugBearClient {
1132
1207
  queued?: boolean;
1133
1208
  error?: string;
1134
1209
  }>;
1210
+ /**
1211
+ * Capture an email for QA testing.
1212
+ * Called by the email interceptor — not typically called directly.
1213
+ */
1214
+ captureEmail(payload: EmailCapturePayload): Promise<EmailCaptureResult>;
1135
1215
  /**
1136
1216
  * Get assigned tests for current user
1137
1217
  * First looks up the tester by email, then fetches their assignments
@@ -1248,6 +1328,14 @@ declare class BugBearClient {
1248
1328
  * reopened issues include original bug context.
1249
1329
  */
1250
1330
  getIssues(category: 'open' | 'done' | 'reopened'): Promise<TesterIssue[]>;
1331
+ /**
1332
+ * Reopen a done issue that the tester believes isn't actually fixed.
1333
+ * Transitions the report from a done status back to 'confirmed'.
1334
+ */
1335
+ reopenReport(reportId: string, reason: string): Promise<{
1336
+ success: boolean;
1337
+ error?: string;
1338
+ }>;
1251
1339
  /**
1252
1340
  * Basic email format validation (defense in depth)
1253
1341
  */
@@ -1284,18 +1372,28 @@ declare class BugBearClient {
1284
1372
  * This is a master switch that admins can toggle in the dashboard
1285
1373
  */
1286
1374
  isQAEnabled(): Promise<boolean>;
1375
+ /**
1376
+ * Fetch server-side widget config set by the project admin.
1377
+ * Returns settings like color scheme override. Returns empty config on error.
1378
+ */
1379
+ getWidgetConfig(): Promise<WidgetConfig>;
1380
+ /**
1381
+ * Check if feedback mode is enabled for this project.
1382
+ * This is a master switch that admins can toggle in the dashboard.
1383
+ */
1384
+ isFeedbackEnabled(): Promise<boolean>;
1287
1385
  /**
1288
1386
  * Check if the widget should be visible.
1289
1387
  * Behavior depends on the configured mode:
1290
1388
  * - 'qa': QA enabled AND user is a registered tester
1291
- * - 'feedback': Any authenticated user
1292
- * - 'auto': Either QA tester OR authenticated non-tester
1389
+ * - 'feedback': Feedback enabled AND user is authenticated
1390
+ * - 'auto': QA tester (if QA enabled) OR authenticated user (if feedback enabled)
1293
1391
  */
1294
1392
  shouldShowWidget(): Promise<boolean>;
1295
1393
  /**
1296
1394
  * Resolve the effective widget mode for the current user.
1297
1395
  * - 'qa' or 'feedback' config → returned as-is
1298
- * - 'auto' → checks if user is a QA tester (role='tester') → 'qa', otherwise 'feedback'
1396
+ * - 'auto' → QA tester with QA enabled 'qa', feedback enabled → 'feedback', else 'qa'
1299
1397
  */
1300
1398
  getEffectiveMode(): Promise<'qa' | 'feedback'>;
1301
1399
  /**
@@ -1543,4 +1641,26 @@ declare function captureError(error: Error, errorInfo?: {
1543
1641
  componentStack?: string;
1544
1642
  };
1545
1643
 
1546
- 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, captureError, contextCapture, createBugBear, generateFingerprint, isBugCategory, isNetworkError, scrubUrl };
1644
+ interface WrapOptions {
1645
+ extractPayload?: EmailPayloadExtractor;
1646
+ }
1647
+ interface EmailInterceptor {
1648
+ wrap<T extends (...args: any[]) => Promise<any>>(sendFn: T, options?: WrapOptions): T;
1649
+ }
1650
+ /**
1651
+ * Create an email interceptor that captures emails sent through a wrapped function.
1652
+ *
1653
+ * Two modes:
1654
+ * - `capture`: emails are sent normally AND captured in BugBear (safe for production)
1655
+ * - `intercept`: emails are NOT sent, only captured in BugBear (for staging/QA)
1656
+ *
1657
+ * @example
1658
+ * ```ts
1659
+ * const interceptor = createEmailInterceptor(bugbearClient, { mode: 'intercept' });
1660
+ * const wrappedSend = interceptor.wrap(originalSendEmail);
1661
+ * await wrappedSend({ to: 'user@example.com', subject: 'Test', html: '<p>Hi</p>' });
1662
+ * ```
1663
+ */
1664
+ declare function createEmailInterceptor(client: BugBearClient, options?: EmailInterceptorOptions): EmailInterceptor;
1665
+
1666
+ 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
@@ -301,8 +301,13 @@ interface MonitoringEvent {
301
301
  /** Sentry breadcrumbs — populated by @bbearai/sentry adapter */
302
302
  sentryBreadcrumbs?: unknown[];
303
303
  }
304
- /** Widget color scheme: 'dark' (default), 'light', or 'auto' (follows system preference). */
304
+ /** Widget color scheme: 'dark', 'light', or 'auto' (follows system preference, default). */
305
305
  type WidgetColorScheme = 'dark' | 'light' | 'auto';
306
+ /** Server-side widget configuration set by the project admin in the dashboard. */
307
+ interface WidgetConfig {
308
+ /** Admin-forced color scheme. When set, overrides the developer's config and user's OS preference. */
309
+ colorScheme?: WidgetColorScheme;
310
+ }
306
311
  interface BugBearTheme {
307
312
  /** Primary brand color */
308
313
  primaryColor?: string;
@@ -312,7 +317,7 @@ interface BugBearTheme {
312
317
  textColor?: string;
313
318
  /** Border radius */
314
319
  borderRadius?: number;
315
- /** Color scheme for the widget. Defaults to 'dark'. */
320
+ /** Color scheme for the widget. Defaults to 'auto' (follows OS preference). */
316
321
  colorScheme?: WidgetColorScheme;
317
322
  }
318
323
  type TestTemplate = 'steps' | 'checklist' | 'rubric' | 'freeform';
@@ -784,6 +789,50 @@ interface TesterIssue {
784
789
  /** Original bug title (for reopened/test_fail issues) */
785
790
  originalBugTitle?: string;
786
791
  }
792
+ /** Delivery status for captured emails */
793
+ type EmailDeliveryStatus = 'pending' | 'sent' | 'delivered' | 'bounced' | 'dropped' | 'deferred';
794
+ /** Email capture mode */
795
+ type EmailCaptureMode = 'capture' | 'intercept';
796
+ /** A captured email payload sent to the BugBear API */
797
+ interface EmailCapturePayload {
798
+ to: string[];
799
+ from?: string;
800
+ subject: string;
801
+ html?: string;
802
+ text?: string;
803
+ templateId?: string;
804
+ captureMode: EmailCaptureMode;
805
+ wasDelivered: boolean;
806
+ metadata?: Record<string, unknown>;
807
+ }
808
+ /** Result of capturing an email */
809
+ interface EmailCaptureResult {
810
+ success: boolean;
811
+ captureId?: string;
812
+ queued?: boolean;
813
+ error?: string;
814
+ }
815
+ /** Options for the email interceptor */
816
+ interface EmailInterceptorOptions {
817
+ /** 'capture' = log + send, 'intercept' = log + block */
818
+ mode: EmailCaptureMode;
819
+ /** Only capture emails sent to these addresses */
820
+ filterTo?: string[];
821
+ /** Replace real recipient addresses with "[redacted]" in captures */
822
+ redactRecipients?: boolean;
823
+ }
824
+ /** Extracted email fields from a send function call */
825
+ interface ExtractedEmailPayload {
826
+ to: string | string[];
827
+ from?: string;
828
+ subject: string;
829
+ html?: string;
830
+ text?: string;
831
+ templateId?: string;
832
+ [key: string]: unknown;
833
+ }
834
+ /** Custom extractor for non-standard email function signatures */
835
+ type EmailPayloadExtractor = (args: unknown[]) => ExtractedEmailPayload;
787
836
 
788
837
  /**
789
838
  * BugBear Offline Queue
@@ -810,7 +859,7 @@ declare class LocalStorageAdapter implements StorageAdapter {
810
859
  setItem(key: string, value: string): Promise<void>;
811
860
  removeItem(key: string): Promise<void>;
812
861
  }
813
- type QueueItemType = 'report' | 'message' | 'feedback';
862
+ type QueueItemType = 'report' | 'message' | 'feedback' | 'email_capture';
814
863
  interface QueueItem {
815
864
  /** Unique ID for this queued operation. */
816
865
  id: string;
@@ -1037,6 +1086,8 @@ declare class BugBearClient {
1037
1086
  private reportSubmitInFlight;
1038
1087
  /** Offline queue — only created when config.offlineQueue.enabled is true. */
1039
1088
  private _queue;
1089
+ /** Session cache storage — defaults to LocalStorageAdapter. */
1090
+ private _sessionStorage;
1040
1091
  /** Active Realtime channel references for cleanup. */
1041
1092
  private realtimeChannels;
1042
1093
  /** Error monitor instance — created when config.monitoring is present. */
@@ -1063,6 +1114,8 @@ declare class BugBearClient {
1063
1114
  private resolveFromApiKey;
1064
1115
  /** Apply resolved credentials and create the Supabase client. */
1065
1116
  private applyResolvedConfig;
1117
+ /** Cache key scoped to the active project. */
1118
+ private get sessionCacheKey();
1066
1119
  /** Initialize offline queue if configured. Shared by both init paths. */
1067
1120
  private initOfflineQueue;
1068
1121
  /** Initialize error monitoring if configured. Shared by both init paths. */
@@ -1075,6 +1128,12 @@ declare class BugBearClient {
1075
1128
  private hashKey;
1076
1129
  /** Ensure the client is initialized before making requests. */
1077
1130
  private ensureReady;
1131
+ /**
1132
+ * Fire-and-forget call to a dashboard notification endpoint.
1133
+ * Only works when apiKey is configured (needed for API auth).
1134
+ * Failures are silently ignored — notifications are best-effort.
1135
+ */
1136
+ private notifyDashboard;
1078
1137
  /**
1079
1138
  * Access the offline queue (if enabled).
1080
1139
  * Use this to check queue.count, subscribe to changes, or trigger flush.
@@ -1086,6 +1145,22 @@ declare class BugBearClient {
1086
1145
  * Web callers can skip this — LocalStorageAdapter is the default.
1087
1146
  */
1088
1147
  initQueue(storage?: StorageAdapter): Promise<void>;
1148
+ /**
1149
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
1150
+ * Must be called before getCachedSession() for persistence across app kills.
1151
+ * Web callers don't need this — LocalStorageAdapter is the default.
1152
+ */
1153
+ setSessionStorage(adapter: StorageAdapter): void;
1154
+ /**
1155
+ * Cache the active QA session locally for instant restore on app restart.
1156
+ * Pass null to clear the cache (e.g. after ending a session).
1157
+ */
1158
+ cacheSession(session: QASession | null): Promise<void>;
1159
+ /**
1160
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
1161
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
1162
+ */
1163
+ getCachedSession(): Promise<QASession | null>;
1089
1164
  private registerQueueHandlers;
1090
1165
  /** Whether realtime is enabled in config. */
1091
1166
  get realtimeEnabled(): boolean;
@@ -1132,6 +1207,11 @@ declare class BugBearClient {
1132
1207
  queued?: boolean;
1133
1208
  error?: string;
1134
1209
  }>;
1210
+ /**
1211
+ * Capture an email for QA testing.
1212
+ * Called by the email interceptor — not typically called directly.
1213
+ */
1214
+ captureEmail(payload: EmailCapturePayload): Promise<EmailCaptureResult>;
1135
1215
  /**
1136
1216
  * Get assigned tests for current user
1137
1217
  * First looks up the tester by email, then fetches their assignments
@@ -1248,6 +1328,14 @@ declare class BugBearClient {
1248
1328
  * reopened issues include original bug context.
1249
1329
  */
1250
1330
  getIssues(category: 'open' | 'done' | 'reopened'): Promise<TesterIssue[]>;
1331
+ /**
1332
+ * Reopen a done issue that the tester believes isn't actually fixed.
1333
+ * Transitions the report from a done status back to 'confirmed'.
1334
+ */
1335
+ reopenReport(reportId: string, reason: string): Promise<{
1336
+ success: boolean;
1337
+ error?: string;
1338
+ }>;
1251
1339
  /**
1252
1340
  * Basic email format validation (defense in depth)
1253
1341
  */
@@ -1284,18 +1372,28 @@ declare class BugBearClient {
1284
1372
  * This is a master switch that admins can toggle in the dashboard
1285
1373
  */
1286
1374
  isQAEnabled(): Promise<boolean>;
1375
+ /**
1376
+ * Fetch server-side widget config set by the project admin.
1377
+ * Returns settings like color scheme override. Returns empty config on error.
1378
+ */
1379
+ getWidgetConfig(): Promise<WidgetConfig>;
1380
+ /**
1381
+ * Check if feedback mode is enabled for this project.
1382
+ * This is a master switch that admins can toggle in the dashboard.
1383
+ */
1384
+ isFeedbackEnabled(): Promise<boolean>;
1287
1385
  /**
1288
1386
  * Check if the widget should be visible.
1289
1387
  * Behavior depends on the configured mode:
1290
1388
  * - 'qa': QA enabled AND user is a registered tester
1291
- * - 'feedback': Any authenticated user
1292
- * - 'auto': Either QA tester OR authenticated non-tester
1389
+ * - 'feedback': Feedback enabled AND user is authenticated
1390
+ * - 'auto': QA tester (if QA enabled) OR authenticated user (if feedback enabled)
1293
1391
  */
1294
1392
  shouldShowWidget(): Promise<boolean>;
1295
1393
  /**
1296
1394
  * Resolve the effective widget mode for the current user.
1297
1395
  * - 'qa' or 'feedback' config → returned as-is
1298
- * - 'auto' → checks if user is a QA tester (role='tester') → 'qa', otherwise 'feedback'
1396
+ * - 'auto' → QA tester with QA enabled 'qa', feedback enabled → 'feedback', else 'qa'
1299
1397
  */
1300
1398
  getEffectiveMode(): Promise<'qa' | 'feedback'>;
1301
1399
  /**
@@ -1543,4 +1641,26 @@ declare function captureError(error: Error, errorInfo?: {
1543
1641
  componentStack?: string;
1544
1642
  };
1545
1643
 
1546
- 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, captureError, contextCapture, createBugBear, generateFingerprint, isBugCategory, isNetworkError, scrubUrl };
1644
+ interface WrapOptions {
1645
+ extractPayload?: EmailPayloadExtractor;
1646
+ }
1647
+ interface EmailInterceptor {
1648
+ wrap<T extends (...args: any[]) => Promise<any>>(sendFn: T, options?: WrapOptions): T;
1649
+ }
1650
+ /**
1651
+ * Create an email interceptor that captures emails sent through a wrapped function.
1652
+ *
1653
+ * Two modes:
1654
+ * - `capture`: emails are sent normally AND captured in BugBear (safe for production)
1655
+ * - `intercept`: emails are NOT sent, only captured in BugBear (for staging/QA)
1656
+ *
1657
+ * @example
1658
+ * ```ts
1659
+ * const interceptor = createEmailInterceptor(bugbearClient, { mode: 'intercept' });
1660
+ * const wrappedSend = interceptor.wrap(originalSendEmail);
1661
+ * await wrappedSend({ to: 'user@example.com', subject: 'Test', html: '<p>Hi</p>' });
1662
+ * ```
1663
+ */
1664
+ declare function createEmailInterceptor(client: BugBearClient, options?: EmailInterceptorOptions): EmailInterceptor;
1665
+
1666
+ 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 = [];
@@ -984,6 +987,10 @@ var BugBearClient = class {
984
987
  this.initOfflineQueue();
985
988
  this.initMonitoring();
986
989
  }
990
+ /** Cache key scoped to the active project. */
991
+ get sessionCacheKey() {
992
+ return `bugbear_session_${this.config.projectId ?? "unknown"}`;
993
+ }
987
994
  /** Initialize offline queue if configured. Shared by both init paths. */
988
995
  initOfflineQueue() {
989
996
  if (this.config.offlineQueue?.enabled) {
@@ -1047,6 +1054,26 @@ var BugBearClient = class {
1047
1054
  await this.pendingInit;
1048
1055
  if (this.initError) throw this.initError;
1049
1056
  }
1057
+ /**
1058
+ * Fire-and-forget call to a dashboard notification endpoint.
1059
+ * Only works when apiKey is configured (needed for API auth).
1060
+ * Failures are silently ignored — notifications are best-effort.
1061
+ */
1062
+ async notifyDashboard(path, body) {
1063
+ if (!this.config.apiKey) return;
1064
+ try {
1065
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
1066
+ await fetch(`${baseUrl}/api/v1/notifications/${path}`, {
1067
+ method: "POST",
1068
+ headers: {
1069
+ "Content-Type": "application/json",
1070
+ "Authorization": `Bearer ${this.config.apiKey}`
1071
+ },
1072
+ body: JSON.stringify(body)
1073
+ });
1074
+ } catch {
1075
+ }
1076
+ }
1050
1077
  // ── Offline Queue ─────────────────────────────────────────
1051
1078
  /**
1052
1079
  * Access the offline queue (if enabled).
@@ -1073,6 +1100,48 @@ var BugBearClient = class {
1073
1100
  }
1074
1101
  await this._queue.load();
1075
1102
  }
1103
+ // ── Session Cache ──────────────────────────────────────────
1104
+ /**
1105
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
1106
+ * Must be called before getCachedSession() for persistence across app kills.
1107
+ * Web callers don't need this — LocalStorageAdapter is the default.
1108
+ */
1109
+ setSessionStorage(adapter) {
1110
+ this._sessionStorage = adapter;
1111
+ }
1112
+ /**
1113
+ * Cache the active QA session locally for instant restore on app restart.
1114
+ * Pass null to clear the cache (e.g. after ending a session).
1115
+ */
1116
+ async cacheSession(session) {
1117
+ try {
1118
+ if (session) {
1119
+ await this._sessionStorage.setItem(this.sessionCacheKey, JSON.stringify(session));
1120
+ } else {
1121
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
1122
+ }
1123
+ } catch {
1124
+ }
1125
+ }
1126
+ /**
1127
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
1128
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
1129
+ */
1130
+ async getCachedSession() {
1131
+ try {
1132
+ const raw = await this._sessionStorage.getItem(this.sessionCacheKey);
1133
+ if (!raw) return null;
1134
+ const session = JSON.parse(raw);
1135
+ const age = Date.now() - new Date(session.startedAt).getTime();
1136
+ if (age > 24 * 60 * 60 * 1e3) {
1137
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
1138
+ return null;
1139
+ }
1140
+ return session;
1141
+ } catch {
1142
+ return null;
1143
+ }
1144
+ }
1076
1145
  registerQueueHandlers() {
1077
1146
  if (!this._queue) return;
1078
1147
  this._queue.registerHandler("report", async (payload) => {
@@ -1090,6 +1159,11 @@ var BugBearClient = class {
1090
1159
  if (error) return { success: false, error: error.message };
1091
1160
  return { success: true };
1092
1161
  });
1162
+ this._queue.registerHandler("email_capture", async (payload) => {
1163
+ const { error } = await this.supabase.from("email_captures").insert(payload).select("id").single();
1164
+ if (error) return { success: false, error: error.message };
1165
+ return { success: true };
1166
+ });
1093
1167
  }
1094
1168
  // ── Realtime Subscriptions ─────────────────────────────────
1095
1169
  /** Whether realtime is enabled in config. */
@@ -1277,6 +1351,8 @@ var BugBearClient = class {
1277
1351
  if (this.config.onReportSubmitted) {
1278
1352
  this.config.onReportSubmitted(report);
1279
1353
  }
1354
+ this.notifyDashboard("report", { reportId: data.id }).catch(() => {
1355
+ });
1280
1356
  return { success: true, reportId: data.id };
1281
1357
  } catch (err) {
1282
1358
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -1289,6 +1365,44 @@ var BugBearClient = class {
1289
1365
  this.reportSubmitInFlight = false;
1290
1366
  }
1291
1367
  }
1368
+ /**
1369
+ * Capture an email for QA testing.
1370
+ * Called by the email interceptor — not typically called directly.
1371
+ */
1372
+ async captureEmail(payload) {
1373
+ try {
1374
+ await this.ready();
1375
+ if (!payload.subject || !payload.to || payload.to.length === 0) {
1376
+ return { success: false, error: "subject and to are required" };
1377
+ }
1378
+ const record = {
1379
+ project_id: this.config.projectId,
1380
+ to_addresses: payload.to,
1381
+ from_address: payload.from || null,
1382
+ subject: payload.subject,
1383
+ html_content: payload.html || null,
1384
+ text_content: payload.text || null,
1385
+ template_id: payload.templateId || null,
1386
+ metadata: payload.metadata || {},
1387
+ capture_mode: payload.captureMode,
1388
+ was_delivered: payload.wasDelivered,
1389
+ delivery_status: payload.wasDelivered ? "sent" : "pending"
1390
+ };
1391
+ const { data, error } = await this.supabase.from("email_captures").insert(record).select("id").single();
1392
+ if (error) {
1393
+ if (this._queue && isNetworkError(error.message)) {
1394
+ await this._queue.enqueue("email_capture", record);
1395
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
1396
+ }
1397
+ console.error("BugBear: Failed to capture email", error.message);
1398
+ return { success: false, error: error.message };
1399
+ }
1400
+ return { success: true, captureId: data.id };
1401
+ } catch (err) {
1402
+ const message = err instanceof Error ? err.message : "Unknown error";
1403
+ return { success: false, error: message };
1404
+ }
1405
+ }
1292
1406
  /**
1293
1407
  * Get assigned tests for current user
1294
1408
  * First looks up the tester by email, then fetches their assignments
@@ -1910,6 +2024,32 @@ var BugBearClient = class {
1910
2024
  return [];
1911
2025
  }
1912
2026
  }
2027
+ /**
2028
+ * Reopen a done issue that the tester believes isn't actually fixed.
2029
+ * Transitions the report from a done status back to 'confirmed'.
2030
+ */
2031
+ async reopenReport(reportId, reason) {
2032
+ try {
2033
+ const testerInfo = await this.getTesterInfo();
2034
+ if (!testerInfo) return { success: false, error: "Not authenticated as tester" };
2035
+ const { data, error } = await this.supabase.rpc("reopen_report", {
2036
+ p_report_id: reportId,
2037
+ p_tester_id: testerInfo.id,
2038
+ p_reason: reason
2039
+ });
2040
+ if (error) {
2041
+ console.error("BugBear: Failed to reopen report", formatPgError(error));
2042
+ return { success: false, error: error.message };
2043
+ }
2044
+ if (!data?.success) {
2045
+ return { success: false, error: data?.error || "Failed to reopen report" };
2046
+ }
2047
+ return { success: true };
2048
+ } catch (err) {
2049
+ console.error("BugBear: Error reopening report", err);
2050
+ return { success: false, error: "Unexpected error" };
2051
+ }
2052
+ }
1913
2053
  /**
1914
2054
  * Basic email format validation (defense in depth)
1915
2055
  */
@@ -2102,12 +2242,56 @@ var BugBearClient = class {
2102
2242
  return true;
2103
2243
  }
2104
2244
  }
2245
+ /**
2246
+ * Fetch server-side widget config set by the project admin.
2247
+ * Returns settings like color scheme override. Returns empty config on error.
2248
+ */
2249
+ async getWidgetConfig() {
2250
+ try {
2251
+ await this.ensureReady();
2252
+ const { data, error } = await this.supabase.rpc("get_widget_config", {
2253
+ p_project_id: this.config.projectId
2254
+ });
2255
+ if (error) {
2256
+ console.warn("BugBear: Could not fetch widget config", error.message || error.code || "Unknown error");
2257
+ return {};
2258
+ }
2259
+ return data ?? {};
2260
+ } catch (err) {
2261
+ const message = err instanceof Error ? err.message : "Unknown error fetching widget config";
2262
+ console.error("BugBear: Error fetching widget config", err);
2263
+ this.config.onError?.(err instanceof Error ? err : new Error(message), { projectId: this.config.projectId });
2264
+ return {};
2265
+ }
2266
+ }
2267
+ /**
2268
+ * Check if feedback mode is enabled for this project.
2269
+ * This is a master switch that admins can toggle in the dashboard.
2270
+ */
2271
+ async isFeedbackEnabled() {
2272
+ try {
2273
+ await this.ensureReady();
2274
+ const { data, error } = await this.supabase.rpc("check_feedback_enabled", {
2275
+ p_project_id: this.config.projectId
2276
+ });
2277
+ if (error) {
2278
+ console.warn("BugBear: Could not check feedback status", error.message || error.code || "Unknown error");
2279
+ return false;
2280
+ }
2281
+ return data ?? false;
2282
+ } catch (err) {
2283
+ const message = err instanceof Error ? err.message : "Unknown error checking feedback status";
2284
+ console.error("BugBear: Error checking feedback status", err);
2285
+ this.config.onError?.(err instanceof Error ? err : new Error(message), { projectId: this.config.projectId });
2286
+ return false;
2287
+ }
2288
+ }
2105
2289
  /**
2106
2290
  * Check if the widget should be visible.
2107
2291
  * Behavior depends on the configured mode:
2108
2292
  * - 'qa': QA enabled AND user is a registered tester
2109
- * - 'feedback': Any authenticated user
2110
- * - 'auto': Either QA tester OR authenticated non-tester
2293
+ * - 'feedback': Feedback enabled AND user is authenticated
2294
+ * - 'auto': QA tester (if QA enabled) OR authenticated user (if feedback enabled)
2111
2295
  */
2112
2296
  async shouldShowWidget() {
2113
2297
  const mode = this.config.mode || "qa";
@@ -2119,33 +2303,39 @@ var BugBearClient = class {
2119
2303
  return qaEnabled2 && tester;
2120
2304
  }
2121
2305
  if (mode === "feedback") {
2122
- const userInfo2 = await this.getCurrentUserInfo();
2123
- return userInfo2 !== null;
2306
+ const [feedbackEnabled2, userInfo2] = await Promise.all([
2307
+ this.isFeedbackEnabled(),
2308
+ this.getCurrentUserInfo()
2309
+ ]);
2310
+ return feedbackEnabled2 && userInfo2 !== null;
2124
2311
  }
2125
- const [qaEnabled, testerInfo, userInfo] = await Promise.all([
2312
+ const [qaEnabled, feedbackEnabled, testerInfo, userInfo] = await Promise.all([
2126
2313
  this.isQAEnabled(),
2314
+ this.isFeedbackEnabled(),
2127
2315
  this.getTesterInfo(),
2128
2316
  this.getCurrentUserInfo()
2129
2317
  ]);
2130
2318
  if (qaEnabled && testerInfo && testerInfo.role !== "feedback") return true;
2131
- if (userInfo) return true;
2319
+ if (feedbackEnabled && userInfo) return true;
2132
2320
  return false;
2133
2321
  }
2134
2322
  /**
2135
2323
  * Resolve the effective widget mode for the current user.
2136
2324
  * - 'qa' or 'feedback' config → returned as-is
2137
- * - 'auto' → checks if user is a QA tester (role='tester') → 'qa', otherwise 'feedback'
2325
+ * - 'auto' → QA tester with QA enabled 'qa', feedback enabled → 'feedback', else 'qa'
2138
2326
  */
2139
2327
  async getEffectiveMode() {
2140
2328
  const mode = this.config.mode || "qa";
2141
2329
  if (mode === "qa") return "qa";
2142
2330
  if (mode === "feedback") return "feedback";
2143
- const [qaEnabled, testerInfo] = await Promise.all([
2331
+ const [qaEnabled, feedbackEnabled, testerInfo] = await Promise.all([
2144
2332
  this.isQAEnabled(),
2333
+ this.isFeedbackEnabled(),
2145
2334
  this.getTesterInfo()
2146
2335
  ]);
2147
2336
  if (qaEnabled && testerInfo && testerInfo.role !== "feedback") return "qa";
2148
- return "feedback";
2337
+ if (feedbackEnabled) return "feedback";
2338
+ return "qa";
2149
2339
  }
2150
2340
  /**
2151
2341
  * Auto-provision a feedback user record in the testers table.
@@ -2446,7 +2636,7 @@ var BugBearClient = class {
2446
2636
  insertData.attachments = safeAttachments;
2447
2637
  }
2448
2638
  }
2449
- const { error } = await this.supabase.from("discussion_messages").insert(insertData);
2639
+ const { data: msgData, error } = await this.supabase.from("discussion_messages").insert(insertData).select("id").single();
2450
2640
  if (error) {
2451
2641
  if (this._queue && isNetworkError(error.message)) {
2452
2642
  await this._queue.enqueue("message", insertData);
@@ -2455,6 +2645,10 @@ var BugBearClient = class {
2455
2645
  console.error("BugBear: Failed to send message", formatPgError(error));
2456
2646
  return false;
2457
2647
  }
2648
+ if (msgData?.id) {
2649
+ this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
2650
+ });
2651
+ }
2458
2652
  await this.markThreadAsRead(threadId);
2459
2653
  return true;
2460
2654
  } catch (err) {
@@ -2580,6 +2774,7 @@ var BugBearClient = class {
2580
2774
  if (!session) {
2581
2775
  return { success: false, error: "Session created but could not be fetched" };
2582
2776
  }
2777
+ await this.cacheSession(session);
2583
2778
  return { success: true, session };
2584
2779
  } catch (err) {
2585
2780
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -2603,6 +2798,7 @@ var BugBearClient = class {
2603
2798
  return { success: false, error: error.message };
2604
2799
  }
2605
2800
  const session = this.transformSession(data);
2801
+ await this.cacheSession(null);
2606
2802
  return { success: true, session };
2607
2803
  } catch (err) {
2608
2804
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -2618,8 +2814,13 @@ var BugBearClient = class {
2618
2814
  const testerInfo = await this.getTesterInfo();
2619
2815
  if (!testerInfo) return null;
2620
2816
  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();
2621
- if (error || !data) return null;
2622
- return this.transformSession(data);
2817
+ if (error || !data) {
2818
+ await this.cacheSession(null);
2819
+ return null;
2820
+ }
2821
+ const session = this.transformSession(data);
2822
+ await this.cacheSession(session);
2823
+ return session;
2623
2824
  } catch (err) {
2624
2825
  console.error("BugBear: Error fetching active session", err);
2625
2826
  return null;
@@ -2801,6 +3002,96 @@ var BugBearClient = class {
2801
3002
  function createBugBear(config) {
2802
3003
  return new BugBearClient(config);
2803
3004
  }
3005
+
3006
+ // src/email-interceptor.ts
3007
+ function defaultExtract(args) {
3008
+ const first = args[0];
3009
+ if (!first || typeof first !== "object") {
3010
+ throw new Error("BugBear email interceptor: first argument must be an object with { to, subject }");
3011
+ }
3012
+ const obj = first;
3013
+ return {
3014
+ to: obj.to,
3015
+ from: obj.from,
3016
+ subject: obj.subject,
3017
+ html: obj.html,
3018
+ text: obj.text,
3019
+ templateId: obj.templateId
3020
+ };
3021
+ }
3022
+ function normalizeToArray(to) {
3023
+ if (Array.isArray(to)) return to;
3024
+ return [to];
3025
+ }
3026
+ function createEmailInterceptor(client, options = { mode: "capture" }) {
3027
+ const { mode, filterTo, redactRecipients } = options;
3028
+ return {
3029
+ wrap(sendFn, wrapOpts) {
3030
+ const extract = wrapOpts?.extractPayload || defaultExtract;
3031
+ const wrapped = async (...args) => {
3032
+ let extracted;
3033
+ try {
3034
+ extracted = extract(args);
3035
+ } catch {
3036
+ return sendFn(...args);
3037
+ }
3038
+ const toArray = normalizeToArray(extracted.to);
3039
+ if (filterTo && filterTo.length > 0) {
3040
+ const matches = toArray.some(
3041
+ (addr) => filterTo.includes(addr.toLowerCase())
3042
+ );
3043
+ if (!matches) {
3044
+ return sendFn(...args);
3045
+ }
3046
+ }
3047
+ const capturedTo = redactRecipients ? toArray.map(() => "[redacted]") : toArray;
3048
+ if (mode === "intercept") {
3049
+ client.captureEmail({
3050
+ to: capturedTo,
3051
+ from: extracted.from,
3052
+ subject: extracted.subject,
3053
+ html: extracted.html,
3054
+ text: extracted.text,
3055
+ templateId: extracted.templateId,
3056
+ captureMode: "intercept",
3057
+ wasDelivered: false
3058
+ }).catch(() => {
3059
+ });
3060
+ return { success: true, intercepted: true };
3061
+ }
3062
+ try {
3063
+ const result = await sendFn(...args);
3064
+ client.captureEmail({
3065
+ to: capturedTo,
3066
+ from: extracted.from,
3067
+ subject: extracted.subject,
3068
+ html: extracted.html,
3069
+ text: extracted.text,
3070
+ templateId: extracted.templateId,
3071
+ captureMode: "capture",
3072
+ wasDelivered: true
3073
+ }).catch(() => {
3074
+ });
3075
+ return result;
3076
+ } catch (err) {
3077
+ client.captureEmail({
3078
+ to: capturedTo,
3079
+ from: extracted.from,
3080
+ subject: extracted.subject,
3081
+ html: extracted.html,
3082
+ text: extracted.text,
3083
+ templateId: extracted.templateId,
3084
+ captureMode: "capture",
3085
+ wasDelivered: false
3086
+ }).catch(() => {
3087
+ });
3088
+ throw err;
3089
+ }
3090
+ };
3091
+ return wrapped;
3092
+ }
3093
+ };
3094
+ }
2804
3095
  // Annotate the CommonJS export names for ESM import in node:
2805
3096
  0 && (module.exports = {
2806
3097
  BUG_CATEGORIES,
@@ -2819,6 +3110,7 @@ function createBugBear(config) {
2819
3110
  captureError,
2820
3111
  contextCapture,
2821
3112
  createBugBear,
3113
+ createEmailInterceptor,
2822
3114
  generateFingerprint,
2823
3115
  isBugCategory,
2824
3116
  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 = [];
@@ -939,6 +941,10 @@ var BugBearClient = class {
939
941
  this.initOfflineQueue();
940
942
  this.initMonitoring();
941
943
  }
944
+ /** Cache key scoped to the active project. */
945
+ get sessionCacheKey() {
946
+ return `bugbear_session_${this.config.projectId ?? "unknown"}`;
947
+ }
942
948
  /** Initialize offline queue if configured. Shared by both init paths. */
943
949
  initOfflineQueue() {
944
950
  if (this.config.offlineQueue?.enabled) {
@@ -1002,6 +1008,26 @@ var BugBearClient = class {
1002
1008
  await this.pendingInit;
1003
1009
  if (this.initError) throw this.initError;
1004
1010
  }
1011
+ /**
1012
+ * Fire-and-forget call to a dashboard notification endpoint.
1013
+ * Only works when apiKey is configured (needed for API auth).
1014
+ * Failures are silently ignored — notifications are best-effort.
1015
+ */
1016
+ async notifyDashboard(path, body) {
1017
+ if (!this.config.apiKey) return;
1018
+ try {
1019
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
1020
+ await fetch(`${baseUrl}/api/v1/notifications/${path}`, {
1021
+ method: "POST",
1022
+ headers: {
1023
+ "Content-Type": "application/json",
1024
+ "Authorization": `Bearer ${this.config.apiKey}`
1025
+ },
1026
+ body: JSON.stringify(body)
1027
+ });
1028
+ } catch {
1029
+ }
1030
+ }
1005
1031
  // ── Offline Queue ─────────────────────────────────────────
1006
1032
  /**
1007
1033
  * Access the offline queue (if enabled).
@@ -1028,6 +1054,48 @@ var BugBearClient = class {
1028
1054
  }
1029
1055
  await this._queue.load();
1030
1056
  }
1057
+ // ── Session Cache ──────────────────────────────────────────
1058
+ /**
1059
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
1060
+ * Must be called before getCachedSession() for persistence across app kills.
1061
+ * Web callers don't need this — LocalStorageAdapter is the default.
1062
+ */
1063
+ setSessionStorage(adapter) {
1064
+ this._sessionStorage = adapter;
1065
+ }
1066
+ /**
1067
+ * Cache the active QA session locally for instant restore on app restart.
1068
+ * Pass null to clear the cache (e.g. after ending a session).
1069
+ */
1070
+ async cacheSession(session) {
1071
+ try {
1072
+ if (session) {
1073
+ await this._sessionStorage.setItem(this.sessionCacheKey, JSON.stringify(session));
1074
+ } else {
1075
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
1076
+ }
1077
+ } catch {
1078
+ }
1079
+ }
1080
+ /**
1081
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
1082
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
1083
+ */
1084
+ async getCachedSession() {
1085
+ try {
1086
+ const raw = await this._sessionStorage.getItem(this.sessionCacheKey);
1087
+ if (!raw) return null;
1088
+ const session = JSON.parse(raw);
1089
+ const age = Date.now() - new Date(session.startedAt).getTime();
1090
+ if (age > 24 * 60 * 60 * 1e3) {
1091
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
1092
+ return null;
1093
+ }
1094
+ return session;
1095
+ } catch {
1096
+ return null;
1097
+ }
1098
+ }
1031
1099
  registerQueueHandlers() {
1032
1100
  if (!this._queue) return;
1033
1101
  this._queue.registerHandler("report", async (payload) => {
@@ -1045,6 +1113,11 @@ var BugBearClient = class {
1045
1113
  if (error) return { success: false, error: error.message };
1046
1114
  return { success: true };
1047
1115
  });
1116
+ this._queue.registerHandler("email_capture", async (payload) => {
1117
+ const { error } = await this.supabase.from("email_captures").insert(payload).select("id").single();
1118
+ if (error) return { success: false, error: error.message };
1119
+ return { success: true };
1120
+ });
1048
1121
  }
1049
1122
  // ── Realtime Subscriptions ─────────────────────────────────
1050
1123
  /** Whether realtime is enabled in config. */
@@ -1232,6 +1305,8 @@ var BugBearClient = class {
1232
1305
  if (this.config.onReportSubmitted) {
1233
1306
  this.config.onReportSubmitted(report);
1234
1307
  }
1308
+ this.notifyDashboard("report", { reportId: data.id }).catch(() => {
1309
+ });
1235
1310
  return { success: true, reportId: data.id };
1236
1311
  } catch (err) {
1237
1312
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -1244,6 +1319,44 @@ var BugBearClient = class {
1244
1319
  this.reportSubmitInFlight = false;
1245
1320
  }
1246
1321
  }
1322
+ /**
1323
+ * Capture an email for QA testing.
1324
+ * Called by the email interceptor — not typically called directly.
1325
+ */
1326
+ async captureEmail(payload) {
1327
+ try {
1328
+ await this.ready();
1329
+ if (!payload.subject || !payload.to || payload.to.length === 0) {
1330
+ return { success: false, error: "subject and to are required" };
1331
+ }
1332
+ const record = {
1333
+ project_id: this.config.projectId,
1334
+ to_addresses: payload.to,
1335
+ from_address: payload.from || null,
1336
+ subject: payload.subject,
1337
+ html_content: payload.html || null,
1338
+ text_content: payload.text || null,
1339
+ template_id: payload.templateId || null,
1340
+ metadata: payload.metadata || {},
1341
+ capture_mode: payload.captureMode,
1342
+ was_delivered: payload.wasDelivered,
1343
+ delivery_status: payload.wasDelivered ? "sent" : "pending"
1344
+ };
1345
+ const { data, error } = await this.supabase.from("email_captures").insert(record).select("id").single();
1346
+ if (error) {
1347
+ if (this._queue && isNetworkError(error.message)) {
1348
+ await this._queue.enqueue("email_capture", record);
1349
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
1350
+ }
1351
+ console.error("BugBear: Failed to capture email", error.message);
1352
+ return { success: false, error: error.message };
1353
+ }
1354
+ return { success: true, captureId: data.id };
1355
+ } catch (err) {
1356
+ const message = err instanceof Error ? err.message : "Unknown error";
1357
+ return { success: false, error: message };
1358
+ }
1359
+ }
1247
1360
  /**
1248
1361
  * Get assigned tests for current user
1249
1362
  * First looks up the tester by email, then fetches their assignments
@@ -1865,6 +1978,32 @@ var BugBearClient = class {
1865
1978
  return [];
1866
1979
  }
1867
1980
  }
1981
+ /**
1982
+ * Reopen a done issue that the tester believes isn't actually fixed.
1983
+ * Transitions the report from a done status back to 'confirmed'.
1984
+ */
1985
+ async reopenReport(reportId, reason) {
1986
+ try {
1987
+ const testerInfo = await this.getTesterInfo();
1988
+ if (!testerInfo) return { success: false, error: "Not authenticated as tester" };
1989
+ const { data, error } = await this.supabase.rpc("reopen_report", {
1990
+ p_report_id: reportId,
1991
+ p_tester_id: testerInfo.id,
1992
+ p_reason: reason
1993
+ });
1994
+ if (error) {
1995
+ console.error("BugBear: Failed to reopen report", formatPgError(error));
1996
+ return { success: false, error: error.message };
1997
+ }
1998
+ if (!data?.success) {
1999
+ return { success: false, error: data?.error || "Failed to reopen report" };
2000
+ }
2001
+ return { success: true };
2002
+ } catch (err) {
2003
+ console.error("BugBear: Error reopening report", err);
2004
+ return { success: false, error: "Unexpected error" };
2005
+ }
2006
+ }
1868
2007
  /**
1869
2008
  * Basic email format validation (defense in depth)
1870
2009
  */
@@ -2057,12 +2196,56 @@ var BugBearClient = class {
2057
2196
  return true;
2058
2197
  }
2059
2198
  }
2199
+ /**
2200
+ * Fetch server-side widget config set by the project admin.
2201
+ * Returns settings like color scheme override. Returns empty config on error.
2202
+ */
2203
+ async getWidgetConfig() {
2204
+ try {
2205
+ await this.ensureReady();
2206
+ const { data, error } = await this.supabase.rpc("get_widget_config", {
2207
+ p_project_id: this.config.projectId
2208
+ });
2209
+ if (error) {
2210
+ console.warn("BugBear: Could not fetch widget config", error.message || error.code || "Unknown error");
2211
+ return {};
2212
+ }
2213
+ return data ?? {};
2214
+ } catch (err) {
2215
+ const message = err instanceof Error ? err.message : "Unknown error fetching widget config";
2216
+ console.error("BugBear: Error fetching widget config", err);
2217
+ this.config.onError?.(err instanceof Error ? err : new Error(message), { projectId: this.config.projectId });
2218
+ return {};
2219
+ }
2220
+ }
2221
+ /**
2222
+ * Check if feedback mode is enabled for this project.
2223
+ * This is a master switch that admins can toggle in the dashboard.
2224
+ */
2225
+ async isFeedbackEnabled() {
2226
+ try {
2227
+ await this.ensureReady();
2228
+ const { data, error } = await this.supabase.rpc("check_feedback_enabled", {
2229
+ p_project_id: this.config.projectId
2230
+ });
2231
+ if (error) {
2232
+ console.warn("BugBear: Could not check feedback status", error.message || error.code || "Unknown error");
2233
+ return false;
2234
+ }
2235
+ return data ?? false;
2236
+ } catch (err) {
2237
+ const message = err instanceof Error ? err.message : "Unknown error checking feedback status";
2238
+ console.error("BugBear: Error checking feedback status", err);
2239
+ this.config.onError?.(err instanceof Error ? err : new Error(message), { projectId: this.config.projectId });
2240
+ return false;
2241
+ }
2242
+ }
2060
2243
  /**
2061
2244
  * Check if the widget should be visible.
2062
2245
  * Behavior depends on the configured mode:
2063
2246
  * - 'qa': QA enabled AND user is a registered tester
2064
- * - 'feedback': Any authenticated user
2065
- * - 'auto': Either QA tester OR authenticated non-tester
2247
+ * - 'feedback': Feedback enabled AND user is authenticated
2248
+ * - 'auto': QA tester (if QA enabled) OR authenticated user (if feedback enabled)
2066
2249
  */
2067
2250
  async shouldShowWidget() {
2068
2251
  const mode = this.config.mode || "qa";
@@ -2074,33 +2257,39 @@ var BugBearClient = class {
2074
2257
  return qaEnabled2 && tester;
2075
2258
  }
2076
2259
  if (mode === "feedback") {
2077
- const userInfo2 = await this.getCurrentUserInfo();
2078
- return userInfo2 !== null;
2260
+ const [feedbackEnabled2, userInfo2] = await Promise.all([
2261
+ this.isFeedbackEnabled(),
2262
+ this.getCurrentUserInfo()
2263
+ ]);
2264
+ return feedbackEnabled2 && userInfo2 !== null;
2079
2265
  }
2080
- const [qaEnabled, testerInfo, userInfo] = await Promise.all([
2266
+ const [qaEnabled, feedbackEnabled, testerInfo, userInfo] = await Promise.all([
2081
2267
  this.isQAEnabled(),
2268
+ this.isFeedbackEnabled(),
2082
2269
  this.getTesterInfo(),
2083
2270
  this.getCurrentUserInfo()
2084
2271
  ]);
2085
2272
  if (qaEnabled && testerInfo && testerInfo.role !== "feedback") return true;
2086
- if (userInfo) return true;
2273
+ if (feedbackEnabled && userInfo) return true;
2087
2274
  return false;
2088
2275
  }
2089
2276
  /**
2090
2277
  * Resolve the effective widget mode for the current user.
2091
2278
  * - 'qa' or 'feedback' config → returned as-is
2092
- * - 'auto' → checks if user is a QA tester (role='tester') → 'qa', otherwise 'feedback'
2279
+ * - 'auto' → QA tester with QA enabled 'qa', feedback enabled → 'feedback', else 'qa'
2093
2280
  */
2094
2281
  async getEffectiveMode() {
2095
2282
  const mode = this.config.mode || "qa";
2096
2283
  if (mode === "qa") return "qa";
2097
2284
  if (mode === "feedback") return "feedback";
2098
- const [qaEnabled, testerInfo] = await Promise.all([
2285
+ const [qaEnabled, feedbackEnabled, testerInfo] = await Promise.all([
2099
2286
  this.isQAEnabled(),
2287
+ this.isFeedbackEnabled(),
2100
2288
  this.getTesterInfo()
2101
2289
  ]);
2102
2290
  if (qaEnabled && testerInfo && testerInfo.role !== "feedback") return "qa";
2103
- return "feedback";
2291
+ if (feedbackEnabled) return "feedback";
2292
+ return "qa";
2104
2293
  }
2105
2294
  /**
2106
2295
  * Auto-provision a feedback user record in the testers table.
@@ -2401,7 +2590,7 @@ var BugBearClient = class {
2401
2590
  insertData.attachments = safeAttachments;
2402
2591
  }
2403
2592
  }
2404
- const { error } = await this.supabase.from("discussion_messages").insert(insertData);
2593
+ const { data: msgData, error } = await this.supabase.from("discussion_messages").insert(insertData).select("id").single();
2405
2594
  if (error) {
2406
2595
  if (this._queue && isNetworkError(error.message)) {
2407
2596
  await this._queue.enqueue("message", insertData);
@@ -2410,6 +2599,10 @@ var BugBearClient = class {
2410
2599
  console.error("BugBear: Failed to send message", formatPgError(error));
2411
2600
  return false;
2412
2601
  }
2602
+ if (msgData?.id) {
2603
+ this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
2604
+ });
2605
+ }
2413
2606
  await this.markThreadAsRead(threadId);
2414
2607
  return true;
2415
2608
  } catch (err) {
@@ -2535,6 +2728,7 @@ var BugBearClient = class {
2535
2728
  if (!session) {
2536
2729
  return { success: false, error: "Session created but could not be fetched" };
2537
2730
  }
2731
+ await this.cacheSession(session);
2538
2732
  return { success: true, session };
2539
2733
  } catch (err) {
2540
2734
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -2558,6 +2752,7 @@ var BugBearClient = class {
2558
2752
  return { success: false, error: error.message };
2559
2753
  }
2560
2754
  const session = this.transformSession(data);
2755
+ await this.cacheSession(null);
2561
2756
  return { success: true, session };
2562
2757
  } catch (err) {
2563
2758
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -2573,8 +2768,13 @@ var BugBearClient = class {
2573
2768
  const testerInfo = await this.getTesterInfo();
2574
2769
  if (!testerInfo) return null;
2575
2770
  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();
2576
- if (error || !data) return null;
2577
- return this.transformSession(data);
2771
+ if (error || !data) {
2772
+ await this.cacheSession(null);
2773
+ return null;
2774
+ }
2775
+ const session = this.transformSession(data);
2776
+ await this.cacheSession(session);
2777
+ return session;
2578
2778
  } catch (err) {
2579
2779
  console.error("BugBear: Error fetching active session", err);
2580
2780
  return null;
@@ -2756,6 +2956,96 @@ var BugBearClient = class {
2756
2956
  function createBugBear(config) {
2757
2957
  return new BugBearClient(config);
2758
2958
  }
2959
+
2960
+ // src/email-interceptor.ts
2961
+ function defaultExtract(args) {
2962
+ const first = args[0];
2963
+ if (!first || typeof first !== "object") {
2964
+ throw new Error("BugBear email interceptor: first argument must be an object with { to, subject }");
2965
+ }
2966
+ const obj = first;
2967
+ return {
2968
+ to: obj.to,
2969
+ from: obj.from,
2970
+ subject: obj.subject,
2971
+ html: obj.html,
2972
+ text: obj.text,
2973
+ templateId: obj.templateId
2974
+ };
2975
+ }
2976
+ function normalizeToArray(to) {
2977
+ if (Array.isArray(to)) return to;
2978
+ return [to];
2979
+ }
2980
+ function createEmailInterceptor(client, options = { mode: "capture" }) {
2981
+ const { mode, filterTo, redactRecipients } = options;
2982
+ return {
2983
+ wrap(sendFn, wrapOpts) {
2984
+ const extract = wrapOpts?.extractPayload || defaultExtract;
2985
+ const wrapped = async (...args) => {
2986
+ let extracted;
2987
+ try {
2988
+ extracted = extract(args);
2989
+ } catch {
2990
+ return sendFn(...args);
2991
+ }
2992
+ const toArray = normalizeToArray(extracted.to);
2993
+ if (filterTo && filterTo.length > 0) {
2994
+ const matches = toArray.some(
2995
+ (addr) => filterTo.includes(addr.toLowerCase())
2996
+ );
2997
+ if (!matches) {
2998
+ return sendFn(...args);
2999
+ }
3000
+ }
3001
+ const capturedTo = redactRecipients ? toArray.map(() => "[redacted]") : toArray;
3002
+ if (mode === "intercept") {
3003
+ client.captureEmail({
3004
+ to: capturedTo,
3005
+ from: extracted.from,
3006
+ subject: extracted.subject,
3007
+ html: extracted.html,
3008
+ text: extracted.text,
3009
+ templateId: extracted.templateId,
3010
+ captureMode: "intercept",
3011
+ wasDelivered: false
3012
+ }).catch(() => {
3013
+ });
3014
+ return { success: true, intercepted: true };
3015
+ }
3016
+ try {
3017
+ const result = await sendFn(...args);
3018
+ client.captureEmail({
3019
+ to: capturedTo,
3020
+ from: extracted.from,
3021
+ subject: extracted.subject,
3022
+ html: extracted.html,
3023
+ text: extracted.text,
3024
+ templateId: extracted.templateId,
3025
+ captureMode: "capture",
3026
+ wasDelivered: true
3027
+ }).catch(() => {
3028
+ });
3029
+ return result;
3030
+ } catch (err) {
3031
+ client.captureEmail({
3032
+ to: capturedTo,
3033
+ from: extracted.from,
3034
+ subject: extracted.subject,
3035
+ html: extracted.html,
3036
+ text: extracted.text,
3037
+ templateId: extracted.templateId,
3038
+ captureMode: "capture",
3039
+ wasDelivered: false
3040
+ }).catch(() => {
3041
+ });
3042
+ throw err;
3043
+ }
3044
+ };
3045
+ return wrapped;
3046
+ }
3047
+ };
3048
+ }
2759
3049
  export {
2760
3050
  BUG_CATEGORIES,
2761
3051
  BugBearClient,
@@ -2773,6 +3063,7 @@ export {
2773
3063
  captureError,
2774
3064
  contextCapture,
2775
3065
  createBugBear,
3066
+ createEmailInterceptor,
2776
3067
  generateFingerprint,
2777
3068
  isBugCategory,
2778
3069
  isNetworkError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/core",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Core utilities and types for BugBear QA platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",