humanbehavior-js 0.4.28 → 0.5.0

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