@bugspotter/sdk 0.1.0-alpha.1 → 0.1.0-alpha.3

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.
@@ -73,8 +73,11 @@ class NetworkCapture extends base_capture_1.BaseCapture {
73
73
  }
74
74
  try {
75
75
  const response = await originalFetch(...args);
76
- const request = this.createNetworkRequest(url, method, response.status, startTime);
77
- this.addRequest(request);
76
+ // Only log if response is valid (handles mocked data URLs that return undefined)
77
+ if (response && typeof response.status === 'number') {
78
+ const request = this.createNetworkRequest(url, method, response.status, startTime);
79
+ this.addRequest(request);
80
+ }
78
81
  return response;
79
82
  }
80
83
  catch (error) {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DOMCollector = void 0;
4
4
  const rrweb_1 = require("rrweb");
5
5
  const buffer_1 = require("../core/buffer");
6
+ const constants_1 = require("../constants");
6
7
  /**
7
8
  * DOM Collector - Records user interactions and DOM mutations
8
9
  * @packageDocumentation
@@ -15,7 +16,7 @@ class DOMCollector {
15
16
  this.isRecording = false;
16
17
  this.sanitizer = config.sanitizer;
17
18
  this.config = {
18
- duration: (_a = config.duration) !== null && _a !== void 0 ? _a : 15,
19
+ duration: (_a = config.duration) !== null && _a !== void 0 ? _a : constants_1.DEFAULT_REPLAY_DURATION_SECONDS,
19
20
  sampling: {
20
21
  mousemove: (_c = (_b = config.sampling) === null || _b === void 0 ? void 0 : _b.mousemove) !== null && _c !== void 0 ? _c : 50,
21
22
  scroll: (_e = (_d = config.sampling) === null || _d === void 0 ? void 0 : _d.scroll) !== null && _e !== void 0 ? _e : 100,
@@ -0,0 +1,13 @@
1
+ /**
2
+ * SDK-wide constants
3
+ */
4
+ /**
5
+ * Default duration in seconds to keep replay events in buffer
6
+ * Used by both BugSpotter and DOMCollector
7
+ */
8
+ export declare const DEFAULT_REPLAY_DURATION_SECONDS = 15;
9
+ /**
10
+ * Maximum recommended replay duration in seconds
11
+ * Longer durations increase memory usage
12
+ */
13
+ export declare const MAX_RECOMMENDED_REPLAY_DURATION_SECONDS = 30;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ /**
3
+ * SDK-wide constants
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MAX_RECOMMENDED_REPLAY_DURATION_SECONDS = exports.DEFAULT_REPLAY_DURATION_SECONDS = void 0;
7
+ /**
8
+ * Default duration in seconds to keep replay events in buffer
9
+ * Used by both BugSpotter and DOMCollector
10
+ */
11
+ exports.DEFAULT_REPLAY_DURATION_SECONDS = 15;
12
+ /**
13
+ * Maximum recommended replay duration in seconds
14
+ * Longer durations increase memory usage
15
+ */
16
+ exports.MAX_RECOMMENDED_REPLAY_DURATION_SECONDS = 30;
@@ -0,0 +1,61 @@
1
+ import type { BugReport } from '../index';
2
+ export interface PresignedUrlData {
3
+ uploadUrl: string;
4
+ storageKey: string;
5
+ }
6
+ export interface FileToUpload {
7
+ type: 'screenshot' | 'replay';
8
+ url: string;
9
+ key: string;
10
+ blob: Blob;
11
+ }
12
+ export interface UploadConfirmation {
13
+ success: boolean;
14
+ type: 'screenshot' | 'replay';
15
+ }
16
+ /**
17
+ * Handles file upload operations using presigned URLs
18
+ * Separates concerns: preparation → upload → confirmation
19
+ *
20
+ * @remarks
21
+ * Upload timeout is set to 60 seconds (UPLOAD_TIMEOUT_MS).
22
+ * This timeout applies to individual file uploads to S3.
23
+ */
24
+ export declare class FileUploadHandler {
25
+ private readonly apiEndpoint;
26
+ private readonly apiKey;
27
+ private static readonly UPLOAD_TIMEOUT_MS;
28
+ constructor(apiEndpoint: string, apiKey: string);
29
+ /**
30
+ * Orchestrates the complete file upload flow
31
+ * @throws Error if any step fails
32
+ */
33
+ uploadFiles(bugId: string, report: BugReport, presignedUrls: {
34
+ screenshot?: PresignedUrlData;
35
+ replay?: PresignedUrlData;
36
+ }): Promise<void>;
37
+ /**
38
+ * Prepare file blobs and validate presigned URLs
39
+ */
40
+ private prepareFiles;
41
+ /**
42
+ * Upload files to storage using presigned URLs (parallel execution)
43
+ */
44
+ private uploadToStorage;
45
+ /**
46
+ * Confirm uploads with backend (parallel execution)
47
+ */
48
+ private confirmUploads;
49
+ /**
50
+ * Get presigned URL with validation
51
+ */
52
+ private getPresignedUrl;
53
+ /**
54
+ * Convert data URL to Blob
55
+ */
56
+ private dataUrlToBlob;
57
+ /**
58
+ * Format file type for error messages (capitalize first letter)
59
+ */
60
+ private formatFileType;
61
+ }
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileUploadHandler = void 0;
4
+ const compress_1 = require("./compress");
5
+ const logger_1 = require("../utils/logger");
6
+ const logger = (0, logger_1.getLogger)();
7
+ /**
8
+ * Handles file upload operations using presigned URLs
9
+ * Separates concerns: preparation → upload → confirmation
10
+ *
11
+ * @remarks
12
+ * Upload timeout is set to 60 seconds (UPLOAD_TIMEOUT_MS).
13
+ * This timeout applies to individual file uploads to S3.
14
+ */
15
+ class FileUploadHandler {
16
+ constructor(apiEndpoint, apiKey) {
17
+ this.apiEndpoint = apiEndpoint;
18
+ this.apiKey = apiKey;
19
+ }
20
+ /**
21
+ * Orchestrates the complete file upload flow
22
+ * @throws Error if any step fails
23
+ */
24
+ async uploadFiles(bugId, report, presignedUrls) {
25
+ const filesToUpload = await this.prepareFiles(report, presignedUrls);
26
+ if (filesToUpload.length === 0) {
27
+ return; // No files to upload
28
+ }
29
+ await this.uploadToStorage(filesToUpload);
30
+ await this.confirmUploads(filesToUpload, bugId);
31
+ }
32
+ /**
33
+ * Prepare file blobs and validate presigned URLs
34
+ */
35
+ async prepareFiles(report, presignedUrls) {
36
+ const files = [];
37
+ // Prepare screenshot
38
+ if (report._screenshotPreview && report._screenshotPreview.startsWith('data:image/')) {
39
+ const screenshotUrl = this.getPresignedUrl('screenshot', presignedUrls);
40
+ const screenshotBlob = await this.dataUrlToBlob(report._screenshotPreview);
41
+ files.push({
42
+ type: 'screenshot',
43
+ url: screenshotUrl.uploadUrl,
44
+ key: screenshotUrl.storageKey,
45
+ blob: screenshotBlob,
46
+ });
47
+ }
48
+ // Prepare replay
49
+ if (report.replay && report.replay.length > 0) {
50
+ const replayUrl = this.getPresignedUrl('replay', presignedUrls);
51
+ const compressed = await (0, compress_1.compressData)(report.replay);
52
+ const replayBlob = new Blob([compressed], { type: 'application/gzip' });
53
+ files.push({
54
+ type: 'replay',
55
+ url: replayUrl.uploadUrl,
56
+ key: replayUrl.storageKey,
57
+ blob: replayBlob,
58
+ });
59
+ }
60
+ return files;
61
+ }
62
+ /**
63
+ * Upload files to storage using presigned URLs (parallel execution)
64
+ */
65
+ async uploadToStorage(files) {
66
+ const uploadPromises = files.map(async (file) => {
67
+ const controller = new AbortController();
68
+ const timeoutId = setTimeout(() => controller.abort(), FileUploadHandler.UPLOAD_TIMEOUT_MS);
69
+ try {
70
+ const response = await fetch(file.url, {
71
+ method: 'PUT',
72
+ headers: {
73
+ 'Content-Type': file.blob.type || 'application/octet-stream',
74
+ },
75
+ body: file.blob,
76
+ signal: controller.signal,
77
+ });
78
+ clearTimeout(timeoutId);
79
+ return { success: response.ok, type: file.type };
80
+ }
81
+ catch (error) {
82
+ clearTimeout(timeoutId);
83
+ logger.error(`Upload failed for ${file.type}:`, error);
84
+ return { success: false, type: file.type };
85
+ }
86
+ });
87
+ const results = await Promise.all(uploadPromises);
88
+ // Check for upload failures
89
+ for (const result of results) {
90
+ if (!result.success) {
91
+ throw new Error(`${this.formatFileType(result.type)} upload failed: Upload to storage failed`);
92
+ }
93
+ }
94
+ }
95
+ /**
96
+ * Confirm uploads with backend (parallel execution)
97
+ */
98
+ async confirmUploads(files, bugId) {
99
+ const confirmPromises = files.map(async (file) => {
100
+ try {
101
+ const response = await fetch(`${this.apiEndpoint}/api/v1/reports/${bugId}/confirm-upload`, {
102
+ method: 'POST',
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ 'X-API-Key': this.apiKey,
106
+ },
107
+ body: JSON.stringify({
108
+ fileType: file.type,
109
+ }),
110
+ });
111
+ return { success: response.ok, type: file.type };
112
+ }
113
+ catch (error) {
114
+ logger.error(`Confirmation failed for ${file.type}:`, error);
115
+ return { success: false, type: file.type };
116
+ }
117
+ });
118
+ const results = await Promise.all(confirmPromises);
119
+ // Check for confirmation failures
120
+ for (const result of results) {
121
+ if (!result.success) {
122
+ throw new Error(`${this.formatFileType(result.type)} confirmation failed: Backend did not acknowledge upload`);
123
+ }
124
+ }
125
+ }
126
+ /**
127
+ * Get presigned URL with validation
128
+ */
129
+ getPresignedUrl(type, presignedUrls) {
130
+ const url = presignedUrls[type];
131
+ if (!url) {
132
+ throw new Error(`${this.formatFileType(type)} presigned URL not provided by server`);
133
+ }
134
+ return url;
135
+ }
136
+ /**
137
+ * Convert data URL to Blob
138
+ */
139
+ async dataUrlToBlob(dataUrl) {
140
+ if (!dataUrl || !dataUrl.startsWith('data:')) {
141
+ throw new Error('Invalid data URL');
142
+ }
143
+ const response = await fetch(dataUrl);
144
+ if (!response || !response.blob) {
145
+ throw new Error('Failed to convert data URL to Blob');
146
+ }
147
+ return await response.blob();
148
+ }
149
+ /**
150
+ * Format file type for error messages (capitalize first letter)
151
+ */
152
+ formatFileType(type) {
153
+ return type.charAt(0).toUpperCase() + type.slice(1);
154
+ }
155
+ }
156
+ exports.FileUploadHandler = FileUploadHandler;
157
+ FileUploadHandler.UPLOAD_TIMEOUT_MS = 60000; // 60 seconds
@@ -9,28 +9,19 @@ export declare class TransportError extends Error {
9
9
  readonly cause?: Error | undefined;
10
10
  constructor(message: string, endpoint: string, cause?: Error | undefined);
11
11
  }
12
- export declare class TokenRefreshError extends TransportError {
13
- constructor(endpoint: string, cause?: Error);
12
+ /**
13
+ * Authentication error - not retryable
14
+ */
15
+ export declare class AuthenticationError extends Error {
16
+ constructor(message: string);
14
17
  }
18
+ /**
19
+ * Authentication configuration - API key only
20
+ */
15
21
  export type AuthConfig = {
16
22
  type: 'api-key';
17
- apiKey?: string;
18
- } | {
19
- type: 'jwt';
20
- token?: string;
21
- onTokenExpired?: () => Promise<string>;
22
- } | {
23
- type: 'bearer';
24
- token?: string;
25
- onTokenExpired?: () => Promise<string>;
26
- } | {
27
- type: 'custom';
28
- customHeader?: {
29
- name: string;
30
- value: string;
31
- };
32
- } | {
33
- type: 'none';
23
+ apiKey: string;
24
+ projectId: string;
34
25
  };
35
26
  export interface RetryConfig {
36
27
  /** Maximum number of retry attempts (default: 3) */
@@ -43,12 +34,10 @@ export interface RetryConfig {
43
34
  retryOn?: number[];
44
35
  }
45
36
  export interface TransportOptions {
46
- /** Authentication configuration */
47
- auth?: AuthConfig;
37
+ /** Authentication configuration (required) */
38
+ auth: AuthConfig;
48
39
  /** Optional logger for debugging */
49
40
  logger?: Logger;
50
- /** Enable retry on token expiration (default: true) */
51
- enableRetry?: boolean;
52
41
  /** Retry configuration */
53
42
  retry?: RetryConfig;
54
43
  /** Offline queue configuration */
@@ -59,7 +48,7 @@ export interface TransportOptions {
59
48
  * @param auth - Authentication configuration
60
49
  * @returns HTTP headers for authentication
61
50
  */
62
- export declare function getAuthHeaders(auth?: AuthConfig): Record<string, string>;
51
+ export declare function getAuthHeaders(auth: AuthConfig): Record<string, string>;
63
52
  /**
64
53
  * Submit request with authentication, exponential backoff retry, and offline queue support
65
54
  *
@@ -69,5 +58,5 @@ export declare function getAuthHeaders(auth?: AuthConfig): Record<string, string
69
58
  * @param authOrOptions - Auth config or TransportOptions
70
59
  * @returns Response from the server
71
60
  */
72
- export declare function submitWithAuth(endpoint: string, body: BodyInit, contentHeaders: Record<string, string>, authOrOptions?: AuthConfig | TransportOptions): Promise<Response>;
61
+ export declare function submitWithAuth(endpoint: string, body: BodyInit, contentHeaders: Record<string, string> | undefined, options: TransportOptions): Promise<Response>;
73
62
  export { clearOfflineQueue, type OfflineConfig } from './offline-queue';
@@ -4,7 +4,7 @@
4
4
  * exponential backoff retry, and offline queue support
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.clearOfflineQueue = exports.TokenRefreshError = exports.TransportError = void 0;
7
+ exports.clearOfflineQueue = exports.AuthenticationError = exports.TransportError = void 0;
8
8
  exports.getAuthHeaders = getAuthHeaders;
9
9
  exports.submitWithAuth = submitWithAuth;
10
10
  const logger_1 = require("../utils/logger");
@@ -12,9 +12,7 @@ const offline_queue_1 = require("./offline-queue");
12
12
  // ============================================================================
13
13
  // CONSTANTS
14
14
  // ============================================================================
15
- const TOKEN_REFRESH_STATUS = 401;
16
15
  const JITTER_PERCENTAGE = 0.1;
17
- const DEFAULT_ENABLE_RETRY = true;
18
16
  // ============================================================================
19
17
  // CUSTOM ERROR TYPES
20
18
  // ============================================================================
@@ -27,13 +25,16 @@ class TransportError extends Error {
27
25
  }
28
26
  }
29
27
  exports.TransportError = TransportError;
30
- class TokenRefreshError extends TransportError {
31
- constructor(endpoint, cause) {
32
- super('Failed to refresh authentication token', endpoint, cause);
33
- this.name = 'TokenRefreshError';
28
+ /**
29
+ * Authentication error - not retryable
30
+ */
31
+ class AuthenticationError extends Error {
32
+ constructor(message) {
33
+ super(message);
34
+ this.name = 'AuthenticationError';
34
35
  }
35
36
  }
36
- exports.TokenRefreshError = TokenRefreshError;
37
+ exports.AuthenticationError = AuthenticationError;
37
38
  // Default configurations
38
39
  const DEFAULT_RETRY_CONFIG = {
39
40
  maxRetries: 3,
@@ -45,31 +46,18 @@ const DEFAULT_OFFLINE_CONFIG = {
45
46
  enabled: false,
46
47
  maxQueueSize: 10,
47
48
  };
48
- const authStrategies = {
49
- 'api-key': (config) => {
50
- const apiKey = config.apiKey;
51
- return apiKey ? { 'X-API-Key': apiKey } : {};
52
- },
53
- jwt: (config) => {
54
- const token = config.token;
55
- return token ? { Authorization: `Bearer ${token}` } : {};
56
- },
57
- bearer: (config) => {
58
- const token = config.token;
59
- return token ? { Authorization: `Bearer ${token}` } : {};
60
- },
61
- custom: (config) => {
62
- const customHeader = config.customHeader;
63
- if (!customHeader) {
64
- return {};
65
- }
66
- const { name, value } = customHeader;
67
- return name && value ? { [name]: value } : {};
68
- },
69
- none: () => {
70
- return {};
71
- },
72
- };
49
+ // ============================================================================
50
+ // AUTHENTICATION
51
+ // ============================================================================
52
+ /**
53
+ * Generate authentication headers for API key
54
+ */
55
+ function generateAuthHeaders(config) {
56
+ if (!config || !config.apiKey) {
57
+ throw new AuthenticationError('Authentication is required: API key must be provided');
58
+ }
59
+ return { 'X-API-Key': config.apiKey };
60
+ }
73
61
  // ============================================================================
74
62
  // RETRY HANDLER - Exponential Backoff Logic
75
63
  // ============================================================================
@@ -98,6 +86,10 @@ class RetryHandler {
98
86
  }
99
87
  catch (error) {
100
88
  lastError = error;
89
+ // Don't retry authentication errors - they won't succeed on retry
90
+ if (error instanceof AuthenticationError) {
91
+ throw error;
92
+ }
101
93
  // Retry on network errors
102
94
  if (attempt < this.config.maxRetries) {
103
95
  const delay = this.calculateDelay(attempt);
@@ -132,57 +124,6 @@ class RetryHandler {
132
124
  return Math.min(delayWithJitter, this.config.maxDelay);
133
125
  }
134
126
  }
135
- /**
136
- * Type guard to check if parameter is TransportOptions
137
- *
138
- * Strategy: Check for properties that ONLY exist in TransportOptions, not in AuthConfig.
139
- * AuthConfig has: type, apiKey?, token?, onTokenExpired?, customHeader?
140
- * TransportOptions has: auth?, logger?, enableRetry?, retry?, offline?
141
- *
142
- * Key distinction: AuthConfig always has 'type' property, TransportOptions never does.
143
- */
144
- function isTransportOptions(obj) {
145
- if (typeof obj !== 'object' || obj === null) {
146
- return false;
147
- }
148
- const record = obj;
149
- // If it has 'type' property, it's an AuthConfig, not TransportOptions
150
- if ('type' in record) {
151
- return false;
152
- }
153
- // Key insight: If object has TransportOptions-specific keys (even if undefined),
154
- // it's likely TransportOptions since AuthConfig never has these keys
155
- const hasTransportOptionsKeys = 'auth' in record ||
156
- 'retry' in record ||
157
- 'offline' in record ||
158
- 'logger' in record ||
159
- 'enableRetry' in record;
160
- // Return true if it has any TransportOptions-specific keys
161
- return hasTransportOptionsKeys;
162
- }
163
- /**
164
- * Parse transport parameters, supporting both legacy and new API signatures
165
- */
166
- function parseTransportParams(authOrOptions) {
167
- var _a;
168
- if (isTransportOptions(authOrOptions)) {
169
- // Type guard ensures authOrOptions is TransportOptions
170
- return {
171
- auth: authOrOptions.auth,
172
- logger: authOrOptions.logger || (0, logger_1.getLogger)(),
173
- enableRetry: (_a = authOrOptions.enableRetry) !== null && _a !== void 0 ? _a : DEFAULT_ENABLE_RETRY,
174
- retryConfig: Object.assign(Object.assign({}, DEFAULT_RETRY_CONFIG), authOrOptions.retry),
175
- offlineConfig: Object.assign(Object.assign({}, DEFAULT_OFFLINE_CONFIG), authOrOptions.offline),
176
- };
177
- }
178
- return {
179
- auth: authOrOptions,
180
- logger: (0, logger_1.getLogger)(),
181
- enableRetry: DEFAULT_ENABLE_RETRY,
182
- retryConfig: DEFAULT_RETRY_CONFIG,
183
- offlineConfig: DEFAULT_OFFLINE_CONFIG,
184
- };
185
- }
186
127
  // ============================================================================
187
128
  // HELPER FUNCTIONS
188
129
  // ============================================================================
@@ -207,7 +148,7 @@ async function handleOfflineFailure(error, endpoint, body, contentHeaders, auth,
207
148
  }
208
149
  logger.warn('Network error detected, queueing request for offline retry');
209
150
  const queue = new offline_queue_1.OfflineQueue(offlineConfig, logger);
210
- const authHeaders = getAuthHeaders(auth);
151
+ const authHeaders = generateAuthHeaders(auth);
211
152
  await queue.enqueue(endpoint, body, Object.assign(Object.assign({}, contentHeaders), authHeaders));
212
153
  }
213
154
  // ============================================================================
@@ -219,13 +160,7 @@ async function handleOfflineFailure(error, endpoint, body, contentHeaders, auth,
219
160
  * @returns HTTP headers for authentication
220
161
  */
221
162
  function getAuthHeaders(auth) {
222
- // No auth
223
- if (!auth) {
224
- return {};
225
- }
226
- // Apply strategy
227
- const strategy = authStrategies[auth.type];
228
- return strategy ? strategy(auth) : {};
163
+ return generateAuthHeaders(auth);
229
164
  }
230
165
  /**
231
166
  * Submit request with authentication, exponential backoff retry, and offline queue support
@@ -236,35 +171,31 @@ function getAuthHeaders(auth) {
236
171
  * @param authOrOptions - Auth config or TransportOptions
237
172
  * @returns Response from the server
238
173
  */
239
- async function submitWithAuth(endpoint, body, contentHeaders, authOrOptions) {
240
- // Parse options (support both old signature and new options-based API)
241
- const { auth, logger, enableRetry, retryConfig, offlineConfig } = parseTransportParams(authOrOptions);
174
+ async function submitWithAuth(endpoint, body, contentHeaders = {}, options) {
175
+ const logger = options.logger || (0, logger_1.getLogger)();
176
+ const retryConfig = Object.assign(Object.assign({}, DEFAULT_RETRY_CONFIG), options.retry);
177
+ const offlineConfig = Object.assign(Object.assign({}, DEFAULT_OFFLINE_CONFIG), options.offline);
242
178
  // Process offline queue on each request (run in background without awaiting)
243
179
  processQueueInBackground(offlineConfig, retryConfig, logger);
244
180
  try {
245
181
  // Send with retry logic
246
- const response = await sendWithRetry(endpoint, body, contentHeaders, auth, retryConfig, logger, enableRetry);
182
+ const response = await sendWithRetry(endpoint, body, contentHeaders, options.auth, retryConfig, logger);
247
183
  return response;
248
184
  }
249
185
  catch (error) {
250
186
  // Queue for offline retry if enabled
251
- await handleOfflineFailure(error, endpoint, body, contentHeaders, auth, offlineConfig, logger);
187
+ await handleOfflineFailure(error, endpoint, body, contentHeaders, options.auth, offlineConfig, logger);
252
188
  throw error;
253
189
  }
254
190
  }
255
- /**
256
- * Check if auth config supports token refresh
257
- */
258
- function shouldRetryWithRefresh(auth) {
259
- return (typeof auth === 'object' &&
260
- (auth.type === 'jwt' || auth.type === 'bearer') &&
261
- typeof auth.onTokenExpired === 'function');
262
- }
191
+ // ============================================================================
192
+ // INTERNAL HELPERS
193
+ // ============================================================================
263
194
  /**
264
195
  * Make HTTP request with auth headers
265
196
  */
266
197
  async function makeRequest(endpoint, body, contentHeaders, auth) {
267
- const authHeaders = getAuthHeaders(auth);
198
+ const authHeaders = generateAuthHeaders(auth);
268
199
  const headers = Object.assign(Object.assign({}, contentHeaders), authHeaders);
269
200
  return fetch(endpoint, {
270
201
  method: 'POST',
@@ -275,25 +206,9 @@ async function makeRequest(endpoint, body, contentHeaders, auth) {
275
206
  /**
276
207
  * Send request with exponential backoff retry
277
208
  */
278
- async function sendWithRetry(endpoint, body, contentHeaders, auth, retryConfig, logger, enableTokenRetry) {
209
+ async function sendWithRetry(endpoint, body, contentHeaders, auth, retryConfig, logger) {
279
210
  const retryHandler = new RetryHandler(retryConfig, logger);
280
- let hasAttemptedRefresh = false;
281
- // Use retry handler with token refresh support
282
- return retryHandler.executeWithRetry(async () => {
283
- const response = await makeRequest(endpoint, body, contentHeaders, auth);
284
- // Check for 401 and retry with token refresh if applicable (only once)
285
- if (response.status === TOKEN_REFRESH_STATUS &&
286
- enableTokenRetry &&
287
- !hasAttemptedRefresh &&
288
- shouldRetryWithRefresh(auth)) {
289
- hasAttemptedRefresh = true;
290
- const refreshedResponse = await retryWithTokenRefresh(endpoint, body, contentHeaders, auth, logger);
291
- return refreshedResponse;
292
- }
293
- return response;
294
- }, (status) => {
295
- return retryConfig.retryOn.includes(status);
296
- });
211
+ return retryHandler.executeWithRetry(async () => makeRequest(endpoint, body, contentHeaders, auth), (status) => retryConfig.retryOn.includes(status));
297
212
  }
298
213
  /**
299
214
  * Sleep for specified milliseconds
@@ -326,27 +241,6 @@ function isNetworkError(error) {
326
241
  // TypeError only if it mentions fetch or network
327
242
  (error.name === 'TypeError' && (message.includes('fetch') || message.includes('network'))));
328
243
  }
329
- /**
330
- * Retry request with refreshed token
331
- */
332
- async function retryWithTokenRefresh(endpoint, body, contentHeaders, auth, logger) {
333
- try {
334
- logger.warn('Token expired, attempting refresh...');
335
- // Get new token
336
- const newToken = await auth.onTokenExpired();
337
- // Create updated auth config
338
- const refreshedAuth = Object.assign(Object.assign({}, auth), { token: newToken });
339
- // Retry request
340
- const response = await makeRequest(endpoint, body, contentHeaders, refreshedAuth);
341
- logger.log('Request retried with refreshed token');
342
- return response;
343
- }
344
- catch (error) {
345
- logger.error('Token refresh failed:', error);
346
- // Return original 401 - caller should handle
347
- return new Response(null, { status: TOKEN_REFRESH_STATUS, statusText: 'Unauthorized' });
348
- }
349
- }
350
244
  // Re-export offline queue utilities
351
245
  var offline_queue_2 = require("./offline-queue");
352
246
  Object.defineProperty(exports, "clearOfflineQueue", { enumerable: true, get: function () { return offline_queue_2.clearOfflineQueue; } });
package/dist/index.d.ts CHANGED
@@ -23,6 +23,20 @@ export declare class BugSpotter {
23
23
  */
24
24
  capture(): Promise<BugReport>;
25
25
  private handleBugReport;
26
+ /**
27
+ * Validate authentication configuration
28
+ * @throws Error if configuration is invalid
29
+ */
30
+ private validateAuthConfig;
31
+ /**
32
+ * Strip endpoint suffix from path
33
+ */
34
+ private stripEndpointSuffix;
35
+ /**
36
+ * Get the base API URL for confirm-upload calls
37
+ * Extracts scheme, host, and base path from the configured endpoint
38
+ */
39
+ private getApiBaseUrl;
26
40
  private submitBugReport;
27
41
  getConfig(): Readonly<BugSpotterConfig>;
28
42
  destroy(): void;
@@ -31,8 +45,11 @@ export interface BugSpotterConfig {
31
45
  endpoint?: string;
32
46
  showWidget?: boolean;
33
47
  widgetOptions?: FloatingButtonOptions;
34
- /** Authentication configuration */
35
- auth?: AuthConfig;
48
+ /**
49
+ * Authentication configuration (required)
50
+ * API key authentication with project ID
51
+ */
52
+ auth: AuthConfig;
36
53
  /** Retry configuration for failed requests */
37
54
  retry?: RetryConfig;
38
55
  /** Offline queue configuration */
@@ -124,6 +141,7 @@ export type { FloatingButtonOptions } from './widget/button';
124
141
  export { BugReportModal } from './widget/modal';
125
142
  export type { BugReportData, BugReportModalOptions, PIIDetection } from './widget/modal';
126
143
  export type { eventWithTime } from '@rrweb/types';
144
+ export { DEFAULT_REPLAY_DURATION_SECONDS, MAX_RECOMMENDED_REPLAY_DURATION_SECONDS, } from './constants';
127
145
  /**
128
146
  * Convenience function to sanitize text with default PII patterns
129
147
  * Useful for quick sanitization without creating a Sanitizer instance