@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 +127 -7
- package/dist/index.d.ts +127 -7
- package/dist/index.js +304 -12
- package/dist/index.mjs +303 -12
- package/package.json +1 -1
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'
|
|
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 '
|
|
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':
|
|
1292
|
-
* - 'auto':
|
|
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' →
|
|
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
|
-
|
|
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'
|
|
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 '
|
|
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':
|
|
1292
|
-
* - 'auto':
|
|
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' →
|
|
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
|
-
|
|
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':
|
|
2110
|
-
* - 'auto':
|
|
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
|
|
2123
|
-
|
|
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' →
|
|
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)
|
|
2622
|
-
|
|
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':
|
|
2065
|
-
* - 'auto':
|
|
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
|
|
2078
|
-
|
|
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' →
|
|
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)
|
|
2577
|
-
|
|
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,
|