humanbehavior-js 0.0.9 → 0.1.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/dist/cjs/index.js +441 -201
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/react/index.js +51 -77
- package/dist/cjs/react/index.js.map +1 -1
- package/dist/esm/index.js +441 -201
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/react/index.js +48 -77
- package/dist/esm/react/index.js.map +1 -1
- package/dist/index.min.js +2 -2
- package/dist/index.min.js.map +1 -1
- package/dist/types/index.d.ts +75 -13
- package/dist/types/react/index.d.ts +8 -9
- package/package.json +2 -1
- package/readme.md +127 -105
- package/simple-spa.html +594 -0
- package/src/api.ts +2 -142
- package/src/react/index.tsx +61 -28
- package/src/tracker.ts +507 -102
package/src/tracker.ts
CHANGED
|
@@ -11,6 +11,7 @@ const isBrowser = typeof window !== 'undefined';
|
|
|
11
11
|
declare global {
|
|
12
12
|
interface Window {
|
|
13
13
|
HumanBehaviorTracker: typeof HumanBehaviorTracker;
|
|
14
|
+
__humanBehaviorGlobalTracker?: HumanBehaviorTracker;
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -20,17 +21,17 @@ export class HumanBehaviorTracker {
|
|
|
20
21
|
private rejectedEvents: any[] = [];
|
|
21
22
|
private isProcessingRejectedEvents: boolean = false;
|
|
22
23
|
|
|
23
|
-
private sessionId
|
|
24
|
+
private sessionId!: string;
|
|
24
25
|
private userProperties: Record<string, any> = {};
|
|
25
26
|
private isProcessing: boolean = false;
|
|
26
27
|
private flushInterval: number | null = null;
|
|
27
28
|
private readonly FLUSH_INTERVAL_MS = 5000; // Flush every 5 seconds
|
|
28
|
-
private api
|
|
29
|
+
private api!: HumanBehaviorAPI;
|
|
29
30
|
private endUserId: string | null = null;
|
|
30
|
-
private apiKey
|
|
31
|
+
private apiKey!: string;
|
|
31
32
|
private initialized: boolean = false;
|
|
32
33
|
public initializationPromise: Promise<void> | null = null;
|
|
33
|
-
private redactionManager
|
|
34
|
+
private redactionManager!: RedactionManager;
|
|
34
35
|
|
|
35
36
|
// Console tracking properties
|
|
36
37
|
private originalConsole: {
|
|
@@ -38,33 +39,69 @@ export class HumanBehaviorTracker {
|
|
|
38
39
|
warn: typeof console.warn;
|
|
39
40
|
error: typeof console.error;
|
|
40
41
|
} | null = null;
|
|
41
|
-
private originalLogger: {
|
|
42
|
-
error: typeof logError;
|
|
43
|
-
warn: typeof logWarn;
|
|
44
|
-
info: typeof logInfo;
|
|
45
|
-
debug: typeof logDebug;
|
|
46
|
-
} | null = null;
|
|
47
42
|
private consoleTrackingEnabled: boolean = false;
|
|
48
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
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Initialize the HumanBehavior tracker
|
|
55
|
+
* This is the main entry point - call this once per page
|
|
56
|
+
*/
|
|
57
|
+
public static init(apiKey: string, options?: {
|
|
58
|
+
ingestionUrl?: string;
|
|
59
|
+
logLevel?: 'none' | 'error' | 'warn' | 'info' | 'debug';
|
|
60
|
+
redactFields?: string[];
|
|
61
|
+
}): HumanBehaviorTracker {
|
|
62
|
+
// Return existing instance if already initialized
|
|
63
|
+
if (isBrowser && window.__humanBehaviorGlobalTracker) {
|
|
64
|
+
logDebug('Tracker already initialized, returning existing instance');
|
|
65
|
+
return window.__humanBehaviorGlobalTracker;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Configure logging if specified
|
|
69
|
+
if (options?.logLevel) {
|
|
70
|
+
this.configureLogging({ level: options.logLevel });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create new tracker instance
|
|
74
|
+
const tracker = new HumanBehaviorTracker(apiKey, options?.ingestionUrl);
|
|
75
|
+
|
|
76
|
+
// Set redacted fields if specified
|
|
77
|
+
if (options?.redactFields) {
|
|
78
|
+
tracker.setRedactedFields(options.redactFields);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Test connection (non-blocking)
|
|
82
|
+
if (isBrowser) {
|
|
83
|
+
const testUrl = tracker.api['baseUrl'] + '/api/health';
|
|
84
|
+
fetch(testUrl, { method: 'HEAD' })
|
|
85
|
+
.then(() => logDebug('Connection test successful'))
|
|
86
|
+
.catch((error) => {
|
|
87
|
+
logWarn('Connection test failed - ad blocker may be active:', error.message);
|
|
88
|
+
tracker._connectionBlocked = true;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Start tracking
|
|
93
|
+
tracker.start();
|
|
94
|
+
|
|
95
|
+
return tracker;
|
|
96
|
+
}
|
|
97
|
+
|
|
49
98
|
constructor(apiKey: string | undefined, ingestionUrl?: string) {
|
|
50
99
|
if (!apiKey) {
|
|
51
100
|
throw new Error('Human Behavior API Key is required');
|
|
52
101
|
}
|
|
53
102
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
// ========================================
|
|
57
|
-
// Uncomment ONE of the following lines to select your server:
|
|
58
|
-
|
|
59
|
-
// AWS Development Server
|
|
60
|
-
const defaultIngestionUrl = 'http://3.137.217.33:3000';
|
|
61
|
-
|
|
62
|
-
// Vercel Production Server
|
|
63
|
-
// const defaultIngestionUrl = 'https://ingestion-server.vercel.app';
|
|
64
|
-
|
|
65
|
-
// Local Development Server
|
|
66
|
-
// const defaultIngestionUrl = 'http://localhost:3000';
|
|
67
|
-
|
|
103
|
+
// Initialize API
|
|
104
|
+
const defaultIngestionUrl = 'http://3.137.217.33:3000'; // AWS Development Server
|
|
68
105
|
this.api = new HumanBehaviorAPI({
|
|
69
106
|
apiKey: apiKey,
|
|
70
107
|
ingestionUrl: ingestionUrl || defaultIngestionUrl
|
|
@@ -72,43 +109,72 @@ export class HumanBehaviorTracker {
|
|
|
72
109
|
this.apiKey = apiKey;
|
|
73
110
|
this.redactionManager = new RedactionManager();
|
|
74
111
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
112
|
+
// Handle session restoration with improved continuity
|
|
113
|
+
if (isBrowser) {
|
|
114
|
+
const existingSessionId = localStorage.getItem('human_behavior_session_id');
|
|
115
|
+
const lastActivity = localStorage.getItem('human_behavior_last_activity');
|
|
116
|
+
const thirtyMinutesAgo = Date.now() - (30 * 60 * 1000);
|
|
117
|
+
|
|
118
|
+
// Check if we have an existing session that's still within the activity window
|
|
119
|
+
if (existingSessionId && lastActivity && parseInt(lastActivity) > thirtyMinutesAgo) {
|
|
120
|
+
this.sessionId = existingSessionId;
|
|
121
|
+
logDebug(`Reusing existing session: ${this.sessionId}`);
|
|
122
|
+
// Update activity timestamp to extend the session window
|
|
123
|
+
localStorage.setItem('human_behavior_last_activity', Date.now().toString());
|
|
124
|
+
} else {
|
|
125
|
+
// Clear old session data if it's expired
|
|
126
|
+
if (existingSessionId) {
|
|
127
|
+
logDebug(`Session expired, clearing old session: ${existingSessionId}`);
|
|
128
|
+
localStorage.removeItem('human_behavior_session_id');
|
|
129
|
+
localStorage.removeItem('human_behavior_last_activity');
|
|
130
|
+
}
|
|
131
|
+
this.sessionId = uuidv1();
|
|
132
|
+
logDebug(`Creating new session: ${this.sessionId}`);
|
|
133
|
+
localStorage.setItem('human_behavior_session_id', this.sessionId);
|
|
134
|
+
localStorage.setItem('human_behavior_last_activity', Date.now().toString());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.currentUrl = window.location.href;
|
|
138
|
+
window.__humanBehaviorGlobalTracker = this;
|
|
139
|
+
} else {
|
|
140
|
+
this.sessionId = uuidv1();
|
|
87
141
|
}
|
|
88
142
|
|
|
89
|
-
// Start initialization
|
|
143
|
+
// Start initialization
|
|
90
144
|
this.initializationPromise = this.init();
|
|
91
145
|
}
|
|
92
146
|
|
|
93
147
|
private async init(): Promise<void> {
|
|
94
148
|
try {
|
|
95
149
|
const userId = this.getCookie(`human_behavior_end_user_id_${this.apiKey}`);
|
|
150
|
+
logDebug(`Initializing with sessionId: ${this.sessionId}, userId: ${userId}`);
|
|
151
|
+
|
|
96
152
|
const { sessionId, endUserId } = await this.api.init(this.sessionId, userId);
|
|
97
|
-
|
|
153
|
+
|
|
154
|
+
// Check if server returned a different session ID (for session continuity)
|
|
155
|
+
if (sessionId !== this.sessionId) {
|
|
156
|
+
logDebug(`Server returned different sessionId: ${sessionId} (client had: ${this.sessionId})`);
|
|
157
|
+
this.sessionId = sessionId;
|
|
158
|
+
// Update localStorage with server's session ID for continuity
|
|
159
|
+
if (isBrowser) {
|
|
160
|
+
localStorage.setItem('human_behavior_session_id', this.sessionId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
98
164
|
this.endUserId = endUserId;
|
|
99
165
|
this.setCookie(`human_behavior_end_user_id_${this.apiKey}`, endUserId, 365);
|
|
100
166
|
|
|
101
167
|
// Only setup browser-specific handlers when in browser environment
|
|
102
168
|
if (isBrowser) {
|
|
103
169
|
this.setupPageUnloadHandler();
|
|
104
|
-
this.
|
|
170
|
+
this.setupNavigationTracking();
|
|
105
171
|
this.processRejectedEvents();
|
|
106
172
|
} else {
|
|
107
173
|
logWarn('HumanBehaviorTracker initialized in a non-browser environment. Session tracking is disabled.');
|
|
108
174
|
}
|
|
109
175
|
|
|
110
176
|
this.initialized = true;
|
|
111
|
-
logInfo(
|
|
177
|
+
logInfo(`HumanBehaviorTracker initialized with sessionId: ${this.sessionId}, endUserId: ${endUserId}`);
|
|
112
178
|
} catch (error) {
|
|
113
179
|
logError('Failed to initialize HumanBehaviorTracker:', error);
|
|
114
180
|
throw error;
|
|
@@ -122,6 +188,196 @@ export class HumanBehaviorTracker {
|
|
|
122
188
|
await this.initializationPromise;
|
|
123
189
|
}
|
|
124
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Setup navigation event tracking for SPA navigation
|
|
193
|
+
*/
|
|
194
|
+
private setupNavigationTracking(): void {
|
|
195
|
+
if (!isBrowser || this.navigationTrackingEnabled) return;
|
|
196
|
+
|
|
197
|
+
this.navigationTrackingEnabled = true;
|
|
198
|
+
logDebug('Setting up navigation tracking');
|
|
199
|
+
|
|
200
|
+
// Store original history methods
|
|
201
|
+
this.originalPushState = history.pushState;
|
|
202
|
+
this.originalReplaceState = history.replaceState;
|
|
203
|
+
|
|
204
|
+
// Override pushState to capture programmatic navigation
|
|
205
|
+
history.pushState = (...args) => {
|
|
206
|
+
this.previousUrl = this.currentUrl;
|
|
207
|
+
this.currentUrl = window.location.href;
|
|
208
|
+
|
|
209
|
+
// Call original method
|
|
210
|
+
this.originalPushState!.apply(history, args);
|
|
211
|
+
|
|
212
|
+
// Track navigation event
|
|
213
|
+
this.trackNavigationEvent('pushState', this.previousUrl, this.currentUrl);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Override replaceState to capture programmatic navigation
|
|
217
|
+
history.replaceState = (...args) => {
|
|
218
|
+
this.previousUrl = this.currentUrl;
|
|
219
|
+
this.currentUrl = window.location.href;
|
|
220
|
+
|
|
221
|
+
// Call original method
|
|
222
|
+
this.originalReplaceState!.apply(history, args);
|
|
223
|
+
|
|
224
|
+
// Track navigation event
|
|
225
|
+
this.trackNavigationEvent('replaceState', this.previousUrl, this.currentUrl);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Listen for popstate events (back/forward navigation)
|
|
229
|
+
const popstateListener = () => {
|
|
230
|
+
this.previousUrl = this.currentUrl;
|
|
231
|
+
this.currentUrl = window.location.href;
|
|
232
|
+
this.trackNavigationEvent('popstate', this.previousUrl, this.currentUrl);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
window.addEventListener('popstate', popstateListener);
|
|
236
|
+
this.navigationListeners.push(() => {
|
|
237
|
+
window.removeEventListener('popstate', popstateListener);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Listen for hashchange events
|
|
241
|
+
const hashchangeListener = () => {
|
|
242
|
+
this.previousUrl = this.currentUrl;
|
|
243
|
+
this.currentUrl = window.location.href;
|
|
244
|
+
this.trackNavigationEvent('hashchange', this.previousUrl, this.currentUrl);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
window.addEventListener('hashchange', hashchangeListener);
|
|
248
|
+
this.navigationListeners.push(() => {
|
|
249
|
+
window.removeEventListener('hashchange', hashchangeListener);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Track initial page load
|
|
253
|
+
this.trackNavigationEvent('pageLoad', '', this.currentUrl);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Track navigation events and send custom events
|
|
258
|
+
*/
|
|
259
|
+
public async trackNavigationEvent(type: string, fromUrl: string, toUrl: string): Promise<void> {
|
|
260
|
+
if (!this.initialized) return;
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const navigationData = {
|
|
264
|
+
type: type,
|
|
265
|
+
from: fromUrl,
|
|
266
|
+
to: toUrl,
|
|
267
|
+
timestamp: new Date().toISOString(),
|
|
268
|
+
pathname: window.location.pathname,
|
|
269
|
+
search: window.location.search,
|
|
270
|
+
hash: window.location.hash,
|
|
271
|
+
referrer: document.referrer
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Add navigation event to the main event stream
|
|
275
|
+
await this.addEvent({
|
|
276
|
+
type: 5, // Custom event type
|
|
277
|
+
data: {
|
|
278
|
+
payload: {
|
|
279
|
+
eventType: 'navigation',
|
|
280
|
+
...navigationData
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
timestamp: Date.now()
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
logDebug(`Navigation tracked: ${type} from ${fromUrl} to ${toUrl}`);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
logError('Failed to track navigation event:', error);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Track a page view event (PostHog-style)
|
|
294
|
+
*/
|
|
295
|
+
public async trackPageView(url?: string): Promise<void> {
|
|
296
|
+
if (!this.initialized) return;
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const pageViewData = {
|
|
300
|
+
url: url || window.location.href,
|
|
301
|
+
pathname: window.location.pathname,
|
|
302
|
+
search: window.location.search,
|
|
303
|
+
hash: window.location.hash,
|
|
304
|
+
referrer: document.referrer,
|
|
305
|
+
timestamp: new Date().toISOString()
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Add pageview event to the main event stream
|
|
309
|
+
await this.addEvent({
|
|
310
|
+
type: 5, // Custom event type
|
|
311
|
+
data: {
|
|
312
|
+
payload: {
|
|
313
|
+
eventType: 'pageview',
|
|
314
|
+
...pageViewData
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
timestamp: Date.now()
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
logDebug(`Pageview tracked: ${pageViewData.url}`);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
logError('Failed to track pageview event:', error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Track a custom event (PostHog-style)
|
|
328
|
+
*/
|
|
329
|
+
public async customEvent(eventName: string, properties?: Record<string, any>): Promise<void> {
|
|
330
|
+
if (!this.initialized) return;
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const customEventData = {
|
|
334
|
+
eventName: eventName,
|
|
335
|
+
properties: properties || {},
|
|
336
|
+
timestamp: new Date().toISOString(),
|
|
337
|
+
url: window.location.href,
|
|
338
|
+
pathname: window.location.pathname
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Add custom event to the main event stream
|
|
342
|
+
await this.addEvent({
|
|
343
|
+
type: 5, // Custom event type
|
|
344
|
+
data: {
|
|
345
|
+
payload: {
|
|
346
|
+
eventType: 'custom',
|
|
347
|
+
...customEventData
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
timestamp: Date.now()
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
logDebug(`Custom event tracked: ${eventName}`, properties);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
logError('Failed to track custom event:', error);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Cleanup navigation tracking
|
|
361
|
+
*/
|
|
362
|
+
private cleanupNavigationTracking(): void {
|
|
363
|
+
if (!this.navigationTrackingEnabled) return;
|
|
364
|
+
|
|
365
|
+
// Restore original history methods
|
|
366
|
+
if (this.originalPushState) {
|
|
367
|
+
history.pushState = this.originalPushState;
|
|
368
|
+
}
|
|
369
|
+
if (this.originalReplaceState) {
|
|
370
|
+
history.replaceState = this.originalReplaceState;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Remove event listeners
|
|
374
|
+
this.navigationListeners.forEach(cleanup => cleanup());
|
|
375
|
+
this.navigationListeners = [];
|
|
376
|
+
|
|
377
|
+
this.navigationTrackingEnabled = false;
|
|
378
|
+
logDebug('Navigation tracking cleaned up');
|
|
379
|
+
}
|
|
380
|
+
|
|
125
381
|
public static logToStorage(message: string) {
|
|
126
382
|
logInfo(message);
|
|
127
383
|
}
|
|
@@ -159,14 +415,6 @@ export class HumanBehaviorTracker {
|
|
|
159
415
|
error: console.error
|
|
160
416
|
};
|
|
161
417
|
|
|
162
|
-
// Store original logger methods
|
|
163
|
-
this.originalLogger = {
|
|
164
|
-
error: logError,
|
|
165
|
-
warn: logWarn,
|
|
166
|
-
info: logInfo,
|
|
167
|
-
debug: logDebug
|
|
168
|
-
};
|
|
169
|
-
|
|
170
418
|
// Override console methods to capture ALL console output (including logger output)
|
|
171
419
|
console.log = (...args) => {
|
|
172
420
|
this.trackConsoleEvent('log', args);
|
|
@@ -184,50 +432,55 @@ export class HumanBehaviorTracker {
|
|
|
184
432
|
};
|
|
185
433
|
|
|
186
434
|
this.consoleTrackingEnabled = true;
|
|
187
|
-
|
|
435
|
+
logDebug('Console tracking enabled');
|
|
188
436
|
}
|
|
189
437
|
|
|
190
438
|
/**
|
|
191
439
|
* Disable console event tracking
|
|
192
440
|
*/
|
|
193
441
|
public disableConsoleTracking(): void {
|
|
194
|
-
if (!isBrowser || !this.consoleTrackingEnabled
|
|
442
|
+
if (!isBrowser || !this.consoleTrackingEnabled) return;
|
|
195
443
|
|
|
196
444
|
// Restore original console methods
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
445
|
+
if (this.originalConsole) {
|
|
446
|
+
console.log = this.originalConsole.log;
|
|
447
|
+
console.warn = this.originalConsole.warn;
|
|
448
|
+
console.error = this.originalConsole.error;
|
|
449
|
+
}
|
|
200
450
|
|
|
201
451
|
this.consoleTrackingEnabled = false;
|
|
202
|
-
|
|
203
|
-
this.originalLogger = null;
|
|
452
|
+
logDebug('Console tracking disabled');
|
|
204
453
|
}
|
|
205
454
|
|
|
206
|
-
/**
|
|
207
|
-
* Track console events
|
|
208
|
-
*/
|
|
209
455
|
private trackConsoleEvent(level: 'log' | 'warn' | 'error', args: any[]): void {
|
|
210
456
|
if (!this.initialized) return;
|
|
211
457
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
458
|
+
try {
|
|
459
|
+
const consoleData = {
|
|
460
|
+
level: level,
|
|
461
|
+
message: args.map(arg =>
|
|
462
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
463
|
+
).join(' '),
|
|
464
|
+
timestamp: new Date().toISOString(),
|
|
465
|
+
url: window.location.href
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// Add console event to the main event stream
|
|
469
|
+
this.addEvent({
|
|
470
|
+
type: 5, // Custom event type
|
|
471
|
+
data: {
|
|
472
|
+
payload: {
|
|
473
|
+
eventType: 'console',
|
|
474
|
+
...consoleData
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
timestamp: Date.now()
|
|
478
|
+
}).catch(error => {
|
|
479
|
+
logError('Failed to track console event:', error);
|
|
480
|
+
});
|
|
481
|
+
} catch (error) {
|
|
482
|
+
logError('Error in trackConsoleEvent:', error);
|
|
483
|
+
}
|
|
231
484
|
}
|
|
232
485
|
|
|
233
486
|
private setupPageUnloadHandler() {
|
|
@@ -246,17 +499,20 @@ export class HumanBehaviorTracker {
|
|
|
246
499
|
|
|
247
500
|
// Handle actual page unload/close
|
|
248
501
|
window.addEventListener('beforeunload', () => {
|
|
249
|
-
// Update last activity time
|
|
250
|
-
localStorage.setItem('human_behavior_last_activity', Date.now().toString());
|
|
251
|
-
|
|
252
502
|
// Send final events
|
|
253
503
|
this.api.sendBeaconEvents(this.eventIngestionQueue, this.sessionId);
|
|
254
504
|
});
|
|
255
505
|
|
|
256
|
-
// Update activity timestamp
|
|
257
|
-
|
|
506
|
+
// Update activity timestamp on user interaction (not on page load)
|
|
507
|
+
const updateActivity = () => {
|
|
258
508
|
localStorage.setItem('human_behavior_last_activity', Date.now().toString());
|
|
259
|
-
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// Listen for user interactions to update activity timestamp
|
|
512
|
+
window.addEventListener('click', updateActivity);
|
|
513
|
+
window.addEventListener('keydown', updateActivity);
|
|
514
|
+
window.addEventListener('scroll', updateActivity);
|
|
515
|
+
window.addEventListener('mousemove', updateActivity);
|
|
260
516
|
}
|
|
261
517
|
|
|
262
518
|
public viewLogs() {
|
|
@@ -290,12 +546,12 @@ export class HumanBehaviorTracker {
|
|
|
290
546
|
if (!this.userProperties || Object.keys(this.userProperties).length === 0) {
|
|
291
547
|
throw new Error('No user info available. Call addUserInfo() first.');
|
|
292
548
|
}
|
|
293
|
-
await this.api.sendUserAuth(this.endUserId, this.userProperties, this.sessionId, authFields);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
549
|
+
const response = await this.api.sendUserAuth(this.endUserId, this.userProperties, this.sessionId, authFields);
|
|
550
|
+
if (response && response.userId && response.userId !== this.endUserId) {
|
|
551
|
+
// Update endUserId and cookie if backend returns a new userId
|
|
552
|
+
this.endUserId = response.userId;
|
|
553
|
+
this.setCookie(`human_behavior_end_user_id_${this.apiKey}`, response.userId, 365);
|
|
554
|
+
}
|
|
299
555
|
}
|
|
300
556
|
|
|
301
557
|
public async start() {
|
|
@@ -335,6 +591,9 @@ export class HumanBehaviorTracker {
|
|
|
335
591
|
|
|
336
592
|
// Disable console tracking
|
|
337
593
|
this.disableConsoleTracking();
|
|
594
|
+
|
|
595
|
+
// Cleanup navigation tracking
|
|
596
|
+
this.cleanupNavigationTracking();
|
|
338
597
|
}
|
|
339
598
|
|
|
340
599
|
public async addEvent(event: any) {
|
|
@@ -398,6 +657,13 @@ export class HumanBehaviorTracker {
|
|
|
398
657
|
logInfo('Session expired, storing events for retry');
|
|
399
658
|
this.rejectedEvents.push(...eventsToProcess);
|
|
400
659
|
this.processRejectedEvents();
|
|
660
|
+
} else if (error.message?.includes('ERR_BLOCKED_BY_CLIENT') ||
|
|
661
|
+
error.message?.includes('Failed to fetch') ||
|
|
662
|
+
error.message?.includes('NetworkError')) {
|
|
663
|
+
// Handle ad blocker or network issues gracefully
|
|
664
|
+
logWarn('Request blocked by ad blocker or network issue, storing events for retry');
|
|
665
|
+
this.rejectedEvents.push(...eventsToProcess);
|
|
666
|
+
// Don't process rejected events immediately to avoid spam
|
|
401
667
|
} else {
|
|
402
668
|
throw error;
|
|
403
669
|
}
|
|
@@ -408,25 +674,69 @@ export class HumanBehaviorTracker {
|
|
|
408
674
|
}
|
|
409
675
|
}
|
|
410
676
|
|
|
411
|
-
// Add helper methods for cookie management
|
|
677
|
+
// Add helper methods for cookie management with localStorage fallback
|
|
412
678
|
private setCookie(name: string, value: string, daysToExpire: number) {
|
|
413
679
|
if (!isBrowser) return;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
// Try to set cookie first
|
|
683
|
+
const date = new Date();
|
|
684
|
+
date.setTime(date.getTime() + (daysToExpire * 24 * 60 * 60 * 1000));
|
|
685
|
+
const expires = `expires=${date.toUTCString()}`;
|
|
686
|
+
document.cookie = `${name}=${value};${expires};path=/;SameSite=Lax`;
|
|
687
|
+
|
|
688
|
+
// Also store in localStorage as backup
|
|
689
|
+
localStorage.setItem(name, value);
|
|
690
|
+
logDebug(`Set cookie and localStorage: ${name}`);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
// If cookie fails, use localStorage only
|
|
693
|
+
try {
|
|
694
|
+
localStorage.setItem(name, value);
|
|
695
|
+
logDebug(`Cookie blocked, using localStorage: ${name}`);
|
|
696
|
+
} catch (localStorageError) {
|
|
697
|
+
logError('Failed to store user ID in both cookie and localStorage:', localStorageError);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
418
700
|
}
|
|
419
701
|
|
|
420
|
-
|
|
702
|
+
public getCookie(name: string): string | null {
|
|
421
703
|
if (!isBrowser) return null;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
704
|
+
|
|
705
|
+
try {
|
|
706
|
+
// Try to get from cookie first
|
|
707
|
+
const nameEQ = name + "=";
|
|
708
|
+
const ca = document.cookie.split(';');
|
|
709
|
+
for (let i = 0; i < ca.length; i++) {
|
|
710
|
+
let c = ca[i];
|
|
711
|
+
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
|
712
|
+
if (c.indexOf(nameEQ) === 0) {
|
|
713
|
+
const cookieValue = c.substring(nameEQ.length, c.length);
|
|
714
|
+
logDebug(`Found cookie: ${name}`);
|
|
715
|
+
return cookieValue;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// If cookie not found, try localStorage
|
|
720
|
+
const localStorageValue = localStorage.getItem(name);
|
|
721
|
+
if (localStorageValue) {
|
|
722
|
+
logDebug(`Cookie not found, using localStorage: ${name}`);
|
|
723
|
+
return localStorageValue;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return null;
|
|
727
|
+
} catch (error) {
|
|
728
|
+
// If cookie access fails, try localStorage
|
|
729
|
+
try {
|
|
730
|
+
const localStorageValue = localStorage.getItem(name);
|
|
731
|
+
if (localStorageValue) {
|
|
732
|
+
logDebug(`Cookie access failed, using localStorage: ${name}`);
|
|
733
|
+
return localStorageValue;
|
|
734
|
+
}
|
|
735
|
+
} catch (localStorageError) {
|
|
736
|
+
logError('Failed to access both cookie and localStorage:', localStorageError);
|
|
737
|
+
}
|
|
738
|
+
return null;
|
|
428
739
|
}
|
|
429
|
-
return null;
|
|
430
740
|
}
|
|
431
741
|
|
|
432
742
|
/**
|
|
@@ -470,6 +780,101 @@ export class HumanBehaviorTracker {
|
|
|
470
780
|
public getRedactedFields(): string[] {
|
|
471
781
|
return this.redactionManager.getSelectedFields();
|
|
472
782
|
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Get the current session ID
|
|
786
|
+
*/
|
|
787
|
+
public getSessionId(): string {
|
|
788
|
+
return this.sessionId;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Get the current URL being tracked
|
|
793
|
+
*/
|
|
794
|
+
public getCurrentUrl(): string {
|
|
795
|
+
return this.currentUrl;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Test if the tracker can reach the ingestion server
|
|
800
|
+
*/
|
|
801
|
+
public async testConnection(): Promise<{ success: boolean; error?: string }> {
|
|
802
|
+
try {
|
|
803
|
+
await this.api.init(this.sessionId, this.endUserId);
|
|
804
|
+
return { success: true };
|
|
805
|
+
} catch (error: any) {
|
|
806
|
+
return {
|
|
807
|
+
success: false,
|
|
808
|
+
error: error.message || 'Unknown error'
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Get connection status and recommendations
|
|
815
|
+
*/
|
|
816
|
+
public getConnectionStatus(): {
|
|
817
|
+
blocked: boolean;
|
|
818
|
+
recommendations: string[]
|
|
819
|
+
} {
|
|
820
|
+
const recommendations: string[] = [];
|
|
821
|
+
let blocked = false;
|
|
822
|
+
|
|
823
|
+
// Check if we have rejected events (might indicate blocking)
|
|
824
|
+
if (this.rejectedEvents.length > 0) {
|
|
825
|
+
blocked = true;
|
|
826
|
+
recommendations.push('Some requests may be blocked by ad blockers');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Check if connection was blocked during initialization
|
|
830
|
+
if (this._connectionBlocked) {
|
|
831
|
+
blocked = true;
|
|
832
|
+
recommendations.push('Initial connection test failed - ad blocker may be active');
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Check if we're in a browser environment
|
|
836
|
+
if (typeof window === 'undefined') {
|
|
837
|
+
recommendations.push('Not running in browser environment');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Check if navigator.sendBeacon is available
|
|
841
|
+
if (typeof navigator.sendBeacon === 'undefined') {
|
|
842
|
+
recommendations.push('sendBeacon not available, using fetch fallback');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return { blocked, recommendations };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Check if the current user is a preexisting user
|
|
850
|
+
* Returns true if the user has an existing endUserId cookie from a previous session
|
|
851
|
+
*/
|
|
852
|
+
public isPreexistingUser(): boolean {
|
|
853
|
+
if (!isBrowser) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// Check if there's an existing endUserId cookie for this API key
|
|
858
|
+
const existingEndUserId = this.getCookie(`human_behavior_end_user_id_${this.apiKey}`);
|
|
859
|
+
return existingEndUserId !== null && existingEndUserId !== this.endUserId;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Get user information including whether they are preexisting
|
|
864
|
+
*/
|
|
865
|
+
public getUserInfo(): {
|
|
866
|
+
endUserId: string | null;
|
|
867
|
+
sessionId: string;
|
|
868
|
+
isPreexistingUser: boolean;
|
|
869
|
+
initialized: boolean;
|
|
870
|
+
} {
|
|
871
|
+
return {
|
|
872
|
+
endUserId: this.endUserId,
|
|
873
|
+
sessionId: this.sessionId,
|
|
874
|
+
isPreexistingUser: this.isPreexistingUser(),
|
|
875
|
+
initialized: this.initialized
|
|
876
|
+
};
|
|
877
|
+
}
|
|
473
878
|
}
|
|
474
879
|
|
|
475
880
|
// Only expose to window object in browser environments
|