@bugspotter/sdk 0.1.0-alpha.1

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/LICENSE +21 -0
  3. package/README.md +639 -0
  4. package/dist/bugspotter.min.js +2 -0
  5. package/dist/bugspotter.min.js.LICENSE.txt +14 -0
  6. package/dist/capture/base-capture.d.ts +34 -0
  7. package/dist/capture/base-capture.js +23 -0
  8. package/dist/capture/capture-lifecycle.d.ts +24 -0
  9. package/dist/capture/capture-lifecycle.js +2 -0
  10. package/dist/capture/console.d.ts +29 -0
  11. package/dist/capture/console.js +107 -0
  12. package/dist/capture/metadata.d.ts +21 -0
  13. package/dist/capture/metadata.js +76 -0
  14. package/dist/capture/network.d.ts +32 -0
  15. package/dist/capture/network.js +135 -0
  16. package/dist/capture/screenshot.d.ts +19 -0
  17. package/dist/capture/screenshot.js +52 -0
  18. package/dist/collectors/dom.d.ts +67 -0
  19. package/dist/collectors/dom.js +164 -0
  20. package/dist/collectors/index.d.ts +2 -0
  21. package/dist/collectors/index.js +5 -0
  22. package/dist/core/buffer.d.ts +50 -0
  23. package/dist/core/buffer.js +88 -0
  24. package/dist/core/circular-buffer.d.ts +42 -0
  25. package/dist/core/circular-buffer.js +77 -0
  26. package/dist/core/compress.d.ts +49 -0
  27. package/dist/core/compress.js +245 -0
  28. package/dist/core/offline-queue.d.ts +76 -0
  29. package/dist/core/offline-queue.js +301 -0
  30. package/dist/core/transport.d.ts +73 -0
  31. package/dist/core/transport.js +352 -0
  32. package/dist/core/upload-helpers.d.ts +32 -0
  33. package/dist/core/upload-helpers.js +79 -0
  34. package/dist/core/uploader.d.ts +70 -0
  35. package/dist/core/uploader.js +185 -0
  36. package/dist/index.d.ts +140 -0
  37. package/dist/index.esm.js +205 -0
  38. package/dist/index.js +244 -0
  39. package/dist/utils/logger.d.ts +28 -0
  40. package/dist/utils/logger.js +84 -0
  41. package/dist/utils/sanitize-patterns.d.ts +103 -0
  42. package/dist/utils/sanitize-patterns.js +282 -0
  43. package/dist/utils/sanitize.d.ts +73 -0
  44. package/dist/utils/sanitize.js +254 -0
  45. package/dist/widget/button.d.ts +33 -0
  46. package/dist/widget/button.js +143 -0
  47. package/dist/widget/components/dom-element-cache.d.ts +62 -0
  48. package/dist/widget/components/dom-element-cache.js +105 -0
  49. package/dist/widget/components/form-validator.d.ts +66 -0
  50. package/dist/widget/components/form-validator.js +115 -0
  51. package/dist/widget/components/pii-detection-display.d.ts +64 -0
  52. package/dist/widget/components/pii-detection-display.js +142 -0
  53. package/dist/widget/components/redaction-canvas.d.ts +95 -0
  54. package/dist/widget/components/redaction-canvas.js +230 -0
  55. package/dist/widget/components/screenshot-processor.d.ts +44 -0
  56. package/dist/widget/components/screenshot-processor.js +191 -0
  57. package/dist/widget/components/style-manager.d.ts +37 -0
  58. package/dist/widget/components/style-manager.js +296 -0
  59. package/dist/widget/components/template-manager.d.ts +66 -0
  60. package/dist/widget/components/template-manager.js +198 -0
  61. package/dist/widget/modal.d.ts +62 -0
  62. package/dist/widget/modal.js +299 -0
  63. package/docs/CDN.md +213 -0
  64. package/docs/FRAMEWORK_INTEGRATION.md +1104 -0
  65. package/docs/PUBLISHING.md +550 -0
  66. package/docs/SESSION_REPLAY.md +381 -0
  67. package/package.json +90 -0
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.compressData = compressData;
7
+ exports.decompressData = decompressData;
8
+ exports.compressImage = compressImage;
9
+ exports.getCompressionRatio = getCompressionRatio;
10
+ exports.estimateSize = estimateSize;
11
+ exports.resetCompressionCache = resetCompressionCache;
12
+ const pako_1 = __importDefault(require("pako"));
13
+ const logger_1 = require("../utils/logger");
14
+ /**
15
+ * Compression utilities for BugSpotter SDK
16
+ * Handles payload and image compression using browser-native APIs
17
+ */
18
+ const logger = (0, logger_1.getLogger)();
19
+ // Configuration constants
20
+ const COMPRESSION_DEFAULTS = {
21
+ GZIP_LEVEL: 6, // Balanced speed/size ratio (0-9)
22
+ IMAGE_MAX_WIDTH: 1920,
23
+ IMAGE_MAX_HEIGHT: 1080,
24
+ IMAGE_WEBP_QUALITY: 0.8,
25
+ IMAGE_JPEG_QUALITY: 0.85,
26
+ IMAGE_LOAD_TIMEOUT: 3000, // milliseconds
27
+ };
28
+ // Singleton instances for performance
29
+ let textEncoder = null;
30
+ let textDecoder = null;
31
+ let webpSupportCache = null;
32
+ /**
33
+ * Get or create TextEncoder instance
34
+ */
35
+ function getTextEncoder() {
36
+ if (!textEncoder) {
37
+ textEncoder = new TextEncoder();
38
+ }
39
+ return textEncoder;
40
+ }
41
+ /**
42
+ * Get or create TextDecoder instance
43
+ */
44
+ function getTextDecoder() {
45
+ if (!textDecoder) {
46
+ textDecoder = new TextDecoder();
47
+ }
48
+ return textDecoder;
49
+ }
50
+ /**
51
+ * Convert data to string representation
52
+ */
53
+ function dataToString(data) {
54
+ return typeof data === 'string' ? data : JSON.stringify(data);
55
+ }
56
+ /**
57
+ * Compress JSON or string data using gzip
58
+ * @param data - Data to compress (will be JSON stringified if object)
59
+ * @param config - Optional compression configuration
60
+ * @returns Compressed data as Uint8Array
61
+ */
62
+ async function compressData(data, config) {
63
+ var _a;
64
+ try {
65
+ const jsonString = dataToString(data);
66
+ const encoder = getTextEncoder();
67
+ const uint8Data = encoder.encode(jsonString);
68
+ const gzipLevel = (_a = config === null || config === void 0 ? void 0 : config.gzipLevel) !== null && _a !== void 0 ? _a : COMPRESSION_DEFAULTS.GZIP_LEVEL;
69
+ // pako.gzip already returns Uint8Array, no need to wrap it
70
+ const compressed = pako_1.default.gzip(uint8Data, { level: gzipLevel });
71
+ return compressed;
72
+ }
73
+ catch (error) {
74
+ logger.error('Compression failed:', error);
75
+ throw error;
76
+ }
77
+ }
78
+ /**
79
+ * Try to parse string as JSON, return string if not valid JSON
80
+ */
81
+ function tryParseJSON(jsonString) {
82
+ try {
83
+ return JSON.parse(jsonString);
84
+ }
85
+ catch (_a) {
86
+ return jsonString;
87
+ }
88
+ }
89
+ /**
90
+ * Decompress gzipped data back to original format
91
+ * Useful for testing and verification
92
+ * @param compressed - Compressed Uint8Array data
93
+ * @param config - Optional configuration
94
+ * @returns Decompressed and parsed data (or string if input was string)
95
+ */
96
+ function decompressData(compressed, config) {
97
+ try {
98
+ const decompressed = pako_1.default.ungzip(compressed);
99
+ const decoder = getTextDecoder();
100
+ const jsonString = decoder.decode(decompressed);
101
+ return tryParseJSON(jsonString);
102
+ }
103
+ catch (error) {
104
+ if ((config === null || config === void 0 ? void 0 : config.verbose) !== false) {
105
+ (0, logger_1.getLogger)().error('Decompression failed:', error);
106
+ }
107
+ throw new Error(`Failed to decompress data: ${error instanceof Error ? error.message : 'Unknown error'}`);
108
+ }
109
+ }
110
+ /**
111
+ * Check if code is running in browser environment
112
+ */
113
+ function isBrowserEnvironment() {
114
+ return (typeof document !== 'undefined' &&
115
+ typeof Image !== 'undefined' &&
116
+ typeof HTMLCanvasElement !== 'undefined');
117
+ }
118
+ /**
119
+ * Check if browser supports WebP format (cached result)
120
+ */
121
+ function supportsWebP() {
122
+ if (webpSupportCache !== null) {
123
+ return webpSupportCache;
124
+ }
125
+ if (!isBrowserEnvironment()) {
126
+ webpSupportCache = false;
127
+ return false;
128
+ }
129
+ try {
130
+ const canvas = document.createElement('canvas');
131
+ webpSupportCache = canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
132
+ }
133
+ catch (_a) {
134
+ webpSupportCache = false;
135
+ }
136
+ return webpSupportCache;
137
+ }
138
+ /**
139
+ * Load image from base64 string with timeout
140
+ */
141
+ function loadImage(base64, timeout) {
142
+ return new Promise((resolve, reject) => {
143
+ const img = new Image();
144
+ const timer = setTimeout(() => {
145
+ reject(new Error(`Image load timeout after ${timeout}ms`));
146
+ }, timeout);
147
+ img.onload = () => {
148
+ clearTimeout(timer);
149
+ resolve(img);
150
+ };
151
+ img.onerror = () => {
152
+ clearTimeout(timer);
153
+ reject(new Error('Failed to load image'));
154
+ };
155
+ img.src = base64;
156
+ });
157
+ }
158
+ /**
159
+ * Calculate resized dimensions maintaining aspect ratio
160
+ */
161
+ function calculateResizedDimensions(width, height, maxWidth, maxHeight) {
162
+ let newWidth = width;
163
+ let newHeight = height;
164
+ if (newWidth > maxWidth) {
165
+ newHeight = (newHeight * maxWidth) / newWidth;
166
+ newWidth = maxWidth;
167
+ }
168
+ if (newHeight > maxHeight) {
169
+ newWidth = (newWidth * maxHeight) / newHeight;
170
+ newHeight = maxHeight;
171
+ }
172
+ return { width: newWidth, height: newHeight };
173
+ }
174
+ /**
175
+ * Optimize and compress screenshot image
176
+ * Converts to WebP if supported, resizes if too large, then compresses
177
+ * @param base64 - Base64 encoded image (PNG or other format)
178
+ * @param config - Optional compression configuration
179
+ * @returns Optimized base64 image string
180
+ */
181
+ async function compressImage(base64, config) {
182
+ var _a, _b, _c, _d;
183
+ try {
184
+ if (!isBrowserEnvironment()) {
185
+ return base64;
186
+ }
187
+ const maxWidth = (_a = config === null || config === void 0 ? void 0 : config.imageMaxWidth) !== null && _a !== void 0 ? _a : COMPRESSION_DEFAULTS.IMAGE_MAX_WIDTH;
188
+ const maxHeight = (_b = config === null || config === void 0 ? void 0 : config.imageMaxHeight) !== null && _b !== void 0 ? _b : COMPRESSION_DEFAULTS.IMAGE_MAX_HEIGHT;
189
+ const webpQuality = (_c = config === null || config === void 0 ? void 0 : config.webpQuality) !== null && _c !== void 0 ? _c : COMPRESSION_DEFAULTS.IMAGE_WEBP_QUALITY;
190
+ const jpegQuality = (_d = config === null || config === void 0 ? void 0 : config.jpegQuality) !== null && _d !== void 0 ? _d : COMPRESSION_DEFAULTS.IMAGE_JPEG_QUALITY;
191
+ const timeout = COMPRESSION_DEFAULTS.IMAGE_LOAD_TIMEOUT;
192
+ const img = await loadImage(base64, timeout);
193
+ const canvas = document.createElement('canvas');
194
+ const ctx = canvas.getContext('2d');
195
+ if (!ctx) {
196
+ throw new Error('Failed to get 2D canvas context');
197
+ }
198
+ const { width, height } = calculateResizedDimensions(img.width, img.height, maxWidth, maxHeight);
199
+ canvas.width = width;
200
+ canvas.height = height;
201
+ ctx.drawImage(img, 0, 0, width, height);
202
+ if (supportsWebP()) {
203
+ return canvas.toDataURL('image/webp', webpQuality);
204
+ }
205
+ else {
206
+ return canvas.toDataURL('image/jpeg', jpegQuality);
207
+ }
208
+ }
209
+ catch (error) {
210
+ if ((config === null || config === void 0 ? void 0 : config.verbose) !== false) {
211
+ (0, logger_1.getLogger)().error('Image compression failed:', error);
212
+ }
213
+ return base64;
214
+ }
215
+ }
216
+ /**
217
+ * Calculate compression ratio for analytics
218
+ * @param originalSize - Original data size in bytes
219
+ * @param compressedSize - Compressed data size in bytes
220
+ * @returns Compression ratio as percentage (0-100)
221
+ */
222
+ function getCompressionRatio(originalSize, compressedSize) {
223
+ if (originalSize <= 0) {
224
+ return 0;
225
+ }
226
+ return Math.round((1 - compressedSize / originalSize) * 100);
227
+ }
228
+ /**
229
+ * Estimate payload size before compression
230
+ * @param data - Data to estimate
231
+ * @returns Estimated size in bytes
232
+ */
233
+ function estimateSize(data) {
234
+ const jsonString = dataToString(data);
235
+ return getTextEncoder().encode(jsonString).length;
236
+ }
237
+ /**
238
+ * Reset cached instances (useful for testing)
239
+ * @internal
240
+ */
241
+ function resetCompressionCache() {
242
+ textEncoder = null;
243
+ textDecoder = null;
244
+ webpSupportCache = null;
245
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Offline queue for storing failed requests with localStorage persistence
3
+ */
4
+ import { type Logger } from '../utils/logger';
5
+ /**
6
+ * Storage interface for queue persistence
7
+ */
8
+ export interface StorageAdapter {
9
+ getItem(key: string): string | null;
10
+ setItem(key: string, value: string): void;
11
+ removeItem(key: string): void;
12
+ }
13
+ /**
14
+ * LocalStorage implementation of StorageAdapter
15
+ */
16
+ export declare class LocalStorageAdapter implements StorageAdapter {
17
+ getItem(key: string): string | null;
18
+ setItem(key: string, value: string): void;
19
+ removeItem(key: string): void;
20
+ }
21
+ export interface OfflineConfig {
22
+ /** Enable offline queue (default: false) */
23
+ enabled: boolean;
24
+ /** Maximum number of requests to queue (default: 10) */
25
+ maxQueueSize?: number;
26
+ }
27
+ export declare class OfflineQueue {
28
+ private config;
29
+ private logger;
30
+ private storage;
31
+ private requestCounter;
32
+ constructor(config: OfflineConfig, logger?: Logger, storage?: StorageAdapter);
33
+ /**
34
+ * Queue a request for offline retry
35
+ */
36
+ enqueue(endpoint: string, body: BodyInit, headers: Record<string, string>): void;
37
+ /**
38
+ * Process offline queue
39
+ */
40
+ process(retryableStatusCodes: number[]): Promise<void>;
41
+ /**
42
+ * Clear offline queue
43
+ */
44
+ clear(): void;
45
+ /**
46
+ * Get queue size
47
+ */
48
+ size(): number;
49
+ /**
50
+ * Serialize body to string format
51
+ */
52
+ private serializeBody;
53
+ /**
54
+ * Create a queued request object
55
+ */
56
+ private createQueuedRequest;
57
+ /**
58
+ * Validate that item size doesn't exceed localStorage limits
59
+ */
60
+ private validateItemSize;
61
+ private getQueue;
62
+ private saveQueue;
63
+ /**
64
+ * Check if error is a quota exceeded error (cross-browser compatible)
65
+ */
66
+ private isQuotaExceededError;
67
+ /**
68
+ * Clear oldest 50% of items and retry save
69
+ */
70
+ private clearOldestItems;
71
+ private generateRequestId;
72
+ }
73
+ /**
74
+ * Global function to clear offline queue
75
+ */
76
+ export declare function clearOfflineQueue(): void;
@@ -0,0 +1,301 @@
1
+ "use strict";
2
+ /**
3
+ * Offline queue for storing failed requests with localStorage persistence
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.OfflineQueue = exports.LocalStorageAdapter = void 0;
7
+ exports.clearOfflineQueue = clearOfflineQueue;
8
+ const logger_1 = require("../utils/logger");
9
+ /**
10
+ * LocalStorage implementation of StorageAdapter
11
+ */
12
+ class LocalStorageAdapter {
13
+ getItem(key) {
14
+ try {
15
+ if (typeof localStorage === 'undefined') {
16
+ return null;
17
+ }
18
+ return localStorage.getItem(key);
19
+ }
20
+ catch (_a) {
21
+ return null;
22
+ }
23
+ }
24
+ setItem(key, value) {
25
+ if (typeof localStorage === 'undefined') {
26
+ return;
27
+ }
28
+ localStorage.setItem(key, value);
29
+ }
30
+ removeItem(key) {
31
+ try {
32
+ if (typeof localStorage === 'undefined') {
33
+ return;
34
+ }
35
+ localStorage.removeItem(key);
36
+ }
37
+ catch (_a) {
38
+ // Ignore errors
39
+ }
40
+ }
41
+ }
42
+ exports.LocalStorageAdapter = LocalStorageAdapter;
43
+ // ============================================================================
44
+ // CONSTANTS
45
+ // ============================================================================
46
+ const QUEUE_STORAGE_KEY = 'bugspotter_offline_queue';
47
+ const QUEUE_EXPIRY_DAYS = 7;
48
+ const MAX_RETRY_ATTEMPTS = 5;
49
+ const MAX_ITEM_SIZE_BYTES = 100 * 1024; // 100KB per item
50
+ const DEFAULT_OFFLINE_CONFIG = {
51
+ enabled: false,
52
+ maxQueueSize: 10,
53
+ };
54
+ // ============================================================================
55
+ // OFFLINE QUEUE CLASS
56
+ // ============================================================================
57
+ class OfflineQueue {
58
+ constructor(config, logger, storage) {
59
+ this.requestCounter = 0;
60
+ this.config = Object.assign(Object.assign({}, DEFAULT_OFFLINE_CONFIG), config);
61
+ this.logger = logger || (0, logger_1.getLogger)();
62
+ this.storage = storage || new LocalStorageAdapter();
63
+ }
64
+ /**
65
+ * Queue a request for offline retry
66
+ */
67
+ enqueue(endpoint, body, headers) {
68
+ try {
69
+ const serializedBody = this.serializeBody(body);
70
+ if (!serializedBody || !this.validateItemSize(serializedBody)) {
71
+ return;
72
+ }
73
+ const queue = this.getQueue();
74
+ // Ensure space in queue
75
+ if (queue.length >= this.config.maxQueueSize) {
76
+ this.logger.warn(`Offline queue is full (${this.config.maxQueueSize}), removing oldest request`);
77
+ queue.shift();
78
+ }
79
+ queue.push(this.createQueuedRequest(endpoint, serializedBody, headers));
80
+ this.saveQueue(queue);
81
+ this.logger.log(`Request queued for offline retry (queue size: ${queue.length})`);
82
+ }
83
+ catch (error) {
84
+ this.logger.error('Failed to queue request for offline retry:', error);
85
+ }
86
+ }
87
+ /**
88
+ * Process offline queue
89
+ */
90
+ async process(retryableStatusCodes) {
91
+ const queue = this.getQueue();
92
+ if (queue.length === 0) {
93
+ return;
94
+ }
95
+ this.logger.log(`Processing offline queue (${queue.length} requests)`);
96
+ const successfulIds = [];
97
+ const failedRequests = [];
98
+ for (const request of queue) {
99
+ // Check if request has exceeded max retry attempts
100
+ if (request.attempts >= MAX_RETRY_ATTEMPTS) {
101
+ this.logger.warn(`Max retry attempts (${MAX_RETRY_ATTEMPTS}) reached for request (id: ${request.id}), removing`);
102
+ continue;
103
+ }
104
+ // Check if request has expired
105
+ const age = Date.now() - request.timestamp;
106
+ const maxAge = QUEUE_EXPIRY_DAYS * 24 * 60 * 60 * 1000;
107
+ if (age > maxAge) {
108
+ this.logger.warn(`Removing expired queued request (id: ${request.id})`);
109
+ continue;
110
+ }
111
+ try {
112
+ // Attempt to send
113
+ const response = await fetch(request.endpoint, {
114
+ method: 'POST',
115
+ headers: request.headers,
116
+ body: request.body,
117
+ });
118
+ if (response.ok) {
119
+ this.logger.log(`Successfully sent queued request (id: ${request.id})`);
120
+ successfulIds.push(request.id);
121
+ }
122
+ else if (retryableStatusCodes.includes(response.status)) {
123
+ // Keep in queue for next attempt
124
+ request.attempts++;
125
+ failedRequests.push(request);
126
+ this.logger.warn(`Queued request failed with status ${response.status}, will retry later (id: ${request.id})`);
127
+ }
128
+ else {
129
+ // Non-retryable error, remove from queue
130
+ this.logger.warn(`Queued request failed with non-retryable status ${response.status}, removing (id: ${request.id})`);
131
+ }
132
+ }
133
+ catch (error) {
134
+ // Network error, keep in queue
135
+ request.attempts++;
136
+ failedRequests.push(request);
137
+ this.logger.warn(`Queued request failed with network error, will retry later (id: ${request.id}):`, error);
138
+ }
139
+ }
140
+ // Update queue (remove successful and expired, keep failed)
141
+ this.saveQueue(failedRequests);
142
+ if (successfulIds.length > 0 || failedRequests.length < queue.length) {
143
+ this.logger.log(`Offline queue processed: ${successfulIds.length} successful, ${failedRequests.length} remaining`);
144
+ }
145
+ }
146
+ /**
147
+ * Clear offline queue
148
+ */
149
+ clear() {
150
+ try {
151
+ this.storage.removeItem(QUEUE_STORAGE_KEY);
152
+ }
153
+ catch (_a) {
154
+ // Ignore storage errors
155
+ }
156
+ }
157
+ /**
158
+ * Get queue size
159
+ */
160
+ size() {
161
+ return this.getQueue().length;
162
+ }
163
+ // ============================================================================
164
+ // PRIVATE METHODS
165
+ // ============================================================================
166
+ /**
167
+ * Serialize body to string format
168
+ */
169
+ serializeBody(body) {
170
+ if (typeof body === 'string') {
171
+ return body;
172
+ }
173
+ if (body instanceof Blob) {
174
+ this.logger.warn('Cannot queue Blob for offline retry, skipping');
175
+ return null;
176
+ }
177
+ return JSON.stringify(body);
178
+ }
179
+ /**
180
+ * Create a queued request object
181
+ */
182
+ createQueuedRequest(endpoint, body, headers) {
183
+ return {
184
+ id: this.generateRequestId(),
185
+ endpoint,
186
+ body,
187
+ headers,
188
+ timestamp: Date.now(),
189
+ attempts: 0,
190
+ };
191
+ }
192
+ /**
193
+ * Validate that item size doesn't exceed localStorage limits
194
+ */
195
+ validateItemSize(body) {
196
+ const sizeInBytes = new Blob([body]).size;
197
+ if (sizeInBytes > MAX_ITEM_SIZE_BYTES) {
198
+ this.logger.warn(`Request body too large (${sizeInBytes} bytes), skipping queue`);
199
+ return false;
200
+ }
201
+ return true;
202
+ }
203
+ getQueue() {
204
+ try {
205
+ const stored = this.storage.getItem(QUEUE_STORAGE_KEY);
206
+ if (!stored) {
207
+ return [];
208
+ }
209
+ return JSON.parse(stored);
210
+ }
211
+ catch (error) {
212
+ // Log corrupted data and clear it to prevent repeated errors
213
+ this.logger.warn('Failed to parse offline queue data, clearing corrupted queue:', error);
214
+ this.clear();
215
+ return [];
216
+ }
217
+ }
218
+ saveQueue(queue) {
219
+ try {
220
+ this.storage.setItem(QUEUE_STORAGE_KEY, JSON.stringify(queue));
221
+ }
222
+ catch (error) {
223
+ // Handle quota exceeded error (check multiple properties for cross-browser compatibility)
224
+ if (this.isQuotaExceededError(error)) {
225
+ this.logger.error('localStorage quota exceeded, clearing oldest items');
226
+ this.clearOldestItems(queue);
227
+ }
228
+ else {
229
+ this.logger.error('Failed to save offline queue:', error);
230
+ }
231
+ }
232
+ }
233
+ /**
234
+ * Check if error is a quota exceeded error (cross-browser compatible)
235
+ */
236
+ isQuotaExceededError(error) {
237
+ if (!(error instanceof Error)) {
238
+ return false;
239
+ }
240
+ // Check error name (standard)
241
+ if (error.name === 'QuotaExceededError') {
242
+ return true;
243
+ }
244
+ // Check DOMException code (Safari, older browsers)
245
+ if ('code' in error && error.code === 22) {
246
+ return true;
247
+ }
248
+ // Check error message as fallback (Firefox, Chrome variants)
249
+ const message = error.message.toLowerCase();
250
+ return message.includes('quota') || message.includes('storage') || message.includes('exceeded');
251
+ }
252
+ /**
253
+ * Clear oldest 50% of items and retry save
254
+ */
255
+ clearOldestItems(queue) {
256
+ try {
257
+ const trimmedQueue = queue.slice(Math.floor(queue.length / 2));
258
+ this.storage.setItem(QUEUE_STORAGE_KEY, JSON.stringify(trimmedQueue));
259
+ this.logger.log(`Trimmed offline queue to ${trimmedQueue.length} items due to quota`);
260
+ }
261
+ catch (_a) {
262
+ // If still failing, clear everything
263
+ this.logger.error('Failed to save even after trimming, clearing queue');
264
+ this.clear();
265
+ }
266
+ }
267
+ generateRequestId() {
268
+ // Increment counter for additional entropy
269
+ this.requestCounter = (this.requestCounter + 1) % 10000;
270
+ // Use crypto.getRandomValues for better randomness (browser-safe fallback)
271
+ let randomPart;
272
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
273
+ const array = new Uint32Array(2);
274
+ crypto.getRandomValues(array);
275
+ randomPart = Array.from(array, (num) => {
276
+ return num.toString(36);
277
+ }).join('');
278
+ }
279
+ else {
280
+ // Fallback to Math.random for environments without crypto
281
+ randomPart =
282
+ Math.random().toString(36).substring(2, 9) + Math.random().toString(36).substring(2, 9);
283
+ }
284
+ return `req_${Date.now()}_${this.requestCounter}_${randomPart}`;
285
+ }
286
+ }
287
+ exports.OfflineQueue = OfflineQueue;
288
+ /**
289
+ * Global function to clear offline queue
290
+ */
291
+ function clearOfflineQueue() {
292
+ try {
293
+ const storage = new LocalStorageAdapter();
294
+ storage.removeItem(QUEUE_STORAGE_KEY);
295
+ }
296
+ catch (error) {
297
+ // Log error for debugging purposes
298
+ const logger = (0, logger_1.getLogger)();
299
+ logger.warn('Failed to clear offline queue:', error);
300
+ }
301
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Transport layer for bug report submission with flexible authentication,
3
+ * exponential backoff retry, and offline queue support
4
+ */
5
+ import { type Logger } from '../utils/logger';
6
+ import { type OfflineConfig } from './offline-queue';
7
+ export declare class TransportError extends Error {
8
+ readonly endpoint: string;
9
+ readonly cause?: Error | undefined;
10
+ constructor(message: string, endpoint: string, cause?: Error | undefined);
11
+ }
12
+ export declare class TokenRefreshError extends TransportError {
13
+ constructor(endpoint: string, cause?: Error);
14
+ }
15
+ export type AuthConfig = {
16
+ 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';
34
+ };
35
+ export interface RetryConfig {
36
+ /** Maximum number of retry attempts (default: 3) */
37
+ maxRetries?: number;
38
+ /** Base delay in milliseconds (default: 1000) */
39
+ baseDelay?: number;
40
+ /** Maximum delay in milliseconds (default: 30000) */
41
+ maxDelay?: number;
42
+ /** HTTP status codes to retry on (default: [502, 503, 504, 429]) */
43
+ retryOn?: number[];
44
+ }
45
+ export interface TransportOptions {
46
+ /** Authentication configuration */
47
+ auth?: AuthConfig;
48
+ /** Optional logger for debugging */
49
+ logger?: Logger;
50
+ /** Enable retry on token expiration (default: true) */
51
+ enableRetry?: boolean;
52
+ /** Retry configuration */
53
+ retry?: RetryConfig;
54
+ /** Offline queue configuration */
55
+ offline?: OfflineConfig;
56
+ }
57
+ /**
58
+ * Get authentication headers based on configuration
59
+ * @param auth - Authentication configuration
60
+ * @returns HTTP headers for authentication
61
+ */
62
+ export declare function getAuthHeaders(auth?: AuthConfig): Record<string, string>;
63
+ /**
64
+ * Submit request with authentication, exponential backoff retry, and offline queue support
65
+ *
66
+ * @param endpoint - API endpoint URL
67
+ * @param body - Request body (must be serializable for retry)
68
+ * @param contentHeaders - Content-related headers (Content-Type, etc.)
69
+ * @param authOrOptions - Auth config or TransportOptions
70
+ * @returns Response from the server
71
+ */
72
+ export declare function submitWithAuth(endpoint: string, body: BodyInit, contentHeaders: Record<string, string>, authOrOptions?: AuthConfig | TransportOptions): Promise<Response>;
73
+ export { clearOfflineQueue, type OfflineConfig } from './offline-queue';