@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.
- package/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +639 -0
- package/dist/bugspotter.min.js +2 -0
- package/dist/bugspotter.min.js.LICENSE.txt +14 -0
- package/dist/capture/base-capture.d.ts +34 -0
- package/dist/capture/base-capture.js +23 -0
- package/dist/capture/capture-lifecycle.d.ts +24 -0
- package/dist/capture/capture-lifecycle.js +2 -0
- package/dist/capture/console.d.ts +29 -0
- package/dist/capture/console.js +107 -0
- package/dist/capture/metadata.d.ts +21 -0
- package/dist/capture/metadata.js +76 -0
- package/dist/capture/network.d.ts +32 -0
- package/dist/capture/network.js +135 -0
- package/dist/capture/screenshot.d.ts +19 -0
- package/dist/capture/screenshot.js +52 -0
- package/dist/collectors/dom.d.ts +67 -0
- package/dist/collectors/dom.js +164 -0
- package/dist/collectors/index.d.ts +2 -0
- package/dist/collectors/index.js +5 -0
- package/dist/core/buffer.d.ts +50 -0
- package/dist/core/buffer.js +88 -0
- package/dist/core/circular-buffer.d.ts +42 -0
- package/dist/core/circular-buffer.js +77 -0
- package/dist/core/compress.d.ts +49 -0
- package/dist/core/compress.js +245 -0
- package/dist/core/offline-queue.d.ts +76 -0
- package/dist/core/offline-queue.js +301 -0
- package/dist/core/transport.d.ts +73 -0
- package/dist/core/transport.js +352 -0
- package/dist/core/upload-helpers.d.ts +32 -0
- package/dist/core/upload-helpers.js +79 -0
- package/dist/core/uploader.d.ts +70 -0
- package/dist/core/uploader.js +185 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.esm.js +205 -0
- package/dist/index.js +244 -0
- package/dist/utils/logger.d.ts +28 -0
- package/dist/utils/logger.js +84 -0
- package/dist/utils/sanitize-patterns.d.ts +103 -0
- package/dist/utils/sanitize-patterns.js +282 -0
- package/dist/utils/sanitize.d.ts +73 -0
- package/dist/utils/sanitize.js +254 -0
- package/dist/widget/button.d.ts +33 -0
- package/dist/widget/button.js +143 -0
- package/dist/widget/components/dom-element-cache.d.ts +62 -0
- package/dist/widget/components/dom-element-cache.js +105 -0
- package/dist/widget/components/form-validator.d.ts +66 -0
- package/dist/widget/components/form-validator.js +115 -0
- package/dist/widget/components/pii-detection-display.d.ts +64 -0
- package/dist/widget/components/pii-detection-display.js +142 -0
- package/dist/widget/components/redaction-canvas.d.ts +95 -0
- package/dist/widget/components/redaction-canvas.js +230 -0
- package/dist/widget/components/screenshot-processor.d.ts +44 -0
- package/dist/widget/components/screenshot-processor.js +191 -0
- package/dist/widget/components/style-manager.d.ts +37 -0
- package/dist/widget/components/style-manager.js +296 -0
- package/dist/widget/components/template-manager.d.ts +66 -0
- package/dist/widget/components/template-manager.js +198 -0
- package/dist/widget/modal.d.ts +62 -0
- package/dist/widget/modal.js +299 -0
- package/docs/CDN.md +213 -0
- package/docs/FRAMEWORK_INTEGRATION.md +1104 -0
- package/docs/PUBLISHING.md +550 -0
- package/docs/SESSION_REPLAY.md +381 -0
- package/package.json +90 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Transport layer for bug report submission with flexible authentication,
|
|
4
|
+
* exponential backoff retry, and offline queue support
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.clearOfflineQueue = exports.TokenRefreshError = exports.TransportError = void 0;
|
|
8
|
+
exports.getAuthHeaders = getAuthHeaders;
|
|
9
|
+
exports.submitWithAuth = submitWithAuth;
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
11
|
+
const offline_queue_1 = require("./offline-queue");
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// CONSTANTS
|
|
14
|
+
// ============================================================================
|
|
15
|
+
const TOKEN_REFRESH_STATUS = 401;
|
|
16
|
+
const JITTER_PERCENTAGE = 0.1;
|
|
17
|
+
const DEFAULT_ENABLE_RETRY = true;
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// CUSTOM ERROR TYPES
|
|
20
|
+
// ============================================================================
|
|
21
|
+
class TransportError extends Error {
|
|
22
|
+
constructor(message, endpoint, cause) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.endpoint = endpoint;
|
|
25
|
+
this.cause = cause;
|
|
26
|
+
this.name = 'TransportError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
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';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.TokenRefreshError = TokenRefreshError;
|
|
37
|
+
// Default configurations
|
|
38
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
39
|
+
maxRetries: 3,
|
|
40
|
+
baseDelay: 1000,
|
|
41
|
+
maxDelay: 30000,
|
|
42
|
+
retryOn: [502, 503, 504, 429],
|
|
43
|
+
};
|
|
44
|
+
const DEFAULT_OFFLINE_CONFIG = {
|
|
45
|
+
enabled: false,
|
|
46
|
+
maxQueueSize: 10,
|
|
47
|
+
};
|
|
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
|
+
};
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// RETRY HANDLER - Exponential Backoff Logic
|
|
75
|
+
// ============================================================================
|
|
76
|
+
class RetryHandler {
|
|
77
|
+
constructor(config, logger) {
|
|
78
|
+
this.config = config;
|
|
79
|
+
this.logger = logger;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Execute operation with exponential backoff retry
|
|
83
|
+
*/
|
|
84
|
+
async executeWithRetry(operation, shouldRetryStatus) {
|
|
85
|
+
let lastError = null;
|
|
86
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
87
|
+
try {
|
|
88
|
+
const response = await operation();
|
|
89
|
+
// Check if we should retry based on status code
|
|
90
|
+
if (shouldRetryStatus(response.status) && attempt < this.config.maxRetries) {
|
|
91
|
+
const delay = this.calculateDelay(attempt, response);
|
|
92
|
+
this.logger.warn(`Request failed with status ${response.status}, retrying in ${delay}ms (attempt ${attempt + 1}/${this.config.maxRetries})`);
|
|
93
|
+
await sleep(delay);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Success or non-retryable status
|
|
97
|
+
return response;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
lastError = error;
|
|
101
|
+
// Retry on network errors
|
|
102
|
+
if (attempt < this.config.maxRetries) {
|
|
103
|
+
const delay = this.calculateDelay(attempt);
|
|
104
|
+
this.logger.warn(`Network error, retrying in ${delay}ms (attempt ${attempt + 1}/${this.config.maxRetries}):`, error);
|
|
105
|
+
await sleep(delay);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// All retries exhausted
|
|
111
|
+
throw lastError || new Error('Request failed after all retry attempts');
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Calculate retry delay with exponential backoff and jitter
|
|
115
|
+
*/
|
|
116
|
+
calculateDelay(attempt, response) {
|
|
117
|
+
var _a, _b;
|
|
118
|
+
// Check for Retry-After header
|
|
119
|
+
if ((_b = (_a = response === null || response === void 0 ? void 0 : response.headers) === null || _a === void 0 ? void 0 : _a.has) === null || _b === void 0 ? void 0 : _b.call(_a, 'Retry-After')) {
|
|
120
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
121
|
+
const retryAfterSeconds = parseInt(retryAfter, 10);
|
|
122
|
+
if (!isNaN(retryAfterSeconds)) {
|
|
123
|
+
return Math.min(retryAfterSeconds * 1000, this.config.maxDelay);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Exponential backoff: baseDelay * 2^attempt
|
|
127
|
+
const exponentialDelay = this.config.baseDelay * Math.pow(2, attempt);
|
|
128
|
+
// Add jitter: ±10% randomization
|
|
129
|
+
const jitter = exponentialDelay * JITTER_PERCENTAGE * (Math.random() * 2 - 1);
|
|
130
|
+
const delayWithJitter = exponentialDelay + jitter;
|
|
131
|
+
// Cap at maxDelay
|
|
132
|
+
return Math.min(delayWithJitter, this.config.maxDelay);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
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
|
+
// ============================================================================
|
|
187
|
+
// HELPER FUNCTIONS
|
|
188
|
+
// ============================================================================
|
|
189
|
+
/**
|
|
190
|
+
* Process offline queue in background
|
|
191
|
+
*/
|
|
192
|
+
async function processQueueInBackground(offlineConfig, retryConfig, logger) {
|
|
193
|
+
if (!offlineConfig.enabled) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const queue = new offline_queue_1.OfflineQueue(offlineConfig, logger);
|
|
197
|
+
queue.process(retryConfig.retryOn).catch((error) => {
|
|
198
|
+
logger.warn('Failed to process offline queue:', error);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Handle offline failure by queueing request
|
|
203
|
+
*/
|
|
204
|
+
async function handleOfflineFailure(error, endpoint, body, contentHeaders, auth, offlineConfig, logger) {
|
|
205
|
+
if (!offlineConfig.enabled || !isNetworkError(error)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
logger.warn('Network error detected, queueing request for offline retry');
|
|
209
|
+
const queue = new offline_queue_1.OfflineQueue(offlineConfig, logger);
|
|
210
|
+
const authHeaders = getAuthHeaders(auth);
|
|
211
|
+
await queue.enqueue(endpoint, body, Object.assign(Object.assign({}, contentHeaders), authHeaders));
|
|
212
|
+
}
|
|
213
|
+
// ============================================================================
|
|
214
|
+
// PUBLIC API
|
|
215
|
+
// ============================================================================
|
|
216
|
+
/**
|
|
217
|
+
* Get authentication headers based on configuration
|
|
218
|
+
* @param auth - Authentication configuration
|
|
219
|
+
* @returns HTTP headers for authentication
|
|
220
|
+
*/
|
|
221
|
+
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) : {};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Submit request with authentication, exponential backoff retry, and offline queue support
|
|
232
|
+
*
|
|
233
|
+
* @param endpoint - API endpoint URL
|
|
234
|
+
* @param body - Request body (must be serializable for retry)
|
|
235
|
+
* @param contentHeaders - Content-related headers (Content-Type, etc.)
|
|
236
|
+
* @param authOrOptions - Auth config or TransportOptions
|
|
237
|
+
* @returns Response from the server
|
|
238
|
+
*/
|
|
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);
|
|
242
|
+
// Process offline queue on each request (run in background without awaiting)
|
|
243
|
+
processQueueInBackground(offlineConfig, retryConfig, logger);
|
|
244
|
+
try {
|
|
245
|
+
// Send with retry logic
|
|
246
|
+
const response = await sendWithRetry(endpoint, body, contentHeaders, auth, retryConfig, logger, enableRetry);
|
|
247
|
+
return response;
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
// Queue for offline retry if enabled
|
|
251
|
+
await handleOfflineFailure(error, endpoint, body, contentHeaders, auth, offlineConfig, logger);
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
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
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Make HTTP request with auth headers
|
|
265
|
+
*/
|
|
266
|
+
async function makeRequest(endpoint, body, contentHeaders, auth) {
|
|
267
|
+
const authHeaders = getAuthHeaders(auth);
|
|
268
|
+
const headers = Object.assign(Object.assign({}, contentHeaders), authHeaders);
|
|
269
|
+
return fetch(endpoint, {
|
|
270
|
+
method: 'POST',
|
|
271
|
+
headers,
|
|
272
|
+
body,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Send request with exponential backoff retry
|
|
277
|
+
*/
|
|
278
|
+
async function sendWithRetry(endpoint, body, contentHeaders, auth, retryConfig, logger, enableTokenRetry) {
|
|
279
|
+
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
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Sleep for specified milliseconds
|
|
300
|
+
*/
|
|
301
|
+
function sleep(ms) {
|
|
302
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Check if error is a network error (more specific to avoid false positives)
|
|
306
|
+
*/
|
|
307
|
+
function isNetworkError(error) {
|
|
308
|
+
if (!(error instanceof Error)) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
const message = error.message.toLowerCase();
|
|
312
|
+
// Check for specific network error patterns
|
|
313
|
+
return (
|
|
314
|
+
// Standard fetch network errors
|
|
315
|
+
message.includes('failed to fetch') ||
|
|
316
|
+
message.includes('network request failed') ||
|
|
317
|
+
message.includes('networkerror') ||
|
|
318
|
+
// Connection issues
|
|
319
|
+
message.includes('network error') ||
|
|
320
|
+
message.includes('connection') ||
|
|
321
|
+
// Timeout errors
|
|
322
|
+
message.includes('timeout') ||
|
|
323
|
+
// Standard error names
|
|
324
|
+
error.name === 'NetworkError' ||
|
|
325
|
+
error.name === 'AbortError' ||
|
|
326
|
+
// TypeError only if it mentions fetch or network
|
|
327
|
+
(error.name === 'TypeError' && (message.includes('fetch') || message.includes('network'))));
|
|
328
|
+
}
|
|
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
|
+
// Re-export offline queue utilities
|
|
351
|
+
var offline_queue_2 = require("./offline-queue");
|
|
352
|
+
Object.defineProperty(exports, "clearOfflineQueue", { enumerable: true, get: function () { return offline_queue_2.clearOfflineQueue; } });
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload Helpers
|
|
3
|
+
* Utilities for preparing data for presigned URL uploads
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Compress replay events using CompressionStream API (browser native)
|
|
7
|
+
* Falls back to no compression if API not available
|
|
8
|
+
* @param events - Session replay events array
|
|
9
|
+
* @returns Compressed Blob (gzip)
|
|
10
|
+
*/
|
|
11
|
+
export declare function compressReplayEvents(events: unknown[]): Promise<Blob>;
|
|
12
|
+
/**
|
|
13
|
+
* Convert screenshot canvas to Blob
|
|
14
|
+
* @param canvas - HTML Canvas element with screenshot
|
|
15
|
+
* @param quality - JPEG quality (0-1), default 0.9
|
|
16
|
+
* @returns Screenshot Blob
|
|
17
|
+
*/
|
|
18
|
+
export declare function canvasToBlob(canvas: HTMLCanvasElement, quality?: number): Promise<Blob>;
|
|
19
|
+
/**
|
|
20
|
+
* Estimate compressed size of replay events
|
|
21
|
+
* Uses rough heuristic: gzip typically achieves 80-90% compression for JSON
|
|
22
|
+
* @param events - Replay events array
|
|
23
|
+
* @returns Estimated compressed size in bytes
|
|
24
|
+
*/
|
|
25
|
+
export declare function estimateCompressedReplaySize(events: unknown[]): number;
|
|
26
|
+
/**
|
|
27
|
+
* Check if file size is within upload limits
|
|
28
|
+
* @param blob - File or Blob to check
|
|
29
|
+
* @param maxSizeMB - Maximum size in megabytes
|
|
30
|
+
* @returns True if within limit
|
|
31
|
+
*/
|
|
32
|
+
export declare function isWithinSizeLimit(blob: Blob, maxSizeMB: number): boolean;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Upload Helpers
|
|
4
|
+
* Utilities for preparing data for presigned URL uploads
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.compressReplayEvents = compressReplayEvents;
|
|
8
|
+
exports.canvasToBlob = canvasToBlob;
|
|
9
|
+
exports.estimateCompressedReplaySize = estimateCompressedReplaySize;
|
|
10
|
+
exports.isWithinSizeLimit = isWithinSizeLimit;
|
|
11
|
+
/**
|
|
12
|
+
* Compress replay events using CompressionStream API (browser native)
|
|
13
|
+
* Falls back to no compression if API not available
|
|
14
|
+
* @param events - Session replay events array
|
|
15
|
+
* @returns Compressed Blob (gzip)
|
|
16
|
+
*/
|
|
17
|
+
async function compressReplayEvents(events) {
|
|
18
|
+
// Convert events to JSON string
|
|
19
|
+
const jsonString = JSON.stringify(events);
|
|
20
|
+
const textEncoder = new TextEncoder();
|
|
21
|
+
const data = textEncoder.encode(jsonString);
|
|
22
|
+
// Check if CompressionStream is supported (Chrome 80+, Firefox 113+, Safari 16.4+)
|
|
23
|
+
if (typeof CompressionStream === 'undefined') {
|
|
24
|
+
console.warn('CompressionStream not supported, uploading uncompressed replay data');
|
|
25
|
+
return new Blob([data], { type: 'application/json' });
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
// Use modern streaming API: Blob → ReadableStream → CompressionStream → Response → Blob
|
|
29
|
+
const blob = new Blob([data]);
|
|
30
|
+
const compressedStream = blob.stream().pipeThrough(new CompressionStream('gzip'));
|
|
31
|
+
return await new Response(compressedStream, {
|
|
32
|
+
headers: { 'Content-Type': 'application/gzip' },
|
|
33
|
+
}).blob();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error('Compression failed, uploading uncompressed:', error);
|
|
37
|
+
return new Blob([data], { type: 'application/json' });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Convert screenshot canvas to Blob
|
|
42
|
+
* @param canvas - HTML Canvas element with screenshot
|
|
43
|
+
* @param quality - JPEG quality (0-1), default 0.9
|
|
44
|
+
* @returns Screenshot Blob
|
|
45
|
+
*/
|
|
46
|
+
async function canvasToBlob(canvas, quality = 0.9) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
canvas.toBlob((blob) => {
|
|
49
|
+
if (blob) {
|
|
50
|
+
resolve(blob);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
reject(new Error('Failed to convert canvas to Blob'));
|
|
54
|
+
}
|
|
55
|
+
}, 'image/png', quality);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Estimate compressed size of replay events
|
|
60
|
+
* Uses rough heuristic: gzip typically achieves 80-90% compression for JSON
|
|
61
|
+
* @param events - Replay events array
|
|
62
|
+
* @returns Estimated compressed size in bytes
|
|
63
|
+
*/
|
|
64
|
+
function estimateCompressedReplaySize(events) {
|
|
65
|
+
const jsonString = JSON.stringify(events);
|
|
66
|
+
const uncompressedSize = new TextEncoder().encode(jsonString).length;
|
|
67
|
+
// Assume 85% compression ratio (conservative estimate)
|
|
68
|
+
return Math.round(uncompressedSize * 0.15);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if file size is within upload limits
|
|
72
|
+
* @param blob - File or Blob to check
|
|
73
|
+
* @param maxSizeMB - Maximum size in megabytes
|
|
74
|
+
* @returns True if within limit
|
|
75
|
+
*/
|
|
76
|
+
function isWithinSizeLimit(blob, maxSizeMB) {
|
|
77
|
+
const maxBytes = maxSizeMB * 1024 * 1024;
|
|
78
|
+
return blob.size <= maxBytes;
|
|
79
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DirectUploader
|
|
3
|
+
* Handles direct client-to-storage uploads using presigned URLs
|
|
4
|
+
*/
|
|
5
|
+
export interface DirectUploadConfig {
|
|
6
|
+
apiEndpoint: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
projectId: string;
|
|
9
|
+
bugId: string;
|
|
10
|
+
}
|
|
11
|
+
export interface UploadProgress {
|
|
12
|
+
loaded: number;
|
|
13
|
+
total: number;
|
|
14
|
+
percentage: number;
|
|
15
|
+
}
|
|
16
|
+
export type UploadProgressCallback = (progress: UploadProgress) => void;
|
|
17
|
+
export interface UploadResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
storageKey?: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* DirectUploader handles uploading files directly to storage using presigned URLs
|
|
24
|
+
* This bypasses the API server for file data, reducing memory usage and improving performance
|
|
25
|
+
*/
|
|
26
|
+
export declare class DirectUploader {
|
|
27
|
+
private readonly config;
|
|
28
|
+
constructor(config: DirectUploadConfig);
|
|
29
|
+
/**
|
|
30
|
+
* Upload a screenshot file directly to storage
|
|
31
|
+
* @param file - Screenshot file or Blob
|
|
32
|
+
* @param onProgress - Optional progress callback
|
|
33
|
+
* @returns Upload result with storage key
|
|
34
|
+
*/
|
|
35
|
+
uploadScreenshot(file: File | Blob, onProgress?: UploadProgressCallback): Promise<UploadResult>;
|
|
36
|
+
/**
|
|
37
|
+
* Upload a compressed session replay directly to storage
|
|
38
|
+
* @param compressedData - Gzip-compressed replay data
|
|
39
|
+
* @param onProgress - Optional progress callback
|
|
40
|
+
* @returns Upload result with storage key
|
|
41
|
+
*/
|
|
42
|
+
uploadReplay(compressedData: Blob, onProgress?: UploadProgressCallback): Promise<UploadResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Upload an attachment file directly to storage
|
|
45
|
+
* @param file - Attachment file
|
|
46
|
+
* @param onProgress - Optional progress callback
|
|
47
|
+
* @returns Upload result with storage key
|
|
48
|
+
*/
|
|
49
|
+
uploadAttachment(file: File, onProgress?: UploadProgressCallback): Promise<UploadResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Generic file upload method
|
|
52
|
+
* 1. Request presigned URL from API
|
|
53
|
+
* 2. Upload file directly to storage using presigned URL
|
|
54
|
+
* 3. Confirm upload with API
|
|
55
|
+
*/
|
|
56
|
+
private uploadFile;
|
|
57
|
+
/**
|
|
58
|
+
* Request a presigned URL from the API
|
|
59
|
+
*/
|
|
60
|
+
private requestPresignedUrl;
|
|
61
|
+
/**
|
|
62
|
+
* Upload file to storage using presigned URL
|
|
63
|
+
* Uses XMLHttpRequest for progress tracking
|
|
64
|
+
*/
|
|
65
|
+
private uploadToStorage;
|
|
66
|
+
/**
|
|
67
|
+
* Confirm successful upload with the API
|
|
68
|
+
*/
|
|
69
|
+
private confirmUpload;
|
|
70
|
+
}
|