@bbearai/core 0.7.1 → 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 +107 -2
- package/dist/index.d.ts +107 -2
- package/dist/index.js +245 -3
- package/dist/index.mjs +244 -3
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -789,6 +789,50 @@ interface TesterIssue {
|
|
|
789
789
|
/** Original bug title (for reopened/test_fail issues) */
|
|
790
790
|
originalBugTitle?: string;
|
|
791
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;
|
|
792
836
|
|
|
793
837
|
/**
|
|
794
838
|
* BugBear Offline Queue
|
|
@@ -815,7 +859,7 @@ declare class LocalStorageAdapter implements StorageAdapter {
|
|
|
815
859
|
setItem(key: string, value: string): Promise<void>;
|
|
816
860
|
removeItem(key: string): Promise<void>;
|
|
817
861
|
}
|
|
818
|
-
type QueueItemType = 'report' | 'message' | 'feedback';
|
|
862
|
+
type QueueItemType = 'report' | 'message' | 'feedback' | 'email_capture';
|
|
819
863
|
interface QueueItem {
|
|
820
864
|
/** Unique ID for this queued operation. */
|
|
821
865
|
id: string;
|
|
@@ -1042,6 +1086,8 @@ declare class BugBearClient {
|
|
|
1042
1086
|
private reportSubmitInFlight;
|
|
1043
1087
|
/** Offline queue — only created when config.offlineQueue.enabled is true. */
|
|
1044
1088
|
private _queue;
|
|
1089
|
+
/** Session cache storage — defaults to LocalStorageAdapter. */
|
|
1090
|
+
private _sessionStorage;
|
|
1045
1091
|
/** Active Realtime channel references for cleanup. */
|
|
1046
1092
|
private realtimeChannels;
|
|
1047
1093
|
/** Error monitor instance — created when config.monitoring is present. */
|
|
@@ -1068,6 +1114,8 @@ declare class BugBearClient {
|
|
|
1068
1114
|
private resolveFromApiKey;
|
|
1069
1115
|
/** Apply resolved credentials and create the Supabase client. */
|
|
1070
1116
|
private applyResolvedConfig;
|
|
1117
|
+
/** Cache key scoped to the active project. */
|
|
1118
|
+
private get sessionCacheKey();
|
|
1071
1119
|
/** Initialize offline queue if configured. Shared by both init paths. */
|
|
1072
1120
|
private initOfflineQueue;
|
|
1073
1121
|
/** Initialize error monitoring if configured. Shared by both init paths. */
|
|
@@ -1080,6 +1128,12 @@ declare class BugBearClient {
|
|
|
1080
1128
|
private hashKey;
|
|
1081
1129
|
/** Ensure the client is initialized before making requests. */
|
|
1082
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;
|
|
1083
1137
|
/**
|
|
1084
1138
|
* Access the offline queue (if enabled).
|
|
1085
1139
|
* Use this to check queue.count, subscribe to changes, or trigger flush.
|
|
@@ -1091,6 +1145,22 @@ declare class BugBearClient {
|
|
|
1091
1145
|
* Web callers can skip this — LocalStorageAdapter is the default.
|
|
1092
1146
|
*/
|
|
1093
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>;
|
|
1094
1164
|
private registerQueueHandlers;
|
|
1095
1165
|
/** Whether realtime is enabled in config. */
|
|
1096
1166
|
get realtimeEnabled(): boolean;
|
|
@@ -1137,6 +1207,11 @@ declare class BugBearClient {
|
|
|
1137
1207
|
queued?: boolean;
|
|
1138
1208
|
error?: string;
|
|
1139
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>;
|
|
1140
1215
|
/**
|
|
1141
1216
|
* Get assigned tests for current user
|
|
1142
1217
|
* First looks up the tester by email, then fetches their assignments
|
|
@@ -1253,6 +1328,14 @@ declare class BugBearClient {
|
|
|
1253
1328
|
* reopened issues include original bug context.
|
|
1254
1329
|
*/
|
|
1255
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
|
+
}>;
|
|
1256
1339
|
/**
|
|
1257
1340
|
* Basic email format validation (defense in depth)
|
|
1258
1341
|
*/
|
|
@@ -1558,4 +1641,26 @@ declare function captureError(error: Error, errorInfo?: {
|
|
|
1558
1641
|
componentStack?: string;
|
|
1559
1642
|
};
|
|
1560
1643
|
|
|
1561
|
-
|
|
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
|
@@ -789,6 +789,50 @@ interface TesterIssue {
|
|
|
789
789
|
/** Original bug title (for reopened/test_fail issues) */
|
|
790
790
|
originalBugTitle?: string;
|
|
791
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;
|
|
792
836
|
|
|
793
837
|
/**
|
|
794
838
|
* BugBear Offline Queue
|
|
@@ -815,7 +859,7 @@ declare class LocalStorageAdapter implements StorageAdapter {
|
|
|
815
859
|
setItem(key: string, value: string): Promise<void>;
|
|
816
860
|
removeItem(key: string): Promise<void>;
|
|
817
861
|
}
|
|
818
|
-
type QueueItemType = 'report' | 'message' | 'feedback';
|
|
862
|
+
type QueueItemType = 'report' | 'message' | 'feedback' | 'email_capture';
|
|
819
863
|
interface QueueItem {
|
|
820
864
|
/** Unique ID for this queued operation. */
|
|
821
865
|
id: string;
|
|
@@ -1042,6 +1086,8 @@ declare class BugBearClient {
|
|
|
1042
1086
|
private reportSubmitInFlight;
|
|
1043
1087
|
/** Offline queue — only created when config.offlineQueue.enabled is true. */
|
|
1044
1088
|
private _queue;
|
|
1089
|
+
/** Session cache storage — defaults to LocalStorageAdapter. */
|
|
1090
|
+
private _sessionStorage;
|
|
1045
1091
|
/** Active Realtime channel references for cleanup. */
|
|
1046
1092
|
private realtimeChannels;
|
|
1047
1093
|
/** Error monitor instance — created when config.monitoring is present. */
|
|
@@ -1068,6 +1114,8 @@ declare class BugBearClient {
|
|
|
1068
1114
|
private resolveFromApiKey;
|
|
1069
1115
|
/** Apply resolved credentials and create the Supabase client. */
|
|
1070
1116
|
private applyResolvedConfig;
|
|
1117
|
+
/** Cache key scoped to the active project. */
|
|
1118
|
+
private get sessionCacheKey();
|
|
1071
1119
|
/** Initialize offline queue if configured. Shared by both init paths. */
|
|
1072
1120
|
private initOfflineQueue;
|
|
1073
1121
|
/** Initialize error monitoring if configured. Shared by both init paths. */
|
|
@@ -1080,6 +1128,12 @@ declare class BugBearClient {
|
|
|
1080
1128
|
private hashKey;
|
|
1081
1129
|
/** Ensure the client is initialized before making requests. */
|
|
1082
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;
|
|
1083
1137
|
/**
|
|
1084
1138
|
* Access the offline queue (if enabled).
|
|
1085
1139
|
* Use this to check queue.count, subscribe to changes, or trigger flush.
|
|
@@ -1091,6 +1145,22 @@ declare class BugBearClient {
|
|
|
1091
1145
|
* Web callers can skip this — LocalStorageAdapter is the default.
|
|
1092
1146
|
*/
|
|
1093
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>;
|
|
1094
1164
|
private registerQueueHandlers;
|
|
1095
1165
|
/** Whether realtime is enabled in config. */
|
|
1096
1166
|
get realtimeEnabled(): boolean;
|
|
@@ -1137,6 +1207,11 @@ declare class BugBearClient {
|
|
|
1137
1207
|
queued?: boolean;
|
|
1138
1208
|
error?: string;
|
|
1139
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>;
|
|
1140
1215
|
/**
|
|
1141
1216
|
* Get assigned tests for current user
|
|
1142
1217
|
* First looks up the tester by email, then fetches their assignments
|
|
@@ -1253,6 +1328,14 @@ declare class BugBearClient {
|
|
|
1253
1328
|
* reopened issues include original bug context.
|
|
1254
1329
|
*/
|
|
1255
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
|
+
}>;
|
|
1256
1339
|
/**
|
|
1257
1340
|
* Basic email format validation (defense in depth)
|
|
1258
1341
|
*/
|
|
@@ -1558,4 +1641,26 @@ declare function captureError(error: Error, errorInfo?: {
|
|
|
1558
1641
|
componentStack?: string;
|
|
1559
1642
|
};
|
|
1560
1643
|
|
|
1561
|
-
|
|
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
|
*/
|
|
@@ -2496,7 +2636,7 @@ var BugBearClient = class {
|
|
|
2496
2636
|
insertData.attachments = safeAttachments;
|
|
2497
2637
|
}
|
|
2498
2638
|
}
|
|
2499
|
-
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();
|
|
2500
2640
|
if (error) {
|
|
2501
2641
|
if (this._queue && isNetworkError(error.message)) {
|
|
2502
2642
|
await this._queue.enqueue("message", insertData);
|
|
@@ -2505,6 +2645,10 @@ var BugBearClient = class {
|
|
|
2505
2645
|
console.error("BugBear: Failed to send message", formatPgError(error));
|
|
2506
2646
|
return false;
|
|
2507
2647
|
}
|
|
2648
|
+
if (msgData?.id) {
|
|
2649
|
+
this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2508
2652
|
await this.markThreadAsRead(threadId);
|
|
2509
2653
|
return true;
|
|
2510
2654
|
} catch (err) {
|
|
@@ -2630,6 +2774,7 @@ var BugBearClient = class {
|
|
|
2630
2774
|
if (!session) {
|
|
2631
2775
|
return { success: false, error: "Session created but could not be fetched" };
|
|
2632
2776
|
}
|
|
2777
|
+
await this.cacheSession(session);
|
|
2633
2778
|
return { success: true, session };
|
|
2634
2779
|
} catch (err) {
|
|
2635
2780
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -2653,6 +2798,7 @@ var BugBearClient = class {
|
|
|
2653
2798
|
return { success: false, error: error.message };
|
|
2654
2799
|
}
|
|
2655
2800
|
const session = this.transformSession(data);
|
|
2801
|
+
await this.cacheSession(null);
|
|
2656
2802
|
return { success: true, session };
|
|
2657
2803
|
} catch (err) {
|
|
2658
2804
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -2668,8 +2814,13 @@ var BugBearClient = class {
|
|
|
2668
2814
|
const testerInfo = await this.getTesterInfo();
|
|
2669
2815
|
if (!testerInfo) return null;
|
|
2670
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();
|
|
2671
|
-
if (error || !data)
|
|
2672
|
-
|
|
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;
|
|
2673
2824
|
} catch (err) {
|
|
2674
2825
|
console.error("BugBear: Error fetching active session", err);
|
|
2675
2826
|
return null;
|
|
@@ -2851,6 +3002,96 @@ var BugBearClient = class {
|
|
|
2851
3002
|
function createBugBear(config) {
|
|
2852
3003
|
return new BugBearClient(config);
|
|
2853
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
|
+
}
|
|
2854
3095
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2855
3096
|
0 && (module.exports = {
|
|
2856
3097
|
BUG_CATEGORIES,
|
|
@@ -2869,6 +3110,7 @@ function createBugBear(config) {
|
|
|
2869
3110
|
captureError,
|
|
2870
3111
|
contextCapture,
|
|
2871
3112
|
createBugBear,
|
|
3113
|
+
createEmailInterceptor,
|
|
2872
3114
|
generateFingerprint,
|
|
2873
3115
|
isBugCategory,
|
|
2874
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
|
*/
|
|
@@ -2451,7 +2590,7 @@ var BugBearClient = class {
|
|
|
2451
2590
|
insertData.attachments = safeAttachments;
|
|
2452
2591
|
}
|
|
2453
2592
|
}
|
|
2454
|
-
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();
|
|
2455
2594
|
if (error) {
|
|
2456
2595
|
if (this._queue && isNetworkError(error.message)) {
|
|
2457
2596
|
await this._queue.enqueue("message", insertData);
|
|
@@ -2460,6 +2599,10 @@ var BugBearClient = class {
|
|
|
2460
2599
|
console.error("BugBear: Failed to send message", formatPgError(error));
|
|
2461
2600
|
return false;
|
|
2462
2601
|
}
|
|
2602
|
+
if (msgData?.id) {
|
|
2603
|
+
this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2463
2606
|
await this.markThreadAsRead(threadId);
|
|
2464
2607
|
return true;
|
|
2465
2608
|
} catch (err) {
|
|
@@ -2585,6 +2728,7 @@ var BugBearClient = class {
|
|
|
2585
2728
|
if (!session) {
|
|
2586
2729
|
return { success: false, error: "Session created but could not be fetched" };
|
|
2587
2730
|
}
|
|
2731
|
+
await this.cacheSession(session);
|
|
2588
2732
|
return { success: true, session };
|
|
2589
2733
|
} catch (err) {
|
|
2590
2734
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -2608,6 +2752,7 @@ var BugBearClient = class {
|
|
|
2608
2752
|
return { success: false, error: error.message };
|
|
2609
2753
|
}
|
|
2610
2754
|
const session = this.transformSession(data);
|
|
2755
|
+
await this.cacheSession(null);
|
|
2611
2756
|
return { success: true, session };
|
|
2612
2757
|
} catch (err) {
|
|
2613
2758
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -2623,8 +2768,13 @@ var BugBearClient = class {
|
|
|
2623
2768
|
const testerInfo = await this.getTesterInfo();
|
|
2624
2769
|
if (!testerInfo) return null;
|
|
2625
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();
|
|
2626
|
-
if (error || !data)
|
|
2627
|
-
|
|
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;
|
|
2628
2778
|
} catch (err) {
|
|
2629
2779
|
console.error("BugBear: Error fetching active session", err);
|
|
2630
2780
|
return null;
|
|
@@ -2806,6 +2956,96 @@ var BugBearClient = class {
|
|
|
2806
2956
|
function createBugBear(config) {
|
|
2807
2957
|
return new BugBearClient(config);
|
|
2808
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
|
+
}
|
|
2809
3049
|
export {
|
|
2810
3050
|
BUG_CATEGORIES,
|
|
2811
3051
|
BugBearClient,
|
|
@@ -2823,6 +3063,7 @@ export {
|
|
|
2823
3063
|
captureError,
|
|
2824
3064
|
contextCapture,
|
|
2825
3065
|
createBugBear,
|
|
3066
|
+
createEmailInterceptor,
|
|
2826
3067
|
generateFingerprint,
|
|
2827
3068
|
isBugCategory,
|
|
2828
3069
|
isNetworkError,
|