humanbehavior-js 0.3.2 → 0.3.4

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/cjs/index.js CHANGED
@@ -4486,7 +4486,8 @@ class HumanBehaviorAPI {
4486
4486
  }
4487
4487
 
4488
4488
  // Redaction functionality for sensitive input fields
4489
- // This module provides methods to redact sensitive input fields in event recordings
4489
+ // This module provides methods to configure rrweb's built-in masking
4490
+ // Uses CSS selectors and classes for reliable redaction without event corruption
4490
4491
  // Check if we're in a browser environment
4491
4492
  const isBrowser$1 = typeof window !== 'undefined';
4492
4493
  class RedactionManager {
@@ -4508,7 +4509,8 @@ class RedactionManager {
4508
4509
  }
4509
4510
  }
4510
4511
  /**
4511
- * Set specific fields to be redacted
4512
+ * Set specific fields to be redacted using CSS selectors
4513
+ * These selectors are used to configure rrweb's built-in masking
4512
4514
  * @param fields Array of CSS selectors for fields to redact
4513
4515
  */
4514
4516
  setFieldsToRedact(fields) {
@@ -4543,6 +4545,8 @@ class RedactionManager {
4543
4545
  }
4544
4546
  /**
4545
4547
  * Process an event and redact sensitive data if needed
4548
+ * NOTE: This method is no longer used - events are handled directly by rrweb
4549
+ * Kept for backward compatibility but not called in the current implementation
4546
4550
  */
4547
4551
  processEvent(event) {
4548
4552
  // Only process if we have fields selected for redaction
@@ -4862,23 +4866,63 @@ class RedactionManager {
4862
4866
  }
4863
4867
  }
4864
4868
  /**
4865
- * Check if an element should be redacted based on user-selected fields
4869
+ * Get CSS selectors for rrweb masking configuration
4870
+ * Used to configure rrweb's maskTextSelector option
4871
+ */
4872
+ getMaskTextSelector() {
4873
+ if (this.userSelectedFields.size === 0) {
4874
+ return null;
4875
+ }
4876
+ return Array.from(this.userSelectedFields).join(',');
4877
+ }
4878
+ /**
4879
+ * Check if an element should be redacted (for rrweb maskTextFn/maskInputFn)
4866
4880
  */
4867
4881
  shouldRedactElement(element) {
4868
- // Check if element is excluded from redaction
4869
- for (const excludeSelector of this.excludeSelectors) {
4870
- if (element.matches(excludeSelector) || element.closest(excludeSelector)) {
4871
- return false;
4872
- }
4882
+ if (this.userSelectedFields.size === 0) {
4883
+ return false;
4873
4884
  }
4874
- // Check if element matches any of the user-selected fields
4885
+ // Check if any selector matches this element
4875
4886
  for (const selector of this.userSelectedFields) {
4876
- if (element.matches(selector)) {
4877
- return true;
4887
+ try {
4888
+ if (element.matches(selector)) {
4889
+ return true;
4890
+ }
4891
+ }
4892
+ catch (e) {
4893
+ // Invalid selector, skip
4894
+ logWarn(`Invalid selector: ${selector}`);
4878
4895
  }
4879
4896
  }
4880
4897
  return false;
4881
4898
  }
4899
+ /**
4900
+ * Apply rrweb masking classes to DOM elements
4901
+ * Adds 'rr-mask' class to elements that should be redacted
4902
+ * This enables rrweb's built-in masking functionality
4903
+ */
4904
+ applyRedactionClasses() {
4905
+ if (this.userSelectedFields.size === 0) {
4906
+ return;
4907
+ }
4908
+ // Remove existing redaction classes
4909
+ document.querySelectorAll('.rr-mask').forEach(element => {
4910
+ element.classList.remove('rr-mask');
4911
+ });
4912
+ // Add redaction classes to matching elements
4913
+ this.userSelectedFields.forEach(selector => {
4914
+ try {
4915
+ const elements = document.querySelectorAll(selector);
4916
+ elements.forEach(element => {
4917
+ element.classList.add('rr-mask');
4918
+ });
4919
+ logDebug(`Applied rr-mask class to ${elements.length} element(s) for selector: ${selector}`);
4920
+ }
4921
+ catch (e) {
4922
+ logWarn(`Invalid selector: ${selector}`);
4923
+ }
4924
+ });
4925
+ }
4882
4926
  /**
4883
4927
  * Get the original value of a redacted element (for debugging)
4884
4928
  */
@@ -4920,6 +4964,8 @@ class HumanBehaviorTracker {
4920
4964
  // Set redacted fields if specified
4921
4965
  if (options === null || options === void 0 ? void 0 : options.redactFields) {
4922
4966
  tracker.setRedactedFields(options.redactFields);
4967
+ // ✅ Apply redaction classes to existing elements
4968
+ tracker.redactionManager.applyRedactionClasses();
4923
4969
  }
4924
4970
  // Setup automatic tracking if enabled
4925
4971
  if ((options === null || options === void 0 ? void 0 : options.enableAutomaticTracking) !== false) {
@@ -4941,7 +4987,6 @@ class HumanBehaviorTracker {
4941
4987
  }
4942
4988
  constructor(apiKey, ingestionUrl) {
4943
4989
  this.eventIngestionQueue = [];
4944
- this.queueSizeBytes = 0;
4945
4990
  this.userProperties = {};
4946
4991
  this.isProcessing = false;
4947
4992
  this.flushInterval = null;
@@ -4961,7 +5006,6 @@ class HumanBehaviorTracker {
4961
5006
  this.navigationListeners = [];
4962
5007
  this._connectionBlocked = false;
4963
5008
  this.recordInstance = null;
4964
- this.frequencyUpdateInterval = null;
4965
5009
  this.sessionStartTime = Date.now();
4966
5010
  if (!apiKey) {
4967
5011
  throw new Error('Human Behavior API Key is required');
@@ -5136,9 +5180,6 @@ class HumanBehaviorTracker {
5136
5180
  }
5137
5181
  });
5138
5182
  }
5139
- /**
5140
- * Track a page view event (PostHog-style)
5141
- */
5142
5183
  trackPageView(url) {
5143
5184
  return __awaiter$1(this, void 0, void 0, function* () {
5144
5185
  if (!this.initialized)
@@ -5167,9 +5208,6 @@ class HumanBehaviorTracker {
5167
5208
  }
5168
5209
  });
5169
5210
  }
5170
- /**
5171
- * Track a custom event (PostHog-style)
5172
- */
5173
5211
  customEvent(eventName, properties) {
5174
5212
  return __awaiter$1(this, void 0, void 0, function* () {
5175
5213
  var _a, _b, _c, _d, _e;
@@ -5500,7 +5538,6 @@ class HumanBehaviorTracker {
5500
5538
  // Send user data with the original endUserId
5501
5539
  yield this.api.sendUserData(originalEndUserId, userProperties, this.sessionId);
5502
5540
  // Don't update endUserId - keep it as the original UUID
5503
- // The posthogName will be updated on the server side with the email
5504
5541
  return originalEndUserId || '';
5505
5542
  });
5506
5543
  }
@@ -5521,51 +5558,16 @@ class HumanBehaviorTracker {
5521
5558
  }, this.FLUSH_INTERVAL_MS);
5522
5559
  // Enable console tracking
5523
5560
  this.enableConsoleTracking();
5524
- // Adaptive snapshot configuration based on session duration
5525
- const sessionStartTime = Date.now();
5526
- let snapshotInterval = 5000; // Start with 5 seconds
5527
- let eventThreshold = 100; // Start with 100 events
5528
- // Function to update snapshot frequency based on session duration
5529
- const updateSnapshotFrequency = () => {
5530
- const sessionDuration = Date.now() - sessionStartTime;
5531
- const thirtyMinutes = 30 * 60 * 1000;
5532
- const twoHours = 2 * 60 * 60 * 1000;
5533
- if (sessionDuration > twoHours) {
5534
- // After 2 hours, very infrequent snapshots
5535
- snapshotInterval = 30000; // 30 seconds
5536
- eventThreshold = 500; // 500 events
5537
- logDebug('Reduced snapshot frequency: 30s/500 events (2+ hours)');
5538
- }
5539
- else if (sessionDuration > thirtyMinutes) {
5540
- // After 30 minutes, moderate frequency
5541
- snapshotInterval = 15000; // 15 seconds
5542
- eventThreshold = 300; // 300 events
5543
- logDebug('Reduced snapshot frequency: 15s/300 events (30+ minutes)');
5544
- }
5545
- // First 30 minutes: 5s/100 events (default)
5546
- };
5547
- // Update frequency every 5 minutes
5548
- const frequencyUpdateInterval = setInterval(updateSnapshotFrequency, 5 * 60 * 1000);
5549
- // Start recording with adaptive redaction enabled
5561
+ // SIMPLIFIED RECORDING - Use rrweb's proven defaults
5562
+ // No complex adaptive logic - rrweb's defaults work well for most use cases
5550
5563
  const recordInstance = record({
5551
5564
  emit: (event) => {
5552
- // Add additional validation for FullSnapshot events
5553
- if (event.type === 2) { // FullSnapshot event
5554
- if (!event.data || !event.data.node) {
5555
- logWarn('rrweb generated malformed FullSnapshot event:', {
5556
- hasData: !!event.data,
5557
- hasNode: !!(event.data && event.data.node),
5558
- dataType: typeof event.data,
5559
- eventType: event.type,
5560
- timestamp: event.timestamp
5561
- });
5562
- // Don't skip - let the addEvent method handle it
5563
- }
5564
- else {
5565
- logDebug('Valid FullSnapshot event received from rrweb');
5566
- }
5567
- }
5565
+ // DIRECT EVENT HANDLING - Let rrweb handle events natively
5568
5566
  this.addEvent(event);
5567
+ // ✅ DEBUG FULLSNAPSHOT GENERATION
5568
+ if (event.type === 2) { // FullSnapshot
5569
+ logDebug(`🎯 FullSnapshot generated at ${new Date().toISOString()}`);
5570
+ }
5569
5571
  },
5570
5572
  inlineStylesheet: true,
5571
5573
  recordCanvas: true,
@@ -5573,14 +5575,18 @@ class HumanBehaviorTracker {
5573
5575
  inlineImages: true,
5574
5576
  blockClass: 'rr-block',
5575
5577
  ignoreClass: 'rr-ignore',
5576
- maskTextClass: 'rr-ignore',
5577
- // Adaptive configuration
5578
- checkoutEveryNms: snapshotInterval,
5579
- checkoutEveryNth: eventThreshold
5578
+ maskTextClass: 'rr-mask',
5579
+ // RRWEB BUILT-IN MASKING - More reliable than custom redaction
5580
+ maskAllInputs: false, // Let users control this via selectors
5581
+ maskTextSelector: this.redactionManager.getMaskTextSelector() || undefined,
5582
+ // ✅ FULLSNAPSHOT GENERATION - Use reasonable intervals (PostHog-style)
5583
+ checkoutEveryNms: 300000, // Take FullSnapshot every 5 minutes (like PostHog)
5584
+ checkoutEveryNth: 1000, // Take FullSnapshot every 1000 events
5585
+ // ✅ SELECTOR-BASED REDACTION - Users control via CSS selectors
5586
+ // No custom masking functions needed - rrweb handles this natively
5580
5587
  });
5581
- // Store the record instance and interval for cleanup
5588
+ // Store the record instance for cleanup
5582
5589
  this.recordInstance = recordInstance;
5583
- this.frequencyUpdateInterval = frequencyUpdateInterval;
5584
5590
  });
5585
5591
  }
5586
5592
  stop() {
@@ -5592,11 +5598,6 @@ class HumanBehaviorTracker {
5592
5598
  clearInterval(this.flushInterval);
5593
5599
  this.flushInterval = null;
5594
5600
  }
5595
- // Cleanup adaptive snapshot intervals
5596
- if (this.frequencyUpdateInterval) {
5597
- clearInterval(this.frequencyUpdateInterval);
5598
- this.frequencyUpdateInterval = null;
5599
- }
5600
5601
  // Stop rrweb recording
5601
5602
  if (this.recordInstance) {
5602
5603
  this.recordInstance();
@@ -5608,28 +5609,29 @@ class HumanBehaviorTracker {
5608
5609
  this.cleanupNavigationTracking();
5609
5610
  });
5610
5611
  }
5612
+ /**
5613
+ * Add an event to the ingestion queue
5614
+ * Events are sent directly without processing to avoid corruption
5615
+ */
5611
5616
  addEvent(event) {
5612
5617
  return __awaiter$1(this, void 0, void 0, function* () {
5618
+ var _a, _b;
5613
5619
  yield this.ensureInitialized();
5614
- // Validate FullSnapshot events before processing
5615
- if (event.type === 2) { // FullSnapshot event
5616
- if (!event.data || !event.data.node) {
5617
- logWarn('Malformed FullSnapshot event detected, skipping:', {
5618
- hasData: !!event.data,
5619
- hasNode: !!(event.data && event.data.node),
5620
- dataType: typeof event.data,
5621
- eventType: event.type
5622
- });
5623
- return; // Skip malformed FullSnapshot events
5624
- }
5620
+ // DIRECT EVENT HANDLING - No custom processing to avoid corruption
5621
+ // Events flow directly from rrweb to ingestion server
5622
+ // LOG FULLSNAPSHOT STATUS FOR DEBUGGING
5623
+ if (event.type === 2) { // FullSnapshot
5624
+ const hasData = !!event.data;
5625
+ const hasNode = !!(event.data && event.data.node);
5626
+ logDebug(`[FIXED] FullSnapshot event: hasData=${hasData}, hasNode=${hasNode}, dataType=${(_b = (_a = event.data) === null || _a === void 0 ? void 0 : _a.node) === null || _b === void 0 ? void 0 : _b.type}`);
5625
5627
  }
5626
- // Process event through redaction manager if active
5627
- const processedEvent = this.redactionManager.processEvent(event);
5628
- const eventSize = new TextEncoder().encode(JSON.stringify(processedEvent)).length;
5629
- this.eventIngestionQueue.push(processedEvent);
5630
- this.queueSizeBytes += eventSize;
5628
+ this.eventIngestionQueue.push(event); // Direct event handling
5631
5629
  });
5632
5630
  }
5631
+ /**
5632
+ * Flush events to the ingestion server
5633
+ * Events are sent in chunks to handle large payloads efficiently
5634
+ */
5633
5635
  flush() {
5634
5636
  return __awaiter$1(this, void 0, void 0, function* () {
5635
5637
  var _a, _b, _c, _d, _e, _f;
@@ -5642,9 +5644,13 @@ class HumanBehaviorTracker {
5642
5644
  // Swap the current queue with an empty one atomically
5643
5645
  const eventsToProcess = this.eventIngestionQueue;
5644
5646
  this.eventIngestionQueue = [];
5645
- this.queueSizeBytes = 0;
5646
5647
  if (eventsToProcess.length > 0) {
5647
5648
  logDebug('Flushing events:', eventsToProcess);
5649
+ // ✅ LOG FULLSNAPSHOT STATUS FOR MONITORING
5650
+ const fullSnapshots = eventsToProcess.filter(e => e.type === 2);
5651
+ if (fullSnapshots.length > 0) {
5652
+ logDebug(`[FIXED] Sending ${fullSnapshots.length} FullSnapshot(s) with valid data`);
5653
+ }
5648
5654
  try {
5649
5655
  // Use chunked sending to handle large payloads
5650
5656
  yield this.api.sendEventsChunked(eventsToProcess, this.sessionId, this.endUserId);
@@ -5809,14 +5815,23 @@ class HumanBehaviorTracker {
5809
5815
  }
5810
5816
  /**
5811
5817
  * Set specific fields to be redacted during session recording
5818
+ * Uses rrweb's built-in masking instead of custom redaction processing
5812
5819
  * @param fields Array of CSS selectors for fields to redact (e.g., ['input[type="password"]', '#email-field'])
5813
5820
  */
5814
5821
  setRedactedFields(fields) {
5815
- if (!isBrowser) {
5816
- logWarn('Redaction is only available in browser environments');
5817
- return;
5818
- }
5819
5822
  this.redactionManager.setFieldsToRedact(fields);
5823
+ // ✅ APPLY RRWEB MASKING CLASSES - More reliable than custom processing
5824
+ this.redactionManager.applyRedactionClasses();
5825
+ // ✅ RESTART RECORDING WITH NEW SETTINGS - Ensures masking is applied
5826
+ if (this.recordInstance) {
5827
+ this.restartWithNewRedaction();
5828
+ }
5829
+ }
5830
+ restartWithNewRedaction() {
5831
+ if (this.recordInstance) {
5832
+ this.recordInstance(); // Stop current recording
5833
+ this.start(); // Restart with new redaction settings
5834
+ }
5820
5835
  }
5821
5836
  /**
5822
5837
  * Check if redaction is currently active
@@ -5844,29 +5859,15 @@ class HumanBehaviorTracker {
5844
5859
  }
5845
5860
  /**
5846
5861
  * Get current snapshot frequency info
5862
+ * Uses configured values (5 minutes, 1000 events) - PostHog-style
5847
5863
  */
5848
5864
  getSnapshotFrequencyInfo() {
5849
5865
  const sessionDuration = Date.now() - this.sessionStartTime;
5850
- const thirtyMinutes = 30 * 60 * 1000;
5851
- const twoHours = 2 * 60 * 60 * 1000;
5852
- let phase = 'initial';
5853
- let interval = 5000;
5854
- let threshold = 100;
5855
- if (sessionDuration > twoHours) {
5856
- phase = 'extended';
5857
- interval = 30000;
5858
- threshold = 500;
5859
- }
5860
- else if (sessionDuration > thirtyMinutes) {
5861
- phase = 'moderate';
5862
- interval = 15000;
5863
- threshold = 300;
5864
- }
5865
5866
  return {
5866
5867
  sessionDuration,
5867
- currentInterval: interval,
5868
- currentThreshold: threshold,
5869
- phase
5868
+ currentInterval: 300000, // Configured - 5 minutes (PostHog-style)
5869
+ currentThreshold: 1000, // Configured - 1000 events
5870
+ phase: 'configured' // Using explicit configuration
5870
5871
  };
5871
5872
  }
5872
5873
  /**