@bugspotter/sdk 0.2.5-alpha.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,7 @@ export declare class ConsoleCapture extends BaseCapture<LogEntry[], ConsoleCaptu
23
23
  /**
24
24
  * Check if log should be filtered (SDK internal logs)
25
25
  * Filters out SDK debug logs (prefix [BugSpotter]) except errors
26
+ * Errors are always captured even if they contain SDK prefix
26
27
  */
27
28
  private shouldFilterLog;
28
29
  private interceptConsole;
@@ -3,7 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConsoleCapture = exports.SDK_LOG_PREFIX = void 0;
4
4
  const base_capture_1 = require("./base-capture");
5
5
  const circular_buffer_1 = require("../core/circular-buffer");
6
- const CONSOLE_METHODS = ['log', 'warn', 'error', 'info', 'debug'];
6
+ const CONSOLE_METHODS = [
7
+ 'log',
8
+ 'warn',
9
+ 'error',
10
+ 'info',
11
+ 'debug',
12
+ ];
7
13
  /**
8
14
  * Prefix used for SDK internal log messages
9
15
  * Exported for testing purposes
@@ -27,7 +33,9 @@ class ConsoleCapture extends base_capture_1.BaseCapture {
27
33
  return '';
28
34
  }
29
35
  // Sanitize args if sanitizer is enabled
30
- const sanitizedArgs = this.sanitizer ? this.sanitizer.sanitizeConsoleArgs(args) : args;
36
+ const sanitizedArgs = this.sanitizer
37
+ ? this.sanitizer.sanitizeConsoleArgs(args)
38
+ : args;
31
39
  return sanitizedArgs
32
40
  .map((arg) => {
33
41
  var _a;
@@ -57,7 +65,10 @@ class ConsoleCapture extends base_capture_1.BaseCapture {
57
65
  };
58
66
  if (this.captureStackTrace && method === 'error') {
59
67
  const stack = this.captureStack();
60
- log.stack = this.sanitizer && stack ? this.sanitizer.sanitize(stack) : stack;
68
+ log.stack =
69
+ this.sanitizer && stack
70
+ ? this.sanitizer.sanitize(stack)
71
+ : stack;
61
72
  }
62
73
  return log;
63
74
  }
@@ -72,13 +83,15 @@ class ConsoleCapture extends base_capture_1.BaseCapture {
72
83
  /**
73
84
  * Check if log should be filtered (SDK internal logs)
74
85
  * Filters out SDK debug logs (prefix [BugSpotter]) except errors
86
+ * Errors are always captured even if they contain SDK prefix
75
87
  */
76
88
  shouldFilterLog(message, level) {
77
89
  // Always keep SDK errors for debugging
78
90
  if (level === 'error') {
79
91
  return false;
80
92
  }
81
- // Filter SDK internal logs (debug/info/warn only)
93
+ // Filter SDK internal logs (debug/info/warn/log only)
94
+ // Use startsWith to only match prefix, not substring anywhere in message
82
95
  return message.startsWith(exports.SDK_LOG_PREFIX);
83
96
  }
84
97
  interceptConsole(levels = CONSOLE_METHODS) {
@@ -2,6 +2,12 @@ import { BaseCapture, type CaptureOptions } from './base-capture';
2
2
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
3
3
  export interface NetworkCaptureOptions extends CaptureOptions {
4
4
  maxRequests?: number;
5
+ /**
6
+ * Optional filter function to exclude URLs from capture
7
+ * Returns true to CAPTURE the URL, false to FILTER it out
8
+ * Note: Error responses (non-2xx status) are always captured regardless of filter result
9
+ * Example: (url) => !url.startsWith('https://api.bugspotter.com') captures all except SDK API
10
+ */
5
11
  filterUrls?: (url: string) => boolean;
6
12
  }
7
13
  export declare class NetworkCapture extends BaseCapture<NetworkRequest[], NetworkCaptureOptions> {
@@ -54,8 +54,18 @@ class NetworkCapture extends base_capture_1.BaseCapture {
54
54
  return request;
55
55
  }
56
56
  addRequest(request) {
57
- if (this.filterUrls && !this.filterUrls(request.url)) {
58
- return; // Skip filtered URLs
57
+ // Check if URL should be filtered
58
+ // filterUrls returns true to CAPTURE, false to FILTER OUT
59
+ // Exception: Always capture error responses (non-2xx) for debugging purposes
60
+ // Rationale: Error responses from SDK API calls (4xx, 5xx) are invaluable for diagnosing
61
+ // connectivity issues, authentication failures, rate limiting, or backend problems that
62
+ // could affect bug report submission. Users need this visibility to troubleshoot SDK issues.
63
+ if (this.filterUrls) {
64
+ const shouldCapture = this.filterUrls(request.url);
65
+ const isError = request.status < 200 || request.status >= 300;
66
+ if (!shouldCapture && !isError) {
67
+ return; // Filter out successful requests from filtered URLs
68
+ }
59
69
  }
60
70
  this.buffer.add(request);
61
71
  }
@@ -17,14 +17,15 @@ class ScreenshotCapture extends base_capture_1.BaseCapture {
17
17
  }
18
18
  getErrorPlaceholder() {
19
19
  var _a;
20
- return (_a = this.options.errorPlaceholder) !== null && _a !== void 0 ? _a : DEFAULT_SCREENSHOT_OPTIONS.errorPlaceholder;
20
+ return ((_a = this.options.errorPlaceholder) !== null && _a !== void 0 ? _a : DEFAULT_SCREENSHOT_OPTIONS.errorPlaceholder);
21
21
  }
22
22
  shouldIncludeNode(node) {
23
23
  if (!('hasAttribute' in node)) {
24
24
  return true;
25
25
  }
26
26
  const element = node;
27
- const excludeAttr = this.options.excludeAttribute || DEFAULT_SCREENSHOT_OPTIONS.excludeAttribute;
27
+ const excludeAttr = this.options.excludeAttribute ||
28
+ DEFAULT_SCREENSHOT_OPTIONS.excludeAttribute;
28
29
  return !element.hasAttribute(excludeAttr);
29
30
  }
30
31
  buildCaptureOptions() {
@@ -57,7 +57,8 @@ class CircularBuffer {
57
57
  }
58
58
  }
59
59
  // Preserve full snapshot even if it's older than cutoff
60
- if (this.lastFullSnapshotIndex >= 0 && this.lastFullSnapshotIndex < firstValidIndex) {
60
+ if (this.lastFullSnapshotIndex >= 0 &&
61
+ this.lastFullSnapshotIndex < firstValidIndex) {
61
62
  firstValidIndex = this.lastFullSnapshotIndex;
62
63
  }
63
64
  // Nothing to prune
@@ -40,12 +40,16 @@ function isBugReportResponse(obj) {
40
40
  // When success is true, data.id must exist
41
41
  if (response.success) {
42
42
  const data = response.data;
43
- if (!data || typeof data !== 'object' || !('id' in data) || typeof data.id !== 'string') {
43
+ if (!data ||
44
+ typeof data !== 'object' ||
45
+ !('id' in data) ||
46
+ typeof data.id !== 'string') {
44
47
  return false;
45
48
  }
46
49
  // If presignedUrls exists, it must be an object
47
50
  if ('presignedUrls' in data && data.presignedUrls !== undefined) {
48
- if (typeof data.presignedUrls !== 'object' || data.presignedUrls === null) {
51
+ if (typeof data.presignedUrls !== 'object' ||
52
+ data.presignedUrls === null) {
49
53
  return false;
50
54
  }
51
55
  }
@@ -183,7 +187,9 @@ class BugReporter {
183
187
  success: result.success,
184
188
  bugId: bugData.id,
185
189
  hasPresignedUrls: !!bugData.presignedUrls,
186
- presignedUrlKeys: bugData.presignedUrls ? Object.keys(bugData.presignedUrls) : [],
190
+ presignedUrlKeys: bugData.presignedUrls
191
+ ? Object.keys(bugData.presignedUrls)
192
+ : [],
187
193
  });
188
194
  return bugData;
189
195
  }
@@ -197,7 +203,9 @@ class BugReporter {
197
203
  const { report } = payload;
198
204
  const fileAnalysis = analyzeReportFiles(report);
199
205
  if (!fileAnalysis.hasScreenshot && !fileAnalysis.hasReplay) {
200
- logger.debug('No files to upload, bug report created successfully', { bugId });
206
+ logger.debug('No files to upload, bug report created successfully', {
207
+ bugId,
208
+ });
201
209
  this.deduplicator.recordSubmission(dedupContext.title, dedupContext.description, dedupContext.errorStacks);
202
210
  return;
203
211
  }
@@ -217,7 +225,10 @@ class BugReporter {
217
225
  this.deduplicator.recordSubmission(dedupContext.title, dedupContext.description, dedupContext.errorStacks);
218
226
  }
219
227
  catch (error) {
220
- logger.error('File upload failed', { bugId, error: formatSubmissionError('Upload', error) });
228
+ logger.error('File upload failed', {
229
+ bugId,
230
+ error: formatSubmissionError('Upload', error),
231
+ });
221
232
  throw new Error(formatSubmissionError(`Bug report created (ID: ${bugId}) but file upload failed`, error));
222
233
  }
223
234
  }
@@ -39,7 +39,10 @@ class CircularBuffer {
39
39
  return [...this.items];
40
40
  }
41
41
  // Return items in chronological order when buffer is full
42
- return [...this.items.slice(this.index), ...this.items.slice(0, this.index)];
42
+ return [
43
+ ...this.items.slice(this.index),
44
+ ...this.items.slice(0, this.index),
45
+ ];
43
46
  }
44
47
  /**
45
48
  * Clear all items from the buffer.
@@ -128,7 +128,8 @@ function supportsWebP() {
128
128
  }
129
129
  try {
130
130
  const canvas = document.createElement('canvas');
131
- webpSupportCache = canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
131
+ webpSupportCache =
132
+ canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
132
133
  }
133
134
  catch (_a) {
134
135
  webpSupportCache = false;
@@ -35,7 +35,8 @@ class FileUploadHandler {
35
35
  async prepareFiles(report, presignedUrls) {
36
36
  const files = [];
37
37
  // Prepare screenshot
38
- if (report._screenshotPreview && report._screenshotPreview.startsWith('data:image/')) {
38
+ if (report._screenshotPreview &&
39
+ report._screenshotPreview.startsWith('data:image/')) {
39
40
  const screenshotUrl = this.getPresignedUrl('screenshot', presignedUrls);
40
41
  const screenshotBlob = await this.dataUrlToBlob(report._screenshotPreview);
41
42
  files.push({
@@ -49,7 +50,9 @@ class FileUploadHandler {
49
50
  if (report.replay && report.replay.length > 0) {
50
51
  const replayUrl = this.getPresignedUrl('replay', presignedUrls);
51
52
  const compressed = await (0, compress_1.compressData)(report.replay);
52
- const replayBlob = new Blob([compressed], { type: 'application/gzip' });
53
+ const replayBlob = new Blob([compressed], {
54
+ type: 'application/gzip',
55
+ });
53
56
  files.push({
54
57
  type: 'replay',
55
58
  url: replayUrl.uploadUrl,
@@ -247,7 +247,9 @@ class OfflineQueue {
247
247
  }
248
248
  // Check error message as fallback (Firefox, Chrome variants)
249
249
  const message = error.message.toLowerCase();
250
- return message.includes('quota') || message.includes('storage') || message.includes('exceeded');
250
+ return (message.includes('quota') ||
251
+ message.includes('storage') ||
252
+ message.includes('exceeded'));
251
253
  }
252
254
  /**
253
255
  * Clear oldest 50% of items and retry save
@@ -279,7 +281,8 @@ class OfflineQueue {
279
281
  else {
280
282
  // Fallback to Math.random for environments without crypto
281
283
  randomPart =
282
- Math.random().toString(36).substring(2, 9) + Math.random().toString(36).substring(2, 9);
284
+ Math.random().toString(36).substring(2, 9) +
285
+ Math.random().toString(36).substring(2, 9);
283
286
  }
284
287
  return `req_${Date.now()}_${this.requestCounter}_${randomPart}`;
285
288
  }
@@ -75,7 +75,8 @@ class RetryHandler {
75
75
  try {
76
76
  const response = await operation();
77
77
  // Check if we should retry based on status code
78
- if (shouldRetryStatus(response.status) && attempt < this.config.maxRetries) {
78
+ if (shouldRetryStatus(response.status) &&
79
+ attempt < this.config.maxRetries) {
79
80
  const delay = this.calculateDelay(attempt, response);
80
81
  this.logger.warn(`Request failed with status ${response.status}, retrying in ${delay}ms (attempt ${attempt + 1}/${this.config.maxRetries})`);
81
82
  await sleep(delay);
@@ -239,7 +240,8 @@ function isNetworkError(error) {
239
240
  error.name === 'NetworkError' ||
240
241
  error.name === 'AbortError' ||
241
242
  // TypeError only if it mentions fetch or network
242
- (error.name === 'TypeError' && (message.includes('fetch') || message.includes('network'))));
243
+ (error.name === 'TypeError' &&
244
+ (message.includes('fetch') || message.includes('network'))));
243
245
  }
244
246
  // Re-export offline queue utilities
245
247
  var offline_queue_2 = require("./offline-queue");
@@ -27,7 +27,9 @@ async function compressReplayEvents(events) {
27
27
  try {
28
28
  // Use modern streaming API: Blob → ReadableStream → CompressionStream → Response → Blob
29
29
  const blob = new Blob([data]);
30
- const compressedStream = blob.stream().pipeThrough(new CompressionStream('gzip'));
30
+ const compressedStream = blob
31
+ .stream()
32
+ .pipeThrough(new CompressionStream('gzip'));
31
33
  return await new Response(compressedStream, {
32
34
  headers: { 'Content-Type': 'application/gzip' },
33
35
  }).blob();
package/dist/index.d.ts CHANGED
@@ -132,8 +132,8 @@ export type { DOMCollectorConfig } from './collectors';
132
132
  export { CircularBuffer } from './core/buffer';
133
133
  export type { CircularBufferConfig } from './core/buffer';
134
134
  export { compressData, decompressData, compressImage, estimateSize, getCompressionRatio, } from './core/compress';
135
- export { submitWithAuth, getAuthHeaders, clearOfflineQueue } from './core/transport';
136
- export type { AuthConfig, TransportOptions, RetryConfig } from './core/transport';
135
+ export { submitWithAuth, getAuthHeaders, clearOfflineQueue, } from './core/transport';
136
+ export type { AuthConfig, TransportOptions, RetryConfig, } from './core/transport';
137
137
  export type { OfflineConfig } from './core/offline-queue';
138
138
  export type { Logger, LogLevel, LoggerConfig } from './utils/logger';
139
139
  export { getLogger, configureLogger, createLogger } from './utils/logger';
@@ -141,8 +141,8 @@ export { DirectUploader } from './core/uploader';
141
141
  export type { UploadResult } from './core/uploader';
142
142
  export { compressReplayEvents, canvasToBlob, estimateCompressedReplaySize, isWithinSizeLimit, } from './core/upload-helpers';
143
143
  export { createSanitizer, Sanitizer } from './utils/sanitize';
144
- export type { PIIPattern, CustomPattern, SanitizeConfig } from './utils/sanitize';
145
- export { getApiBaseUrl, stripEndpointSuffix, InvalidEndpointError } from './utils/url-helpers';
144
+ export type { PIIPattern, CustomPattern, SanitizeConfig, } from './utils/sanitize';
145
+ export { getApiBaseUrl, stripEndpointSuffix, InvalidEndpointError, } from './utils/url-helpers';
146
146
  export { validateAuthConfig } from './utils/config-validator';
147
147
  export type { ValidationContext } from './utils/config-validator';
148
148
  export { DEFAULT_PATTERNS, PATTERN_PRESETS, PATTERN_CATEGORIES, PatternBuilder, createPatternConfig, getPattern, getPatternsByCategory, validatePattern, } from './utils/sanitize';
@@ -150,7 +150,7 @@ export type { PIIPatternName, PatternDefinition } from './utils/sanitize';
150
150
  export { FloatingButton } from './widget/button';
151
151
  export type { FloatingButtonOptions } from './widget/button';
152
152
  export { BugReportModal } from './widget/modal';
153
- export type { BugReportData, BugReportModalOptions, PIIDetection } from './widget/modal';
153
+ export type { BugReportData, BugReportModalOptions, PIIDetection, } from './widget/modal';
154
154
  export type { eventWithTime } from '@rrweb/types';
155
155
  export { DEFAULT_REPLAY_DURATION_SECONDS, MAX_RECOMMENDED_REPLAY_DURATION_SECONDS, } from './constants';
156
156
  /**