@bugspotter/sdk 0.1.0-alpha.2 → 0.1.2-alpha.5
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 +46 -0
- package/README.md +156 -84
- package/dist/bugspotter.min.js +1 -1
- package/dist/capture/network.js +5 -2
- package/dist/collectors/dom.js +2 -1
- package/dist/constants.d.ts +13 -0
- package/dist/constants.js +16 -0
- package/dist/core/file-upload-handler.d.ts +61 -0
- package/dist/core/file-upload-handler.js +157 -0
- package/dist/core/transport.d.ts +14 -25
- package/dist/core/transport.js +39 -145
- package/dist/index.d.ts +19 -7
- package/dist/index.esm.js +103 -47
- package/dist/index.js +115 -54
- package/dist/utils/config-validator.d.ts +14 -0
- package/dist/utils/config-validator.js +28 -0
- package/dist/utils/url-helpers.d.ts +28 -0
- package/dist/utils/url-helpers.js +64 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +11 -0
- package/dist/widget/button.d.ts +5 -0
- package/dist/widget/button.js +50 -12
- package/dist/widget/components/form-validator.d.ts +2 -2
- package/dist/widget/components/form-validator.js +7 -2
- package/dist/widget/modal.d.ts +1 -1
- package/dist/widget/modal.js +2 -1
- package/docs/SESSION_REPLAY.md +66 -4
- package/package.json +5 -2
- package/scripts/generate-version.js +39 -0
- package/tsconfig.cjs.json +15 -0
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { type FloatingButtonOptions } from './widget/button';
|
|
|
3
3
|
import type { eventWithTime } from '@rrweb/types';
|
|
4
4
|
import { type AuthConfig, type RetryConfig } from './core/transport';
|
|
5
5
|
import type { OfflineConfig } from './core/offline-queue';
|
|
6
|
+
import { VERSION } from './version';
|
|
7
|
+
export { VERSION };
|
|
6
8
|
export declare class BugSpotter {
|
|
7
9
|
private static instance;
|
|
8
10
|
private config;
|
|
@@ -19,11 +21,16 @@ export declare class BugSpotter {
|
|
|
19
21
|
/**
|
|
20
22
|
* Capture bug report data
|
|
21
23
|
* Note: Screenshot is captured for modal preview only (_screenshotPreview)
|
|
22
|
-
*
|
|
24
|
+
* File uploads use presigned URLs returned from the backend
|
|
23
25
|
*/
|
|
24
26
|
capture(): Promise<BugReport>;
|
|
25
27
|
private handleBugReport;
|
|
26
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Submit a bug report with file uploads via presigned URLs
|
|
30
|
+
* @param payload - Bug report payload with title, description, and report data
|
|
31
|
+
* @public - Exposed for programmatic submission (bypassing modal)
|
|
32
|
+
*/
|
|
33
|
+
submit(payload: BugReportPayload): Promise<void>;
|
|
27
34
|
getConfig(): Readonly<BugSpotterConfig>;
|
|
28
35
|
destroy(): void;
|
|
29
36
|
}
|
|
@@ -31,8 +38,11 @@ export interface BugSpotterConfig {
|
|
|
31
38
|
endpoint?: string;
|
|
32
39
|
showWidget?: boolean;
|
|
33
40
|
widgetOptions?: FloatingButtonOptions;
|
|
34
|
-
/**
|
|
35
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Authentication configuration (required)
|
|
43
|
+
* API key authentication with project ID
|
|
44
|
+
*/
|
|
45
|
+
auth: AuthConfig;
|
|
36
46
|
/** Retry configuration for failed requests */
|
|
37
47
|
retry?: RetryConfig;
|
|
38
48
|
/** Offline queue configuration */
|
|
@@ -73,11 +83,10 @@ export interface BugSpotterConfig {
|
|
|
73
83
|
}
|
|
74
84
|
export interface BugReportPayload {
|
|
75
85
|
title: string;
|
|
76
|
-
description
|
|
86
|
+
description?: string;
|
|
77
87
|
report: BugReport;
|
|
78
88
|
}
|
|
79
89
|
export interface BugReport {
|
|
80
|
-
screenshotKey?: string;
|
|
81
90
|
console: Array<{
|
|
82
91
|
level: string;
|
|
83
92
|
message: string;
|
|
@@ -94,7 +103,6 @@ export interface BugReport {
|
|
|
94
103
|
}>;
|
|
95
104
|
metadata: BrowserMetadata;
|
|
96
105
|
replay?: eventWithTime[];
|
|
97
|
-
replayKey?: string;
|
|
98
106
|
_screenshotPreview?: string;
|
|
99
107
|
}
|
|
100
108
|
export type { BrowserMetadata } from './capture/metadata';
|
|
@@ -117,6 +125,9 @@ export type { UploadResult } from './core/uploader';
|
|
|
117
125
|
export { compressReplayEvents, canvasToBlob, estimateCompressedReplaySize, isWithinSizeLimit, } from './core/upload-helpers';
|
|
118
126
|
export { createSanitizer, Sanitizer } from './utils/sanitize';
|
|
119
127
|
export type { PIIPattern, CustomPattern, SanitizeConfig } from './utils/sanitize';
|
|
128
|
+
export { getApiBaseUrl, stripEndpointSuffix, InvalidEndpointError } from './utils/url-helpers';
|
|
129
|
+
export { validateAuthConfig } from './utils/config-validator';
|
|
130
|
+
export type { ValidationContext } from './utils/config-validator';
|
|
120
131
|
export { DEFAULT_PATTERNS, PATTERN_PRESETS, PATTERN_CATEGORIES, PatternBuilder, createPatternConfig, getPattern, getPatternsByCategory, validatePattern, } from './utils/sanitize';
|
|
121
132
|
export type { PIIPatternName, PatternDefinition } from './utils/sanitize';
|
|
122
133
|
export { FloatingButton } from './widget/button';
|
|
@@ -124,6 +135,7 @@ export type { FloatingButtonOptions } from './widget/button';
|
|
|
124
135
|
export { BugReportModal } from './widget/modal';
|
|
125
136
|
export type { BugReportData, BugReportModalOptions, PIIDetection } from './widget/modal';
|
|
126
137
|
export type { eventWithTime } from '@rrweb/types';
|
|
138
|
+
export { DEFAULT_REPLAY_DURATION_SECONDS, MAX_RECOMMENDED_REPLAY_DURATION_SECONDS, } from './constants';
|
|
127
139
|
/**
|
|
128
140
|
* Convenience function to sanitize text with default PII patterns
|
|
129
141
|
* Useful for quick sanitization without creating a Sanitizer instance
|
package/dist/index.esm.js
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
1
12
|
import { ScreenshotCapture } from './capture/screenshot';
|
|
2
13
|
import { ConsoleCapture } from './capture/console';
|
|
3
14
|
import { NetworkCapture } from './capture/network';
|
|
4
15
|
import { MetadataCapture } from './capture/metadata';
|
|
5
|
-
import { compressData, estimateSize, getCompressionRatio } from './core/compress';
|
|
6
16
|
import { FloatingButton } from './widget/button';
|
|
7
17
|
import { BugReportModal } from './widget/modal';
|
|
8
18
|
import { DOMCollector } from './collectors';
|
|
9
19
|
import { createSanitizer } from './utils/sanitize';
|
|
10
20
|
import { getLogger } from './utils/logger';
|
|
11
21
|
import { submitWithAuth } from './core/transport';
|
|
22
|
+
import { FileUploadHandler } from './core/file-upload-handler';
|
|
23
|
+
import { DEFAULT_REPLAY_DURATION_SECONDS } from './constants';
|
|
24
|
+
import { getApiBaseUrl } from './utils/url-helpers';
|
|
25
|
+
import { validateAuthConfig } from './utils/config-validator';
|
|
26
|
+
import { VERSION } from './version';
|
|
12
27
|
const logger = getLogger();
|
|
28
|
+
// Re-export VERSION for public API
|
|
29
|
+
export { VERSION };
|
|
13
30
|
export class BugSpotter {
|
|
14
31
|
constructor(config) {
|
|
15
32
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
@@ -27,12 +44,12 @@ export class BugSpotter {
|
|
|
27
44
|
this.console = new ConsoleCapture({ sanitizer: this.sanitizer });
|
|
28
45
|
this.network = new NetworkCapture({ sanitizer: this.sanitizer });
|
|
29
46
|
this.metadata = new MetadataCapture({ sanitizer: this.sanitizer });
|
|
30
|
-
// Note:
|
|
31
|
-
// See
|
|
47
|
+
// Note: FileUploadHandler is created per-report since it needs bugId
|
|
48
|
+
// See submit() method for initialization
|
|
32
49
|
// Initialize DOM collector if replay is enabled
|
|
33
50
|
if (((_g = config.replay) === null || _g === void 0 ? void 0 : _g.enabled) !== false) {
|
|
34
51
|
this.domCollector = new DOMCollector({
|
|
35
|
-
duration: (_j = (_h = config.replay) === null || _h === void 0 ? void 0 : _h.duration) !== null && _j !== void 0 ? _j :
|
|
52
|
+
duration: (_j = (_h = config.replay) === null || _h === void 0 ? void 0 : _h.duration) !== null && _j !== void 0 ? _j : DEFAULT_REPLAY_DURATION_SECONDS,
|
|
36
53
|
sampling: (_k = config.replay) === null || _k === void 0 ? void 0 : _k.sampling,
|
|
37
54
|
sanitizer: this.sanitizer,
|
|
38
55
|
});
|
|
@@ -58,7 +75,7 @@ export class BugSpotter {
|
|
|
58
75
|
/**
|
|
59
76
|
* Capture bug report data
|
|
60
77
|
* Note: Screenshot is captured for modal preview only (_screenshotPreview)
|
|
61
|
-
*
|
|
78
|
+
* File uploads use presigned URLs returned from the backend
|
|
62
79
|
*/
|
|
63
80
|
async capture() {
|
|
64
81
|
var _a, _b;
|
|
@@ -81,7 +98,7 @@ export class BugSpotter {
|
|
|
81
98
|
// Send to endpoint if configured
|
|
82
99
|
if (this.config.endpoint) {
|
|
83
100
|
try {
|
|
84
|
-
await this.
|
|
101
|
+
await this.submit(Object.assign(Object.assign({}, data), { report }));
|
|
85
102
|
logger.log('Bug report submitted successfully');
|
|
86
103
|
}
|
|
87
104
|
catch (error) {
|
|
@@ -94,57 +111,90 @@ export class BugSpotter {
|
|
|
94
111
|
});
|
|
95
112
|
modal.show(report._screenshotPreview || '');
|
|
96
113
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Submit a bug report with file uploads via presigned URLs
|
|
116
|
+
* @param payload - Bug report payload with title, description, and report data
|
|
117
|
+
* @public - Exposed for programmatic submission (bypassing modal)
|
|
118
|
+
*/
|
|
119
|
+
async submit(payload) {
|
|
120
|
+
var _a, _b, _c, _d, _e, _f;
|
|
121
|
+
validateAuthConfig({
|
|
122
|
+
endpoint: this.config.endpoint,
|
|
123
|
+
auth: this.config.auth,
|
|
124
|
+
});
|
|
125
|
+
logger.debug(`Submitting bug report to ${this.config.endpoint}`);
|
|
126
|
+
// Step 1: Create bug report and request presigned URLs
|
|
127
|
+
const { report } = payload, metadata = __rest(payload, ["report"]);
|
|
128
|
+
// Check what files we need to upload
|
|
129
|
+
const hasScreenshot = !!(report._screenshotPreview && report._screenshotPreview.startsWith('data:image/'));
|
|
130
|
+
const hasReplay = !!(report.replay && report.replay.length > 0);
|
|
131
|
+
logger.debug('File upload detection', {
|
|
132
|
+
hasScreenshot,
|
|
133
|
+
screenshotSize: ((_a = report._screenshotPreview) === null || _a === void 0 ? void 0 : _a.length) || 0,
|
|
134
|
+
hasReplay,
|
|
135
|
+
replayEventCount: ((_b = report.replay) === null || _b === void 0 ? void 0 : _b.length) || 0,
|
|
136
|
+
});
|
|
137
|
+
const createPayload = Object.assign(Object.assign({}, metadata), { report: {
|
|
138
|
+
console: report.console,
|
|
139
|
+
network: report.network,
|
|
140
|
+
metadata: report.metadata,
|
|
141
|
+
// Don't send replay events or screenshot in initial request
|
|
142
|
+
},
|
|
143
|
+
// Tell backend we have files so it can generate presigned URLs
|
|
144
|
+
hasScreenshot,
|
|
145
|
+
hasReplay });
|
|
101
146
|
const contentHeaders = {
|
|
102
147
|
'Content-Type': 'application/json',
|
|
103
148
|
};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// Try to compress the payload
|
|
108
|
-
const originalSize = estimateSize(payload);
|
|
109
|
-
const compressed = await compressData(payload);
|
|
110
|
-
const compressedSize = compressed.byteLength;
|
|
111
|
-
const ratio = getCompressionRatio(originalSize, compressedSize);
|
|
112
|
-
logger.log(`Payload compression: ${(originalSize / 1024).toFixed(1)}KB → ${(compressedSize / 1024).toFixed(1)}KB (${ratio}% reduction)`);
|
|
113
|
-
// Use compression if it actually reduces size
|
|
114
|
-
if (compressedSize < originalSize) {
|
|
115
|
-
// Create a Blob from the compressed Uint8Array for proper binary upload
|
|
116
|
-
// Use Uint8Array constructor to ensure clean ArrayBuffer (no extra padding bytes)
|
|
117
|
-
body = new Blob([new Uint8Array(compressed)], { type: 'application/gzip' });
|
|
118
|
-
contentHeaders['Content-Encoding'] = 'gzip';
|
|
119
|
-
contentHeaders['Content-Type'] = 'application/gzip';
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
body = JSON.stringify(payload);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
// Fallback to uncompressed if compression fails
|
|
127
|
-
logger.warn('Compression failed, sending uncompressed payload:', error);
|
|
128
|
-
body = JSON.stringify(payload);
|
|
129
|
-
}
|
|
130
|
-
// Determine auth configuration
|
|
131
|
-
const auth = this.config.auth;
|
|
132
|
-
// Submit with authentication, retry logic, and offline queue
|
|
133
|
-
const response = await submitWithAuth(this.config.endpoint, body, contentHeaders, {
|
|
134
|
-
auth,
|
|
149
|
+
const response = await submitWithAuth(this.config.endpoint, // Validated in validateAuthConfig
|
|
150
|
+
JSON.stringify(createPayload), contentHeaders, {
|
|
151
|
+
auth: this.config.auth,
|
|
135
152
|
retry: this.config.retry,
|
|
136
153
|
offline: this.config.offline,
|
|
137
154
|
});
|
|
138
|
-
logger.warn(`${JSON.stringify(response)}`);
|
|
139
155
|
if (!response.ok) {
|
|
140
|
-
const errorText = await response.text().catch(() =>
|
|
141
|
-
return 'Unknown error';
|
|
142
|
-
});
|
|
156
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
143
157
|
throw new Error(`Failed to submit bug report: ${response.status} ${response.statusText}. ${errorText}`);
|
|
144
158
|
}
|
|
145
|
-
|
|
146
|
-
|
|
159
|
+
const result = await response.json().catch(() => ({ success: false }));
|
|
160
|
+
logger.debug('Bug report creation response', {
|
|
161
|
+
success: result.success,
|
|
162
|
+
bugId: (_c = result.data) === null || _c === void 0 ? void 0 : _c.id,
|
|
163
|
+
hasPresignedUrls: !!((_d = result.data) === null || _d === void 0 ? void 0 : _d.presignedUrls),
|
|
164
|
+
presignedUrlKeys: ((_e = result.data) === null || _e === void 0 ? void 0 : _e.presignedUrls) ? Object.keys(result.data.presignedUrls) : [],
|
|
147
165
|
});
|
|
166
|
+
if (!result.success || !((_f = result.data) === null || _f === void 0 ? void 0 : _f.id)) {
|
|
167
|
+
throw new Error('Bug report ID not returned from server');
|
|
168
|
+
}
|
|
169
|
+
const bugId = result.data.id;
|
|
170
|
+
// Step 2: Upload screenshot and replay using presigned URLs from response
|
|
171
|
+
if (!hasScreenshot && !hasReplay) {
|
|
172
|
+
logger.debug('No files to upload, bug report created successfully', { bugId });
|
|
173
|
+
return; // No files to upload, nothing more to do
|
|
174
|
+
}
|
|
175
|
+
// Validate presigned URLs were returned
|
|
176
|
+
if (!result.data.presignedUrls) {
|
|
177
|
+
logger.error('Presigned URLs not returned despite requesting file uploads', {
|
|
178
|
+
bugId,
|
|
179
|
+
hasScreenshot,
|
|
180
|
+
hasReplay,
|
|
181
|
+
});
|
|
182
|
+
throw new Error('Server did not provide presigned URLs for file uploads. Check backend configuration.');
|
|
183
|
+
}
|
|
184
|
+
// Use FileUploadHandler to handle all file upload operations
|
|
185
|
+
const apiEndpoint = getApiBaseUrl(this.config.endpoint);
|
|
186
|
+
const uploadHandler = new FileUploadHandler(apiEndpoint, this.config.auth.apiKey);
|
|
187
|
+
try {
|
|
188
|
+
await uploadHandler.uploadFiles(bugId, report, result.data.presignedUrls);
|
|
189
|
+
logger.debug('File uploads completed successfully', { bugId });
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
logger.error('File upload failed', {
|
|
193
|
+
bugId,
|
|
194
|
+
error: error instanceof Error ? error.message : String(error),
|
|
195
|
+
});
|
|
196
|
+
throw new Error(`Bug report created (ID: ${bugId}) but file upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
197
|
+
}
|
|
148
198
|
}
|
|
149
199
|
getConfig() {
|
|
150
200
|
return Object.assign({}, this.config);
|
|
@@ -176,11 +226,17 @@ export { DirectUploader } from './core/uploader';
|
|
|
176
226
|
export { compressReplayEvents, canvasToBlob, estimateCompressedReplaySize, isWithinSizeLimit, } from './core/upload-helpers';
|
|
177
227
|
// Export sanitization utilities
|
|
178
228
|
export { createSanitizer, Sanitizer } from './utils/sanitize';
|
|
229
|
+
// Export URL helpers
|
|
230
|
+
export { getApiBaseUrl, stripEndpointSuffix, InvalidEndpointError } from './utils/url-helpers';
|
|
231
|
+
// Export config validation
|
|
232
|
+
export { validateAuthConfig } from './utils/config-validator';
|
|
179
233
|
// Export pattern configuration utilities
|
|
180
234
|
export { DEFAULT_PATTERNS, PATTERN_PRESETS, PATTERN_CATEGORIES, PatternBuilder, createPatternConfig, getPattern, getPatternsByCategory, validatePattern, } from './utils/sanitize';
|
|
181
235
|
// Export widget components
|
|
182
236
|
export { FloatingButton } from './widget/button';
|
|
183
237
|
export { BugReportModal } from './widget/modal';
|
|
238
|
+
// Export constants
|
|
239
|
+
export { DEFAULT_REPLAY_DURATION_SECONDS, MAX_RECOMMENDED_REPLAY_DURATION_SECONDS, } from './constants';
|
|
184
240
|
/**
|
|
185
241
|
* Convenience function to sanitize text with default PII patterns
|
|
186
242
|
* Useful for quick sanitization without creating a Sanitizer instance
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
2
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BugReportModal = exports.FloatingButton = exports.validatePattern = exports.getPatternsByCategory = exports.getPattern = exports.createPatternConfig = exports.PatternBuilder = exports.PATTERN_CATEGORIES = exports.PATTERN_PRESETS = exports.DEFAULT_PATTERNS = exports.Sanitizer = exports.createSanitizer = exports.isWithinSizeLimit = exports.estimateCompressedReplaySize = exports.canvasToBlob = exports.compressReplayEvents = exports.DirectUploader = exports.createLogger = exports.configureLogger = exports.getLogger = exports.clearOfflineQueue = exports.getAuthHeaders = exports.submitWithAuth = exports.getCompressionRatio = exports.estimateSize = exports.compressImage = exports.decompressData = exports.compressData = exports.CircularBuffer = exports.DOMCollector = exports.MetadataCapture = exports.NetworkCapture = exports.ConsoleCapture = exports.ScreenshotCapture = exports.BugSpotter = void 0;
|
|
14
|
+
exports.MAX_RECOMMENDED_REPLAY_DURATION_SECONDS = exports.DEFAULT_REPLAY_DURATION_SECONDS = exports.BugReportModal = exports.FloatingButton = exports.validatePattern = exports.getPatternsByCategory = exports.getPattern = exports.createPatternConfig = exports.PatternBuilder = exports.PATTERN_CATEGORIES = exports.PATTERN_PRESETS = exports.DEFAULT_PATTERNS = exports.validateAuthConfig = exports.InvalidEndpointError = exports.stripEndpointSuffix = exports.getApiBaseUrl = exports.Sanitizer = exports.createSanitizer = exports.isWithinSizeLimit = exports.estimateCompressedReplaySize = exports.canvasToBlob = exports.compressReplayEvents = exports.DirectUploader = exports.createLogger = exports.configureLogger = exports.getLogger = exports.clearOfflineQueue = exports.getAuthHeaders = exports.submitWithAuth = exports.getCompressionRatio = exports.estimateSize = exports.compressImage = exports.decompressData = exports.compressData = exports.CircularBuffer = exports.DOMCollector = exports.MetadataCapture = exports.NetworkCapture = exports.ConsoleCapture = exports.ScreenshotCapture = exports.BugSpotter = exports.VERSION = void 0;
|
|
4
15
|
exports.sanitize = sanitize;
|
|
5
16
|
const screenshot_1 = require("./capture/screenshot");
|
|
6
17
|
const console_1 = require("./capture/console");
|
|
7
18
|
const network_1 = require("./capture/network");
|
|
8
19
|
const metadata_1 = require("./capture/metadata");
|
|
9
|
-
const compress_1 = require("./core/compress");
|
|
10
20
|
const button_1 = require("./widget/button");
|
|
11
21
|
const modal_1 = require("./widget/modal");
|
|
12
22
|
const collectors_1 = require("./collectors");
|
|
13
23
|
const sanitize_1 = require("./utils/sanitize");
|
|
14
24
|
const logger_1 = require("./utils/logger");
|
|
15
25
|
const transport_1 = require("./core/transport");
|
|
26
|
+
const file_upload_handler_1 = require("./core/file-upload-handler");
|
|
27
|
+
const constants_1 = require("./constants");
|
|
28
|
+
const url_helpers_1 = require("./utils/url-helpers");
|
|
29
|
+
const config_validator_1 = require("./utils/config-validator");
|
|
30
|
+
const version_1 = require("./version");
|
|
31
|
+
Object.defineProperty(exports, "VERSION", { enumerable: true, get: function () { return version_1.VERSION; } });
|
|
16
32
|
const logger = (0, logger_1.getLogger)();
|
|
17
33
|
class BugSpotter {
|
|
18
34
|
constructor(config) {
|
|
@@ -31,12 +47,12 @@ class BugSpotter {
|
|
|
31
47
|
this.console = new console_1.ConsoleCapture({ sanitizer: this.sanitizer });
|
|
32
48
|
this.network = new network_1.NetworkCapture({ sanitizer: this.sanitizer });
|
|
33
49
|
this.metadata = new metadata_1.MetadataCapture({ sanitizer: this.sanitizer });
|
|
34
|
-
// Note:
|
|
35
|
-
// See
|
|
50
|
+
// Note: FileUploadHandler is created per-report since it needs bugId
|
|
51
|
+
// See submit() method for initialization
|
|
36
52
|
// Initialize DOM collector if replay is enabled
|
|
37
53
|
if (((_g = config.replay) === null || _g === void 0 ? void 0 : _g.enabled) !== false) {
|
|
38
54
|
this.domCollector = new collectors_1.DOMCollector({
|
|
39
|
-
duration: (_j = (_h = config.replay) === null || _h === void 0 ? void 0 : _h.duration) !== null && _j !== void 0 ? _j :
|
|
55
|
+
duration: (_j = (_h = config.replay) === null || _h === void 0 ? void 0 : _h.duration) !== null && _j !== void 0 ? _j : constants_1.DEFAULT_REPLAY_DURATION_SECONDS,
|
|
40
56
|
sampling: (_k = config.replay) === null || _k === void 0 ? void 0 : _k.sampling,
|
|
41
57
|
sanitizer: this.sanitizer,
|
|
42
58
|
});
|
|
@@ -62,7 +78,7 @@ class BugSpotter {
|
|
|
62
78
|
/**
|
|
63
79
|
* Capture bug report data
|
|
64
80
|
* Note: Screenshot is captured for modal preview only (_screenshotPreview)
|
|
65
|
-
*
|
|
81
|
+
* File uploads use presigned URLs returned from the backend
|
|
66
82
|
*/
|
|
67
83
|
async capture() {
|
|
68
84
|
var _a, _b;
|
|
@@ -85,7 +101,7 @@ class BugSpotter {
|
|
|
85
101
|
// Send to endpoint if configured
|
|
86
102
|
if (this.config.endpoint) {
|
|
87
103
|
try {
|
|
88
|
-
await this.
|
|
104
|
+
await this.submit(Object.assign(Object.assign({}, data), { report }));
|
|
89
105
|
logger.log('Bug report submitted successfully');
|
|
90
106
|
}
|
|
91
107
|
catch (error) {
|
|
@@ -98,57 +114,90 @@ class BugSpotter {
|
|
|
98
114
|
});
|
|
99
115
|
modal.show(report._screenshotPreview || '');
|
|
100
116
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Submit a bug report with file uploads via presigned URLs
|
|
119
|
+
* @param payload - Bug report payload with title, description, and report data
|
|
120
|
+
* @public - Exposed for programmatic submission (bypassing modal)
|
|
121
|
+
*/
|
|
122
|
+
async submit(payload) {
|
|
123
|
+
var _a, _b, _c, _d, _e, _f;
|
|
124
|
+
(0, config_validator_1.validateAuthConfig)({
|
|
125
|
+
endpoint: this.config.endpoint,
|
|
126
|
+
auth: this.config.auth,
|
|
127
|
+
});
|
|
128
|
+
logger.debug(`Submitting bug report to ${this.config.endpoint}`);
|
|
129
|
+
// Step 1: Create bug report and request presigned URLs
|
|
130
|
+
const { report } = payload, metadata = __rest(payload, ["report"]);
|
|
131
|
+
// Check what files we need to upload
|
|
132
|
+
const hasScreenshot = !!(report._screenshotPreview && report._screenshotPreview.startsWith('data:image/'));
|
|
133
|
+
const hasReplay = !!(report.replay && report.replay.length > 0);
|
|
134
|
+
logger.debug('File upload detection', {
|
|
135
|
+
hasScreenshot,
|
|
136
|
+
screenshotSize: ((_a = report._screenshotPreview) === null || _a === void 0 ? void 0 : _a.length) || 0,
|
|
137
|
+
hasReplay,
|
|
138
|
+
replayEventCount: ((_b = report.replay) === null || _b === void 0 ? void 0 : _b.length) || 0,
|
|
139
|
+
});
|
|
140
|
+
const createPayload = Object.assign(Object.assign({}, metadata), { report: {
|
|
141
|
+
console: report.console,
|
|
142
|
+
network: report.network,
|
|
143
|
+
metadata: report.metadata,
|
|
144
|
+
// Don't send replay events or screenshot in initial request
|
|
145
|
+
},
|
|
146
|
+
// Tell backend we have files so it can generate presigned URLs
|
|
147
|
+
hasScreenshot,
|
|
148
|
+
hasReplay });
|
|
105
149
|
const contentHeaders = {
|
|
106
150
|
'Content-Type': 'application/json',
|
|
107
151
|
};
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// Try to compress the payload
|
|
112
|
-
const originalSize = (0, compress_1.estimateSize)(payload);
|
|
113
|
-
const compressed = await (0, compress_1.compressData)(payload);
|
|
114
|
-
const compressedSize = compressed.byteLength;
|
|
115
|
-
const ratio = (0, compress_1.getCompressionRatio)(originalSize, compressedSize);
|
|
116
|
-
logger.log(`Payload compression: ${(originalSize / 1024).toFixed(1)}KB → ${(compressedSize / 1024).toFixed(1)}KB (${ratio}% reduction)`);
|
|
117
|
-
// Use compression if it actually reduces size
|
|
118
|
-
if (compressedSize < originalSize) {
|
|
119
|
-
// Create a Blob from the compressed Uint8Array for proper binary upload
|
|
120
|
-
// Use Uint8Array constructor to ensure clean ArrayBuffer (no extra padding bytes)
|
|
121
|
-
body = new Blob([new Uint8Array(compressed)], { type: 'application/gzip' });
|
|
122
|
-
contentHeaders['Content-Encoding'] = 'gzip';
|
|
123
|
-
contentHeaders['Content-Type'] = 'application/gzip';
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
body = JSON.stringify(payload);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
catch (error) {
|
|
130
|
-
// Fallback to uncompressed if compression fails
|
|
131
|
-
logger.warn('Compression failed, sending uncompressed payload:', error);
|
|
132
|
-
body = JSON.stringify(payload);
|
|
133
|
-
}
|
|
134
|
-
// Determine auth configuration
|
|
135
|
-
const auth = this.config.auth;
|
|
136
|
-
// Submit with authentication, retry logic, and offline queue
|
|
137
|
-
const response = await (0, transport_1.submitWithAuth)(this.config.endpoint, body, contentHeaders, {
|
|
138
|
-
auth,
|
|
152
|
+
const response = await (0, transport_1.submitWithAuth)(this.config.endpoint, // Validated in validateAuthConfig
|
|
153
|
+
JSON.stringify(createPayload), contentHeaders, {
|
|
154
|
+
auth: this.config.auth,
|
|
139
155
|
retry: this.config.retry,
|
|
140
156
|
offline: this.config.offline,
|
|
141
157
|
});
|
|
142
|
-
logger.warn(`${JSON.stringify(response)}`);
|
|
143
158
|
if (!response.ok) {
|
|
144
|
-
const errorText = await response.text().catch(() =>
|
|
145
|
-
return 'Unknown error';
|
|
146
|
-
});
|
|
159
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
147
160
|
throw new Error(`Failed to submit bug report: ${response.status} ${response.statusText}. ${errorText}`);
|
|
148
161
|
}
|
|
149
|
-
|
|
150
|
-
|
|
162
|
+
const result = await response.json().catch(() => ({ success: false }));
|
|
163
|
+
logger.debug('Bug report creation response', {
|
|
164
|
+
success: result.success,
|
|
165
|
+
bugId: (_c = result.data) === null || _c === void 0 ? void 0 : _c.id,
|
|
166
|
+
hasPresignedUrls: !!((_d = result.data) === null || _d === void 0 ? void 0 : _d.presignedUrls),
|
|
167
|
+
presignedUrlKeys: ((_e = result.data) === null || _e === void 0 ? void 0 : _e.presignedUrls) ? Object.keys(result.data.presignedUrls) : [],
|
|
151
168
|
});
|
|
169
|
+
if (!result.success || !((_f = result.data) === null || _f === void 0 ? void 0 : _f.id)) {
|
|
170
|
+
throw new Error('Bug report ID not returned from server');
|
|
171
|
+
}
|
|
172
|
+
const bugId = result.data.id;
|
|
173
|
+
// Step 2: Upload screenshot and replay using presigned URLs from response
|
|
174
|
+
if (!hasScreenshot && !hasReplay) {
|
|
175
|
+
logger.debug('No files to upload, bug report created successfully', { bugId });
|
|
176
|
+
return; // No files to upload, nothing more to do
|
|
177
|
+
}
|
|
178
|
+
// Validate presigned URLs were returned
|
|
179
|
+
if (!result.data.presignedUrls) {
|
|
180
|
+
logger.error('Presigned URLs not returned despite requesting file uploads', {
|
|
181
|
+
bugId,
|
|
182
|
+
hasScreenshot,
|
|
183
|
+
hasReplay,
|
|
184
|
+
});
|
|
185
|
+
throw new Error('Server did not provide presigned URLs for file uploads. Check backend configuration.');
|
|
186
|
+
}
|
|
187
|
+
// Use FileUploadHandler to handle all file upload operations
|
|
188
|
+
const apiEndpoint = (0, url_helpers_1.getApiBaseUrl)(this.config.endpoint);
|
|
189
|
+
const uploadHandler = new file_upload_handler_1.FileUploadHandler(apiEndpoint, this.config.auth.apiKey);
|
|
190
|
+
try {
|
|
191
|
+
await uploadHandler.uploadFiles(bugId, report, result.data.presignedUrls);
|
|
192
|
+
logger.debug('File uploads completed successfully', { bugId });
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
logger.error('File upload failed', {
|
|
196
|
+
bugId,
|
|
197
|
+
error: error instanceof Error ? error.message : String(error),
|
|
198
|
+
});
|
|
199
|
+
throw new Error(`Bug report created (ID: ${bugId}) but file upload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
200
|
+
}
|
|
152
201
|
}
|
|
153
202
|
getConfig() {
|
|
154
203
|
return Object.assign({}, this.config);
|
|
@@ -178,12 +227,12 @@ Object.defineProperty(exports, "DOMCollector", { enumerable: true, get: function
|
|
|
178
227
|
var buffer_1 = require("./core/buffer");
|
|
179
228
|
Object.defineProperty(exports, "CircularBuffer", { enumerable: true, get: function () { return buffer_1.CircularBuffer; } });
|
|
180
229
|
// Export compression utilities
|
|
181
|
-
var
|
|
182
|
-
Object.defineProperty(exports, "compressData", { enumerable: true, get: function () { return
|
|
183
|
-
Object.defineProperty(exports, "decompressData", { enumerable: true, get: function () { return
|
|
184
|
-
Object.defineProperty(exports, "compressImage", { enumerable: true, get: function () { return
|
|
185
|
-
Object.defineProperty(exports, "estimateSize", { enumerable: true, get: function () { return
|
|
186
|
-
Object.defineProperty(exports, "getCompressionRatio", { enumerable: true, get: function () { return
|
|
230
|
+
var compress_1 = require("./core/compress");
|
|
231
|
+
Object.defineProperty(exports, "compressData", { enumerable: true, get: function () { return compress_1.compressData; } });
|
|
232
|
+
Object.defineProperty(exports, "decompressData", { enumerable: true, get: function () { return compress_1.decompressData; } });
|
|
233
|
+
Object.defineProperty(exports, "compressImage", { enumerable: true, get: function () { return compress_1.compressImage; } });
|
|
234
|
+
Object.defineProperty(exports, "estimateSize", { enumerable: true, get: function () { return compress_1.estimateSize; } });
|
|
235
|
+
Object.defineProperty(exports, "getCompressionRatio", { enumerable: true, get: function () { return compress_1.getCompressionRatio; } });
|
|
187
236
|
// Export transport and authentication
|
|
188
237
|
var transport_2 = require("./core/transport");
|
|
189
238
|
Object.defineProperty(exports, "submitWithAuth", { enumerable: true, get: function () { return transport_2.submitWithAuth; } });
|
|
@@ -205,6 +254,14 @@ Object.defineProperty(exports, "isWithinSizeLimit", { enumerable: true, get: fun
|
|
|
205
254
|
var sanitize_2 = require("./utils/sanitize");
|
|
206
255
|
Object.defineProperty(exports, "createSanitizer", { enumerable: true, get: function () { return sanitize_2.createSanitizer; } });
|
|
207
256
|
Object.defineProperty(exports, "Sanitizer", { enumerable: true, get: function () { return sanitize_2.Sanitizer; } });
|
|
257
|
+
// Export URL helpers
|
|
258
|
+
var url_helpers_2 = require("./utils/url-helpers");
|
|
259
|
+
Object.defineProperty(exports, "getApiBaseUrl", { enumerable: true, get: function () { return url_helpers_2.getApiBaseUrl; } });
|
|
260
|
+
Object.defineProperty(exports, "stripEndpointSuffix", { enumerable: true, get: function () { return url_helpers_2.stripEndpointSuffix; } });
|
|
261
|
+
Object.defineProperty(exports, "InvalidEndpointError", { enumerable: true, get: function () { return url_helpers_2.InvalidEndpointError; } });
|
|
262
|
+
// Export config validation
|
|
263
|
+
var config_validator_2 = require("./utils/config-validator");
|
|
264
|
+
Object.defineProperty(exports, "validateAuthConfig", { enumerable: true, get: function () { return config_validator_2.validateAuthConfig; } });
|
|
208
265
|
// Export pattern configuration utilities
|
|
209
266
|
var sanitize_3 = require("./utils/sanitize");
|
|
210
267
|
Object.defineProperty(exports, "DEFAULT_PATTERNS", { enumerable: true, get: function () { return sanitize_3.DEFAULT_PATTERNS; } });
|
|
@@ -220,6 +277,10 @@ var button_2 = require("./widget/button");
|
|
|
220
277
|
Object.defineProperty(exports, "FloatingButton", { enumerable: true, get: function () { return button_2.FloatingButton; } });
|
|
221
278
|
var modal_2 = require("./widget/modal");
|
|
222
279
|
Object.defineProperty(exports, "BugReportModal", { enumerable: true, get: function () { return modal_2.BugReportModal; } });
|
|
280
|
+
// Export constants
|
|
281
|
+
var constants_2 = require("./constants");
|
|
282
|
+
Object.defineProperty(exports, "DEFAULT_REPLAY_DURATION_SECONDS", { enumerable: true, get: function () { return constants_2.DEFAULT_REPLAY_DURATION_SECONDS; } });
|
|
283
|
+
Object.defineProperty(exports, "MAX_RECOMMENDED_REPLAY_DURATION_SECONDS", { enumerable: true, get: function () { return constants_2.MAX_RECOMMENDED_REPLAY_DURATION_SECONDS; } });
|
|
223
284
|
/**
|
|
224
285
|
* Convenience function to sanitize text with default PII patterns
|
|
225
286
|
* Useful for quick sanitization without creating a Sanitizer instance
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Validation Utilities
|
|
3
|
+
* Validates BugSpotter configuration before use
|
|
4
|
+
*/
|
|
5
|
+
import type { AuthConfig } from '../core/transport';
|
|
6
|
+
export interface ValidationContext {
|
|
7
|
+
endpoint?: string;
|
|
8
|
+
auth?: AuthConfig;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Validate authentication configuration
|
|
12
|
+
* @throws Error if configuration is invalid
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateAuthConfig(context: ValidationContext): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration Validation Utilities
|
|
4
|
+
* Validates BugSpotter configuration before use
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.validateAuthConfig = validateAuthConfig;
|
|
8
|
+
/**
|
|
9
|
+
* Validate authentication configuration
|
|
10
|
+
* @throws Error if configuration is invalid
|
|
11
|
+
*/
|
|
12
|
+
function validateAuthConfig(context) {
|
|
13
|
+
if (!context.endpoint) {
|
|
14
|
+
throw new Error('No endpoint configured for bug report submission');
|
|
15
|
+
}
|
|
16
|
+
if (!context.auth) {
|
|
17
|
+
throw new Error('API key authentication is required');
|
|
18
|
+
}
|
|
19
|
+
if (context.auth.type !== 'api-key') {
|
|
20
|
+
throw new Error('API key authentication is required');
|
|
21
|
+
}
|
|
22
|
+
if (!context.auth.apiKey) {
|
|
23
|
+
throw new Error('API key is required in auth configuration');
|
|
24
|
+
}
|
|
25
|
+
if (!context.auth.projectId) {
|
|
26
|
+
throw new Error('Project ID is required in auth configuration');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL Helper Utilities
|
|
3
|
+
* Extract base API URL from endpoint configuration
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Custom error for invalid endpoint URLs
|
|
7
|
+
*/
|
|
8
|
+
export declare class InvalidEndpointError extends Error {
|
|
9
|
+
readonly endpoint: string;
|
|
10
|
+
readonly reason: string;
|
|
11
|
+
constructor(endpoint: string, reason: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Strip known endpoint suffixes from path
|
|
15
|
+
* Removes /api/v1/reports path
|
|
16
|
+
*/
|
|
17
|
+
export declare function stripEndpointSuffix(path: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Extract base API URL from endpoint
|
|
20
|
+
* Returns scheme + host + base path (without /api/v1/reports suffix)
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* getApiBaseUrl('https://api.example.com/api/v1/reports')
|
|
24
|
+
* // Returns: 'https://api.example.com'
|
|
25
|
+
*
|
|
26
|
+
* @throws InvalidEndpointError if endpoint is not a valid absolute URL
|
|
27
|
+
*/
|
|
28
|
+
export declare function getApiBaseUrl(endpoint: string): string;
|