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/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/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,72 @@ 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 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 immediately
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
- this.sessionId = sessionId;
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.start();
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('HumanBehaviorTracker initialized');
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
- this.originalLogger!.debug('Console tracking enabled');
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 || !this.originalConsole) return;
442
+ if (!isBrowser || !this.consoleTrackingEnabled) return;
195
443
 
196
444
  // Restore original console methods
197
- console.log = this.originalConsole.log;
198
- console.warn = this.originalConsole.warn;
199
- console.error = this.originalConsole.error;
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
- this.originalConsole = null;
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
- 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);
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 periodically
257
- setInterval(() => {
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
- }, 60000); // Update every minute
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
- public async customEvent(eventName: string, eventProperties: Record<string, any> = {}) {
297
- await this.ensureInitialized();
298
- this.api.sendBeaconCustomEvent(eventName, eventProperties, this.sessionId);
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
- const date = new Date();
415
- date.setTime(date.getTime() + (daysToExpire * 24 * 60 * 60 * 1000));
416
- const expires = `expires=${date.toUTCString()}`;
417
- document.cookie = `${name}=${value};${expires};path=/;SameSite=Lax`;
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
- private getCookie(name: string): string | null {
702
+ public getCookie(name: string): string | null {
421
703
  if (!isBrowser) return null;
422
- const nameEQ = name + "=";
423
- const ca = document.cookie.split(';');
424
- for (let i = 0; i < ca.length; i++) {
425
- let c = ca[i];
426
- while (c.charAt(0) === ' ') c = c.substring(1, c.length);
427
- if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
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