humanbehavior-js 0.0.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +558 -184
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/react/index.js +197 -86
- package/dist/cjs/react/index.js.map +1 -1
- package/dist/esm/index.js +554 -185
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/react/index.js +195 -87
- 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 +115 -10
- package/dist/types/react/index.d.ts +8 -9
- package/package.json +3 -2
- package/readme.md +127 -105
- package/simple-spa.html +544 -0
- package/src/api.ts +9 -137
- package/src/index.ts +3 -0
- package/src/react/index.tsx +137 -81
- package/src/redact.ts +19 -17
- package/src/tracker.ts +498 -62
- package/src/utils/logger.ts +144 -0
package/src/tracker.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as rrweb from 'rrweb';
|
|
|
2
2
|
import { v1 as uuidv1 } from 'uuid';
|
|
3
3
|
import { HumanBehaviorAPI } from './api';
|
|
4
4
|
import { RedactionManager, RedactionOptions } from './redact';
|
|
5
|
+
import { logger, logError, logWarn, logInfo, logDebug } from './utils/logger';
|
|
5
6
|
|
|
6
7
|
// Check if we're in a browser environment
|
|
7
8
|
const isBrowser = typeof window !== 'undefined';
|
|
@@ -10,6 +11,7 @@ const isBrowser = typeof window !== 'undefined';
|
|
|
10
11
|
declare global {
|
|
11
12
|
interface Window {
|
|
12
13
|
HumanBehaviorTracker: typeof HumanBehaviorTracker;
|
|
14
|
+
__humanBehaviorGlobalTracker?: HumanBehaviorTracker;
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -19,37 +21,87 @@ export class HumanBehaviorTracker {
|
|
|
19
21
|
private rejectedEvents: any[] = [];
|
|
20
22
|
private isProcessingRejectedEvents: boolean = false;
|
|
21
23
|
|
|
22
|
-
private sessionId
|
|
24
|
+
private sessionId!: string;
|
|
23
25
|
private userProperties: Record<string, any> = {};
|
|
24
26
|
private isProcessing: boolean = false;
|
|
25
27
|
private flushInterval: number | null = null;
|
|
26
28
|
private readonly FLUSH_INTERVAL_MS = 5000; // Flush every 5 seconds
|
|
27
|
-
private api
|
|
29
|
+
private api!: HumanBehaviorAPI;
|
|
28
30
|
private endUserId: string | null = null;
|
|
29
|
-
private apiKey
|
|
31
|
+
private apiKey!: string;
|
|
30
32
|
private initialized: boolean = false;
|
|
31
33
|
public initializationPromise: Promise<void> | null = null;
|
|
32
|
-
private redactionManager
|
|
34
|
+
private redactionManager!: RedactionManager;
|
|
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
|
+
|
|
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/ingestion/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
|
+
}
|
|
33
97
|
|
|
34
98
|
constructor(apiKey: string | undefined, ingestionUrl?: string) {
|
|
35
99
|
if (!apiKey) {
|
|
36
100
|
throw new Error('Human Behavior API Key is required');
|
|
37
101
|
}
|
|
38
102
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
// ========================================
|
|
42
|
-
// Uncomment ONE of the following lines to select your server:
|
|
43
|
-
|
|
44
|
-
// AWS Development Server
|
|
45
|
-
const defaultIngestionUrl = 'http://3.137.217.33:3000';
|
|
46
|
-
|
|
47
|
-
// Vercel Production Server
|
|
48
|
-
// const defaultIngestionUrl = 'https://ingestion-server.vercel.app';
|
|
49
|
-
|
|
50
|
-
// Local Development Server
|
|
51
|
-
// const defaultIngestionUrl = 'http://localhost:3000';
|
|
52
|
-
|
|
103
|
+
// Initialize API
|
|
104
|
+
const defaultIngestionUrl = 'http://3.137.217.33:3000'; // AWS Development Server
|
|
53
105
|
this.api = new HumanBehaviorAPI({
|
|
54
106
|
apiKey: apiKey,
|
|
55
107
|
ingestionUrl: ingestionUrl || defaultIngestionUrl
|
|
@@ -57,45 +109,64 @@ export class HumanBehaviorTracker {
|
|
|
57
109
|
this.apiKey = apiKey;
|
|
58
110
|
this.redactionManager = new RedactionManager();
|
|
59
111
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
112
|
+
// Handle session restoration
|
|
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
|
+
if (existingSessionId && lastActivity && parseInt(lastActivity) > thirtyMinutesAgo) {
|
|
119
|
+
this.sessionId = existingSessionId;
|
|
120
|
+
logDebug(`Reusing existing session: ${this.sessionId}`);
|
|
121
|
+
} else {
|
|
122
|
+
this.sessionId = uuidv1();
|
|
123
|
+
logDebug(`Creating new session: ${this.sessionId}`);
|
|
124
|
+
localStorage.setItem('human_behavior_session_id', this.sessionId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.currentUrl = window.location.href;
|
|
128
|
+
window.__humanBehaviorGlobalTracker = this;
|
|
129
|
+
} else {
|
|
130
|
+
this.sessionId = uuidv1();
|
|
72
131
|
}
|
|
73
132
|
|
|
74
|
-
// Start initialization
|
|
133
|
+
// Start initialization
|
|
75
134
|
this.initializationPromise = this.init();
|
|
76
135
|
}
|
|
77
136
|
|
|
78
137
|
private async init(): Promise<void> {
|
|
79
138
|
try {
|
|
80
139
|
const userId = this.getCookie(`human_behavior_end_user_id_${this.apiKey}`);
|
|
140
|
+
logDebug(`Initializing with sessionId: ${this.sessionId}, userId: ${userId}`);
|
|
141
|
+
|
|
81
142
|
const { sessionId, endUserId } = await this.api.init(this.sessionId, userId);
|
|
82
|
-
|
|
143
|
+
|
|
144
|
+
// Check if server returned a different session ID
|
|
145
|
+
if (sessionId !== this.sessionId) {
|
|
146
|
+
logDebug(`Server returned different sessionId: ${sessionId} (client had: ${this.sessionId})`);
|
|
147
|
+
this.sessionId = sessionId;
|
|
148
|
+
// Update localStorage with server's session ID
|
|
149
|
+
if (isBrowser) {
|
|
150
|
+
localStorage.setItem('human_behavior_session_id', this.sessionId);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
83
154
|
this.endUserId = endUserId;
|
|
84
155
|
this.setCookie(`human_behavior_end_user_id_${this.apiKey}`, endUserId, 365);
|
|
85
156
|
|
|
86
157
|
// Only setup browser-specific handlers when in browser environment
|
|
87
158
|
if (isBrowser) {
|
|
88
159
|
this.setupPageUnloadHandler();
|
|
89
|
-
this.
|
|
160
|
+
this.setupNavigationTracking();
|
|
90
161
|
this.processRejectedEvents();
|
|
91
162
|
} else {
|
|
92
|
-
|
|
163
|
+
logWarn('HumanBehaviorTracker initialized in a non-browser environment. Session tracking is disabled.');
|
|
93
164
|
}
|
|
94
165
|
|
|
95
166
|
this.initialized = true;
|
|
96
|
-
|
|
167
|
+
logInfo(`HumanBehaviorTracker initialized with sessionId: ${this.sessionId}, endUserId: ${endUserId}`);
|
|
97
168
|
} catch (error) {
|
|
98
|
-
|
|
169
|
+
logError('Failed to initialize HumanBehaviorTracker:', error);
|
|
99
170
|
throw error;
|
|
100
171
|
}
|
|
101
172
|
}
|
|
@@ -107,52 +178,342 @@ export class HumanBehaviorTracker {
|
|
|
107
178
|
await this.initializationPromise;
|
|
108
179
|
}
|
|
109
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Setup navigation event tracking for SPA navigation
|
|
183
|
+
*/
|
|
184
|
+
private setupNavigationTracking(): void {
|
|
185
|
+
if (!isBrowser || this.navigationTrackingEnabled) return;
|
|
186
|
+
|
|
187
|
+
this.navigationTrackingEnabled = true;
|
|
188
|
+
logDebug('Setting up navigation tracking');
|
|
189
|
+
|
|
190
|
+
// Store original history methods
|
|
191
|
+
this.originalPushState = history.pushState;
|
|
192
|
+
this.originalReplaceState = history.replaceState;
|
|
193
|
+
|
|
194
|
+
// Override pushState to capture programmatic navigation
|
|
195
|
+
history.pushState = (...args) => {
|
|
196
|
+
this.previousUrl = this.currentUrl;
|
|
197
|
+
this.currentUrl = window.location.href;
|
|
198
|
+
|
|
199
|
+
// Call original method
|
|
200
|
+
this.originalPushState!.apply(history, args);
|
|
201
|
+
|
|
202
|
+
// Track navigation event
|
|
203
|
+
this.trackNavigationEvent('pushState', this.previousUrl, this.currentUrl);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Override replaceState to capture programmatic navigation
|
|
207
|
+
history.replaceState = (...args) => {
|
|
208
|
+
this.previousUrl = this.currentUrl;
|
|
209
|
+
this.currentUrl = window.location.href;
|
|
210
|
+
|
|
211
|
+
// Call original method
|
|
212
|
+
this.originalReplaceState!.apply(history, args);
|
|
213
|
+
|
|
214
|
+
// Track navigation event
|
|
215
|
+
this.trackNavigationEvent('replaceState', this.previousUrl, this.currentUrl);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Listen for popstate events (back/forward navigation)
|
|
219
|
+
const popstateListener = () => {
|
|
220
|
+
this.previousUrl = this.currentUrl;
|
|
221
|
+
this.currentUrl = window.location.href;
|
|
222
|
+
this.trackNavigationEvent('popstate', this.previousUrl, this.currentUrl);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
window.addEventListener('popstate', popstateListener);
|
|
226
|
+
this.navigationListeners.push(() => {
|
|
227
|
+
window.removeEventListener('popstate', popstateListener);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Listen for hashchange events
|
|
231
|
+
const hashchangeListener = () => {
|
|
232
|
+
this.previousUrl = this.currentUrl;
|
|
233
|
+
this.currentUrl = window.location.href;
|
|
234
|
+
this.trackNavigationEvent('hashchange', this.previousUrl, this.currentUrl);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
window.addEventListener('hashchange', hashchangeListener);
|
|
238
|
+
this.navigationListeners.push(() => {
|
|
239
|
+
window.removeEventListener('hashchange', hashchangeListener);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Track initial page load
|
|
243
|
+
this.trackNavigationEvent('pageLoad', '', this.currentUrl);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Track navigation events and send custom events
|
|
248
|
+
*/
|
|
249
|
+
public async trackNavigationEvent(type: string, fromUrl: string, toUrl: string): Promise<void> {
|
|
250
|
+
if (!this.initialized) return;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const navigationData = {
|
|
254
|
+
type: type,
|
|
255
|
+
from: fromUrl,
|
|
256
|
+
to: toUrl,
|
|
257
|
+
timestamp: new Date().toISOString(),
|
|
258
|
+
pathname: window.location.pathname,
|
|
259
|
+
search: window.location.search,
|
|
260
|
+
hash: window.location.hash,
|
|
261
|
+
referrer: document.referrer
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Add navigation event to the main event stream
|
|
265
|
+
await this.addEvent({
|
|
266
|
+
type: 5, // Custom event type
|
|
267
|
+
data: {
|
|
268
|
+
payload: {
|
|
269
|
+
eventType: 'navigation',
|
|
270
|
+
...navigationData
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
timestamp: Date.now()
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
logDebug(`Navigation tracked: ${type} from ${fromUrl} to ${toUrl}`);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
logError('Failed to track navigation event:', error);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Track a page view event (PostHog-style)
|
|
284
|
+
*/
|
|
285
|
+
public async trackPageView(url?: string): Promise<void> {
|
|
286
|
+
if (!this.initialized) return;
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const pageViewData = {
|
|
290
|
+
url: url || window.location.href,
|
|
291
|
+
pathname: window.location.pathname,
|
|
292
|
+
search: window.location.search,
|
|
293
|
+
hash: window.location.hash,
|
|
294
|
+
referrer: document.referrer,
|
|
295
|
+
timestamp: new Date().toISOString()
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Add pageview event to the main event stream
|
|
299
|
+
await this.addEvent({
|
|
300
|
+
type: 5, // Custom event type
|
|
301
|
+
data: {
|
|
302
|
+
payload: {
|
|
303
|
+
eventType: 'pageview',
|
|
304
|
+
...pageViewData
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
timestamp: Date.now()
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
logDebug(`Pageview tracked: ${pageViewData.url}`);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
logError('Failed to track pageview event:', error);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Track a custom event (PostHog-style)
|
|
318
|
+
*/
|
|
319
|
+
public async customEvent(eventName: string, properties?: Record<string, any>): Promise<void> {
|
|
320
|
+
if (!this.initialized) return;
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const customEventData = {
|
|
324
|
+
eventName: eventName,
|
|
325
|
+
properties: properties || {},
|
|
326
|
+
timestamp: new Date().toISOString(),
|
|
327
|
+
url: window.location.href,
|
|
328
|
+
pathname: window.location.pathname
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Add custom event to the main event stream
|
|
332
|
+
await this.addEvent({
|
|
333
|
+
type: 5, // Custom event type
|
|
334
|
+
data: {
|
|
335
|
+
payload: {
|
|
336
|
+
eventType: 'custom',
|
|
337
|
+
...customEventData
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
timestamp: Date.now()
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
logDebug(`Custom event tracked: ${eventName}`, properties);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
logError('Failed to track custom event:', error);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Cleanup navigation tracking
|
|
351
|
+
*/
|
|
352
|
+
private cleanupNavigationTracking(): void {
|
|
353
|
+
if (!this.navigationTrackingEnabled) return;
|
|
354
|
+
|
|
355
|
+
// Restore original history methods
|
|
356
|
+
if (this.originalPushState) {
|
|
357
|
+
history.pushState = this.originalPushState;
|
|
358
|
+
}
|
|
359
|
+
if (this.originalReplaceState) {
|
|
360
|
+
history.replaceState = this.originalReplaceState;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Remove event listeners
|
|
364
|
+
this.navigationListeners.forEach(cleanup => cleanup());
|
|
365
|
+
this.navigationListeners = [];
|
|
366
|
+
|
|
367
|
+
this.navigationTrackingEnabled = false;
|
|
368
|
+
logDebug('Navigation tracking cleaned up');
|
|
369
|
+
}
|
|
370
|
+
|
|
110
371
|
public static logToStorage(message: string) {
|
|
372
|
+
logInfo(message);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Configure logging behavior for the SDK
|
|
377
|
+
* @param config Logger configuration options
|
|
378
|
+
*/
|
|
379
|
+
public static configureLogging(config: { level?: 'none' | 'error' | 'warn' | 'info' | 'debug', enableConsole?: boolean, enableStorage?: boolean }) {
|
|
380
|
+
const levelMap = {
|
|
381
|
+
'none': 0,
|
|
382
|
+
'error': 1,
|
|
383
|
+
'warn': 2,
|
|
384
|
+
'info': 3,
|
|
385
|
+
'debug': 4
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
logger.setConfig({
|
|
389
|
+
level: levelMap[config.level || 'error'],
|
|
390
|
+
enableConsole: config.enableConsole !== false,
|
|
391
|
+
enableStorage: config.enableStorage || false
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Enable console event tracking
|
|
397
|
+
*/
|
|
398
|
+
public enableConsoleTracking(): void {
|
|
399
|
+
if (!isBrowser || this.consoleTrackingEnabled) return;
|
|
400
|
+
|
|
401
|
+
// Store original console methods
|
|
402
|
+
this.originalConsole = {
|
|
403
|
+
log: console.log,
|
|
404
|
+
warn: console.warn,
|
|
405
|
+
error: console.error
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
// Override console methods to capture ALL console output (including logger output)
|
|
411
|
+
console.log = (...args) => {
|
|
412
|
+
this.trackConsoleEvent('log', args);
|
|
413
|
+
this.originalConsole!.log(...args);
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
console.warn = (...args) => {
|
|
417
|
+
this.trackConsoleEvent('warn', args);
|
|
418
|
+
this.originalConsole!.warn(...args);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
console.error = (...args) => {
|
|
422
|
+
this.trackConsoleEvent('error', args);
|
|
423
|
+
this.originalConsole!.error(...args);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
this.consoleTrackingEnabled = true;
|
|
427
|
+
logDebug('Console tracking enabled');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Disable console event tracking
|
|
432
|
+
*/
|
|
433
|
+
public disableConsoleTracking(): void {
|
|
434
|
+
if (!isBrowser || !this.consoleTrackingEnabled) return;
|
|
435
|
+
|
|
436
|
+
// Restore original console methods
|
|
437
|
+
if (this.originalConsole) {
|
|
438
|
+
console.log = this.originalConsole.log;
|
|
439
|
+
console.warn = this.originalConsole.warn;
|
|
440
|
+
console.error = this.originalConsole.error;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
this.consoleTrackingEnabled = false;
|
|
444
|
+
logDebug('Console tracking disabled');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private trackConsoleEvent(level: 'log' | 'warn' | 'error', args: any[]): void {
|
|
448
|
+
if (!this.initialized) return;
|
|
449
|
+
|
|
111
450
|
try {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
451
|
+
const consoleData = {
|
|
452
|
+
level: level,
|
|
453
|
+
message: args.map(arg =>
|
|
454
|
+
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
|
455
|
+
).join(' '),
|
|
456
|
+
timestamp: new Date().toISOString(),
|
|
457
|
+
url: window.location.href
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Add console event to the main event stream
|
|
461
|
+
this.addEvent({
|
|
462
|
+
type: 5, // Custom event type
|
|
463
|
+
data: {
|
|
464
|
+
payload: {
|
|
465
|
+
eventType: 'console',
|
|
466
|
+
...consoleData
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
timestamp: Date.now()
|
|
470
|
+
}).catch(error => {
|
|
471
|
+
logError('Failed to track console event:', error);
|
|
472
|
+
});
|
|
473
|
+
} catch (error) {
|
|
474
|
+
logError('Error in trackConsoleEvent:', error);
|
|
117
475
|
}
|
|
118
476
|
}
|
|
119
477
|
|
|
120
478
|
private setupPageUnloadHandler() {
|
|
121
479
|
if (!isBrowser) return;
|
|
122
480
|
|
|
123
|
-
|
|
481
|
+
logDebug('Setting up page unload handler');
|
|
124
482
|
|
|
125
483
|
// Handle visibility changes for sending events
|
|
126
484
|
window.addEventListener('visibilitychange', () => {
|
|
127
485
|
// Only send events when page becomes hidden
|
|
128
486
|
if (document.visibilityState === 'hidden') {
|
|
129
|
-
|
|
487
|
+
logDebug('Page hidden - sending pending events');
|
|
130
488
|
this.api.sendBeaconEvents(this.eventIngestionQueue, this.sessionId);
|
|
131
489
|
}
|
|
132
490
|
});
|
|
133
491
|
|
|
134
492
|
// Handle actual page unload/close
|
|
135
493
|
window.addEventListener('beforeunload', () => {
|
|
136
|
-
// Update last activity time
|
|
137
|
-
localStorage.setItem('human_behavior_last_activity', Date.now().toString());
|
|
138
|
-
|
|
139
494
|
// Send final events
|
|
140
495
|
this.api.sendBeaconEvents(this.eventIngestionQueue, this.sessionId);
|
|
141
496
|
});
|
|
142
497
|
|
|
143
|
-
// Update activity timestamp
|
|
144
|
-
|
|
498
|
+
// Update activity timestamp on user interaction (not on page load)
|
|
499
|
+
const updateActivity = () => {
|
|
145
500
|
localStorage.setItem('human_behavior_last_activity', Date.now().toString());
|
|
146
|
-
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Listen for user interactions to update activity timestamp
|
|
504
|
+
window.addEventListener('click', updateActivity);
|
|
505
|
+
window.addEventListener('keydown', updateActivity);
|
|
506
|
+
window.addEventListener('scroll', updateActivity);
|
|
507
|
+
window.addEventListener('mousemove', updateActivity);
|
|
147
508
|
}
|
|
148
509
|
|
|
149
510
|
public viewLogs() {
|
|
150
511
|
try {
|
|
151
|
-
const logs =
|
|
512
|
+
const logs = logger.getLogs();
|
|
152
513
|
console.log('HumanBehavior Logs:', logs);
|
|
153
|
-
|
|
514
|
+
logger.clearLogs(); // Clear logs after viewing
|
|
154
515
|
} catch (e) {
|
|
155
|
-
|
|
516
|
+
logError('Failed to read logs:', e);
|
|
156
517
|
}
|
|
157
518
|
}
|
|
158
519
|
|
|
@@ -180,11 +541,6 @@ export class HumanBehaviorTracker {
|
|
|
180
541
|
await this.api.sendUserAuth(this.endUserId, this.userProperties, this.sessionId, authFields);
|
|
181
542
|
}
|
|
182
543
|
|
|
183
|
-
public async customEvent(eventName: string, eventProperties: Record<string, any> = {}) {
|
|
184
|
-
await this.ensureInitialized();
|
|
185
|
-
this.api.sendBeaconCustomEvent(eventName, eventProperties, this.sessionId);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
544
|
public async start() {
|
|
189
545
|
await this.ensureInitialized();
|
|
190
546
|
if (!isBrowser) return;
|
|
@@ -194,6 +550,9 @@ export class HumanBehaviorTracker {
|
|
|
194
550
|
this.flush();
|
|
195
551
|
}, this.FLUSH_INTERVAL_MS);
|
|
196
552
|
|
|
553
|
+
// Enable console tracking
|
|
554
|
+
this.enableConsoleTracking();
|
|
555
|
+
|
|
197
556
|
// Start recording with redaction enabled
|
|
198
557
|
rrweb.record({
|
|
199
558
|
emit: (event) => {
|
|
@@ -216,6 +575,12 @@ export class HumanBehaviorTracker {
|
|
|
216
575
|
clearInterval(this.flushInterval);
|
|
217
576
|
this.flushInterval = null;
|
|
218
577
|
}
|
|
578
|
+
|
|
579
|
+
// Disable console tracking
|
|
580
|
+
this.disableConsoleTracking();
|
|
581
|
+
|
|
582
|
+
// Cleanup navigation tracking
|
|
583
|
+
this.cleanupNavigationTracking();
|
|
219
584
|
}
|
|
220
585
|
|
|
221
586
|
public async addEvent(event: any) {
|
|
@@ -250,7 +615,7 @@ export class HumanBehaviorTracker {
|
|
|
250
615
|
this.rejectedEvents = [];
|
|
251
616
|
this.sessionId = newSessionId;
|
|
252
617
|
} catch (error) {
|
|
253
|
-
|
|
618
|
+
logError('Failed to process rejected events:', error);
|
|
254
619
|
} finally {
|
|
255
620
|
this.isProcessingRejectedEvents = false;
|
|
256
621
|
}
|
|
@@ -270,15 +635,22 @@ export class HumanBehaviorTracker {
|
|
|
270
635
|
this.queueSizeBytes = 0;
|
|
271
636
|
|
|
272
637
|
if (eventsToProcess.length > 0) {
|
|
273
|
-
|
|
638
|
+
logDebug('Flushing events:', eventsToProcess);
|
|
274
639
|
try {
|
|
275
640
|
await this.api.sendEvents(eventsToProcess, this.sessionId, this.endUserId!);
|
|
276
641
|
} catch (error: any) {
|
|
277
642
|
// If we get a 400 error, store events for retry
|
|
278
643
|
if (error.message?.includes('ERROR: Session already completed')) {
|
|
279
|
-
|
|
644
|
+
logInfo('Session expired, storing events for retry');
|
|
280
645
|
this.rejectedEvents.push(...eventsToProcess);
|
|
281
646
|
this.processRejectedEvents();
|
|
647
|
+
} else if (error.message?.includes('ERR_BLOCKED_BY_CLIENT') ||
|
|
648
|
+
error.message?.includes('Failed to fetch') ||
|
|
649
|
+
error.message?.includes('NetworkError')) {
|
|
650
|
+
// Handle ad blocker or network issues gracefully
|
|
651
|
+
logWarn('Request blocked by ad blocker or network issue, storing events for retry');
|
|
652
|
+
this.rejectedEvents.push(...eventsToProcess);
|
|
653
|
+
// Don't process rejected events immediately to avoid spam
|
|
282
654
|
} else {
|
|
283
655
|
throw error;
|
|
284
656
|
}
|
|
@@ -317,7 +689,7 @@ export class HumanBehaviorTracker {
|
|
|
317
689
|
public async redact(options?: RedactionOptions): Promise<void> {
|
|
318
690
|
await this.ensureInitialized();
|
|
319
691
|
if (!isBrowser) {
|
|
320
|
-
|
|
692
|
+
logWarn('Redaction is only available in browser environments');
|
|
321
693
|
return;
|
|
322
694
|
}
|
|
323
695
|
|
|
@@ -331,7 +703,7 @@ export class HumanBehaviorTracker {
|
|
|
331
703
|
*/
|
|
332
704
|
public setRedactedFields(fields: string[]): void {
|
|
333
705
|
if (!isBrowser) {
|
|
334
|
-
|
|
706
|
+
logWarn('Redaction is only available in browser environments');
|
|
335
707
|
return;
|
|
336
708
|
}
|
|
337
709
|
|
|
@@ -351,6 +723,70 @@ export class HumanBehaviorTracker {
|
|
|
351
723
|
public getRedactedFields(): string[] {
|
|
352
724
|
return this.redactionManager.getSelectedFields();
|
|
353
725
|
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Get the current session ID
|
|
729
|
+
*/
|
|
730
|
+
public getSessionId(): string {
|
|
731
|
+
return this.sessionId;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Get the current URL being tracked
|
|
736
|
+
*/
|
|
737
|
+
public getCurrentUrl(): string {
|
|
738
|
+
return this.currentUrl;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Test if the tracker can reach the ingestion server
|
|
743
|
+
*/
|
|
744
|
+
public async testConnection(): Promise<{ success: boolean; error?: string }> {
|
|
745
|
+
try {
|
|
746
|
+
await this.api.init(this.sessionId, this.endUserId);
|
|
747
|
+
return { success: true };
|
|
748
|
+
} catch (error: any) {
|
|
749
|
+
return {
|
|
750
|
+
success: false,
|
|
751
|
+
error: error.message || 'Unknown error'
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Get connection status and recommendations
|
|
758
|
+
*/
|
|
759
|
+
public getConnectionStatus(): {
|
|
760
|
+
blocked: boolean;
|
|
761
|
+
recommendations: string[]
|
|
762
|
+
} {
|
|
763
|
+
const recommendations: string[] = [];
|
|
764
|
+
let blocked = false;
|
|
765
|
+
|
|
766
|
+
// Check if we have rejected events (might indicate blocking)
|
|
767
|
+
if (this.rejectedEvents.length > 0) {
|
|
768
|
+
blocked = true;
|
|
769
|
+
recommendations.push('Some requests may be blocked by ad blockers');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Check if connection was blocked during initialization
|
|
773
|
+
if (this._connectionBlocked) {
|
|
774
|
+
blocked = true;
|
|
775
|
+
recommendations.push('Initial connection test failed - ad blocker may be active');
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Check if we're in a browser environment
|
|
779
|
+
if (typeof window === 'undefined') {
|
|
780
|
+
recommendations.push('Not running in browser environment');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Check if navigator.sendBeacon is available
|
|
784
|
+
if (typeof navigator.sendBeacon === 'undefined') {
|
|
785
|
+
recommendations.push('sendBeacon not available, using fetch fallback');
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return { blocked, recommendations };
|
|
789
|
+
}
|
|
354
790
|
}
|
|
355
791
|
|
|
356
792
|
// Only expose to window object in browser environments
|