humanbehavior-js 0.4.16 → 0.4.18

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.
Files changed (89) hide show
  1. package/dist/cjs/wizard/index.cjs +6 -8
  2. package/dist/cjs/wizard/index.cjs.map +1 -1
  3. package/dist/cli/ai-auto-install.js +6 -8
  4. package/dist/cli/ai-auto-install.js.map +1 -1
  5. package/dist/esm/wizard/index.js +6 -8
  6. package/dist/esm/wizard/index.js.map +1 -1
  7. package/package/WIZARD_USAGE_GUIDE.md +381 -0
  8. package/package/canvas-recording-demo.html +143 -0
  9. package/package/clean-console-demo.html +39 -0
  10. package/package/dist/cjs/angular/index.cjs +14354 -0
  11. package/package/dist/cjs/angular/index.cjs.map +1 -0
  12. package/package/dist/cjs/index.cjs +14323 -0
  13. package/package/dist/cjs/index.cjs.map +1 -0
  14. package/package/dist/cjs/install-wizard.cjs +1530 -0
  15. package/package/dist/cjs/install-wizard.cjs.map +1 -0
  16. package/package/dist/cjs/react/index.cjs +14478 -0
  17. package/package/dist/cjs/react/index.cjs.map +1 -0
  18. package/package/dist/cjs/remix/index.cjs +14452 -0
  19. package/package/dist/cjs/remix/index.cjs.map +1 -0
  20. package/package/dist/cjs/svelte/index.cjs +14308 -0
  21. package/package/dist/cjs/svelte/index.cjs.map +1 -0
  22. package/package/dist/cjs/vue/index.cjs +14317 -0
  23. package/package/dist/cjs/vue/index.cjs.map +1 -0
  24. package/package/dist/cjs/wizard/index.cjs +3446 -0
  25. package/package/dist/cjs/wizard/index.cjs.map +1 -0
  26. package/package/dist/cli/ai-auto-install.cjs +57161 -0
  27. package/package/dist/cli/ai-auto-install.cjs.map +1 -0
  28. package/package/dist/cli/ai-auto-install.js +1969 -0
  29. package/package/dist/cli/ai-auto-install.js.map +1 -0
  30. package/package/dist/cli/auto-install.cjs +56352 -0
  31. package/package/dist/cli/auto-install.cjs.map +1 -0
  32. package/package/dist/cli/auto-install.js +1957 -0
  33. package/package/dist/cli/auto-install.js.map +1 -0
  34. package/package/dist/esm/angular/index.js +14350 -0
  35. package/package/dist/esm/angular/index.js.map +1 -0
  36. package/package/dist/esm/index.js +14309 -0
  37. package/package/dist/esm/index.js.map +1 -0
  38. package/package/dist/esm/install-wizard.js +1507 -0
  39. package/package/dist/esm/install-wizard.js.map +1 -0
  40. package/package/dist/esm/react/index.js +14472 -0
  41. package/package/dist/esm/react/index.js.map +1 -0
  42. package/package/dist/esm/remix/index.js +14448 -0
  43. package/package/dist/esm/remix/index.js.map +1 -0
  44. package/package/dist/esm/svelte/index.js +14306 -0
  45. package/package/dist/esm/svelte/index.js.map +1 -0
  46. package/package/dist/esm/vue/index.js +14315 -0
  47. package/package/dist/esm/vue/index.js.map +1 -0
  48. package/package/dist/esm/wizard/index.js +3415 -0
  49. package/package/dist/esm/wizard/index.js.map +1 -0
  50. package/package/dist/index.min.js +2 -0
  51. package/package/dist/index.min.js.map +1 -0
  52. package/package/dist/types/angular/index.d.ts +267 -0
  53. package/package/dist/types/index.d.ts +373 -0
  54. package/package/dist/types/install-wizard.d.ts +156 -0
  55. package/package/dist/types/react/index.d.ts +255 -0
  56. package/package/dist/types/remix/index.d.ts +246 -0
  57. package/package/dist/types/svelte/index.d.ts +232 -0
  58. package/package/dist/types/vue/index.d.ts +15 -0
  59. package/package/dist/types/wizard/index.d.ts +523 -0
  60. package/package/package.json +105 -0
  61. package/package/readme.md +281 -0
  62. package/package/rollup.config.js +422 -0
  63. package/package/simple-demo.html +26 -0
  64. package/package/simple-spa.html +838 -0
  65. package/package/src/angular/index.ts +79 -0
  66. package/package/src/api.ts +376 -0
  67. package/package/src/index.ts +28 -0
  68. package/package/src/react/AutoInstallWizard.tsx +557 -0
  69. package/package/src/react/browser.ts +8 -0
  70. package/package/src/react/index.tsx +308 -0
  71. package/package/src/redact.ts +521 -0
  72. package/package/src/remix/index.ts +16 -0
  73. package/package/src/svelte/index.ts +14 -0
  74. package/package/src/tracker.ts +1319 -0
  75. package/package/src/types/clack.d.ts +31 -0
  76. package/package/src/utils/logger.ts +144 -0
  77. package/package/src/vue/index.ts +29 -0
  78. package/package/src/wizard/README.md +114 -0
  79. package/package/src/wizard/ai/ai-install-wizard.ts +897 -0
  80. package/package/src/wizard/ai/manual-framework-wizard.ts +238 -0
  81. package/package/src/wizard/cli/ai-auto-install.ts +243 -0
  82. package/package/src/wizard/cli/auto-install.ts +224 -0
  83. package/package/src/wizard/core/install-wizard.ts +1744 -0
  84. package/package/src/wizard/index.ts +23 -0
  85. package/package/src/wizard/services/centralized-ai-service.ts +668 -0
  86. package/package/src/wizard/services/remote-ai-service.ts +240 -0
  87. package/package/tsconfig.json +24 -0
  88. package/package.json +1 -1
  89. package/src/wizard/cli/ai-auto-install.ts +4 -6
@@ -0,0 +1,1319 @@
1
+ import { record } from '@rrweb/record';
2
+ import type { listenerHandler } from '@rrweb/types';
3
+ import { v1 as uuidv1 } from 'uuid';
4
+ import { HumanBehaviorAPI } from './api';
5
+ import { RedactionManager, RedactionOptions } from './redact';
6
+ import { logger, logError, logWarn, logInfo, logDebug } from './utils/logger';
7
+
8
+ // Check if we're in a browser environment
9
+ const isBrowser = typeof window !== 'undefined';
10
+
11
+ // Add type declaration at the top level
12
+ declare global {
13
+ interface Window {
14
+ HumanBehaviorTracker: typeof HumanBehaviorTracker;
15
+ __humanBehaviorGlobalTracker?: HumanBehaviorTracker;
16
+ }
17
+ }
18
+
19
+ export class HumanBehaviorTracker {
20
+ private eventIngestionQueue: any[] = [];
21
+ private sessionId!: string;
22
+ private userProperties: Record<string, any> = {};
23
+ private isProcessing: boolean = false;
24
+ private flushInterval: number | null = null;
25
+ private readonly FLUSH_INTERVAL_MS = 5000; // Flush every 5 seconds
26
+ private api!: HumanBehaviorAPI;
27
+ private endUserId: string | null = null;
28
+ private apiKey!: string;
29
+ private initialized: boolean = false;
30
+ public initializationPromise: Promise<void> | null = null;
31
+ private redactionManager!: RedactionManager;
32
+
33
+ // Console tracking properties
34
+ private originalConsole: {
35
+ log: typeof console.log;
36
+ warn: typeof console.warn;
37
+ error: typeof console.error;
38
+ } | null = null;
39
+ private consoleTrackingEnabled: boolean = false;
40
+
41
+ // Navigation tracking properties
42
+ public navigationTrackingEnabled: boolean = false;
43
+ private currentUrl: string = '';
44
+ private previousUrl: string = '';
45
+ private originalPushState: typeof history.pushState | null = null;
46
+ private originalReplaceState: typeof history.replaceState | null = null;
47
+ private navigationListeners: Array<() => void> = [];
48
+ private _connectionBlocked: boolean = false;
49
+ private recordInstance: listenerHandler | null = null;
50
+ private sessionStartTime: number = Date.now();
51
+ private rrwebRecord: any = null;
52
+ private fullSnapshotTimeout: number | null = null;
53
+ private recordCanvas: boolean = false; // Store canvas recording preference
54
+
55
+ /**
56
+ * Initialize the HumanBehavior tracker
57
+ * This is the main entry point - call this once per page
58
+ */
59
+ public static init(apiKey: string, options?: {
60
+ ingestionUrl?: string;
61
+ logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug';
62
+ redactFields?: string[];
63
+ enableAutomaticTracking?: boolean;
64
+ suppressConsoleErrors?: boolean; // New option to control error suppression
65
+ recordCanvas?: boolean; // Enable canvas recording with PostHog-style protection
66
+ automaticTrackingOptions?: {
67
+ trackButtons?: boolean;
68
+ trackLinks?: boolean;
69
+ trackForms?: boolean;
70
+ includeText?: boolean;
71
+ includeClasses?: boolean;
72
+ };
73
+ }): HumanBehaviorTracker {
74
+ // ✅ SUPPRESS COMMON RRWEB ERRORS FOR CLEAN CONSOLE
75
+ if (isBrowser && options?.suppressConsoleErrors !== false) {
76
+ // Suppress canvas security errors
77
+ const originalConsoleError = console.error;
78
+ console.error = (...args: any[]) => {
79
+ const message = args.join(' ');
80
+ if (
81
+ message.includes('SecurityError: Failed to execute \'toDataURL\'') ||
82
+ message.includes('Tainted canvases may not be exported') ||
83
+ message.includes('Cannot inline img src=') ||
84
+ message.includes('Cross-Origin') ||
85
+ message.includes('CORS') ||
86
+ message.includes('Access-Control-Allow-Origin') ||
87
+ message.includes('Failed to load resource') ||
88
+ message.includes('net::ERR_BLOCKED_BY_CLIENT')
89
+ ) {
90
+ // Silently suppress these common rrweb errors
91
+ return;
92
+ }
93
+ originalConsoleError.apply(console, args);
94
+ };
95
+
96
+ // Suppress console.warn for similar issues
97
+ const originalConsoleWarn = console.warn;
98
+ console.warn = (...args: any[]) => {
99
+ const message = args.join(' ');
100
+ if (
101
+ message.includes('Cannot inline img src=') ||
102
+ message.includes('Cross-Origin') ||
103
+ message.includes('CORS') ||
104
+ message.includes('Access-Control-Allow-Origin') ||
105
+ message.includes('Failed to load resource') ||
106
+ message.includes('net::ERR_BLOCKED_BY_CLIENT')
107
+ ) {
108
+ // Silently suppress these common rrweb warnings
109
+ return;
110
+ }
111
+ originalConsoleWarn.apply(console, args);
112
+ };
113
+
114
+ // Add global error handler for any remaining rrweb errors
115
+ window.addEventListener('error', (event) => {
116
+ const message = event.message || '';
117
+ if (
118
+ message.includes('SecurityError') ||
119
+ message.includes('Tainted canvases') ||
120
+ message.includes('toDataURL') ||
121
+ message.includes('Cross-Origin') ||
122
+ message.includes('CORS')
123
+ ) {
124
+ event.preventDefault();
125
+ return false;
126
+ }
127
+ });
128
+ }
129
+ // Return existing instance if already initialized
130
+ if (isBrowser && window.__humanBehaviorGlobalTracker) {
131
+ logDebug('Tracker already initialized, returning existing instance');
132
+ return window.__humanBehaviorGlobalTracker;
133
+ }
134
+
135
+ // Configure logging if specified
136
+ if (options?.logLevel) {
137
+ this.configureLogging({ level: options.logLevel });
138
+ }
139
+
140
+ // Create new tracker instance
141
+ const tracker = new HumanBehaviorTracker(apiKey, options?.ingestionUrl);
142
+
143
+ // Store canvas recording preference
144
+ tracker.recordCanvas = options?.recordCanvas ?? false;
145
+
146
+ // Set redacted fields if specified
147
+ if (options?.redactFields) {
148
+ tracker.setRedactedFields(options.redactFields);
149
+ // ✅ Apply redaction classes to existing elements
150
+ tracker.redactionManager.applyRedactionClasses();
151
+ }
152
+
153
+ // Setup automatic tracking if enabled
154
+ if (options?.enableAutomaticTracking !== false) {
155
+ tracker.setupAutomaticTracking(options?.automaticTrackingOptions);
156
+ }
157
+
158
+ // Start tracking
159
+ tracker.start();
160
+
161
+ return tracker;
162
+ }
163
+
164
+ constructor(apiKey: string | undefined, ingestionUrl?: string) {
165
+ if (!apiKey) {
166
+ throw new Error('Human Behavior API Key is required');
167
+ }
168
+
169
+ // Initialize API
170
+ //const defaultIngestionUrl = 'http://3.137.217.33:3000'; // AWS Development Server
171
+ //const defaultIngestionUrl = 'http://ingestion-server-alb-1823866402.us-east-2.elb.amazonaws.com'; // ALB
172
+ const defaultIngestionUrl = 'https://ingest.humanbehavior.co'; // HTTPS ALB
173
+ this.api = new HumanBehaviorAPI({
174
+ apiKey: apiKey,
175
+ ingestionUrl: ingestionUrl || defaultIngestionUrl
176
+ });
177
+ this.apiKey = apiKey;
178
+ this.redactionManager = new RedactionManager();
179
+
180
+ // Handle session restoration with improved continuity
181
+ if (isBrowser) {
182
+ const existingSessionId = localStorage.getItem(`human_behavior_session_id_${this.apiKey}`);
183
+ const lastActivity = localStorage.getItem(`human_behavior_last_activity_${this.apiKey}`);
184
+ const fifteenMinutesAgo = Date.now() - (15 * 60 * 1000);
185
+
186
+ // Check if we have an existing session that's still within the activity window
187
+ if (existingSessionId && lastActivity && parseInt(lastActivity) > fifteenMinutesAgo) {
188
+ this.sessionId = existingSessionId;
189
+ logDebug(`Reusing existing session: ${this.sessionId}`);
190
+ // Update activity timestamp to extend the session window
191
+ localStorage.setItem(`human_behavior_last_activity_${this.apiKey}`, Date.now().toString());
192
+ } else {
193
+ // Clear old session data if it's expired
194
+ if (existingSessionId) {
195
+ logDebug(`Session expired, clearing old session: ${existingSessionId}`);
196
+ localStorage.removeItem(`human_behavior_session_id_${this.apiKey}`);
197
+ localStorage.removeItem(`human_behavior_last_activity_${this.apiKey}`);
198
+ }
199
+ this.sessionId = uuidv1();
200
+ logDebug(`Creating new session: ${this.sessionId}`);
201
+ localStorage.setItem(`human_behavior_session_id_${this.apiKey}`, this.sessionId);
202
+ localStorage.setItem(`human_behavior_last_activity_${this.apiKey}`, Date.now().toString());
203
+ }
204
+
205
+ this.currentUrl = window.location.href;
206
+ window.__humanBehaviorGlobalTracker = this;
207
+ } else {
208
+ this.sessionId = uuidv1();
209
+ }
210
+
211
+ // Start initialization
212
+ this.initializationPromise = this.init();
213
+ }
214
+
215
+ private async init(): Promise<void> {
216
+ try {
217
+ const userId = this.getCookie(`human_behavior_end_user_id_${this.apiKey}`);
218
+ logDebug(`Initializing with sessionId: ${this.sessionId}, userId: ${userId}`);
219
+
220
+ const { sessionId, endUserId } = await this.api.init(this.sessionId, userId);
221
+
222
+ // Check if server returned a different session ID (for session continuity)
223
+ if (sessionId !== this.sessionId) {
224
+ logDebug(`Server returned different sessionId: ${sessionId} (client had: ${this.sessionId})`);
225
+ this.sessionId = sessionId;
226
+ // Update localStorage with server's session ID for continuity
227
+ if (isBrowser) {
228
+ localStorage.setItem(`human_behavior_session_id_${this.apiKey}`, this.sessionId);
229
+ }
230
+ }
231
+
232
+ this.endUserId = endUserId;
233
+ this.setCookie(`human_behavior_end_user_id_${this.apiKey}`, endUserId, 365);
234
+
235
+ // Only setup browser-specific handlers when in browser environment
236
+ if (isBrowser) {
237
+ this.setupPageUnloadHandler();
238
+ this.setupNavigationTracking();
239
+ } else {
240
+ logWarn('HumanBehaviorTracker initialized in a non-browser environment. Session tracking is disabled.');
241
+ }
242
+
243
+ this.initialized = true;
244
+ logInfo(`HumanBehaviorTracker initialized with sessionId: ${this.sessionId}, endUserId: ${endUserId}`);
245
+ } catch (error) {
246
+ logError('Failed to initialize HumanBehaviorTracker:', error);
247
+ throw error;
248
+ }
249
+ }
250
+
251
+ private async ensureInitialized(): Promise<void> {
252
+ if (!this.initializationPromise) {
253
+ throw new Error('HumanBehaviorTracker initialization failed');
254
+ }
255
+ await this.initializationPromise;
256
+ }
257
+
258
+ /**
259
+ * Setup navigation event tracking for SPA navigation
260
+ */
261
+ private setupNavigationTracking(): void {
262
+ if (!isBrowser || this.navigationTrackingEnabled) return;
263
+
264
+ this.navigationTrackingEnabled = true;
265
+ logDebug('Setting up navigation tracking');
266
+
267
+ // Store original history methods
268
+ this.originalPushState = history.pushState;
269
+ this.originalReplaceState = history.replaceState;
270
+
271
+ // Override pushState to capture programmatic navigation
272
+ history.pushState = (...args) => {
273
+ this.previousUrl = this.currentUrl;
274
+ this.currentUrl = window.location.href;
275
+
276
+ // Call original method
277
+ this.originalPushState!.apply(history, args);
278
+
279
+ // Track navigation event
280
+ this.trackNavigationEvent('pushState', this.previousUrl, this.currentUrl);
281
+
282
+ // Take FullSnapshot on navigation
283
+ this.takeFullSnapshot();
284
+ };
285
+
286
+ // Override replaceState to capture programmatic navigation
287
+ history.replaceState = (...args) => {
288
+ this.previousUrl = this.currentUrl;
289
+ this.currentUrl = window.location.href;
290
+
291
+ // Call original method
292
+ this.originalReplaceState!.apply(history, args);
293
+
294
+ // Track navigation event
295
+ this.trackNavigationEvent('replaceState', this.previousUrl, this.currentUrl);
296
+
297
+ // Take FullSnapshot on navigation
298
+ this.takeFullSnapshot();
299
+ };
300
+
301
+ // Listen for popstate events (back/forward navigation)
302
+ const popstateListener = () => {
303
+ this.previousUrl = this.currentUrl;
304
+ this.currentUrl = window.location.href;
305
+ this.trackNavigationEvent('popstate', this.previousUrl, this.currentUrl);
306
+
307
+ // Take FullSnapshot on navigation
308
+ this.takeFullSnapshot();
309
+ };
310
+
311
+ window.addEventListener('popstate', popstateListener);
312
+ this.navigationListeners.push(() => {
313
+ window.removeEventListener('popstate', popstateListener);
314
+ });
315
+
316
+ // Listen for hashchange events
317
+ const hashchangeListener = () => {
318
+ this.previousUrl = this.currentUrl;
319
+ this.currentUrl = window.location.href;
320
+ this.trackNavigationEvent('hashchange', this.previousUrl, this.currentUrl);
321
+ };
322
+
323
+ window.addEventListener('hashchange', hashchangeListener);
324
+ this.navigationListeners.push(() => {
325
+ window.removeEventListener('hashchange', hashchangeListener);
326
+ });
327
+
328
+ // Track initial page load
329
+ this.trackNavigationEvent('pageLoad', '', this.currentUrl);
330
+ }
331
+
332
+ /**
333
+ * Track navigation events and send custom events
334
+ */
335
+ public async trackNavigationEvent(type: string, fromUrl: string, toUrl: string): Promise<void> {
336
+ if (!this.initialized) return;
337
+
338
+ try {
339
+ const navigationData = {
340
+ type: type,
341
+ from: fromUrl,
342
+ to: toUrl,
343
+ timestamp: new Date().toISOString(),
344
+ pathname: window.location.pathname,
345
+ search: window.location.search,
346
+ hash: window.location.hash,
347
+ referrer: document.referrer
348
+ };
349
+
350
+ // Add navigation event to the main event stream
351
+ await this.addEvent({
352
+ type: 5, // Custom event type
353
+ data: {
354
+ payload: {
355
+ eventType: 'navigation',
356
+ ...navigationData
357
+ }
358
+ },
359
+ timestamp: Date.now()
360
+ });
361
+
362
+ logDebug(`Navigation tracked: ${type} from ${fromUrl} to ${toUrl}`);
363
+ } catch (error) {
364
+ logError('Failed to track navigation event:', error);
365
+ }
366
+ }
367
+
368
+ public async trackPageView(url?: string): Promise<void> {
369
+ if (!this.initialized) return;
370
+
371
+ try {
372
+ const pageViewData = {
373
+ url: url || window.location.href,
374
+ pathname: window.location.pathname,
375
+ search: window.location.search,
376
+ hash: window.location.hash,
377
+ referrer: document.referrer,
378
+ timestamp: new Date().toISOString()
379
+ };
380
+
381
+ // Add pageview event to the main event stream
382
+ await this.addEvent({
383
+ type: 5, // Custom event type
384
+ data: {
385
+ payload: {
386
+ eventType: 'pageview',
387
+ ...pageViewData
388
+ }
389
+ },
390
+ timestamp: Date.now()
391
+ });
392
+
393
+ logDebug(`Pageview tracked: ${pageViewData.url}`);
394
+ } catch (error) {
395
+ logError('Failed to track pageview event:', error);
396
+ }
397
+ }
398
+
399
+ public async customEvent(eventName: string, properties?: Record<string, any>): Promise<void> {
400
+ if (!this.initialized) return;
401
+
402
+ try {
403
+ // Send custom event directly to the API
404
+ await this.api.sendCustomEvent(this.sessionId, eventName, properties);
405
+
406
+ logDebug(`Custom event tracked: ${eventName}`, properties);
407
+ } catch (error: any) {
408
+ logError('Failed to track custom event:', error);
409
+
410
+ // Handle specific error types - check for any custom event failure
411
+ if (error.message?.includes('500') ||
412
+ error.message?.includes('Internal Server Error') ||
413
+ error.message?.includes('Failed to send custom event')) {
414
+ logWarn('Custom event endpoint failed, using fallback');
415
+ } else if (error.message?.includes('ERR_BLOCKED_BY_CLIENT')) {
416
+ logWarn('Custom event request blocked by ad blocker, using fallback');
417
+ } else if (error.message?.includes('Failed to fetch')) {
418
+ logWarn('Custom event network error, using fallback');
419
+ }
420
+
421
+ // Always try fallback for any custom event error
422
+ try {
423
+ const customEventData = {
424
+ eventName: eventName,
425
+ properties: properties || {},
426
+ timestamp: new Date().toISOString(),
427
+ url: window.location.href,
428
+ pathname: window.location.pathname
429
+ };
430
+
431
+ await this.addEvent({
432
+ type: 5, // Custom event type
433
+ data: {
434
+ payload: {
435
+ eventType: 'custom',
436
+ ...customEventData
437
+ }
438
+ },
439
+ timestamp: Date.now()
440
+ });
441
+
442
+ logDebug(`Custom event added to event stream as fallback: ${eventName}`);
443
+ } catch (fallbackError) {
444
+ logError('Failed to add custom event to event stream as fallback:', fallbackError);
445
+ }
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Setup automatic tracking for buttons, links, and forms
451
+ */
452
+ private setupAutomaticTracking(options?: {
453
+ trackButtons?: boolean;
454
+ trackLinks?: boolean;
455
+ trackForms?: boolean;
456
+ includeText?: boolean;
457
+ includeClasses?: boolean;
458
+ }): void {
459
+ if (!isBrowser) return;
460
+
461
+ const config = {
462
+ trackButtons: options?.trackButtons !== false,
463
+ trackLinks: options?.trackLinks !== false,
464
+ trackForms: options?.trackForms !== false,
465
+ includeText: options?.includeText !== false,
466
+ includeClasses: options?.includeClasses || false
467
+ };
468
+
469
+ logDebug('Setting up automatic tracking with config:', config);
470
+
471
+ // Setup button tracking
472
+ if (config.trackButtons) {
473
+ this.setupAutomaticButtonTracking(config);
474
+ }
475
+
476
+ // Setup link tracking
477
+ if (config.trackLinks) {
478
+ this.setupAutomaticLinkTracking(config);
479
+ }
480
+
481
+ // Setup form tracking
482
+ if (config.trackForms) {
483
+ this.setupAutomaticFormTracking(config);
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Setup automatic button tracking
489
+ */
490
+ private setupAutomaticButtonTracking(config: {
491
+ includeText?: boolean;
492
+ includeClasses?: boolean;
493
+ }): void {
494
+ document.addEventListener('click', async (event) => {
495
+ const target = event.target as HTMLElement;
496
+
497
+ // Track button clicks
498
+ if (target.tagName === 'BUTTON' || target.closest('button')) {
499
+ const button = target.tagName === 'BUTTON'
500
+ ? target as HTMLButtonElement
501
+ : target.closest('button') as HTMLButtonElement;
502
+
503
+ const properties: Record<string, any> = {
504
+ buttonId: button.id || null,
505
+ buttonType: button.type || 'button',
506
+ page: window.location.pathname,
507
+ timestamp: Date.now()
508
+ };
509
+
510
+ if (config.includeText) {
511
+ properties.buttonText = button.textContent?.trim() || null;
512
+ }
513
+
514
+ if (config.includeClasses) {
515
+ properties.buttonClass = button.className || null;
516
+ }
517
+
518
+ // Remove null values
519
+ Object.keys(properties).forEach(key => {
520
+ if (properties[key] === null) {
521
+ delete properties[key];
522
+ }
523
+ });
524
+
525
+ await this.customEvent('button_clicked', properties);
526
+ }
527
+ });
528
+ }
529
+
530
+ /**
531
+ * Setup automatic link tracking
532
+ */
533
+ private setupAutomaticLinkTracking(config: {
534
+ includeText?: boolean;
535
+ includeClasses?: boolean;
536
+ }): void {
537
+ document.addEventListener('click', async (event) => {
538
+ const target = event.target as HTMLElement;
539
+
540
+ // Track link clicks
541
+ if (target.tagName === 'A' || target.closest('a')) {
542
+ const link = target.tagName === 'A'
543
+ ? target as HTMLAnchorElement
544
+ : target.closest('a') as HTMLAnchorElement;
545
+
546
+ const properties: Record<string, any> = {
547
+ linkUrl: link.href || null,
548
+ linkId: link.id || null,
549
+ linkTarget: link.target || null,
550
+ page: window.location.pathname,
551
+ timestamp: Date.now()
552
+ };
553
+
554
+ if (config.includeText) {
555
+ properties.linkText = link.textContent?.trim() || null;
556
+ }
557
+
558
+ if (config.includeClasses) {
559
+ properties.linkClass = link.className || null;
560
+ }
561
+
562
+ // Remove null values
563
+ Object.keys(properties).forEach(key => {
564
+ if (properties[key] === null) {
565
+ delete properties[key];
566
+ }
567
+ });
568
+
569
+ await this.customEvent('link_clicked', properties);
570
+ }
571
+ });
572
+ }
573
+
574
+ /**
575
+ * Setup automatic form tracking
576
+ */
577
+ private setupAutomaticFormTracking(config: {
578
+ includeText?: boolean;
579
+ includeClasses?: boolean;
580
+ }): void {
581
+ document.addEventListener('submit', async (event) => {
582
+ const form = event.target as HTMLFormElement;
583
+ const formData = new FormData(form);
584
+
585
+ const properties: Record<string, any> = {
586
+ formId: form.id || null,
587
+ formAction: form.action || null,
588
+ formMethod: form.method || 'get',
589
+ fields: Array.from(formData.keys()),
590
+ page: window.location.pathname,
591
+ timestamp: Date.now()
592
+ };
593
+
594
+ if (config.includeClasses) {
595
+ properties.formClass = form.className || null;
596
+ }
597
+
598
+ // Remove null values
599
+ Object.keys(properties).forEach(key => {
600
+ if (properties[key] === null) {
601
+ delete properties[key];
602
+ }
603
+ });
604
+
605
+ await this.customEvent('form_submitted', properties);
606
+ });
607
+ }
608
+
609
+ /**
610
+ * Cleanup navigation tracking
611
+ */
612
+ private cleanupNavigationTracking(): void {
613
+ if (!this.navigationTrackingEnabled) return;
614
+
615
+ // Restore original history methods
616
+ if (this.originalPushState) {
617
+ history.pushState = this.originalPushState;
618
+ }
619
+ if (this.originalReplaceState) {
620
+ history.replaceState = this.originalReplaceState;
621
+ }
622
+
623
+ // Remove event listeners
624
+ this.navigationListeners.forEach(cleanup => cleanup());
625
+ this.navigationListeners = [];
626
+
627
+ this.navigationTrackingEnabled = false;
628
+ logDebug('Navigation tracking cleaned up');
629
+ }
630
+
631
+ public static logToStorage(message: string) {
632
+ logInfo(message);
633
+ }
634
+
635
+ /**
636
+ * Configure logging behavior for the SDK
637
+ * @param config Logger configuration options
638
+ */
639
+ public static configureLogging(config: { level?: 'none' | 'error' | 'warn' | 'info' | 'debug', enableConsole?: boolean, enableStorage?: boolean }) {
640
+ const levelMap = {
641
+ 'none': 0,
642
+ 'error': 1,
643
+ 'warn': 2,
644
+ 'info': 3,
645
+ 'debug': 4
646
+ };
647
+
648
+ logger.setConfig({
649
+ level: levelMap[config.level || 'error'],
650
+ enableConsole: config.enableConsole !== false,
651
+ enableStorage: config.enableStorage || false
652
+ });
653
+ }
654
+
655
+ /**
656
+ * Enable console event tracking
657
+ */
658
+ public enableConsoleTracking(): void {
659
+ if (!isBrowser || this.consoleTrackingEnabled) return;
660
+
661
+ // Store original console methods
662
+ this.originalConsole = {
663
+ log: console.log,
664
+ warn: console.warn,
665
+ error: console.error
666
+ };
667
+
668
+ // Override console methods to capture ALL console output (including logger output)
669
+ console.log = (...args) => {
670
+ this.trackConsoleEvent('log', args);
671
+ this.originalConsole!.log(...args);
672
+ };
673
+
674
+ console.warn = (...args) => {
675
+ this.trackConsoleEvent('warn', args);
676
+ this.originalConsole!.warn(...args);
677
+ };
678
+
679
+ console.error = (...args) => {
680
+ this.trackConsoleEvent('error', args);
681
+ this.originalConsole!.error(...args);
682
+ };
683
+
684
+ this.consoleTrackingEnabled = true;
685
+ logDebug('Console tracking enabled');
686
+ }
687
+
688
+ /**
689
+ * Disable console event tracking
690
+ */
691
+ public disableConsoleTracking(): void {
692
+ if (!isBrowser || !this.consoleTrackingEnabled) return;
693
+
694
+ // Restore original console methods
695
+ if (this.originalConsole) {
696
+ console.log = this.originalConsole.log;
697
+ console.warn = this.originalConsole.warn;
698
+ console.error = this.originalConsole.error;
699
+ }
700
+
701
+ this.consoleTrackingEnabled = false;
702
+ logDebug('Console tracking disabled');
703
+ }
704
+
705
+ private trackConsoleEvent(level: 'log' | 'warn' | 'error', args: any[]): void {
706
+ if (!this.initialized) return;
707
+
708
+ try {
709
+ const consoleData = {
710
+ level: level,
711
+ message: args.map(arg =>
712
+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
713
+ ).join(' '),
714
+ timestamp: new Date().toISOString(),
715
+ url: window.location.href
716
+ };
717
+
718
+ // Add console event to the main event stream
719
+ this.addEvent({
720
+ type: 5, // Custom event type
721
+ data: {
722
+ payload: {
723
+ eventType: 'console',
724
+ ...consoleData
725
+ }
726
+ },
727
+ timestamp: Date.now()
728
+ }).catch(error => {
729
+ logError('Failed to track console event:', error);
730
+ });
731
+ } catch (error) {
732
+ logError('Error in trackConsoleEvent:', error);
733
+ }
734
+ }
735
+
736
+ private setupPageUnloadHandler() {
737
+ if (!isBrowser) return;
738
+
739
+ logDebug('Setting up page unload handler');
740
+
741
+ // Handle visibility changes for sending events
742
+ window.addEventListener('visibilitychange', () => {
743
+ // Only send events when page becomes hidden
744
+ if (document.visibilityState === 'hidden') {
745
+ logDebug('Page hidden - sending pending events');
746
+ this.api.sendBeaconEvents(this.eventIngestionQueue, this.sessionId);
747
+ }
748
+ });
749
+
750
+ // Handle actual page unload/close
751
+ window.addEventListener('beforeunload', () => {
752
+ // Send final events
753
+ this.api.sendBeaconEvents(this.eventIngestionQueue, this.sessionId);
754
+ });
755
+
756
+ // Update activity timestamp on user interaction (not on page load)
757
+ const updateActivity = () => {
758
+ localStorage.setItem(`human_behavior_last_activity_${this.apiKey}`, Date.now().toString());
759
+ };
760
+
761
+ // Listen for user interactions to update activity timestamp
762
+ window.addEventListener('click', updateActivity);
763
+ window.addEventListener('keydown', updateActivity);
764
+ window.addEventListener('scroll', updateActivity);
765
+ window.addEventListener('mousemove', updateActivity);
766
+ }
767
+
768
+ public viewLogs() {
769
+ try {
770
+ const logs = logger.getLogs();
771
+ logInfo('HumanBehavior Logs:', logs);
772
+ logger.clearLogs(); // Clear logs after viewing
773
+ } catch (e) {
774
+ logError('Failed to read logs:', e);
775
+ }
776
+ }
777
+
778
+ /**
779
+ * Add user identification information to the tracker
780
+ * If userId is not provided, will use userProperties.email as the userId (if present)
781
+ */
782
+ public async identifyUser(
783
+ { userProperties }: { userProperties: Record<string, any> }
784
+ ): Promise<string> {
785
+ await this.ensureInitialized();
786
+
787
+ // Keep the original endUserId (UUID) - don't change it
788
+ const originalEndUserId = this.endUserId;
789
+
790
+ // Store user properties
791
+ this.userProperties = userProperties;
792
+
793
+ logDebug('Identifying user:', { userProperties, originalEndUserId, sessionId: this.sessionId });
794
+
795
+ // Send user data with the original endUserId
796
+ await this.api.sendUserData(originalEndUserId!, userProperties, this.sessionId);
797
+
798
+ // Don't update endUserId - keep it as the original UUID
799
+
800
+ return originalEndUserId || '';
801
+ }
802
+ /**
803
+ * Get current user attributes
804
+ */
805
+ public getUserAttributes(): Record<string, any> {
806
+ return { ...this.userProperties };
807
+ }
808
+
809
+ public async start() {
810
+ await this.ensureInitialized();
811
+ if (!isBrowser) return;
812
+
813
+ // Start periodic flushing
814
+ this.flushInterval = window.setInterval(() => {
815
+ this.flush();
816
+ }, this.FLUSH_INTERVAL_MS);
817
+
818
+ // Disable console tracking to reduce event pollution
819
+ // this.enableConsoleTracking();
820
+
821
+ // ✅ DOM READY DETECTION
822
+ // Wait for DOM to be ready before starting recording
823
+ const startRecording = () => {
824
+ logDebug('🎯 DOM ready, starting session recording');
825
+
826
+ // ✅ HUMANBEHAVIOR RRWEB CONFIGURATION
827
+ this.rrwebRecord = record;
828
+ const recordInstance = record({
829
+ emit: (event) => {
830
+ // ✅ DIRECT EVENT HANDLING - Let rrweb handle events natively
831
+ this.addEvent(event);
832
+
833
+ // ✅ DEBUG FULLSNAPSHOT GENERATION
834
+ if (event.type === 2) { // FullSnapshot
835
+ logDebug(`🎯 FullSnapshot generated at ${new Date().toISOString()}`);
836
+ }
837
+ },
838
+ // ✅ HUMANBEHAVIOR'S CUSTOM SETTINGS
839
+ maskTextSelector: this.redactionManager.getMaskTextSelector() || undefined,
840
+ maskTextFn: undefined,
841
+ maskAllInputs: true, // HumanBehavior default
842
+ maskInputOptions: { password: true }, // HumanBehavior default
843
+ maskInputFn: undefined,
844
+ slimDOMOptions: {},
845
+ // ✅ ERROR SUPPRESSION SETTINGS - Disabled to prevent console noise
846
+ collectFonts: false, // Disable font collection to reduce errors
847
+ inlineStylesheet: true, // Keep styles for proper session replay
848
+ recordCrossOriginIframes: false, // Prevent cross-origin iframe errors
849
+
850
+ // ✅ CANVAS RECORDING - PostHog-style protection against overwhelm
851
+ recordCanvas: this.recordCanvas, // Opt-in only
852
+ sampling: this.recordCanvas ? { canvas: 4 } : undefined, // 4 FPS throttle
853
+ dataURLOptions: this.recordCanvas ? {
854
+ type: 'image/webp',
855
+ quality: 0.4
856
+ } : undefined, // WebP with 40% quality
857
+
858
+ // ✅ FULLSNAPSHOT GENERATION - No periodic snapshots to avoid animation issues
859
+ // Rely on initial FullSnapshot + navigation-triggered ones only
860
+ });
861
+
862
+ // Store the record instance for cleanup
863
+ this.recordInstance = recordInstance || null;
864
+ };
865
+
866
+ // ✅ DOM READY DETECTION
867
+ logDebug(`🎯 DOM ready state: ${document.readyState}`);
868
+ if (document.readyState === 'complete') {
869
+ // DOM already ready, start immediately
870
+ logDebug('🎯 DOM already complete, starting recording immediately');
871
+ startRecording();
872
+ } else {
873
+ // Wait for DOM to be ready
874
+ logDebug('🎯 DOM not ready, waiting for DOMContentLoaded event');
875
+ document.addEventListener('DOMContentLoaded', () => {
876
+ logDebug('🎯 DOMContentLoaded fired, starting recording');
877
+ startRecording();
878
+ }, { once: true });
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Manually trigger a FullSnapshot (for navigation events)
884
+ * Delays snapshot to avoid capturing mid-animation states
885
+ */
886
+ private takeFullSnapshot(): void {
887
+ // Clear any existing timeout to avoid multiple snapshots
888
+ if (this.fullSnapshotTimeout) {
889
+ clearTimeout(this.fullSnapshotTimeout);
890
+ }
891
+
892
+ // Delay FullSnapshot to let animations settle
893
+ this.fullSnapshotTimeout = window.setTimeout(() => {
894
+ try {
895
+ // Wait for any pending animations/transitions to complete
896
+ requestAnimationFrame(() => {
897
+ requestAnimationFrame(() => {
898
+ // Access takeFullSnapshot from the rrweb record function
899
+ if (this.rrwebRecord && typeof this.rrwebRecord.takeFullSnapshot === 'function') {
900
+ this.rrwebRecord.takeFullSnapshot();
901
+ logDebug('✅ FullSnapshot taken for navigation (delayed for animations)');
902
+ } else {
903
+ logWarn('⚠️ takeFullSnapshot not available on record function');
904
+ }
905
+ });
906
+ });
907
+ } catch (error) {
908
+ logError('❌ Failed to take FullSnapshot for navigation:', error);
909
+ }
910
+ }, 1000); // Wait 1 second for animations to settle
911
+ }
912
+
913
+ public async stop() {
914
+ await this.ensureInitialized();
915
+ if (!isBrowser) return;
916
+
917
+ if (this.flushInterval) {
918
+ clearInterval(this.flushInterval);
919
+ this.flushInterval = null;
920
+ }
921
+
922
+ // Stop rrweb recording
923
+ if (this.recordInstance) {
924
+ this.recordInstance();
925
+ this.recordInstance = null;
926
+ }
927
+
928
+ // Clear any pending FullSnapshot timeouts
929
+ if (this.fullSnapshotTimeout) {
930
+ clearTimeout(this.fullSnapshotTimeout);
931
+ this.fullSnapshotTimeout = null;
932
+ }
933
+
934
+ this.rrwebRecord = null;
935
+
936
+ // Disable console tracking
937
+ this.disableConsoleTracking();
938
+
939
+ // Cleanup navigation tracking
940
+ this.cleanupNavigationTracking();
941
+ }
942
+
943
+ /**
944
+ * Add an event to the ingestion queue
945
+ * Events are sent directly without processing to avoid corruption
946
+ */
947
+ public async addEvent(event: any) {
948
+ await this.ensureInitialized();
949
+
950
+ // ✅ DIRECT EVENT HANDLING - No custom processing to avoid corruption
951
+ // Events flow directly from rrweb to ingestion server
952
+
953
+ // ✅ EVENT VALIDATION
954
+ if (!event || typeof event !== 'object') {
955
+ logDebug('⚠️ Skipping invalid event:', event);
956
+ return;
957
+ }
958
+
959
+ // ✅ LOG FULLSNAPSHOT STATUS FOR DEBUGGING
960
+ if (event.type === 2) { // FullSnapshot
961
+ const hasData = !!event.data;
962
+ const hasNode = !!(event.data && event.data.node);
963
+
964
+ if (!hasData || !hasNode) {
965
+ logDebug(`⚠️ Empty FullSnapshot detected: hasData=${hasData}, hasNode=${hasNode} - continuing session`);
966
+ } else {
967
+ logDebug(`✅ Valid FullSnapshot: hasData=${hasData}, hasNode=${hasNode}, dataType=${event.data?.node?.type}`);
968
+ }
969
+ }
970
+
971
+ this.eventIngestionQueue.push(event); // Direct event handling
972
+ }
973
+
974
+ /**
975
+ * Flush events to the ingestion server
976
+ * Events are sent in chunks to handle large payloads efficiently
977
+ */
978
+ private async flush() {
979
+ // Prevent concurrent flushes
980
+ if (this.isProcessing || !this.initialized) {
981
+ return;
982
+ }
983
+
984
+ this.isProcessing = true;
985
+ try {
986
+ // Swap the current queue with an empty one atomically
987
+ const eventsToProcess = this.eventIngestionQueue;
988
+ this.eventIngestionQueue = [];
989
+
990
+ if (eventsToProcess.length > 0) {
991
+ logDebug('Flushing events:', eventsToProcess);
992
+
993
+ // ✅ LOG FULLSNAPSHOT STATUS FOR MONITORING
994
+ const fullSnapshots = eventsToProcess.filter(e => e.type === 2);
995
+ if (fullSnapshots.length > 0) {
996
+ logDebug(`[FIXED] Sending ${fullSnapshots.length} FullSnapshot(s) with valid data`);
997
+ }
998
+
999
+ try {
1000
+ // Use chunked sending to handle large payloads
1001
+ await this.api.sendEventsChunked(eventsToProcess, this.sessionId, this.endUserId!);
1002
+ } catch (error: any) {
1003
+ // Handle specific error types with graceful degradation
1004
+ if (error.message?.includes('ERROR: Session already completed')) {
1005
+ logWarn('Session expired, events will be lost');
1006
+ } else if (error.message?.includes('413') || error.message?.includes('Content Too Large')) {
1007
+ logWarn('Payload too large, events will be lost');
1008
+ } else if (error.message?.includes('ERR_BLOCKED_BY_CLIENT') ||
1009
+ error.message?.includes('Failed to fetch') ||
1010
+ error.message?.includes('NetworkError')) {
1011
+ logWarn('Request blocked by ad blocker or network issue, events will be lost');
1012
+ } else {
1013
+ throw error;
1014
+ }
1015
+ }
1016
+ }
1017
+ } finally {
1018
+ this.isProcessing = false;
1019
+ }
1020
+ }
1021
+
1022
+ // Add helper methods for cookie management with localStorage fallback
1023
+ private setCookie(name: string, value: string, daysToExpire: number) {
1024
+ if (!isBrowser) return;
1025
+
1026
+ try {
1027
+ // Try to set cookie first
1028
+ const date = new Date();
1029
+ date.setTime(date.getTime() + (daysToExpire * 24 * 60 * 60 * 1000));
1030
+ const expires = `expires=${date.toUTCString()}`;
1031
+ document.cookie = `${name}=${value};${expires};path=/;SameSite=Lax`;
1032
+
1033
+ // Also store in localStorage as backup
1034
+ localStorage.setItem(name, value);
1035
+ logDebug(`Set cookie and localStorage: ${name}`);
1036
+ } catch (error) {
1037
+ // If cookie fails, use localStorage only
1038
+ try {
1039
+ localStorage.setItem(name, value);
1040
+ logDebug(`Cookie blocked, using localStorage: ${name}`);
1041
+ } catch (localStorageError) {
1042
+ logError('Failed to store user ID in both cookie and localStorage:', localStorageError);
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ public getCookie(name: string): string | null {
1048
+ if (!isBrowser) return null;
1049
+
1050
+ try {
1051
+ // Try to get from cookie first
1052
+ const nameEQ = name + "=";
1053
+ const ca = document.cookie.split(';');
1054
+ for (let i = 0; i < ca.length; i++) {
1055
+ let c = ca[i];
1056
+ while (c.charAt(0) === ' ') c = c.substring(1, c.length);
1057
+ if (c.indexOf(nameEQ) === 0) {
1058
+ const cookieValue = c.substring(nameEQ.length, c.length);
1059
+ logDebug(`Found cookie: ${name}`);
1060
+ return cookieValue;
1061
+ }
1062
+ }
1063
+
1064
+ // If cookie not found, try localStorage
1065
+ const localStorageValue = localStorage.getItem(name);
1066
+ if (localStorageValue) {
1067
+ logDebug(`Cookie not found, using localStorage: ${name}`);
1068
+ return localStorageValue;
1069
+ }
1070
+
1071
+ return null;
1072
+ } catch (error) {
1073
+ // If cookie access fails, try localStorage
1074
+ try {
1075
+ const localStorageValue = localStorage.getItem(name);
1076
+ if (localStorageValue) {
1077
+ logDebug(`Cookie access failed, using localStorage: ${name}`);
1078
+ return localStorageValue;
1079
+ }
1080
+ } catch (localStorageError) {
1081
+ logError('Failed to access both cookie and localStorage:', localStorageError);
1082
+ }
1083
+ return null;
1084
+ }
1085
+ }
1086
+
1087
+ /**
1088
+ * Delete a cookie by setting its expiration date to the past
1089
+ * @param name The name of the cookie to delete
1090
+ */
1091
+ private deleteCookie(name: string) {
1092
+ if (!isBrowser) return;
1093
+
1094
+ try {
1095
+ // Delete cookie by setting expiration to past
1096
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Lax`;
1097
+ logDebug(`Deleted cookie: ${name}`);
1098
+ } catch (error) {
1099
+ logError(`Failed to delete cookie: ${name}`, error);
1100
+ }
1101
+
1102
+ // Also remove from localStorage
1103
+ try {
1104
+ localStorage.removeItem(name);
1105
+ logDebug(`Removed from localStorage: ${name}`);
1106
+ } catch (error) {
1107
+ logError(`Failed to remove from localStorage: ${name}`, error);
1108
+ }
1109
+ }
1110
+
1111
+ /**
1112
+ * Clear user data and reset session when user signs out of the site
1113
+ * This should be called when a user logs out of your application to prevent
1114
+ * data contamination between different users
1115
+ */
1116
+ public logout(): void {
1117
+ if (!isBrowser) return;
1118
+
1119
+ try {
1120
+ // Clear user ID cookie and localStorage
1121
+ const userIdCookieName = `human_behavior_end_user_id_${this.apiKey}`;
1122
+ this.deleteCookie(userIdCookieName);
1123
+
1124
+ // Clear session data from localStorage
1125
+ localStorage.removeItem(`human_behavior_session_id_${this.apiKey}`);
1126
+ localStorage.removeItem(`human_behavior_last_activity_${this.apiKey}`);
1127
+
1128
+ // Reset user-related properties
1129
+ this.endUserId = null;
1130
+ this.userProperties = {};
1131
+
1132
+ // Generate a new session ID for the next user
1133
+ this.sessionId = uuidv1();
1134
+ if (isBrowser) {
1135
+ localStorage.setItem(`human_behavior_session_id_${this.apiKey}`, this.sessionId);
1136
+ localStorage.setItem(`human_behavior_last_activity_${this.apiKey}`, Date.now().toString());
1137
+ }
1138
+
1139
+ logInfo('User logged out - cleared all user data and started fresh session');
1140
+ } catch (error) {
1141
+ logError('Error during logout:', error);
1142
+ }
1143
+ }
1144
+
1145
+ /**
1146
+ * Start redaction functionality for sensitive input fields
1147
+ * @param options Optional configuration for redaction behavior
1148
+ */
1149
+ public async redact(options?: RedactionOptions): Promise<void> {
1150
+ await this.ensureInitialized();
1151
+ if (!isBrowser) {
1152
+ logWarn('Redaction is only available in browser environments');
1153
+ return;
1154
+ }
1155
+
1156
+ // Create a new redaction manager with the provided options
1157
+ this.redactionManager = new RedactionManager(options);
1158
+ }
1159
+
1160
+ /**
1161
+ * Set specific fields to be redacted during session recording
1162
+ * Uses rrweb's built-in masking instead of custom redaction processing
1163
+ * @param fields Array of CSS selectors for fields to redact (e.g., ['input[type="password"]', '#email-field'])
1164
+ */
1165
+ public setRedactedFields(fields: string[]): void {
1166
+ this.redactionManager.setFieldsToRedact(fields);
1167
+
1168
+ // ✅ APPLY RRWEB MASKING CLASSES - More reliable than custom processing
1169
+ this.redactionManager.applyRedactionClasses();
1170
+
1171
+ // ✅ RESTART RECORDING WITH NEW SETTINGS - Ensures masking is applied
1172
+ if (this.recordInstance) {
1173
+ this.restartWithNewRedaction();
1174
+ }
1175
+ }
1176
+
1177
+ private restartWithNewRedaction(): void {
1178
+ if (this.recordInstance) {
1179
+ this.recordInstance(); // Stop current recording
1180
+ this.start(); // Restart with new redaction settings
1181
+ }
1182
+ }
1183
+
1184
+ /**
1185
+ * Check if redaction is currently active
1186
+ */
1187
+ public isRedactionActive(): boolean {
1188
+ return this.redactionManager.isActive();
1189
+ }
1190
+
1191
+ /**
1192
+ * Get the currently selected fields for redaction
1193
+ */
1194
+ public getRedactedFields(): string[] {
1195
+ return this.redactionManager.getSelectedFields();
1196
+ }
1197
+
1198
+ /**
1199
+ * Get the current session ID
1200
+ */
1201
+ public getSessionId(): string {
1202
+ return this.sessionId;
1203
+ }
1204
+
1205
+ /**
1206
+ * Get the current URL being tracked
1207
+ */
1208
+ public getCurrentUrl(): string {
1209
+ return this.currentUrl;
1210
+ }
1211
+
1212
+ /**
1213
+ * Get current snapshot frequency info
1214
+ * Uses configured values (5 minutes, 1000 events)
1215
+ */
1216
+ public getSnapshotFrequencyInfo(): {
1217
+ sessionDuration: number;
1218
+ currentInterval: number;
1219
+ currentThreshold: number;
1220
+ phase: string;
1221
+ } {
1222
+ const sessionDuration = Date.now() - this.sessionStartTime;
1223
+
1224
+ return {
1225
+ sessionDuration,
1226
+ currentInterval: 300000, // Configured - 5 minutes
1227
+ currentThreshold: 1000, // Configured - 1000 events
1228
+ phase: 'configured' // Using explicit configuration
1229
+ };
1230
+ }
1231
+
1232
+ /**
1233
+ * Test if the tracker can reach the ingestion server
1234
+ */
1235
+ public async testConnection(): Promise<{ success: boolean; error?: string }> {
1236
+ try {
1237
+ await this.api.init(this.sessionId, this.endUserId);
1238
+ return { success: true };
1239
+ } catch (error: any) {
1240
+ return {
1241
+ success: false,
1242
+ error: error.message || 'Unknown error'
1243
+ };
1244
+ }
1245
+ }
1246
+
1247
+ /**
1248
+ * Get connection status and recommendations
1249
+ */
1250
+ public getConnectionStatus(): {
1251
+ blocked: boolean;
1252
+ recommendations: string[]
1253
+ } {
1254
+ const recommendations: string[] = [];
1255
+ let blocked = false;
1256
+
1257
+ // Check if we have queued events (might indicate blocking)
1258
+ if (this.eventIngestionQueue.length > 0) {
1259
+ blocked = true;
1260
+ recommendations.push('Some requests may be blocked by ad blockers');
1261
+ }
1262
+
1263
+ // Check if connection was blocked during initialization
1264
+ if (this._connectionBlocked) {
1265
+ blocked = true;
1266
+ recommendations.push('Initial connection test failed - ad blocker may be active');
1267
+ }
1268
+
1269
+ // Check if we're in a browser environment
1270
+ if (typeof window === 'undefined') {
1271
+ recommendations.push('Not running in browser environment');
1272
+ }
1273
+
1274
+ // Check if navigator.sendBeacon is available
1275
+ if (typeof navigator.sendBeacon === 'undefined') {
1276
+ recommendations.push('sendBeacon not available, using fetch fallback');
1277
+ }
1278
+
1279
+ return { blocked, recommendations };
1280
+ }
1281
+
1282
+ /**
1283
+ * Check if the current user is a preexisting user
1284
+ * Returns true if the user has an existing endUserId cookie from a previous session
1285
+ */
1286
+ public isPreexistingUser(): boolean {
1287
+ if (!isBrowser) {
1288
+ return false;
1289
+ }
1290
+
1291
+ // Check if there's an existing endUserId cookie for this API key
1292
+ const existingEndUserId = this.getCookie(`human_behavior_end_user_id_${this.apiKey}`);
1293
+ return existingEndUserId !== null && existingEndUserId !== this.endUserId;
1294
+ }
1295
+
1296
+ /**
1297
+ * Get user information including whether they are preexisting
1298
+ */
1299
+ public getUserInfo(): {
1300
+ endUserId: string | null;
1301
+ sessionId: string;
1302
+ isPreexistingUser: boolean;
1303
+ initialized: boolean;
1304
+ } {
1305
+ return {
1306
+ endUserId: this.endUserId,
1307
+ sessionId: this.sessionId,
1308
+ isPreexistingUser: this.isPreexistingUser(),
1309
+ initialized: this.initialized
1310
+ };
1311
+ }
1312
+ }
1313
+
1314
+ // Only expose to window object in browser environments
1315
+ if (isBrowser) {
1316
+ window.HumanBehaviorTracker = HumanBehaviorTracker;
1317
+ }
1318
+
1319
+ export default HumanBehaviorTracker;