@bugspotter/sdk 2.0.5 → 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');
@@ -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
@@ -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
  /**
@@ -43,6 +46,13 @@ export interface BugSpotterConfig {
43
46
  endpoint?: string;
44
47
  /** API key for authentication (starts with 'bgs_'). Required. */
45
48
  apiKey: string;
49
+ /**
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)
54
+ */
55
+ sampleRate?: number;
46
56
  showWidget?: boolean;
47
57
  widgetOptions?: FloatingButtonOptions;
48
58
  /** Retry configuration for failed requests */
@@ -74,6 +84,19 @@ export interface BugSpotterConfig {
74
84
  recordCanvas?: boolean;
75
85
  /** Whether to record cross-origin iframes (default: backend controlled) */
76
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;
77
100
  };
78
101
  sanitize?: {
79
102
  /** Enable PII sanitization (default: true) */
@@ -83,7 +106,7 @@ export interface BugSpotterConfig {
83
106
  * - Can be a preset name: 'all', 'minimal', 'financial', 'contact', 'gdpr', 'pci', etc.
84
107
  * - Or an array of pattern names: ['email', 'phone', 'ip']
85
108
  */
86
- 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'>;
87
110
  /** Custom regex patterns for PII detection */
88
111
  customPatterns?: Array<{
89
112
  name: string;
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,7 +2959,7 @@ 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.5';
2962
+ const VERSION = '2.1.0';
2948
2963
 
2949
2964
  /**
2950
2965
  * Configuration Validation Utilities
@@ -15451,6 +15466,8 @@ class DOMCollector {
15451
15466
  collectFonts: (_h = config.collectFonts) !== null && _h !== void 0 ? _h : false,
15452
15467
  recordCanvas: (_j = config.recordCanvas) !== null && _j !== void 0 ? _j : false,
15453
15468
  recordCrossOriginIframes: (_k = config.recordCrossOriginIframes) !== null && _k !== void 0 ? _k : false,
15469
+ blockSelectors: config.blockSelectors,
15470
+ blockClass: config.blockClass,
15454
15471
  sanitizer: config.sanitizer,
15455
15472
  };
15456
15473
  this.buffer = new CircularBuffer({
@@ -15461,30 +15478,26 @@ class DOMCollector {
15461
15478
  * Start recording DOM events
15462
15479
  */
15463
15480
  startRecording() {
15464
- var _a, _b, _c, _d;
15481
+ var _a, _b, _c, _d, _e;
15465
15482
  if (this.isRecording) {
15466
15483
  getLogger().warn('DOMCollector: Recording already in progress');
15467
15484
  return;
15468
15485
  }
15469
15486
  try {
15470
- const recordConfig = {
15471
- emit: (event) => {
15487
+ const recordConfig = Object.assign(Object.assign({ emit: (event) => {
15472
15488
  this.buffer.add(event);
15473
- },
15474
- sampling: {
15489
+ }, sampling: {
15475
15490
  mousemove: (_b = (_a = this.config.sampling) === null || _a === void 0 ? void 0 : _a.mousemove) !== null && _b !== void 0 ? _b : 50,
15476
15491
  scroll: (_d = (_c = this.config.sampling) === null || _c === void 0 ? void 0 : _c.scroll) !== null && _d !== void 0 ? _d : 100,
15477
15492
  // Record all mouse interactions for replay visibility
15478
15493
  mouseInteraction: true,
15479
- },
15480
- recordCanvas: this.config.recordCanvas,
15481
- recordCrossOriginIframes: this.config.recordCrossOriginIframes,
15494
+ }, recordCanvas: this.config.recordCanvas, recordCrossOriginIframes: this.config.recordCrossOriginIframes,
15482
15495
  // PII sanitization for text content
15483
15496
  maskTextFn: this.sanitizer
15484
15497
  ? (text, element) => {
15485
15498
  return this.sanitizer.sanitizeTextNode(text, element);
15486
15499
  }
15487
- : undefined,
15500
+ : undefined,
15488
15501
  // Performance optimizations
15489
15502
  slimDOMOptions: {
15490
15503
  script: true, // Don't record script tags
@@ -15496,12 +15509,13 @@ class DOMCollector {
15496
15509
  headMetaHttpEquiv: true, // Don't record http-equiv meta tags
15497
15510
  headMetaAuthorship: true, // Don't record authorship meta tags
15498
15511
  headMetaVerification: true, // Don't record verification meta tags
15499
- },
15512
+ },
15500
15513
  // Quality settings (controlled by backend or user config)
15501
- inlineStylesheet: this.config.inlineStylesheet,
15502
- inlineImages: this.config.inlineImages,
15503
- collectFonts: this.config.collectFonts,
15504
- };
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
+ }));
15505
15519
  this.stopRecordingFn = record(recordConfig);
15506
15520
  this.isRecording = true;
15507
15521
  getLogger().debug('DOMCollector: Started recording');
@@ -15591,7 +15605,7 @@ class DOMCollector {
15591
15605
  */
15592
15606
  class CaptureManager {
15593
15607
  constructor(config) {
15594
- 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;
15595
15609
  // Initialize core capture modules
15596
15610
  this.screenshot = new ScreenshotCapture();
15597
15611
  this.console = new ConsoleCapture({ sanitizer: config.sanitizer });
@@ -15616,6 +15630,8 @@ class CaptureManager {
15616
15630
  collectFonts: (_h = config.replay) === null || _h === void 0 ? void 0 : _h.collectFonts,
15617
15631
  recordCanvas: (_j = config.replay) === null || _j === void 0 ? void 0 : _j.recordCanvas,
15618
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,
15619
15635
  sanitizer: config.sanitizer,
15620
15636
  });
15621
15637
  this.domCollector.startRecording();
@@ -17220,13 +17236,20 @@ function fetchReplaySettings(endpoint, apiKey) {
17220
17236
  });
17221
17237
  }
17222
17238
  class BugSpotter {
17223
- constructor(config) {
17239
+ constructor(config, sampled = true) {
17224
17240
  var _a, _b, _c, _d, _e, _f;
17225
17241
  // Validate deduplication configuration if provided
17226
17242
  if (config.deduplication) {
17227
17243
  validateDeduplicationConfig(config.deduplication);
17228
17244
  }
17229
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
+ }
17230
17253
  // Initialize sanitizer (enabled by default)
17231
17254
  const sanitizeEnabled = (_b = (_a = config.sanitize) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : true;
17232
17255
  if (sanitizeEnabled) {
@@ -17239,8 +17262,6 @@ class BugSpotter {
17239
17262
  }
17240
17263
  // Initialize capture manager
17241
17264
  this.captureManager = new CaptureManager(Object.assign(Object.assign({ sanitizer: this.sanitizer }, (config.endpoint && { apiEndpoint: getApiBaseUrl(config.endpoint) })), { replay: config.replay }));
17242
- // Initialize bug reporter
17243
- this.bugReporter = new BugReporter(config);
17244
17265
  // Initialize widget (enabled by default)
17245
17266
  const widgetEnabled = (_f = config.showWidget) !== null && _f !== void 0 ? _f : true;
17246
17267
  if (widgetEnabled) {
@@ -17282,6 +17303,19 @@ class BugSpotter {
17282
17303
  static createInstance(config) {
17283
17304
  return __awaiter$1(this, void 0, void 0, function* () {
17284
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
+ }
17285
17319
  // Fetch replay quality settings from backend if replay is enabled
17286
17320
  let backendSettings = null;
17287
17321
  const replayEnabled = (_b = (_a = config.replay) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : true;
@@ -17310,8 +17344,29 @@ class BugSpotter {
17310
17344
  * Note: Screenshot is captured for modal preview only (_screenshotPreview)
17311
17345
  * File uploads use presigned URLs returned from the backend
17312
17346
  */
17347
+ /** Whether this session was sampled for capture */
17348
+ get isSampled() {
17349
+ return this._sampled;
17350
+ }
17313
17351
  capture() {
17314
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
+ }
17315
17370
  return yield this.captureManager.captureAll();
17316
17371
  });
17317
17372
  }
@@ -17355,9 +17410,9 @@ class BugSpotter {
17355
17410
  return Object.assign({}, this.config);
17356
17411
  }
17357
17412
  destroy() {
17358
- var _a;
17359
- this.captureManager.destroy();
17360
- (_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();
17361
17416
  this.bugReporter.destroy();
17362
17417
  BugSpotter.instance = undefined;
17363
17418
  BugSpotter.initPromise = undefined;