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.
@@ -9,7 +9,8 @@ declare class RedactionManager {
9
9
  private excludeSelectors;
10
10
  constructor(options?: RedactionOptions);
11
11
  /**
12
- * Set specific fields to be redacted
12
+ * Set specific fields to be redacted using CSS selectors
13
+ * These selectors are used to configure rrweb's built-in masking
13
14
  * @param fields Array of CSS selectors for fields to redact
14
15
  */
15
16
  setFieldsToRedact(fields: string[]): void;
@@ -23,6 +24,8 @@ declare class RedactionManager {
23
24
  getSelectedFields(): string[];
24
25
  /**
25
26
  * Process an event and redact sensitive data if needed
27
+ * NOTE: This method is no longer used - events are handled directly by rrweb
28
+ * Kept for backward compatibility but not called in the current implementation
26
29
  */
27
30
  processEvent(event: any): any;
28
31
  /**
@@ -66,9 +69,20 @@ declare class RedactionManager {
66
69
  */
67
70
  private isFieldSelected;
68
71
  /**
69
- * Check if an element should be redacted based on user-selected fields
72
+ * Get CSS selectors for rrweb masking configuration
73
+ * Used to configure rrweb's maskTextSelector option
70
74
  */
71
- private shouldRedactElement;
75
+ getMaskTextSelector(): string | null;
76
+ /**
77
+ * Check if an element should be redacted (for rrweb maskTextFn/maskInputFn)
78
+ */
79
+ shouldRedactElement(element: HTMLElement): boolean;
80
+ /**
81
+ * Apply rrweb masking classes to DOM elements
82
+ * Adds 'rr-mask' class to elements that should be redacted
83
+ * This enables rrweb's built-in masking functionality
84
+ */
85
+ applyRedactionClasses(): void;
72
86
  /**
73
87
  * Get the original value of a redacted element (for debugging)
74
88
  */
@@ -88,7 +102,6 @@ declare global {
88
102
  }
89
103
  declare class HumanBehaviorTracker {
90
104
  private eventIngestionQueue;
91
- private queueSizeBytes;
92
105
  private sessionId;
93
106
  private userProperties;
94
107
  private isProcessing;
@@ -110,7 +123,6 @@ declare class HumanBehaviorTracker {
110
123
  private navigationListeners;
111
124
  private _connectionBlocked;
112
125
  private recordInstance;
113
- private frequencyUpdateInterval;
114
126
  private sessionStartTime;
115
127
  /**
116
128
  * Initialize the HumanBehavior tracker
@@ -140,13 +152,7 @@ declare class HumanBehaviorTracker {
140
152
  * Track navigation events and send custom events
141
153
  */
142
154
  trackNavigationEvent(type: string, fromUrl: string, toUrl: string): Promise<void>;
143
- /**
144
- * Track a page view event (PostHog-style)
145
- */
146
155
  trackPageView(url?: string): Promise<void>;
147
- /**
148
- * Track a custom event (PostHog-style)
149
- */
150
156
  customEvent(eventName: string, properties?: Record<string, any>): Promise<void>;
151
157
  /**
152
158
  * Setup automatic tracking for buttons, links, and forms
@@ -203,7 +209,15 @@ declare class HumanBehaviorTracker {
203
209
  getUserAttributes(): Record<string, any>;
204
210
  start(): Promise<void>;
205
211
  stop(): Promise<void>;
212
+ /**
213
+ * Add an event to the ingestion queue
214
+ * Events are sent directly without processing to avoid corruption
215
+ */
206
216
  addEvent(event: any): Promise<void>;
217
+ /**
218
+ * Flush events to the ingestion server
219
+ * Events are sent in chunks to handle large payloads efficiently
220
+ */
207
221
  private flush;
208
222
  private setCookie;
209
223
  getCookie(name: string): string | null;
@@ -225,9 +239,11 @@ declare class HumanBehaviorTracker {
225
239
  redact(options?: RedactionOptions): Promise<void>;
226
240
  /**
227
241
  * Set specific fields to be redacted during session recording
242
+ * Uses rrweb's built-in masking instead of custom redaction processing
228
243
  * @param fields Array of CSS selectors for fields to redact (e.g., ['input[type="password"]', '#email-field'])
229
244
  */
230
245
  setRedactedFields(fields: string[]): void;
246
+ private restartWithNewRedaction;
231
247
  /**
232
248
  * Check if redaction is currently active
233
249
  */
@@ -246,6 +262,7 @@ declare class HumanBehaviorTracker {
246
262
  getCurrentUrl(): string;
247
263
  /**
248
264
  * Get current snapshot frequency info
265
+ * Uses configured values (5 minutes, 1000 events) - PostHog-style
249
266
  */
250
267
  getSnapshotFrequencyInfo(): {
251
268
  sessionDuration: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanbehavior-js",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "SDK for HumanBehavior session and event recording",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -111,6 +111,17 @@ export const HumanBehaviorProvider = ({ apiKey, client, children, options }: Hum
111
111
  apiKeyRef.current.trim(),
112
112
  'https://ingest.humanbehavior.co'
113
113
  );
114
+
115
+ // ✅ APPLY LOGGING CONFIGURATION FROM OPTIONS
116
+ if (options?.logLevel) {
117
+ HumanBehaviorTracker.configureLogging({ level: options.logLevel });
118
+ }
119
+
120
+ // ✅ APPLY REDACTION FIELDS FROM OPTIONS
121
+ if (options?.redactFields && options.redactFields.length > 0) {
122
+ tracker.setRedactedFields(options.redactFields);
123
+ }
124
+
114
125
  setHumanBehavior(tracker);
115
126
 
116
127
  // Wait for initialization to complete
package/src/redact.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // Redaction functionality for sensitive input fields
2
- // This module provides methods to redact sensitive input fields in event recordings
2
+ // This module provides methods to configure rrweb's built-in masking
3
+ // Uses CSS selectors and classes for reliable redaction without event corruption
3
4
 
4
5
  import { logDebug, logWarn } from './utils/logger';
5
6
 
@@ -33,7 +34,8 @@ export class RedactionManager {
33
34
  }
34
35
 
35
36
  /**
36
- * Set specific fields to be redacted
37
+ * Set specific fields to be redacted using CSS selectors
38
+ * These selectors are used to configure rrweb's built-in masking
37
39
  * @param fields Array of CSS selectors for fields to redact
38
40
  */
39
41
  public setFieldsToRedact(fields: string[]): void {
@@ -72,6 +74,8 @@ export class RedactionManager {
72
74
 
73
75
  /**
74
76
  * Process an event and redact sensitive data if needed
77
+ * NOTE: This method is no longer used - events are handled directly by rrweb
78
+ * Kept for backward compatibility but not called in the current implementation
75
79
  */
76
80
  public processEvent(event: any): any {
77
81
  // Only process if we have fields selected for redaction
@@ -431,26 +435,67 @@ export class RedactionManager {
431
435
  }
432
436
 
433
437
  /**
434
- * Check if an element should be redacted based on user-selected fields
438
+ * Get CSS selectors for rrweb masking configuration
439
+ * Used to configure rrweb's maskTextSelector option
435
440
  */
436
- private shouldRedactElement(element: HTMLElement): boolean {
437
- // Check if element is excluded from redaction
438
- for (const excludeSelector of this.excludeSelectors) {
439
- if (element.matches(excludeSelector) || element.closest(excludeSelector)) {
440
- return false;
441
- }
441
+ public getMaskTextSelector(): string | null {
442
+ if (this.userSelectedFields.size === 0) {
443
+ return null;
442
444
  }
445
+ return Array.from(this.userSelectedFields).join(',');
446
+ }
443
447
 
444
- // Check if element matches any of the user-selected fields
448
+ /**
449
+ * Check if an element should be redacted (for rrweb maskTextFn/maskInputFn)
450
+ */
451
+ public shouldRedactElement(element: HTMLElement): boolean {
452
+ if (this.userSelectedFields.size === 0) {
453
+ return false;
454
+ }
455
+
456
+ // Check if any selector matches this element
445
457
  for (const selector of this.userSelectedFields) {
446
- if (element.matches(selector)) {
447
- return true;
458
+ try {
459
+ if (element.matches(selector)) {
460
+ return true;
461
+ }
462
+ } catch (e) {
463
+ // Invalid selector, skip
464
+ logWarn(`Invalid selector: ${selector}`);
448
465
  }
449
466
  }
450
-
451
467
  return false;
452
468
  }
453
469
 
470
+ /**
471
+ * Apply rrweb masking classes to DOM elements
472
+ * Adds 'rr-mask' class to elements that should be redacted
473
+ * This enables rrweb's built-in masking functionality
474
+ */
475
+ public applyRedactionClasses(): void {
476
+ if (this.userSelectedFields.size === 0) {
477
+ return;
478
+ }
479
+
480
+ // Remove existing redaction classes
481
+ document.querySelectorAll('.rr-mask').forEach(element => {
482
+ element.classList.remove('rr-mask');
483
+ });
484
+
485
+ // Add redaction classes to matching elements
486
+ this.userSelectedFields.forEach(selector => {
487
+ try {
488
+ const elements = document.querySelectorAll(selector);
489
+ elements.forEach(element => {
490
+ element.classList.add('rr-mask');
491
+ });
492
+ logDebug(`Applied rr-mask class to ${elements.length} element(s) for selector: ${selector}`);
493
+ } catch (e) {
494
+ logWarn(`Invalid selector: ${selector}`);
495
+ }
496
+ });
497
+ }
498
+
454
499
  /**
455
500
  * Get the original value of a redacted element (for debugging)
456
501
  */
package/src/tracker.ts CHANGED
@@ -17,8 +17,6 @@ declare global {
17
17
 
18
18
  export class HumanBehaviorTracker {
19
19
  private eventIngestionQueue: any[] = [];
20
- private queueSizeBytes: number = 0;
21
-
22
20
  private sessionId!: string;
23
21
  private userProperties: Record<string, any> = {};
24
22
  private isProcessing: boolean = false;
@@ -48,7 +46,6 @@ export class HumanBehaviorTracker {
48
46
  private navigationListeners: Array<() => void> = [];
49
47
  private _connectionBlocked: boolean = false;
50
48
  private recordInstance: any = null;
51
- private frequencyUpdateInterval: any = null;
52
49
  private sessionStartTime: number = Date.now();
53
50
 
54
51
  /**
@@ -85,6 +82,8 @@ export class HumanBehaviorTracker {
85
82
  // Set redacted fields if specified
86
83
  if (options?.redactFields) {
87
84
  tracker.setRedactedFields(options.redactFields);
85
+ // ✅ Apply redaction classes to existing elements
86
+ tracker.redactionManager.applyRedactionClasses();
88
87
  }
89
88
 
90
89
  // Setup automatic tracking if enabled
@@ -304,9 +303,6 @@ export class HumanBehaviorTracker {
304
303
  }
305
304
  }
306
305
 
307
- /**
308
- * Track a page view event (PostHog-style)
309
- */
310
306
  public async trackPageView(url?: string): Promise<void> {
311
307
  if (!this.initialized) return;
312
308
 
@@ -338,9 +334,6 @@ export class HumanBehaviorTracker {
338
334
  }
339
335
  }
340
336
 
341
- /**
342
- * Track a custom event (PostHog-style)
343
- */
344
337
  public async customEvent(eventName: string, properties?: Record<string, any>): Promise<void> {
345
338
  if (!this.initialized) return;
346
339
 
@@ -739,7 +732,6 @@ export class HumanBehaviorTracker {
739
732
  await this.api.sendUserData(originalEndUserId!, userProperties, this.sessionId);
740
733
 
741
734
  // Don't update endUserId - keep it as the original UUID
742
- // The posthogName will be updated on the server side with the email
743
735
 
744
736
  return originalEndUserId || '';
745
737
  }
@@ -762,70 +754,40 @@ export class HumanBehaviorTracker {
762
754
  // Enable console tracking
763
755
  this.enableConsoleTracking();
764
756
 
765
- // Adaptive snapshot configuration based on session duration
766
- const sessionStartTime = Date.now();
767
- let snapshotInterval = 5000; // Start with 5 seconds
768
- let eventThreshold = 100; // Start with 100 events
769
-
770
- // Function to update snapshot frequency based on session duration
771
- const updateSnapshotFrequency = () => {
772
- const sessionDuration = Date.now() - sessionStartTime;
773
- const thirtyMinutes = 30 * 60 * 1000;
774
- const twoHours = 2 * 60 * 60 * 1000;
775
- const fourHours = 4 * 60 * 60 * 1000;
776
-
777
- if (sessionDuration > twoHours) {
778
- // After 2 hours, very infrequent snapshots
779
- snapshotInterval = 30000; // 30 seconds
780
- eventThreshold = 500; // 500 events
781
- logDebug('Reduced snapshot frequency: 30s/500 events (2+ hours)');
782
- } else if (sessionDuration > thirtyMinutes) {
783
- // After 30 minutes, moderate frequency
784
- snapshotInterval = 15000; // 15 seconds
785
- eventThreshold = 300; // 300 events
786
- logDebug('Reduced snapshot frequency: 15s/300 events (30+ minutes)');
787
- }
788
- // First 30 minutes: 5s/100 events (default)
789
- };
790
-
791
- // Update frequency every 5 minutes
792
- const frequencyUpdateInterval = setInterval(updateSnapshotFrequency, 5 * 60 * 1000);
793
-
794
- // Start recording with adaptive redaction enabled
795
- const recordInstance = rrweb.record({
796
- emit: (event) => {
797
- // Add additional validation for FullSnapshot events
798
- if (event.type === 2) { // FullSnapshot event
799
- if (!event.data || !event.data.node) {
800
- logWarn('rrweb generated malformed FullSnapshot event:', {
801
- hasData: !!event.data,
802
- hasNode: !!(event.data && event.data.node),
803
- dataType: typeof event.data,
804
- eventType: event.type,
805
- timestamp: event.timestamp
806
- });
807
- // Don't skip - let the addEvent method handle it
808
- } else {
809
- logDebug('Valid FullSnapshot event received from rrweb');
757
+ // SIMPLIFIED RECORDING - Use rrweb's proven defaults
758
+ // No complex adaptive logic - rrweb's defaults work well for most use cases
759
+ const recordInstance = rrweb.record({
760
+ emit: (event) => {
761
+ // ✅ DIRECT EVENT HANDLING - Let rrweb handle events natively
762
+ this.addEvent(event);
763
+
764
+ // DEBUG FULLSNAPSHOT GENERATION
765
+ if (event.type === 2) { // FullSnapshot
766
+ logDebug(`🎯 FullSnapshot generated at ${new Date().toISOString()}`);
810
767
  }
811
- }
812
- this.addEvent(event);
813
- },
814
- inlineStylesheet: true,
815
- recordCanvas: true,
816
- collectFonts: true,
817
- inlineImages: true,
818
- blockClass: 'rr-block',
819
- ignoreClass: 'rr-ignore',
820
- maskTextClass: 'rr-ignore',
821
- // Adaptive configuration
822
- checkoutEveryNms: snapshotInterval,
823
- checkoutEveryNth: eventThreshold
824
- });
825
-
826
- // Store the record instance and interval for cleanup
827
- this.recordInstance = recordInstance;
828
- this.frequencyUpdateInterval = frequencyUpdateInterval;
768
+ },
769
+ inlineStylesheet: true,
770
+ recordCanvas: true,
771
+ collectFonts: true,
772
+ inlineImages: true,
773
+ blockClass: 'rr-block',
774
+ ignoreClass: 'rr-ignore',
775
+ maskTextClass: 'rr-mask',
776
+
777
+ // ✅ RRWEB BUILT-IN MASKING - More reliable than custom redaction
778
+ maskAllInputs: false, // Let users control this via selectors
779
+ maskTextSelector: this.redactionManager.getMaskTextSelector() || undefined,
780
+
781
+ // ✅ FULLSNAPSHOT GENERATION - Use reasonable intervals (PostHog-style)
782
+ checkoutEveryNms: 300000, // Take FullSnapshot every 5 minutes (like PostHog)
783
+ checkoutEveryNth: 1000, // Take FullSnapshot every 1000 events
784
+
785
+ // SELECTOR-BASED REDACTION - Users control via CSS selectors
786
+ // No custom masking functions needed - rrweb handles this natively
787
+ });
788
+
789
+ // Store the record instance for cleanup
790
+ this.recordInstance = recordInstance;
829
791
  }
830
792
 
831
793
  public async stop() {
@@ -837,12 +799,6 @@ export class HumanBehaviorTracker {
837
799
  this.flushInterval = null;
838
800
  }
839
801
 
840
- // Cleanup adaptive snapshot intervals
841
- if (this.frequencyUpdateInterval) {
842
- clearInterval(this.frequencyUpdateInterval);
843
- this.frequencyUpdateInterval = null;
844
- }
845
-
846
802
  // Stop rrweb recording
847
803
  if (this.recordInstance) {
848
804
  this.recordInstance();
@@ -856,30 +812,30 @@ export class HumanBehaviorTracker {
856
812
  this.cleanupNavigationTracking();
857
813
  }
858
814
 
815
+ /**
816
+ * Add an event to the ingestion queue
817
+ * Events are sent directly without processing to avoid corruption
818
+ */
859
819
  public async addEvent(event: any) {
860
820
  await this.ensureInitialized();
861
821
 
862
- // Validate FullSnapshot events before processing
863
- if (event.type === 2) { // FullSnapshot event
864
- if (!event.data || !event.data.node) {
865
- logWarn('Malformed FullSnapshot event detected, skipping:', {
866
- hasData: !!event.data,
867
- hasNode: !!(event.data && event.data.node),
868
- dataType: typeof event.data,
869
- eventType: event.type
870
- });
871
- return; // Skip malformed FullSnapshot events
872
- }
873
- }
822
+ // DIRECT EVENT HANDLING - No custom processing to avoid corruption
823
+ // Events flow directly from rrweb to ingestion server
874
824
 
875
- // Process event through redaction manager if active
876
- const processedEvent = this.redactionManager.processEvent(event);
825
+ // LOG FULLSNAPSHOT STATUS FOR DEBUGGING
826
+ if (event.type === 2) { // FullSnapshot
827
+ const hasData = !!event.data;
828
+ const hasNode = !!(event.data && event.data.node);
829
+ logDebug(`[FIXED] FullSnapshot event: hasData=${hasData}, hasNode=${hasNode}, dataType=${event.data?.node?.type}`);
830
+ }
877
831
 
878
- const eventSize = new TextEncoder().encode(JSON.stringify(processedEvent)).length;
879
- this.eventIngestionQueue.push(processedEvent);
880
- this.queueSizeBytes += eventSize;
832
+ this.eventIngestionQueue.push(event); // Direct event handling
881
833
  }
882
834
 
835
+ /**
836
+ * Flush events to the ingestion server
837
+ * Events are sent in chunks to handle large payloads efficiently
838
+ */
883
839
  private async flush() {
884
840
  // Prevent concurrent flushes
885
841
  if (this.isProcessing || !this.initialized) {
@@ -891,10 +847,16 @@ export class HumanBehaviorTracker {
891
847
  // Swap the current queue with an empty one atomically
892
848
  const eventsToProcess = this.eventIngestionQueue;
893
849
  this.eventIngestionQueue = [];
894
- this.queueSizeBytes = 0;
895
850
 
896
851
  if (eventsToProcess.length > 0) {
897
852
  logDebug('Flushing events:', eventsToProcess);
853
+
854
+ // ✅ LOG FULLSNAPSHOT STATUS FOR MONITORING
855
+ const fullSnapshots = eventsToProcess.filter(e => e.type === 2);
856
+ if (fullSnapshots.length > 0) {
857
+ logDebug(`[FIXED] Sending ${fullSnapshots.length} FullSnapshot(s) with valid data`);
858
+ }
859
+
898
860
  try {
899
861
  // Use chunked sending to handle large payloads
900
862
  await this.api.sendEventsChunked(eventsToProcess, this.sessionId, this.endUserId!);
@@ -1058,15 +1020,26 @@ export class HumanBehaviorTracker {
1058
1020
 
1059
1021
  /**
1060
1022
  * Set specific fields to be redacted during session recording
1023
+ * Uses rrweb's built-in masking instead of custom redaction processing
1061
1024
  * @param fields Array of CSS selectors for fields to redact (e.g., ['input[type="password"]', '#email-field'])
1062
1025
  */
1063
1026
  public setRedactedFields(fields: string[]): void {
1064
- if (!isBrowser) {
1065
- logWarn('Redaction is only available in browser environments');
1066
- return;
1067
- }
1068
-
1069
1027
  this.redactionManager.setFieldsToRedact(fields);
1028
+
1029
+ // ✅ APPLY RRWEB MASKING CLASSES - More reliable than custom processing
1030
+ this.redactionManager.applyRedactionClasses();
1031
+
1032
+ // ✅ RESTART RECORDING WITH NEW SETTINGS - Ensures masking is applied
1033
+ if (this.recordInstance) {
1034
+ this.restartWithNewRedaction();
1035
+ }
1036
+ }
1037
+
1038
+ private restartWithNewRedaction(): void {
1039
+ if (this.recordInstance) {
1040
+ this.recordInstance(); // Stop current recording
1041
+ this.start(); // Restart with new redaction settings
1042
+ }
1070
1043
  }
1071
1044
 
1072
1045
  /**
@@ -1099,6 +1072,7 @@ export class HumanBehaviorTracker {
1099
1072
 
1100
1073
  /**
1101
1074
  * Get current snapshot frequency info
1075
+ * Uses configured values (5 minutes, 1000 events) - PostHog-style
1102
1076
  */
1103
1077
  public getSnapshotFrequencyInfo(): {
1104
1078
  sessionDuration: number;
@@ -1107,28 +1081,12 @@ export class HumanBehaviorTracker {
1107
1081
  phase: string;
1108
1082
  } {
1109
1083
  const sessionDuration = Date.now() - this.sessionStartTime;
1110
- const thirtyMinutes = 30 * 60 * 1000;
1111
- const twoHours = 2 * 60 * 60 * 1000;
1112
-
1113
- let phase = 'initial';
1114
- let interval = 5000;
1115
- let threshold = 100;
1116
-
1117
- if (sessionDuration > twoHours) {
1118
- phase = 'extended';
1119
- interval = 30000;
1120
- threshold = 500;
1121
- } else if (sessionDuration > thirtyMinutes) {
1122
- phase = 'moderate';
1123
- interval = 15000;
1124
- threshold = 300;
1125
- }
1126
1084
 
1127
1085
  return {
1128
1086
  sessionDuration,
1129
- currentInterval: interval,
1130
- currentThreshold: threshold,
1131
- phase
1087
+ currentInterval: 300000, // Configured - 5 minutes (PostHog-style)
1088
+ currentThreshold: 1000, // Configured - 1000 events
1089
+ phase: 'configured' // Using explicit configuration
1132
1090
  };
1133
1091
  }
1134
1092