@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/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 : 15,
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
- async submitBugReport(payload) {
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
- const contentHeaders = {
102
- 'Content-Type': 'application/json',
103
- };
104
- logger.warn(`Submitting bug report to ${this.config.endpoint}`);
105
- let body;
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
- // 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
- }
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 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,
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
- return response.json().catch(() => {
146
- return undefined;
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 : 15,
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
- async submitBugReport(payload) {
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
- const contentHeaders = {
106
- 'Content-Type': 'application/json',
107
- };
108
- logger.warn(`Submitting bug report to ${this.config.endpoint}`);
109
- let body;
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
- // 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
- }
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 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,
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
- return response.json().catch(() => {
150
- return undefined;
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 compress_2 = require("./core/compress");
182
- Object.defineProperty(exports, "compressData", { enumerable: true, get: function () { return compress_2.compressData; } });
183
- Object.defineProperty(exports, "decompressData", { enumerable: true, get: function () { return compress_2.decompressData; } });
184
- Object.defineProperty(exports, "compressImage", { enumerable: true, get: function () { return compress_2.compressImage; } });
185
- Object.defineProperty(exports, "estimateSize", { enumerable: true, get: function () { return compress_2.estimateSize; } });
186
- Object.defineProperty(exports, "getCompressionRatio", { enumerable: true, get: function () { return compress_2.getCompressionRatio; } });
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
@@ -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;
@@ -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
- backgroundColor: '#ef4444',
8
- size: 60,
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
- backgroundColor: (_c = options.backgroundColor) !== null && _c !== void 0 ? _c : DEFAULT_BUTTON_OPTIONS.backgroundColor,
46
- size: (_d = options.size) !== null && _d !== void 0 ? _d : DEFAULT_BUTTON_OPTIONS.size,
47
- offset: (_e = options.offset) !== null && _e !== void 0 ? _e : DEFAULT_BUTTON_OPTIONS.offset,
48
- zIndex: (_f = options.zIndex) !== null && _f !== void 0 ? _f : DEFAULT_BUTTON_OPTIONS.zIndex,
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
- btn.textContent = this.options.icon;
64
- btn.setAttribute('aria-label', 'Report Bug');
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: ${size * 0.5}px;
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.button.textContent = icon;
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;
@@ -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 included in the bug report payload:
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
- replay: [...] // rrweb events
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.1",
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.build.json --module CommonJS --outDir dist",
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",