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 +113 -112
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/react/index.js +8 -0
- package/dist/cjs/react/index.js.map +1 -1
- package/dist/esm/index.js +113 -112
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/react/index.js +8 -0
- package/dist/esm/react/index.js.map +1 -1
- package/dist/index.min.js +2 -2
- package/dist/index.min.js.map +1 -1
- package/dist/types/index.d.ts +28 -11
- package/package.json +1 -1
- package/src/react/index.tsx +11 -0
- package/src/redact.ts +58 -13
- package/src/tracker.ts +79 -121
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
|
|
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
|
-
*
|
|
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
|
-
|
|
4869
|
-
|
|
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
|
|
4885
|
+
// Check if any selector matches this element
|
|
4875
4886
|
for (const selector of this.userSelectedFields) {
|
|
4876
|
-
|
|
4877
|
-
|
|
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
|
-
//
|
|
5525
|
-
|
|
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
|
-
//
|
|
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-
|
|
5577
|
-
//
|
|
5578
|
-
|
|
5579
|
-
|
|
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
|
|
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
|
-
//
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
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
|
-
//
|
|
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:
|
|
5868
|
-
currentThreshold:
|
|
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
|
/**
|