humanbehavior-js 0.3.1 → 0.3.3

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;
@@ -5490,7 +5528,7 @@ class HumanBehaviorTracker {
5490
5528
  * Add user identification information to the tracker
5491
5529
  * If userId is not provided, will use userProperties.email as the userId (if present)
5492
5530
  */
5493
- addUserInfo(_a) {
5531
+ identifyUser(_a) {
5494
5532
  return __awaiter$1(this, arguments, void 0, function* ({ userId, userProperties }) {
5495
5533
  yield this.ensureInitialized();
5496
5534
  // Keep the original endUserId (UUID) - don't change it
@@ -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,50 +5558,11 @@ 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);
5569
5567
  },
5570
5568
  inlineStylesheet: true,
@@ -5573,14 +5571,15 @@ class HumanBehaviorTracker {
5573
5571
  inlineImages: true,
5574
5572
  blockClass: 'rr-block',
5575
5573
  ignoreClass: 'rr-ignore',
5576
- maskTextClass: 'rr-ignore',
5577
- // Adaptive configuration
5578
- checkoutEveryNms: snapshotInterval,
5579
- checkoutEveryNth: eventThreshold
5574
+ maskTextClass: 'rr-mask',
5575
+ // RRWEB BUILT-IN MASKING - More reliable than custom redaction
5576
+ maskAllInputs: false, // Let users control this via selectors
5577
+ maskTextSelector: this.redactionManager.getMaskTextSelector() || undefined,
5578
+ // ✅ SELECTOR-BASED REDACTION - Users control via CSS selectors
5579
+ // No custom masking functions needed - rrweb handles this natively
5580
5580
  });
5581
- // Store the record instance and interval for cleanup
5581
+ // Store the record instance for cleanup
5582
5582
  this.recordInstance = recordInstance;
5583
- this.frequencyUpdateInterval = frequencyUpdateInterval;
5584
5583
  });
5585
5584
  }
5586
5585
  stop() {
@@ -5592,11 +5591,6 @@ class HumanBehaviorTracker {
5592
5591
  clearInterval(this.flushInterval);
5593
5592
  this.flushInterval = null;
5594
5593
  }
5595
- // Cleanup adaptive snapshot intervals
5596
- if (this.frequencyUpdateInterval) {
5597
- clearInterval(this.frequencyUpdateInterval);
5598
- this.frequencyUpdateInterval = null;
5599
- }
5600
5594
  // Stop rrweb recording
5601
5595
  if (this.recordInstance) {
5602
5596
  this.recordInstance();
@@ -5608,28 +5602,29 @@ class HumanBehaviorTracker {
5608
5602
  this.cleanupNavigationTracking();
5609
5603
  });
5610
5604
  }
5605
+ /**
5606
+ * Add an event to the ingestion queue
5607
+ * Events are sent directly without processing to avoid corruption
5608
+ */
5611
5609
  addEvent(event) {
5612
5610
  return __awaiter$1(this, void 0, void 0, function* () {
5611
+ var _a, _b;
5613
5612
  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
- }
5613
+ // DIRECT EVENT HANDLING - No custom processing to avoid corruption
5614
+ // Events flow directly from rrweb to ingestion server
5615
+ // LOG FULLSNAPSHOT STATUS FOR DEBUGGING
5616
+ if (event.type === 2) { // FullSnapshot
5617
+ const hasData = !!event.data;
5618
+ const hasNode = !!(event.data && event.data.node);
5619
+ 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
5620
  }
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;
5621
+ this.eventIngestionQueue.push(event); // Direct event handling
5631
5622
  });
5632
5623
  }
5624
+ /**
5625
+ * Flush events to the ingestion server
5626
+ * Events are sent in chunks to handle large payloads efficiently
5627
+ */
5633
5628
  flush() {
5634
5629
  return __awaiter$1(this, void 0, void 0, function* () {
5635
5630
  var _a, _b, _c, _d, _e, _f;
@@ -5642,9 +5637,13 @@ class HumanBehaviorTracker {
5642
5637
  // Swap the current queue with an empty one atomically
5643
5638
  const eventsToProcess = this.eventIngestionQueue;
5644
5639
  this.eventIngestionQueue = [];
5645
- this.queueSizeBytes = 0;
5646
5640
  if (eventsToProcess.length > 0) {
5647
5641
  logDebug('Flushing events:', eventsToProcess);
5642
+ // ✅ LOG FULLSNAPSHOT STATUS FOR MONITORING
5643
+ const fullSnapshots = eventsToProcess.filter(e => e.type === 2);
5644
+ if (fullSnapshots.length > 0) {
5645
+ logDebug(`[FIXED] Sending ${fullSnapshots.length} FullSnapshot(s) with valid data`);
5646
+ }
5648
5647
  try {
5649
5648
  // Use chunked sending to handle large payloads
5650
5649
  yield this.api.sendEventsChunked(eventsToProcess, this.sessionId, this.endUserId);
@@ -5809,14 +5808,23 @@ class HumanBehaviorTracker {
5809
5808
  }
5810
5809
  /**
5811
5810
  * Set specific fields to be redacted during session recording
5811
+ * Uses rrweb's built-in masking instead of custom redaction processing
5812
5812
  * @param fields Array of CSS selectors for fields to redact (e.g., ['input[type="password"]', '#email-field'])
5813
5813
  */
5814
5814
  setRedactedFields(fields) {
5815
- if (!isBrowser) {
5816
- logWarn('Redaction is only available in browser environments');
5817
- return;
5818
- }
5819
5815
  this.redactionManager.setFieldsToRedact(fields);
5816
+ // ✅ APPLY RRWEB MASKING CLASSES - More reliable than custom processing
5817
+ this.redactionManager.applyRedactionClasses();
5818
+ // ✅ RESTART RECORDING WITH NEW SETTINGS - Ensures masking is applied
5819
+ if (this.recordInstance) {
5820
+ this.restartWithNewRedaction();
5821
+ }
5822
+ }
5823
+ restartWithNewRedaction() {
5824
+ if (this.recordInstance) {
5825
+ this.recordInstance(); // Stop current recording
5826
+ this.start(); // Restart with new redaction settings
5827
+ }
5820
5828
  }
5821
5829
  /**
5822
5830
  * Check if redaction is currently active
@@ -5844,29 +5852,15 @@ class HumanBehaviorTracker {
5844
5852
  }
5845
5853
  /**
5846
5854
  * Get current snapshot frequency info
5855
+ * Uses rrweb's sensible defaults (5 seconds, 100 events)
5847
5856
  */
5848
5857
  getSnapshotFrequencyInfo() {
5849
5858
  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
5859
  return {
5866
5860
  sessionDuration,
5867
- currentInterval: interval,
5868
- currentThreshold: threshold,
5869
- phase
5861
+ currentInterval: 5000, // rrweb default - 5 seconds
5862
+ currentThreshold: 100, // rrweb default - 100 events
5863
+ phase: 'default' // Using rrweb's proven defaults
5870
5864
  };
5871
5865
  }
5872
5866
  /**