@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/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
- * Actual file uploads use presigned URLs (screenshotKey/replayKey set after upload)
24
+ * File uploads use presigned URLs returned from the backend
23
25
  */
24
26
  capture(): Promise<BugReport>;
25
27
  private handleBugReport;
26
- private submitBugReport;
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
- /** Authentication configuration */
35
- auth?: AuthConfig;
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: string;
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: DirectUploader is created per-report since it needs bugId
31
- // See submitBugReport() for initialization
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 : 15,
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
- * Actual file uploads use presigned URLs (screenshotKey/replayKey set after upload)
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.submitBugReport(Object.assign(Object.assign({}, data), { report }));
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
- async submitBugReport(payload) {
98
- if (!this.config.endpoint) {
99
- throw new Error('No endpoint configured for bug report submission');
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
- logger.warn(`Submitting bug report to ${this.config.endpoint}`);
105
- let body;
106
- 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
- }
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
- return response.json().catch(() => {
146
- return undefined;
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: DirectUploader is created per-report since it needs bugId
35
- // See submitBugReport() for initialization
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 : 15,
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
- * Actual file uploads use presigned URLs (screenshotKey/replayKey set after upload)
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.submitBugReport(Object.assign(Object.assign({}, data), { report }));
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
- async submitBugReport(payload) {
102
- if (!this.config.endpoint) {
103
- throw new Error('No endpoint configured for bug report submission');
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
- logger.warn(`Submitting bug report to ${this.config.endpoint}`);
109
- let body;
110
- 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
- }
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
- return response.json().catch(() => {
150
- return undefined;
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 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; } });
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;