humanbehavior-js 0.0.9 → 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/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: string;
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: HumanBehaviorAPI;
29
+ private api!: HumanBehaviorAPI;
29
30
  private endUserId: string | null = null;
30
- private apiKey: string;
31
+ private apiKey!: string;
31
32
  private initialized: boolean = false;
32
33
  public initializationPromise: Promise<void> | null = null;
33
- private redactionManager: 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/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
+ }
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
- // DEVELOPER: Choose your ingestion server
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,62 @@ export class HumanBehaviorTracker {
72
109
  this.apiKey = apiKey;
73
110
  this.redactionManager = new RedactionManager();
74
111
 
75
- // Check for existing session ID and last activity time in localStorage
76
- const existingSessionId = isBrowser ? localStorage.getItem('human_behavior_session_id') : null;
77
- const lastActivity = isBrowser ? localStorage.getItem('human_behavior_last_activity') : null;
78
-
79
- // If we have a last activity time, check if it's within 30 minutes
80
- const thirtyMinutesAgo = Date.now() - (30 * 60 * 1000);
81
- const shouldUseExistingSession = lastActivity && parseInt(lastActivity) > thirtyMinutesAgo;
82
- this.sessionId = (existingSessionId && shouldUseExistingSession) ? existingSessionId : uuidv1();
83
-
84
- // Store the session ID if it's new
85
- if ((!existingSessionId || !shouldUseExistingSession) && isBrowser) {
86
- localStorage.setItem('human_behavior_session_id', this.sessionId);
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();
87
131
  }
88
132
 
89
- // Start initialization immediately
133
+ // Start initialization
90
134
  this.initializationPromise = this.init();
91
135
  }
92
136
 
93
137
  private async init(): Promise<void> {
94
138
  try {
95
139
  const userId = this.getCookie(`human_behavior_end_user_id_${this.apiKey}`);
140
+ logDebug(`Initializing with sessionId: ${this.sessionId}, userId: ${userId}`);
141
+
96
142
  const { sessionId, endUserId } = await this.api.init(this.sessionId, userId);
97
- this.sessionId = sessionId;
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
+
98
154
  this.endUserId = endUserId;
99
155
  this.setCookie(`human_behavior_end_user_id_${this.apiKey}`, endUserId, 365);
100
156
 
101
157
  // Only setup browser-specific handlers when in browser environment
102
158
  if (isBrowser) {
103
159
  this.setupPageUnloadHandler();
104
- this.start();
160
+ this.setupNavigationTracking();
105
161
  this.processRejectedEvents();
106
162
  } else {
107
163
  logWarn('HumanBehaviorTracker initialized in a non-browser environment. Session tracking is disabled.');
108
164
  }
109
165
 
110
166
  this.initialized = true;
111
- logInfo('HumanBehaviorTracker initialized');
167
+ logInfo(`HumanBehaviorTracker initialized with sessionId: ${this.sessionId}, endUserId: ${endUserId}`);
112
168
  } catch (error) {
113
169
  logError('Failed to initialize HumanBehaviorTracker:', error);
114
170
  throw error;
@@ -122,6 +178,196 @@ export class HumanBehaviorTracker {
122
178
  await this.initializationPromise;
123
179
  }
124
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
+
125
371
  public static logToStorage(message: string) {
126
372
  logInfo(message);
127
373
  }
@@ -159,13 +405,7 @@ export class HumanBehaviorTracker {
159
405
  error: console.error
160
406
  };
161
407
 
162
- // Store original logger methods
163
- this.originalLogger = {
164
- error: logError,
165
- warn: logWarn,
166
- info: logInfo,
167
- debug: logDebug
168
- };
408
+
169
409
 
170
410
  // Override console methods to capture ALL console output (including logger output)
171
411
  console.log = (...args) => {
@@ -184,50 +424,55 @@ export class HumanBehaviorTracker {
184
424
  };
185
425
 
186
426
  this.consoleTrackingEnabled = true;
187
- this.originalLogger!.debug('Console tracking enabled');
427
+ logDebug('Console tracking enabled');
188
428
  }
189
429
 
190
430
  /**
191
431
  * Disable console event tracking
192
432
  */
193
433
  public disableConsoleTracking(): void {
194
- if (!isBrowser || !this.consoleTrackingEnabled || !this.originalConsole) return;
434
+ if (!isBrowser || !this.consoleTrackingEnabled) return;
195
435
 
196
436
  // Restore original console methods
197
- console.log = this.originalConsole.log;
198
- console.warn = this.originalConsole.warn;
199
- console.error = this.originalConsole.error;
437
+ if (this.originalConsole) {
438
+ console.log = this.originalConsole.log;
439
+ console.warn = this.originalConsole.warn;
440
+ console.error = this.originalConsole.error;
441
+ }
200
442
 
201
443
  this.consoleTrackingEnabled = false;
202
- this.originalConsole = null;
203
- this.originalLogger = null;
444
+ logDebug('Console tracking disabled');
204
445
  }
205
446
 
206
- /**
207
- * Track console events
208
- */
209
447
  private trackConsoleEvent(level: 'log' | 'warn' | 'error', args: any[]): void {
210
448
  if (!this.initialized) return;
211
449
 
212
- const consoleEvent = {
213
- type: 5, // Custom event type
214
- data: {
215
- payload: {
216
- type: 'console',
217
- level: level,
218
- message: args.map(arg =>
219
- typeof arg === 'string' ? arg :
220
- typeof arg === 'object' ? JSON.stringify(arg) :
221
- String(arg)
222
- ).join(' '),
223
- timestamp: Date.now(),
224
- url: window.location.href
225
- }
226
- },
227
- timestamp: Date.now()
228
- };
229
-
230
- this.addEvent(consoleEvent);
450
+ try {
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);
475
+ }
231
476
  }
232
477
 
233
478
  private setupPageUnloadHandler() {
@@ -246,17 +491,20 @@ export class HumanBehaviorTracker {
246
491
 
247
492
  // Handle actual page unload/close
248
493
  window.addEventListener('beforeunload', () => {
249
- // Update last activity time
250
- localStorage.setItem('human_behavior_last_activity', Date.now().toString());
251
-
252
494
  // Send final events
253
495
  this.api.sendBeaconEvents(this.eventIngestionQueue, this.sessionId);
254
496
  });
255
497
 
256
- // Update activity timestamp periodically
257
- setInterval(() => {
498
+ // Update activity timestamp on user interaction (not on page load)
499
+ const updateActivity = () => {
258
500
  localStorage.setItem('human_behavior_last_activity', Date.now().toString());
259
- }, 60000); // Update every minute
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);
260
508
  }
261
509
 
262
510
  public viewLogs() {
@@ -293,11 +541,6 @@ export class HumanBehaviorTracker {
293
541
  await this.api.sendUserAuth(this.endUserId, this.userProperties, this.sessionId, authFields);
294
542
  }
295
543
 
296
- public async customEvent(eventName: string, eventProperties: Record<string, any> = {}) {
297
- await this.ensureInitialized();
298
- this.api.sendBeaconCustomEvent(eventName, eventProperties, this.sessionId);
299
- }
300
-
301
544
  public async start() {
302
545
  await this.ensureInitialized();
303
546
  if (!isBrowser) return;
@@ -335,6 +578,9 @@ export class HumanBehaviorTracker {
335
578
 
336
579
  // Disable console tracking
337
580
  this.disableConsoleTracking();
581
+
582
+ // Cleanup navigation tracking
583
+ this.cleanupNavigationTracking();
338
584
  }
339
585
 
340
586
  public async addEvent(event: any) {
@@ -398,6 +644,13 @@ export class HumanBehaviorTracker {
398
644
  logInfo('Session expired, storing events for retry');
399
645
  this.rejectedEvents.push(...eventsToProcess);
400
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
401
654
  } else {
402
655
  throw error;
403
656
  }
@@ -470,6 +723,70 @@ export class HumanBehaviorTracker {
470
723
  public getRedactedFields(): string[] {
471
724
  return this.redactionManager.getSelectedFields();
472
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
+ }
473
790
  }
474
791
 
475
792
  // Only expose to window object in browser environments