@bugspotter/sdk 0.1.0-alpha.1 → 0.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +93 -62
- 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 +20 -2
- package/dist/index.esm.js +103 -40
- package/dist/index.js +112 -47
- package/dist/widget/button.d.ts +5 -0
- package/dist/widget/button.js +50 -12
- package/docs/SESSION_REPLAY.md +66 -4
- package/package.json +2 -3
- package/tsconfig.cjs.json +15 -0
package/dist/index.esm.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
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';
|
|
12
24
|
const logger = getLogger();
|
|
13
25
|
export class BugSpotter {
|
|
14
26
|
constructor(config) {
|
|
@@ -32,7 +44,7 @@ export class BugSpotter {
|
|
|
32
44
|
// Initialize DOM collector if replay is enabled
|
|
33
45
|
if (((_g = config.replay) === null || _g === void 0 ? void 0 : _g.enabled) !== false) {
|
|
34
46
|
this.domCollector = new DOMCollector({
|
|
35
|
-
duration: (_j = (_h = config.replay) === null || _h === void 0 ? void 0 : _h.duration) !== null && _j !== void 0 ? _j :
|
|
47
|
+
duration: (_j = (_h = config.replay) === null || _h === void 0 ? void 0 : _h.duration) !== null && _j !== void 0 ? _j : DEFAULT_REPLAY_DURATION_SECONDS,
|
|
36
48
|
sampling: (_k = config.replay) === null || _k === void 0 ? void 0 : _k.sampling,
|
|
37
49
|
sanitizer: this.sanitizer,
|
|
38
50
|
});
|
|
@@ -94,57 +106,106 @@ export class BugSpotter {
|
|
|
94
106
|
});
|
|
95
107
|
modal.show(report._screenshotPreview || '');
|
|
96
108
|
}
|
|
97
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Validate authentication configuration
|
|
111
|
+
* @throws Error if configuration is invalid
|
|
112
|
+
*/
|
|
113
|
+
validateAuthConfig() {
|
|
98
114
|
if (!this.config.endpoint) {
|
|
99
115
|
throw new Error('No endpoint configured for bug report submission');
|
|
100
116
|
}
|
|
101
|
-
|
|
102
|
-
'
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
if (!this.config.auth) {
|
|
118
|
+
throw new Error('API key authentication is required');
|
|
119
|
+
}
|
|
120
|
+
if (this.config.auth.type !== 'api-key') {
|
|
121
|
+
throw new Error('API key authentication is required');
|
|
122
|
+
}
|
|
123
|
+
if (!this.config.auth.apiKey) {
|
|
124
|
+
throw new Error('API key is required in auth configuration');
|
|
125
|
+
}
|
|
126
|
+
if (!this.config.auth.projectId) {
|
|
127
|
+
throw new Error('Project ID is required in auth configuration');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Strip endpoint suffix from path
|
|
132
|
+
*/
|
|
133
|
+
stripEndpointSuffix(path) {
|
|
134
|
+
if (path.endsWith('/bugs')) {
|
|
135
|
+
return path.slice(0, -5);
|
|
136
|
+
}
|
|
137
|
+
else if (path.includes('/api/v1/reports')) {
|
|
138
|
+
return path.substring(0, path.indexOf('/api/v1/reports'));
|
|
139
|
+
}
|
|
140
|
+
return path.replace(/\/$/, '') || '';
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get the base API URL for confirm-upload calls
|
|
144
|
+
* Extracts scheme, host, and base path from the configured endpoint
|
|
145
|
+
*/
|
|
146
|
+
getApiBaseUrl() {
|
|
147
|
+
if (!this.config.endpoint) {
|
|
148
|
+
throw new Error('No endpoint configured');
|
|
149
|
+
}
|
|
106
150
|
try {
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
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
|
-
}
|
|
151
|
+
const url = new URL(this.config.endpoint);
|
|
152
|
+
const basePath = this.stripEndpointSuffix(url.pathname);
|
|
153
|
+
return url.origin + basePath;
|
|
124
154
|
}
|
|
125
155
|
catch (error) {
|
|
126
|
-
// Fallback
|
|
127
|
-
logger.warn('
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
156
|
+
// Fallback for invalid URLs
|
|
157
|
+
logger.warn('Failed to parse endpoint URL, using fallback', {
|
|
158
|
+
endpoint: this.config.endpoint,
|
|
159
|
+
error: error instanceof Error ? error.message : String(error),
|
|
160
|
+
});
|
|
161
|
+
return this.stripEndpointSuffix(this.config.endpoint);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async submitBugReport(payload) {
|
|
165
|
+
var _a;
|
|
166
|
+
this.validateAuthConfig();
|
|
167
|
+
logger.warn(`Submitting bug report to ${this.config.endpoint}`);
|
|
168
|
+
// Step 1: Create bug report and request presigned URLs
|
|
169
|
+
const { report } = payload, metadata = __rest(payload, ["report"]);
|
|
170
|
+
// Check what files we need to upload
|
|
171
|
+
const hasScreenshot = !!(report._screenshotPreview && report._screenshotPreview.startsWith('data:image/'));
|
|
172
|
+
const hasReplay = !!(report.replay && report.replay.length > 0);
|
|
173
|
+
const createPayload = Object.assign(Object.assign({}, metadata), { report: {
|
|
174
|
+
console: report.console,
|
|
175
|
+
network: report.network,
|
|
176
|
+
metadata: report.metadata,
|
|
177
|
+
// Don't send replay events or screenshot in initial request
|
|
178
|
+
},
|
|
179
|
+
// Tell backend we have files so it can generate presigned URLs
|
|
180
|
+
hasScreenshot,
|
|
181
|
+
hasReplay });
|
|
182
|
+
const contentHeaders = {
|
|
183
|
+
'Content-Type': 'application/json',
|
|
184
|
+
};
|
|
185
|
+
const response = await submitWithAuth(this.config.endpoint, // Validated in validateAuthConfig
|
|
186
|
+
JSON.stringify(createPayload), contentHeaders, {
|
|
187
|
+
auth: this.config.auth,
|
|
135
188
|
retry: this.config.retry,
|
|
136
189
|
offline: this.config.offline,
|
|
137
190
|
});
|
|
138
191
|
logger.warn(`${JSON.stringify(response)}`);
|
|
139
192
|
if (!response.ok) {
|
|
140
|
-
const errorText = await response.text().catch(() =>
|
|
141
|
-
return 'Unknown error';
|
|
142
|
-
});
|
|
193
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
143
194
|
throw new Error(`Failed to submit bug report: ${response.status} ${response.statusText}. ${errorText}`);
|
|
144
195
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
196
|
+
const result = await response.json().catch(() => ({ success: false }));
|
|
197
|
+
if (!result.success || !((_a = result.data) === null || _a === void 0 ? void 0 : _a.id)) {
|
|
198
|
+
throw new Error('Bug report ID not returned from server');
|
|
199
|
+
}
|
|
200
|
+
const bugId = result.data.id;
|
|
201
|
+
// Step 2: Upload screenshot and replay using presigned URLs from response
|
|
202
|
+
if (!hasScreenshot && !hasReplay) {
|
|
203
|
+
return; // No files to upload, nothing more to do
|
|
204
|
+
}
|
|
205
|
+
// Use FileUploadHandler to handle all file upload operations
|
|
206
|
+
const apiEndpoint = this.getApiBaseUrl();
|
|
207
|
+
const uploadHandler = new FileUploadHandler(apiEndpoint, this.config.auth.apiKey);
|
|
208
|
+
await uploadHandler.uploadFiles(bugId, report, result.data.presignedUrls);
|
|
148
209
|
}
|
|
149
210
|
getConfig() {
|
|
150
211
|
return Object.assign({}, this.config);
|
|
@@ -181,6 +242,8 @@ export { DEFAULT_PATTERNS, PATTERN_PRESETS, PATTERN_CATEGORIES, PatternBuilder,
|
|
|
181
242
|
// Export widget components
|
|
182
243
|
export { FloatingButton } from './widget/button';
|
|
183
244
|
export { BugReportModal } from './widget/modal';
|
|
245
|
+
// Export constants
|
|
246
|
+
export { DEFAULT_REPLAY_DURATION_SECONDS, MAX_RECOMMENDED_REPLAY_DURATION_SECONDS, } from './constants';
|
|
184
247
|
/**
|
|
185
248
|
* Convenience function to sanitize text with default PII patterns
|
|
186
249
|
* Useful for quick sanitization without creating a Sanitizer instance
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
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.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;
|
|
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");
|
|
16
28
|
const logger = (0, logger_1.getLogger)();
|
|
17
29
|
class BugSpotter {
|
|
18
30
|
constructor(config) {
|
|
@@ -36,7 +48,7 @@ class BugSpotter {
|
|
|
36
48
|
// Initialize DOM collector if replay is enabled
|
|
37
49
|
if (((_g = config.replay) === null || _g === void 0 ? void 0 : _g.enabled) !== false) {
|
|
38
50
|
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 :
|
|
51
|
+
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
52
|
sampling: (_k = config.replay) === null || _k === void 0 ? void 0 : _k.sampling,
|
|
41
53
|
sanitizer: this.sanitizer,
|
|
42
54
|
});
|
|
@@ -98,57 +110,106 @@ class BugSpotter {
|
|
|
98
110
|
});
|
|
99
111
|
modal.show(report._screenshotPreview || '');
|
|
100
112
|
}
|
|
101
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Validate authentication configuration
|
|
115
|
+
* @throws Error if configuration is invalid
|
|
116
|
+
*/
|
|
117
|
+
validateAuthConfig() {
|
|
102
118
|
if (!this.config.endpoint) {
|
|
103
119
|
throw new Error('No endpoint configured for bug report submission');
|
|
104
120
|
}
|
|
105
|
-
|
|
106
|
-
'
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
121
|
+
if (!this.config.auth) {
|
|
122
|
+
throw new Error('API key authentication is required');
|
|
123
|
+
}
|
|
124
|
+
if (this.config.auth.type !== 'api-key') {
|
|
125
|
+
throw new Error('API key authentication is required');
|
|
126
|
+
}
|
|
127
|
+
if (!this.config.auth.apiKey) {
|
|
128
|
+
throw new Error('API key is required in auth configuration');
|
|
129
|
+
}
|
|
130
|
+
if (!this.config.auth.projectId) {
|
|
131
|
+
throw new Error('Project ID is required in auth configuration');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Strip endpoint suffix from path
|
|
136
|
+
*/
|
|
137
|
+
stripEndpointSuffix(path) {
|
|
138
|
+
if (path.endsWith('/bugs')) {
|
|
139
|
+
return path.slice(0, -5);
|
|
140
|
+
}
|
|
141
|
+
else if (path.includes('/api/v1/reports')) {
|
|
142
|
+
return path.substring(0, path.indexOf('/api/v1/reports'));
|
|
143
|
+
}
|
|
144
|
+
return path.replace(/\/$/, '') || '';
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get the base API URL for confirm-upload calls
|
|
148
|
+
* Extracts scheme, host, and base path from the configured endpoint
|
|
149
|
+
*/
|
|
150
|
+
getApiBaseUrl() {
|
|
151
|
+
if (!this.config.endpoint) {
|
|
152
|
+
throw new Error('No endpoint configured');
|
|
153
|
+
}
|
|
110
154
|
try {
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
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
|
-
}
|
|
155
|
+
const url = new URL(this.config.endpoint);
|
|
156
|
+
const basePath = this.stripEndpointSuffix(url.pathname);
|
|
157
|
+
return url.origin + basePath;
|
|
128
158
|
}
|
|
129
159
|
catch (error) {
|
|
130
|
-
// Fallback
|
|
131
|
-
logger.warn('
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
160
|
+
// Fallback for invalid URLs
|
|
161
|
+
logger.warn('Failed to parse endpoint URL, using fallback', {
|
|
162
|
+
endpoint: this.config.endpoint,
|
|
163
|
+
error: error instanceof Error ? error.message : String(error),
|
|
164
|
+
});
|
|
165
|
+
return this.stripEndpointSuffix(this.config.endpoint);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async submitBugReport(payload) {
|
|
169
|
+
var _a;
|
|
170
|
+
this.validateAuthConfig();
|
|
171
|
+
logger.warn(`Submitting bug report to ${this.config.endpoint}`);
|
|
172
|
+
// Step 1: Create bug report and request presigned URLs
|
|
173
|
+
const { report } = payload, metadata = __rest(payload, ["report"]);
|
|
174
|
+
// Check what files we need to upload
|
|
175
|
+
const hasScreenshot = !!(report._screenshotPreview && report._screenshotPreview.startsWith('data:image/'));
|
|
176
|
+
const hasReplay = !!(report.replay && report.replay.length > 0);
|
|
177
|
+
const createPayload = Object.assign(Object.assign({}, metadata), { report: {
|
|
178
|
+
console: report.console,
|
|
179
|
+
network: report.network,
|
|
180
|
+
metadata: report.metadata,
|
|
181
|
+
// Don't send replay events or screenshot in initial request
|
|
182
|
+
},
|
|
183
|
+
// Tell backend we have files so it can generate presigned URLs
|
|
184
|
+
hasScreenshot,
|
|
185
|
+
hasReplay });
|
|
186
|
+
const contentHeaders = {
|
|
187
|
+
'Content-Type': 'application/json',
|
|
188
|
+
};
|
|
189
|
+
const response = await (0, transport_1.submitWithAuth)(this.config.endpoint, // Validated in validateAuthConfig
|
|
190
|
+
JSON.stringify(createPayload), contentHeaders, {
|
|
191
|
+
auth: this.config.auth,
|
|
139
192
|
retry: this.config.retry,
|
|
140
193
|
offline: this.config.offline,
|
|
141
194
|
});
|
|
142
195
|
logger.warn(`${JSON.stringify(response)}`);
|
|
143
196
|
if (!response.ok) {
|
|
144
|
-
const errorText = await response.text().catch(() =>
|
|
145
|
-
return 'Unknown error';
|
|
146
|
-
});
|
|
197
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
147
198
|
throw new Error(`Failed to submit bug report: ${response.status} ${response.statusText}. ${errorText}`);
|
|
148
199
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
200
|
+
const result = await response.json().catch(() => ({ success: false }));
|
|
201
|
+
if (!result.success || !((_a = result.data) === null || _a === void 0 ? void 0 : _a.id)) {
|
|
202
|
+
throw new Error('Bug report ID not returned from server');
|
|
203
|
+
}
|
|
204
|
+
const bugId = result.data.id;
|
|
205
|
+
// Step 2: Upload screenshot and replay using presigned URLs from response
|
|
206
|
+
if (!hasScreenshot && !hasReplay) {
|
|
207
|
+
return; // No files to upload, nothing more to do
|
|
208
|
+
}
|
|
209
|
+
// Use FileUploadHandler to handle all file upload operations
|
|
210
|
+
const apiEndpoint = this.getApiBaseUrl();
|
|
211
|
+
const uploadHandler = new file_upload_handler_1.FileUploadHandler(apiEndpoint, this.config.auth.apiKey);
|
|
212
|
+
await uploadHandler.uploadFiles(bugId, report, result.data.presignedUrls);
|
|
152
213
|
}
|
|
153
214
|
getConfig() {
|
|
154
215
|
return Object.assign({}, this.config);
|
|
@@ -178,12 +239,12 @@ Object.defineProperty(exports, "DOMCollector", { enumerable: true, get: function
|
|
|
178
239
|
var buffer_1 = require("./core/buffer");
|
|
179
240
|
Object.defineProperty(exports, "CircularBuffer", { enumerable: true, get: function () { return buffer_1.CircularBuffer; } });
|
|
180
241
|
// 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
|
|
242
|
+
var compress_1 = require("./core/compress");
|
|
243
|
+
Object.defineProperty(exports, "compressData", { enumerable: true, get: function () { return compress_1.compressData; } });
|
|
244
|
+
Object.defineProperty(exports, "decompressData", { enumerable: true, get: function () { return compress_1.decompressData; } });
|
|
245
|
+
Object.defineProperty(exports, "compressImage", { enumerable: true, get: function () { return compress_1.compressImage; } });
|
|
246
|
+
Object.defineProperty(exports, "estimateSize", { enumerable: true, get: function () { return compress_1.estimateSize; } });
|
|
247
|
+
Object.defineProperty(exports, "getCompressionRatio", { enumerable: true, get: function () { return compress_1.getCompressionRatio; } });
|
|
187
248
|
// Export transport and authentication
|
|
188
249
|
var transport_2 = require("./core/transport");
|
|
189
250
|
Object.defineProperty(exports, "submitWithAuth", { enumerable: true, get: function () { return transport_2.submitWithAuth; } });
|
|
@@ -220,6 +281,10 @@ var button_2 = require("./widget/button");
|
|
|
220
281
|
Object.defineProperty(exports, "FloatingButton", { enumerable: true, get: function () { return button_2.FloatingButton; } });
|
|
221
282
|
var modal_2 = require("./widget/modal");
|
|
222
283
|
Object.defineProperty(exports, "BugReportModal", { enumerable: true, get: function () { return modal_2.BugReportModal; } });
|
|
284
|
+
// Export constants
|
|
285
|
+
var constants_2 = require("./constants");
|
|
286
|
+
Object.defineProperty(exports, "DEFAULT_REPLAY_DURATION_SECONDS", { enumerable: true, get: function () { return constants_2.DEFAULT_REPLAY_DURATION_SECONDS; } });
|
|
287
|
+
Object.defineProperty(exports, "MAX_RECOMMENDED_REPLAY_DURATION_SECONDS", { enumerable: true, get: function () { return constants_2.MAX_RECOMMENDED_REPLAY_DURATION_SECONDS; } });
|
|
223
288
|
/**
|
|
224
289
|
* Convenience function to sanitize text with default PII patterns
|
|
225
290
|
* Useful for quick sanitization without creating a Sanitizer instance
|
package/dist/widget/button.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
type ButtonPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
2
2
|
export interface FloatingButtonOptions {
|
|
3
3
|
position?: ButtonPosition;
|
|
4
|
+
/** Icon to display - can be text/emoji or 'svg' for default bug icon */
|
|
4
5
|
icon?: string;
|
|
6
|
+
/** Custom SVG icon (overrides icon if provided) */
|
|
7
|
+
customSvg?: string;
|
|
5
8
|
backgroundColor?: string;
|
|
6
9
|
size?: number;
|
|
7
10
|
offset?: {
|
|
@@ -9,6 +12,8 @@ export interface FloatingButtonOptions {
|
|
|
9
12
|
y: number;
|
|
10
13
|
};
|
|
11
14
|
zIndex?: number;
|
|
15
|
+
/** Custom tooltip text */
|
|
16
|
+
tooltip?: string;
|
|
12
17
|
}
|
|
13
18
|
export declare class FloatingButton {
|
|
14
19
|
private button;
|
package/dist/widget/button.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FloatingButton = void 0;
|
|
4
|
+
// Professional bug report icon SVG
|
|
5
|
+
const DEFAULT_SVG_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
6
|
+
<path d="M8 2v4"/>
|
|
7
|
+
<path d="M16 2v4"/>
|
|
8
|
+
<path d="M12 12v5"/>
|
|
9
|
+
<circle cx="12" cy="10" r="4"/>
|
|
10
|
+
<path d="M9 16c-1.5 1-3 2-3 4h12c0-2-1.5-3-3-4"/>
|
|
11
|
+
<path d="M3 8h4"/>
|
|
12
|
+
<path d="M17 8h4"/>
|
|
13
|
+
<path d="M5 12h2"/>
|
|
14
|
+
<path d="M17 12h2"/>
|
|
15
|
+
<path d="M6 16h2"/>
|
|
16
|
+
<path d="M16 16h2"/>
|
|
17
|
+
</svg>`;
|
|
4
18
|
const DEFAULT_BUTTON_OPTIONS = {
|
|
5
19
|
position: 'bottom-right',
|
|
6
|
-
icon: '
|
|
7
|
-
|
|
8
|
-
|
|
20
|
+
icon: 'svg', // Use SVG icon by default
|
|
21
|
+
customSvg: undefined,
|
|
22
|
+
backgroundColor: '#2563eb', // Professional blue color
|
|
23
|
+
size: 56,
|
|
9
24
|
offset: { x: 20, y: 20 },
|
|
10
25
|
zIndex: 999999,
|
|
26
|
+
tooltip: 'Report an Issue',
|
|
11
27
|
};
|
|
12
28
|
const BUTTON_STYLES = {
|
|
13
29
|
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
|
@@ -23,7 +39,7 @@ const BUTTON_STYLES = {
|
|
|
23
39
|
};
|
|
24
40
|
class FloatingButton {
|
|
25
41
|
constructor(options = {}) {
|
|
26
|
-
var _a, _b, _c, _d, _e, _f;
|
|
42
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
27
43
|
this.eventHandlers = new Map();
|
|
28
44
|
this.handleMouseEnter = () => {
|
|
29
45
|
this.button.style.transform = BUTTON_STYLES.transform.hover;
|
|
@@ -42,10 +58,12 @@ class FloatingButton {
|
|
|
42
58
|
this.options = {
|
|
43
59
|
position: (_a = options.position) !== null && _a !== void 0 ? _a : DEFAULT_BUTTON_OPTIONS.position,
|
|
44
60
|
icon: (_b = options.icon) !== null && _b !== void 0 ? _b : DEFAULT_BUTTON_OPTIONS.icon,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
customSvg: (_c = options.customSvg) !== null && _c !== void 0 ? _c : DEFAULT_BUTTON_OPTIONS.customSvg,
|
|
62
|
+
backgroundColor: (_d = options.backgroundColor) !== null && _d !== void 0 ? _d : DEFAULT_BUTTON_OPTIONS.backgroundColor,
|
|
63
|
+
size: (_e = options.size) !== null && _e !== void 0 ? _e : DEFAULT_BUTTON_OPTIONS.size,
|
|
64
|
+
offset: (_f = options.offset) !== null && _f !== void 0 ? _f : DEFAULT_BUTTON_OPTIONS.offset,
|
|
65
|
+
zIndex: (_g = options.zIndex) !== null && _g !== void 0 ? _g : DEFAULT_BUTTON_OPTIONS.zIndex,
|
|
66
|
+
tooltip: (_h = options.tooltip) !== null && _h !== void 0 ? _h : DEFAULT_BUTTON_OPTIONS.tooltip,
|
|
49
67
|
};
|
|
50
68
|
this.button = this.createButton();
|
|
51
69
|
// Ensure DOM is ready before appending
|
|
@@ -60,8 +78,18 @@ class FloatingButton {
|
|
|
60
78
|
}
|
|
61
79
|
createButton() {
|
|
62
80
|
const btn = document.createElement('button');
|
|
63
|
-
|
|
64
|
-
|
|
81
|
+
// Set button content (SVG or text)
|
|
82
|
+
if (this.options.customSvg) {
|
|
83
|
+
btn.innerHTML = this.options.customSvg;
|
|
84
|
+
}
|
|
85
|
+
else if (this.options.icon === 'svg') {
|
|
86
|
+
btn.innerHTML = DEFAULT_SVG_ICON;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
btn.textContent = this.options.icon;
|
|
90
|
+
}
|
|
91
|
+
btn.setAttribute('aria-label', this.options.tooltip);
|
|
92
|
+
btn.setAttribute('title', this.options.tooltip);
|
|
65
93
|
btn.setAttribute('data-bugspotter-exclude', 'true');
|
|
66
94
|
btn.style.cssText = this.getButtonStyles();
|
|
67
95
|
this.addHoverEffects(btn);
|
|
@@ -70,6 +98,9 @@ class FloatingButton {
|
|
|
70
98
|
getButtonStyles() {
|
|
71
99
|
const { position, size, offset, backgroundColor, zIndex } = this.options;
|
|
72
100
|
const positionStyles = this.getPositionStyles(position, offset);
|
|
101
|
+
// SVG icons need slightly different sizing
|
|
102
|
+
const isSvgIcon = this.options.customSvg || this.options.icon === 'svg';
|
|
103
|
+
const iconSize = size * 0.5;
|
|
73
104
|
return `
|
|
74
105
|
position: fixed;
|
|
75
106
|
${positionStyles}
|
|
@@ -80,10 +111,11 @@ class FloatingButton {
|
|
|
80
111
|
color: white;
|
|
81
112
|
border: none;
|
|
82
113
|
cursor: pointer;
|
|
83
|
-
font-size: ${
|
|
114
|
+
font-size: ${iconSize}px;
|
|
84
115
|
display: flex;
|
|
85
116
|
align-items: center;
|
|
86
117
|
justify-content: center;
|
|
118
|
+
padding: ${isSvgIcon ? size * 0.25 : 0}px;
|
|
87
119
|
box-shadow: ${BUTTON_STYLES.boxShadow.default};
|
|
88
120
|
transition: ${BUTTON_STYLES.transition};
|
|
89
121
|
z-index: ${zIndex};
|
|
@@ -125,7 +157,13 @@ class FloatingButton {
|
|
|
125
157
|
this.button.style.display = 'none';
|
|
126
158
|
}
|
|
127
159
|
setIcon(icon) {
|
|
128
|
-
this.
|
|
160
|
+
this.options.icon = icon;
|
|
161
|
+
if (icon === 'svg') {
|
|
162
|
+
this.button.innerHTML = DEFAULT_SVG_ICON;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
this.button.textContent = icon;
|
|
166
|
+
}
|
|
129
167
|
}
|
|
130
168
|
setBackgroundColor(color) {
|
|
131
169
|
this.button.style.backgroundColor = color;
|
package/docs/SESSION_REPLAY.md
CHANGED
|
@@ -85,22 +85,84 @@ console.log(report.replay); // Array of rrweb events
|
|
|
85
85
|
|
|
86
86
|
### 4. Event Transmission
|
|
87
87
|
|
|
88
|
-
The replay events are
|
|
88
|
+
The replay events are compressed and uploaded via presigned URLs using an optimized 3-request flow:
|
|
89
|
+
|
|
90
|
+
**Step 1: Create Bug Report with Presigned URLs**
|
|
89
91
|
|
|
90
92
|
```typescript
|
|
93
|
+
// Initial bug report with flags indicating which files will be uploaded
|
|
94
|
+
POST /api/v1/reports
|
|
91
95
|
{
|
|
92
96
|
title: "Bug title",
|
|
93
97
|
description: "Bug description",
|
|
94
98
|
report: {
|
|
95
|
-
screenshot: "...",
|
|
96
99
|
console: [...],
|
|
97
100
|
network: [...],
|
|
98
|
-
metadata: {...}
|
|
99
|
-
|
|
101
|
+
metadata: {...}
|
|
102
|
+
},
|
|
103
|
+
hasScreenshot: true, // SDK sets this if screenshot was captured
|
|
104
|
+
hasReplay: true // SDK sets this if replay events were recorded
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Response includes bug report ID AND presigned URLs
|
|
108
|
+
{
|
|
109
|
+
"success": true,
|
|
110
|
+
"data": {
|
|
111
|
+
"id": "bug-uuid-here",
|
|
112
|
+
"title": "Bug title",
|
|
113
|
+
"presignedUrls": {
|
|
114
|
+
"screenshot": {
|
|
115
|
+
"uploadUrl": "https://s3.amazonaws.com/...",
|
|
116
|
+
"storageKey": "screenshots/project/bug/screenshot.png"
|
|
117
|
+
},
|
|
118
|
+
"replay": {
|
|
119
|
+
"uploadUrl": "https://s3.amazonaws.com/...",
|
|
120
|
+
"storageKey": "replays/project/bug/replay.gz"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
100
123
|
}
|
|
101
124
|
}
|
|
102
125
|
```
|
|
103
126
|
|
|
127
|
+
**Step 2: Upload Files Directly to S3**
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// Upload screenshot to presigned URL (parallel with replay)
|
|
131
|
+
PUT https://s3.amazonaws.com/presigned-screenshot-url
|
|
132
|
+
Content-Type: image/png
|
|
133
|
+
<binary screenshot data>
|
|
134
|
+
|
|
135
|
+
// Upload compressed replay to presigned URL (parallel with screenshot)
|
|
136
|
+
PUT https://s3.amazonaws.com/presigned-replay-url
|
|
137
|
+
Content-Type: application/gzip
|
|
138
|
+
<compressed replay events>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Step 3: Confirm Uploads**
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// Confirm screenshot upload
|
|
145
|
+
POST /api/v1/reports/{bugId}/confirm-upload
|
|
146
|
+
{
|
|
147
|
+
"fileType": "screenshot"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Confirm replay upload
|
|
151
|
+
POST /api/v1/reports/{bugId}/confirm-upload
|
|
152
|
+
{
|
|
153
|
+
"fileType": "replay"
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Optimized Flow Benefits:**
|
|
158
|
+
|
|
159
|
+
- **40% fewer HTTP requests** - 3 requests vs 5 in old flow
|
|
160
|
+
- **Reduces server load** - Files go directly to storage (S3)
|
|
161
|
+
- **Improves performance** - No API server bottleneck for large files
|
|
162
|
+
- **Better scalability** - Storage handles the bandwidth
|
|
163
|
+
- **Parallel uploads** - Screenshot and replay upload concurrently
|
|
164
|
+
- **Automatic compression** - Replay events are gzipped before upload
|
|
165
|
+
|
|
104
166
|
## Event Types
|
|
105
167
|
|
|
106
168
|
The DOM collector captures the following event types:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bugspotter/sdk",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
4
4
|
"description": "Professional bug reporting SDK with screenshots, session replay, and automatic error capture for web applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"build": "npm run build:webpack && npm run build:esm && npm run build:cjs",
|
|
20
20
|
"build:webpack": "webpack --mode production",
|
|
21
21
|
"build:esm": "tsc --project tsconfig.build.json --module ES2020 --outDir dist/esm && shx mv dist/esm/index.js dist/index.esm.js && shx rm -rf dist/esm",
|
|
22
|
-
"build:cjs": "tsc --project tsconfig.
|
|
22
|
+
"build:cjs": "tsc --project tsconfig.cjs.json",
|
|
23
23
|
"prepublishOnly": "npm run build && npm test",
|
|
24
24
|
"lint": "eslint \"src/**/*.{ts,js}\"",
|
|
25
25
|
"lint:fix": "eslint \"src/**/*.{ts,js}\" --fix",
|
|
@@ -65,7 +65,6 @@
|
|
|
65
65
|
"registry": "https://registry.npmjs.org/"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@bugspotter/types": "workspace:*",
|
|
69
68
|
"@rrweb/types": "2.0.0-alpha.18",
|
|
70
69
|
"html-to-image": "^1.11.13",
|
|
71
70
|
"pako": "^2.1.0",
|