@bugspotter/sdk 2.0.0 → 2.1.0

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.
@@ -20,6 +20,10 @@ export interface DOMCollectorConfig {
20
20
  recordCanvas?: boolean;
21
21
  /** Whether to record cross-origin iframes (default: false) */
22
22
  recordCrossOriginIframes?: boolean;
23
+ /** CSS selectors for elements to block from recording */
24
+ blockSelectors?: string[];
25
+ /** CSS class name to block elements from recording */
26
+ blockClass?: string;
23
27
  /** Sanitizer for PII protection */
24
28
  sanitizer?: Sanitizer;
25
29
  }
@@ -26,6 +26,8 @@ class DOMCollector {
26
26
  collectFonts: (_h = config.collectFonts) !== null && _h !== void 0 ? _h : false,
27
27
  recordCanvas: (_j = config.recordCanvas) !== null && _j !== void 0 ? _j : false,
28
28
  recordCrossOriginIframes: (_k = config.recordCrossOriginIframes) !== null && _k !== void 0 ? _k : false,
29
+ blockSelectors: config.blockSelectors,
30
+ blockClass: config.blockClass,
29
31
  sanitizer: config.sanitizer,
30
32
  };
31
33
  this.buffer = new buffer_1.CircularBuffer({
@@ -36,30 +38,26 @@ class DOMCollector {
36
38
  * Start recording DOM events
37
39
  */
38
40
  startRecording() {
39
- var _a, _b, _c, _d;
41
+ var _a, _b, _c, _d, _e;
40
42
  if (this.isRecording) {
41
43
  (0, logger_1.getLogger)().warn('DOMCollector: Recording already in progress');
42
44
  return;
43
45
  }
44
46
  try {
45
- const recordConfig = {
46
- emit: (event) => {
47
+ const recordConfig = Object.assign(Object.assign({ emit: (event) => {
47
48
  this.buffer.add(event);
48
- },
49
- sampling: {
49
+ }, sampling: {
50
50
  mousemove: (_b = (_a = this.config.sampling) === null || _a === void 0 ? void 0 : _a.mousemove) !== null && _b !== void 0 ? _b : 50,
51
51
  scroll: (_d = (_c = this.config.sampling) === null || _c === void 0 ? void 0 : _c.scroll) !== null && _d !== void 0 ? _d : 100,
52
52
  // Record all mouse interactions for replay visibility
53
53
  mouseInteraction: true,
54
- },
55
- recordCanvas: this.config.recordCanvas,
56
- recordCrossOriginIframes: this.config.recordCrossOriginIframes,
54
+ }, recordCanvas: this.config.recordCanvas, recordCrossOriginIframes: this.config.recordCrossOriginIframes,
57
55
  // PII sanitization for text content
58
56
  maskTextFn: this.sanitizer
59
57
  ? (text, element) => {
60
58
  return this.sanitizer.sanitizeTextNode(text, element);
61
59
  }
62
- : undefined,
60
+ : undefined,
63
61
  // Performance optimizations
64
62
  slimDOMOptions: {
65
63
  script: true, // Don't record script tags
@@ -71,12 +69,13 @@ class DOMCollector {
71
69
  headMetaHttpEquiv: true, // Don't record http-equiv meta tags
72
70
  headMetaAuthorship: true, // Don't record authorship meta tags
73
71
  headMetaVerification: true, // Don't record verification meta tags
74
- },
72
+ },
75
73
  // Quality settings (controlled by backend or user config)
76
- inlineStylesheet: this.config.inlineStylesheet,
77
- inlineImages: this.config.inlineImages,
78
- collectFonts: this.config.collectFonts,
79
- };
74
+ inlineStylesheet: this.config.inlineStylesheet, inlineImages: this.config.inlineImages, collectFonts: this.config.collectFonts }, (((_e = this.config.blockSelectors) === null || _e === void 0 ? void 0 : _e.length) && {
75
+ blockSelector: this.config.blockSelectors.join(','),
76
+ })), (this.config.blockClass && {
77
+ blockClass: this.config.blockClass,
78
+ }));
80
79
  this.stopRecordingFn = (0, rrweb_1.record)(recordConfig);
81
80
  this.isRecording = true;
82
81
  (0, logger_1.getLogger)().debug('DOMCollector: Started recording');
@@ -137,7 +137,7 @@ class BugReporter {
137
137
  var _a;
138
138
  (0, config_validator_1.validateAuthConfig)({
139
139
  endpoint: this.config.endpoint,
140
- auth: this.config.auth,
140
+ apiKey: this.config.apiKey,
141
141
  });
142
142
  const dedupContext = createDeduplicationContext(payload);
143
143
  if (this.deduplicator.isDuplicate(dedupContext.title, dedupContext.description, dedupContext.errorStacks)) {
@@ -165,8 +165,10 @@ class BugReporter {
165
165
  network: report.network,
166
166
  metadata: report.metadata,
167
167
  }, hasScreenshot: fileAnalysis.hasScreenshot, hasReplay: fileAnalysis.hasReplay });
168
- const response = await (0, transport_1.submitWithAuth)(this.config.endpoint, JSON.stringify(createPayload), { 'Content-Type': 'application/json' }, {
169
- auth: this.config.auth,
168
+ const apiBaseUrl = (0, url_helpers_1.getApiBaseUrl)(this.config.endpoint);
169
+ const submitUrl = `${apiBaseUrl}/api/v1/reports`;
170
+ const response = await (0, transport_1.submitWithAuth)(submitUrl, JSON.stringify(createPayload), { 'Content-Type': 'application/json' }, {
171
+ auth: { apiKey: this.config.apiKey },
170
172
  retry: this.config.retry,
171
173
  offline: this.config.offline,
172
174
  });
@@ -218,7 +220,7 @@ class BugReporter {
218
220
  throw new Error('Server did not provide presigned URLs for file uploads. Check backend configuration.');
219
221
  }
220
222
  const apiEndpoint = (0, url_helpers_1.getApiBaseUrl)(this.config.endpoint);
221
- const uploadHandler = new file_upload_handler_1.FileUploadHandler(apiEndpoint, this.config.auth.apiKey);
223
+ const uploadHandler = new file_upload_handler_1.FileUploadHandler(apiEndpoint, this.config.apiKey);
222
224
  try {
223
225
  await uploadHandler.uploadFiles(bugId, report, bugReportData.presignedUrls);
224
226
  logger.debug('File uploads completed successfully', { bugId });
@@ -22,6 +22,8 @@ export interface CaptureManagerConfig {
22
22
  collectFonts?: boolean;
23
23
  recordCanvas?: boolean;
24
24
  recordCrossOriginIframes?: boolean;
25
+ blockSelectors?: string[];
26
+ blockClass?: string;
25
27
  };
26
28
  }
27
29
  /**
@@ -17,7 +17,7 @@ const constants_1 = require("../constants");
17
17
  */
18
18
  class CaptureManager {
19
19
  constructor(config) {
20
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
20
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
21
21
  // Initialize core capture modules
22
22
  this.screenshot = new screenshot_1.ScreenshotCapture();
23
23
  this.console = new console_1.ConsoleCapture({ sanitizer: config.sanitizer });
@@ -42,6 +42,8 @@ class CaptureManager {
42
42
  collectFonts: (_h = config.replay) === null || _h === void 0 ? void 0 : _h.collectFonts,
43
43
  recordCanvas: (_j = config.replay) === null || _j === void 0 ? void 0 : _j.recordCanvas,
44
44
  recordCrossOriginIframes: (_k = config.replay) === null || _k === void 0 ? void 0 : _k.recordCrossOriginIframes,
45
+ blockSelectors: (_l = config.replay) === null || _l === void 0 ? void 0 : _l.blockSelectors,
46
+ blockClass: (_m = config.replay) === null || _m === void 0 ? void 0 : _m.blockClass,
45
47
  sanitizer: config.sanitizer,
46
48
  });
47
49
  this.domCollector.startRecording();
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { BrowserMetadata } from './capture/metadata';
2
2
  import { type FloatingButtonOptions } from './widget/button';
3
3
  import type { eventWithTime } from '@rrweb/types';
4
- import type { AuthConfig, RetryConfig } from './core/transport';
4
+ import type { RetryConfig } from './core/transport';
5
5
  import type { OfflineConfig } from './core/offline-queue';
6
6
  import { VERSION } from './version';
7
7
  import { type DeduplicationConfig } from './utils/deduplicator';
@@ -12,9 +12,10 @@ export declare class BugSpotter {
12
12
  private config;
13
13
  private widget?;
14
14
  private sanitizer?;
15
- private captureManager;
15
+ private captureManager?;
16
16
  private bugReporter;
17
- constructor(config: BugSpotterConfig);
17
+ private _sampled;
18
+ constructor(config: BugSpotterConfig, sampled?: boolean);
18
19
  static init(config: BugSpotterConfig): Promise<BugSpotter>;
19
20
  /**
20
21
  * Internal factory method to create a new BugSpotter instance
@@ -27,6 +28,8 @@ export declare class BugSpotter {
27
28
  * Note: Screenshot is captured for modal preview only (_screenshotPreview)
28
29
  * File uploads use presigned URLs returned from the backend
29
30
  */
31
+ /** Whether this session was sampled for capture */
32
+ get isSampled(): boolean;
30
33
  capture(): Promise<BugReport>;
31
34
  private handleBugReport;
32
35
  /**
@@ -39,14 +42,19 @@ export declare class BugSpotter {
39
42
  destroy(): void;
40
43
  }
41
44
  export interface BugSpotterConfig {
45
+ /** Base URL of BugSpotter API (e.g., https://api.example.com). SDK appends paths internally. */
42
46
  endpoint?: string;
43
- showWidget?: boolean;
44
- widgetOptions?: FloatingButtonOptions;
47
+ /** API key for authentication (starts with 'bgs_'). Required. */
48
+ apiKey: string;
45
49
  /**
46
- * Authentication configuration (required)
47
- * API key authentication with project ID
50
+ * Session sampling rate (0 to 1). Controls what fraction of sessions activate capture.
51
+ * - 1 = capture all sessions (default)
52
+ * - 0.1 = capture 10% of sessions
53
+ * - 0 = capture nothing (SDK initializes but is inactive)
48
54
  */
49
- auth: AuthConfig;
55
+ sampleRate?: number;
56
+ showWidget?: boolean;
57
+ widgetOptions?: FloatingButtonOptions;
50
58
  /** Retry configuration for failed requests */
51
59
  retry?: RetryConfig;
52
60
  /** Offline queue configuration */
@@ -76,6 +84,19 @@ export interface BugSpotterConfig {
76
84
  recordCanvas?: boolean;
77
85
  /** Whether to record cross-origin iframes (default: backend controlled) */
78
86
  recordCrossOriginIframes?: boolean;
87
+ /**
88
+ * CSS selectors for DOM elements to exclude from session replay.
89
+ * Matched elements are replaced with a placeholder in the recording.
90
+ * Use for sensitive content that isn't PII (e.g., financial data, portfolios).
91
+ * Example: ['.portfolio-table', '#balance-widget', '[data-sensitive]']
92
+ */
93
+ blockSelectors?: string[];
94
+ /**
95
+ * CSS class name to mark elements for blocking. Any element with this class
96
+ * will be excluded from replay. Alternative to blockSelectors.
97
+ * Example: 'bugspotter-block'
98
+ */
99
+ blockClass?: string;
79
100
  };
80
101
  sanitize?: {
81
102
  /** Enable PII sanitization (default: true) */
@@ -85,7 +106,7 @@ export interface BugSpotterConfig {
85
106
  * - Can be a preset name: 'all', 'minimal', 'financial', 'contact', 'gdpr', 'pci', etc.
86
107
  * - Or an array of pattern names: ['email', 'phone', 'ip']
87
108
  */
88
- patterns?: 'all' | 'minimal' | 'financial' | 'contact' | 'identification' | 'kazakhstan' | 'gdpr' | 'pci' | Array<'email' | 'phone' | 'creditcard' | 'ssn' | 'iin' | 'ip' | 'custom'>;
109
+ patterns?: 'all' | 'minimal' | 'financial' | 'contact' | 'identification' | 'credentials' | 'kazakhstan' | 'gdpr' | 'pci' | Array<'email' | 'phone' | 'creditcard' | 'ssn' | 'iin' | 'ip' | 'apikey' | 'token' | 'password' | 'custom'>;
89
110
  /** Custom regex patterns for PII detection */
90
111
  customPatterns?: Array<{
91
112
  name: string;
@@ -133,7 +154,7 @@ export { CircularBuffer } from './core/buffer';
133
154
  export type { CircularBufferConfig } from './core/buffer';
134
155
  export { compressData, decompressData, compressImage, estimateSize, getCompressionRatio, } from './core/compress';
135
156
  export { submitWithAuth, getAuthHeaders, clearOfflineQueue, } from './core/transport';
136
- export type { AuthConfig, TransportOptions, RetryConfig, } from './core/transport';
157
+ export type { TransportOptions, RetryConfig } from './core/transport';
137
158
  export type { OfflineConfig } from './core/offline-queue';
138
159
  export type { Logger, LogLevel, LoggerConfig } from './utils/logger';
139
160
  export { getLogger, configureLogger, createLogger } from './utils/logger';
package/dist/index.esm.js CHANGED
@@ -2344,6 +2344,7 @@ class StringSanitizer {
2344
2344
  class ValueSanitizer {
2345
2345
  constructor(stringSanitizer) {
2346
2346
  this.stringSanitizer = stringSanitizer;
2347
+ this.seen = new WeakSet();
2347
2348
  }
2348
2349
  sanitize(value) {
2349
2350
  // Handle null/undefined
@@ -2354,26 +2355,40 @@ class ValueSanitizer {
2354
2355
  if (typeof value === 'string') {
2355
2356
  return this.stringSanitizer.sanitize(value);
2356
2357
  }
2357
- // Handle arrays
2358
+ // Handle arrays (with circular reference protection)
2358
2359
  if (Array.isArray(value)) {
2359
- return value.map((item) => {
2360
- return this.sanitize(item);
2361
- });
2360
+ if (this.seen.has(value))
2361
+ return '[Circular]';
2362
+ this.seen.add(value);
2363
+ try {
2364
+ return value.map((item) => this.sanitize(item));
2365
+ }
2366
+ finally {
2367
+ this.seen.delete(value);
2368
+ }
2362
2369
  }
2363
- // Handle objects
2370
+ // Handle objects (with circular reference protection)
2364
2371
  if (typeof value === 'object') {
2372
+ if (this.seen.has(value))
2373
+ return '[Circular]';
2365
2374
  return this.sanitizeObject(value);
2366
2375
  }
2367
2376
  // Return primitives as-is
2368
2377
  return value;
2369
2378
  }
2370
2379
  sanitizeObject(obj) {
2371
- const sanitized = {};
2372
- for (const [key, val] of Object.entries(obj)) {
2373
- const sanitizedKey = this.stringSanitizer.sanitize(key);
2374
- sanitized[sanitizedKey] = this.sanitize(val);
2380
+ this.seen.add(obj);
2381
+ try {
2382
+ const sanitized = {};
2383
+ for (const [key, val] of Object.entries(obj)) {
2384
+ const sanitizedKey = this.stringSanitizer.sanitize(key);
2385
+ sanitized[sanitizedKey] = this.sanitize(val);
2386
+ }
2387
+ return sanitized;
2388
+ }
2389
+ finally {
2390
+ this.seen.delete(obj);
2375
2391
  }
2376
- return sanitized;
2377
2392
  }
2378
2393
  }
2379
2394
  /**
@@ -2944,14 +2959,14 @@ const MAX_RECOMMENDED_REPLAY_DURATION_SECONDS = 30;
2944
2959
  * This file is automatically generated during the build process.
2945
2960
  * To update the version, modify package.json
2946
2961
  */
2947
- const VERSION = '2.0.0';
2962
+ const VERSION = '2.1.0';
2948
2963
 
2949
2964
  /**
2950
2965
  * Configuration Validation Utilities
2951
2966
  * Validates BugSpotter configuration before use
2952
2967
  */
2953
2968
  /**
2954
- * Validate authentication configuration
2969
+ * Validate configuration before submitting a bug report
2955
2970
  * @throws Error if configuration is invalid
2956
2971
  */
2957
2972
  function validateAuthConfig(context) {
@@ -2959,15 +2974,11 @@ function validateAuthConfig(context) {
2959
2974
  throw new Error('No endpoint configured for bug report submission');
2960
2975
  }
2961
2976
  // SECURITY: Ensure endpoint uses HTTPS
2962
- // This prevents credentials and sensitive data from being sent over plain HTTP
2963
2977
  if (!isSecureEndpoint(context.endpoint)) {
2964
2978
  throw new InsecureEndpointError(context.endpoint);
2965
2979
  }
2966
- if (!context.auth) {
2967
- throw new Error('API key authentication is required');
2968
- }
2969
- if (!context.auth.apiKey) {
2970
- throw new Error('API key is required in auth configuration');
2980
+ if (!context.apiKey) {
2981
+ throw new Error('API key is required');
2971
2982
  }
2972
2983
  }
2973
2984
  /**
@@ -15455,6 +15466,8 @@ class DOMCollector {
15455
15466
  collectFonts: (_h = config.collectFonts) !== null && _h !== void 0 ? _h : false,
15456
15467
  recordCanvas: (_j = config.recordCanvas) !== null && _j !== void 0 ? _j : false,
15457
15468
  recordCrossOriginIframes: (_k = config.recordCrossOriginIframes) !== null && _k !== void 0 ? _k : false,
15469
+ blockSelectors: config.blockSelectors,
15470
+ blockClass: config.blockClass,
15458
15471
  sanitizer: config.sanitizer,
15459
15472
  };
15460
15473
  this.buffer = new CircularBuffer({
@@ -15465,30 +15478,26 @@ class DOMCollector {
15465
15478
  * Start recording DOM events
15466
15479
  */
15467
15480
  startRecording() {
15468
- var _a, _b, _c, _d;
15481
+ var _a, _b, _c, _d, _e;
15469
15482
  if (this.isRecording) {
15470
15483
  getLogger().warn('DOMCollector: Recording already in progress');
15471
15484
  return;
15472
15485
  }
15473
15486
  try {
15474
- const recordConfig = {
15475
- emit: (event) => {
15487
+ const recordConfig = Object.assign(Object.assign({ emit: (event) => {
15476
15488
  this.buffer.add(event);
15477
- },
15478
- sampling: {
15489
+ }, sampling: {
15479
15490
  mousemove: (_b = (_a = this.config.sampling) === null || _a === void 0 ? void 0 : _a.mousemove) !== null && _b !== void 0 ? _b : 50,
15480
15491
  scroll: (_d = (_c = this.config.sampling) === null || _c === void 0 ? void 0 : _c.scroll) !== null && _d !== void 0 ? _d : 100,
15481
15492
  // Record all mouse interactions for replay visibility
15482
15493
  mouseInteraction: true,
15483
- },
15484
- recordCanvas: this.config.recordCanvas,
15485
- recordCrossOriginIframes: this.config.recordCrossOriginIframes,
15494
+ }, recordCanvas: this.config.recordCanvas, recordCrossOriginIframes: this.config.recordCrossOriginIframes,
15486
15495
  // PII sanitization for text content
15487
15496
  maskTextFn: this.sanitizer
15488
15497
  ? (text, element) => {
15489
15498
  return this.sanitizer.sanitizeTextNode(text, element);
15490
15499
  }
15491
- : undefined,
15500
+ : undefined,
15492
15501
  // Performance optimizations
15493
15502
  slimDOMOptions: {
15494
15503
  script: true, // Don't record script tags
@@ -15500,12 +15509,13 @@ class DOMCollector {
15500
15509
  headMetaHttpEquiv: true, // Don't record http-equiv meta tags
15501
15510
  headMetaAuthorship: true, // Don't record authorship meta tags
15502
15511
  headMetaVerification: true, // Don't record verification meta tags
15503
- },
15512
+ },
15504
15513
  // Quality settings (controlled by backend or user config)
15505
- inlineStylesheet: this.config.inlineStylesheet,
15506
- inlineImages: this.config.inlineImages,
15507
- collectFonts: this.config.collectFonts,
15508
- };
15514
+ inlineStylesheet: this.config.inlineStylesheet, inlineImages: this.config.inlineImages, collectFonts: this.config.collectFonts }, (((_e = this.config.blockSelectors) === null || _e === void 0 ? void 0 : _e.length) && {
15515
+ blockSelector: this.config.blockSelectors.join(','),
15516
+ })), (this.config.blockClass && {
15517
+ blockClass: this.config.blockClass,
15518
+ }));
15509
15519
  this.stopRecordingFn = record(recordConfig);
15510
15520
  this.isRecording = true;
15511
15521
  getLogger().debug('DOMCollector: Started recording');
@@ -15595,7 +15605,7 @@ class DOMCollector {
15595
15605
  */
15596
15606
  class CaptureManager {
15597
15607
  constructor(config) {
15598
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
15608
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
15599
15609
  // Initialize core capture modules
15600
15610
  this.screenshot = new ScreenshotCapture();
15601
15611
  this.console = new ConsoleCapture({ sanitizer: config.sanitizer });
@@ -15620,6 +15630,8 @@ class CaptureManager {
15620
15630
  collectFonts: (_h = config.replay) === null || _h === void 0 ? void 0 : _h.collectFonts,
15621
15631
  recordCanvas: (_j = config.replay) === null || _j === void 0 ? void 0 : _j.recordCanvas,
15622
15632
  recordCrossOriginIframes: (_k = config.replay) === null || _k === void 0 ? void 0 : _k.recordCrossOriginIframes,
15633
+ blockSelectors: (_l = config.replay) === null || _l === void 0 ? void 0 : _l.blockSelectors,
15634
+ blockClass: (_m = config.replay) === null || _m === void 0 ? void 0 : _m.blockClass,
15623
15635
  sanitizer: config.sanitizer,
15624
15636
  });
15625
15637
  this.domCollector.startRecording();
@@ -16742,7 +16754,7 @@ class BugReporter {
16742
16754
  var _a;
16743
16755
  validateAuthConfig({
16744
16756
  endpoint: this.config.endpoint,
16745
- auth: this.config.auth,
16757
+ apiKey: this.config.apiKey,
16746
16758
  });
16747
16759
  const dedupContext = createDeduplicationContext(payload);
16748
16760
  if (this.deduplicator.isDuplicate(dedupContext.title, dedupContext.description, dedupContext.errorStacks)) {
@@ -16771,8 +16783,10 @@ class BugReporter {
16771
16783
  network: report.network,
16772
16784
  metadata: report.metadata,
16773
16785
  }, hasScreenshot: fileAnalysis.hasScreenshot, hasReplay: fileAnalysis.hasReplay });
16774
- const response = yield submitWithAuth(this.config.endpoint, JSON.stringify(createPayload), { 'Content-Type': 'application/json' }, {
16775
- auth: this.config.auth,
16786
+ const apiBaseUrl = getApiBaseUrl(this.config.endpoint);
16787
+ const submitUrl = `${apiBaseUrl}/api/v1/reports`;
16788
+ const response = yield submitWithAuth(submitUrl, JSON.stringify(createPayload), { 'Content-Type': 'application/json' }, {
16789
+ auth: { apiKey: this.config.apiKey },
16776
16790
  retry: this.config.retry,
16777
16791
  offline: this.config.offline,
16778
16792
  });
@@ -16826,7 +16840,7 @@ class BugReporter {
16826
16840
  throw new Error('Server did not provide presigned URLs for file uploads. Check backend configuration.');
16827
16841
  }
16828
16842
  const apiEndpoint = getApiBaseUrl(this.config.endpoint);
16829
- const uploadHandler = new FileUploadHandler(apiEndpoint, this.config.auth.apiKey);
16843
+ const uploadHandler = new FileUploadHandler(apiEndpoint, this.config.apiKey);
16830
16844
  try {
16831
16845
  yield uploadHandler.uploadFiles(bugId, report, bugReportData.presignedUrls);
16832
16846
  logger$1.debug('File uploads completed successfully', { bugId });
@@ -17222,13 +17236,20 @@ function fetchReplaySettings(endpoint, apiKey) {
17222
17236
  });
17223
17237
  }
17224
17238
  class BugSpotter {
17225
- constructor(config) {
17239
+ constructor(config, sampled = true) {
17226
17240
  var _a, _b, _c, _d, _e, _f;
17227
17241
  // Validate deduplication configuration if provided
17228
17242
  if (config.deduplication) {
17229
17243
  validateDeduplicationConfig(config.deduplication);
17230
17244
  }
17231
17245
  this.config = config;
17246
+ this._sampled = sampled;
17247
+ this.bugReporter = new BugReporter(config);
17248
+ // If not sampled, skip all capture initialization — true zero overhead
17249
+ // No console/network interception, no DOM recording, no widget
17250
+ if (!sampled) {
17251
+ return;
17252
+ }
17232
17253
  // Initialize sanitizer (enabled by default)
17233
17254
  const sanitizeEnabled = (_b = (_a = config.sanitize) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : true;
17234
17255
  if (sanitizeEnabled) {
@@ -17241,8 +17262,6 @@ class BugSpotter {
17241
17262
  }
17242
17263
  // Initialize capture manager
17243
17264
  this.captureManager = new CaptureManager(Object.assign(Object.assign({ sanitizer: this.sanitizer }, (config.endpoint && { apiEndpoint: getApiBaseUrl(config.endpoint) })), { replay: config.replay }));
17244
- // Initialize bug reporter
17245
- this.bugReporter = new BugReporter(config);
17246
17265
  // Initialize widget (enabled by default)
17247
17266
  const widgetEnabled = (_f = config.showWidget) !== null && _f !== void 0 ? _f : true;
17248
17267
  if (widgetEnabled) {
@@ -17283,17 +17302,33 @@ class BugSpotter {
17283
17302
  */
17284
17303
  static createInstance(config) {
17285
17304
  return __awaiter$1(this, void 0, void 0, function* () {
17286
- var _a, _b, _c;
17305
+ var _a, _b;
17306
+ // Check sampling rate — if this session is not sampled, disable all capture
17307
+ if (config.sampleRate !== undefined) {
17308
+ if (typeof config.sampleRate !== 'number' ||
17309
+ !Number.isFinite(config.sampleRate) ||
17310
+ config.sampleRate < 0 ||
17311
+ config.sampleRate > 1) {
17312
+ throw new Error('sampleRate must be a finite number between 0 and 1');
17313
+ }
17314
+ if (Math.random() >= config.sampleRate) {
17315
+ // Create a lightweight no-op instance — zero overhead (no console/network interception)
17316
+ return new BugSpotter(config, /* sampled */ false);
17317
+ }
17318
+ }
17287
17319
  // Fetch replay quality settings from backend if replay is enabled
17288
17320
  let backendSettings = null;
17289
17321
  const replayEnabled = (_b = (_a = config.replay) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : true;
17290
17322
  if (replayEnabled && config.endpoint) {
17291
- // Validate auth is configured before attempting fetch
17292
- if (!((_c = config.auth) === null || _c === void 0 ? void 0 : _c.apiKey)) {
17323
+ // SECURITY: Don't send API key over insecure connection
17324
+ if (!isSecureEndpoint(config.endpoint)) {
17325
+ logger.warn('Insecure endpoint — skipping backend settings fetch to protect API key.');
17326
+ }
17327
+ else if (!config.apiKey) {
17293
17328
  logger.warn('Endpoint provided but no API key configured. Skipping backend settings fetch.');
17294
17329
  }
17295
17330
  else {
17296
- backendSettings = yield fetchReplaySettings(config.endpoint, config.auth.apiKey);
17331
+ backendSettings = yield fetchReplaySettings(config.endpoint, config.apiKey);
17297
17332
  }
17298
17333
  }
17299
17334
  // Merge backend settings with user config (user config takes precedence)
@@ -17309,8 +17344,29 @@ class BugSpotter {
17309
17344
  * Note: Screenshot is captured for modal preview only (_screenshotPreview)
17310
17345
  * File uploads use presigned URLs returned from the backend
17311
17346
  */
17347
+ /** Whether this session was sampled for capture */
17348
+ get isSampled() {
17349
+ return this._sampled;
17350
+ }
17312
17351
  capture() {
17313
17352
  return __awaiter$1(this, void 0, void 0, function* () {
17353
+ if (!this.captureManager) {
17354
+ // Unsampled session — return minimal valid report
17355
+ return {
17356
+ console: [],
17357
+ network: [],
17358
+ metadata: {
17359
+ userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
17360
+ url: typeof window !== 'undefined' ? window.location.href : '',
17361
+ timestamp: Date.now(),
17362
+ viewport: typeof window !== 'undefined'
17363
+ ? { width: window.innerWidth, height: window.innerHeight }
17364
+ : { width: 0, height: 0 },
17365
+ browser: 'unknown',
17366
+ os: 'unknown',
17367
+ },
17368
+ };
17369
+ }
17314
17370
  return yield this.captureManager.captureAll();
17315
17371
  });
17316
17372
  }
@@ -17354,9 +17410,9 @@ class BugSpotter {
17354
17410
  return Object.assign({}, this.config);
17355
17411
  }
17356
17412
  destroy() {
17357
- var _a;
17358
- this.captureManager.destroy();
17359
- (_a = this.widget) === null || _a === void 0 ? void 0 : _a.destroy();
17413
+ var _a, _b;
17414
+ (_a = this.captureManager) === null || _a === void 0 ? void 0 : _a.destroy();
17415
+ (_b = this.widget) === null || _b === void 0 ? void 0 : _b.destroy();
17360
17416
  this.bugReporter.destroy();
17361
17417
  BugSpotter.instance = undefined;
17362
17418
  BugSpotter.initPromise = undefined;