humanbehavior-js 0.4.28 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -0
- package/package.json +116 -78
- package/packages/angular/dist/index.d.ts +46 -0
- package/packages/angular/dist/index.d.ts.map +1 -0
- package/packages/angular/dist/index.js +2 -0
- package/packages/angular/dist/index.js.map +1 -0
- package/packages/angular/dist/index.mjs +2 -0
- package/packages/angular/dist/index.mjs.map +1 -0
- package/packages/browser/dist/index.d.ts +5 -0
- package/packages/browser/dist/index.d.ts.map +1 -0
- package/packages/browser/dist/index.iife.js +12095 -0
- package/packages/browser/dist/index.iife.js.map +1 -0
- package/packages/browser/dist/index.js +2 -0
- package/packages/browser/dist/index.js.map +1 -0
- package/packages/browser/dist/index.min.js +2 -0
- package/packages/browser/dist/index.min.js.map +1 -0
- package/packages/browser/dist/index.mjs +2 -0
- package/packages/browser/dist/index.mjs.map +1 -0
- package/packages/react/dist/browser.d.ts +2 -0
- package/packages/react/dist/browser.d.ts.map +1 -0
- package/packages/react/dist/index.d.ts +48 -0
- package/packages/react/dist/index.d.ts.map +1 -0
- package/packages/react/dist/index.js +2 -0
- package/packages/react/dist/index.js.map +1 -0
- package/packages/react/dist/index.mjs +2 -0
- package/packages/react/dist/index.mjs.map +1 -0
- package/packages/remix/dist/index.d.ts +8 -0
- package/packages/remix/dist/index.d.ts.map +1 -0
- package/packages/remix/dist/index.js +2 -0
- package/packages/remix/dist/index.js.map +1 -0
- package/packages/remix/dist/index.mjs +2 -0
- package/packages/remix/dist/index.mjs.map +1 -0
- package/packages/svelte/dist/index.d.ts +11 -0
- package/packages/svelte/dist/index.d.ts.map +1 -0
- package/packages/svelte/dist/index.js +2 -0
- package/packages/svelte/dist/index.js.map +1 -0
- package/packages/svelte/dist/index.mjs +2 -0
- package/packages/svelte/dist/index.mjs.map +1 -0
- package/{dist/types/vue → packages/vue/dist}/index.d.ts +4 -5
- package/packages/vue/dist/index.d.ts.map +1 -0
- package/packages/vue/dist/index.js +2 -0
- package/packages/vue/dist/index.js.map +1 -0
- package/packages/vue/dist/index.mjs +2 -0
- package/packages/vue/dist/index.mjs.map +1 -0
- package/packages/wizard/dist/ai/ai-install-wizard.d.ts +145 -0
- package/packages/wizard/dist/ai/ai-install-wizard.d.ts.map +1 -0
- package/packages/wizard/dist/ai/manual-framework-wizard.d.ts +52 -0
- package/packages/wizard/dist/ai/manual-framework-wizard.d.ts.map +1 -0
- package/packages/wizard/dist/cli/ai-auto-install.d.ts +27 -0
- package/packages/wizard/dist/cli/ai-auto-install.d.ts.map +1 -0
- package/{dist → packages/wizard/dist}/cli/ai-auto-install.js +821 -905
- package/packages/wizard/dist/cli/ai-auto-install.js.map +1 -0
- package/packages/wizard/dist/cli/auto-install.d.ts +26 -0
- package/packages/wizard/dist/cli/auto-install.d.ts.map +1 -0
- package/{dist → packages/wizard/dist}/cli/auto-install.js +821 -905
- package/packages/wizard/dist/cli/auto-install.js.map +1 -0
- package/{dist/types → packages/wizard/dist/core}/install-wizard.d.ts +6 -8
- package/packages/wizard/dist/core/install-wizard.d.ts.map +1 -0
- package/packages/wizard/dist/index.d.ts +18 -0
- package/packages/wizard/dist/index.d.ts.map +1 -0
- package/packages/wizard/dist/index.js +2 -0
- package/packages/wizard/dist/index.js.map +1 -0
- package/packages/wizard/dist/index.mjs +2 -0
- package/packages/wizard/dist/index.mjs.map +1 -0
- package/packages/wizard/dist/services/centralized-ai-service.d.ts +159 -0
- package/packages/wizard/dist/services/centralized-ai-service.d.ts.map +1 -0
- package/packages/wizard/dist/services/remote-ai-service.d.ts +58 -0
- package/packages/wizard/dist/services/remote-ai-service.d.ts.map +1 -0
- package/WIZARD_USAGE_GUIDE.md +0 -381
- package/dist/cjs/angular/index.cjs +0 -14979
- package/dist/cjs/angular/index.cjs.map +0 -1
- package/dist/cjs/index.cjs +0 -14964
- package/dist/cjs/index.cjs.map +0 -1
- package/dist/cjs/install-wizard.cjs +0 -1576
- package/dist/cjs/install-wizard.cjs.map +0 -1
- package/dist/cjs/react/index.cjs +0 -15103
- package/dist/cjs/react/index.cjs.map +0 -1
- package/dist/cjs/remix/index.cjs +0 -15077
- package/dist/cjs/remix/index.cjs.map +0 -1
- package/dist/cjs/svelte/index.cjs +0 -14933
- package/dist/cjs/svelte/index.cjs.map +0 -1
- package/dist/cjs/vue/index.cjs +0 -14942
- package/dist/cjs/vue/index.cjs.map +0 -1
- package/dist/cjs/wizard/index.cjs +0 -3490
- package/dist/cjs/wizard/index.cjs.map +0 -1
- package/dist/cli/ai-auto-install.js.map +0 -1
- package/dist/cli/auto-install.js.map +0 -1
- package/dist/esm/angular/index.js +0 -14975
- package/dist/esm/angular/index.js.map +0 -1
- package/dist/esm/index.js +0 -14941
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/install-wizard.js +0 -1553
- package/dist/esm/install-wizard.js.map +0 -1
- package/dist/esm/react/index.js +0 -15097
- package/dist/esm/react/index.js.map +0 -1
- package/dist/esm/remix/index.js +0 -15073
- package/dist/esm/remix/index.js.map +0 -1
- package/dist/esm/svelte/index.js +0 -14931
- package/dist/esm/svelte/index.js.map +0 -1
- package/dist/esm/vue/index.js +0 -14940
- package/dist/esm/vue/index.js.map +0 -1
- package/dist/esm/wizard/index.js +0 -3459
- package/dist/esm/wizard/index.js.map +0 -1
- package/dist/index.min.js +0 -2
- package/dist/index.min.js.map +0 -1
- package/dist/types/angular/index.d.ts +0 -357
- package/dist/types/index.d.ts +0 -644
- package/dist/types/react/index.d.ts +0 -345
- package/dist/types/remix/index.d.ts +0 -336
- package/dist/types/svelte/index.d.ts +0 -322
- package/dist/types/wizard/index.d.ts +0 -523
- package/readme.md +0 -335
- package/rollup.config.js +0 -422
- package/simple-spa.html +0 -1000
- package/src/angular/index.ts +0 -79
- package/src/api.ts +0 -416
- package/src/index.ts +0 -35
- package/src/react/AutoInstallWizard.tsx +0 -557
- package/src/react/browser.ts +0 -8
- package/src/react/index.tsx +0 -308
- package/src/redact.ts +0 -327
- package/src/remix/index.ts +0 -16
- package/src/svelte/index.ts +0 -14
- package/src/tracker.ts +0 -1587
- package/src/types/clack.d.ts +0 -31
- package/src/utils/ip-detector.ts +0 -158
- package/src/utils/logger.ts +0 -144
- package/src/utils/property-detector.ts +0 -345
- package/src/utils/property-manager.ts +0 -274
- package/src/vue/index.ts +0 -29
- package/src/wizard/README.md +0 -114
- package/src/wizard/ai/ai-install-wizard.ts +0 -897
- package/src/wizard/ai/manual-framework-wizard.ts +0 -238
- package/src/wizard/cli/ai-auto-install.ts +0 -241
- package/src/wizard/cli/auto-install.ts +0 -224
- package/src/wizard/core/install-wizard.ts +0 -1794
- package/src/wizard/index.ts +0 -23
- package/src/wizard/services/centralized-ai-service.ts +0 -668
- package/src/wizard/services/remote-ai-service.ts +0 -240
- 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;
|