humanbehavior-js 0.1.8 → 0.1.9

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.
@@ -89,8 +89,6 @@ declare global {
89
89
  declare class HumanBehaviorTracker {
90
90
  private eventIngestionQueue;
91
91
  private queueSizeBytes;
92
- private rejectedEvents;
93
- private isProcessingRejectedEvents;
94
92
  private sessionId;
95
93
  private userProperties;
96
94
  private isProcessing;
@@ -197,7 +195,6 @@ declare class HumanBehaviorTracker {
197
195
  start(): Promise<void>;
198
196
  stop(): Promise<void>;
199
197
  addEvent(event: any): Promise<void>;
200
- private processRejectedEvents;
201
198
  private flush;
202
199
  private setCookie;
203
200
  getCookie(name: string): string | null;
@@ -260,6 +257,7 @@ declare class HumanBehaviorTracker {
260
257
  declare const MAX_CHUNK_SIZE_BYTES: number;
261
258
  declare function isChunkSizeExceeded(currentChunk: any[], newEvent: any, sessionId: string): boolean;
262
259
  declare function validateSingleEventSize(event: any, sessionId: string): void;
260
+ declare function splitLargeEvent(event: any, sessionId: string): any[];
263
261
  declare class HumanBehaviorAPI {
264
262
  private apiKey;
265
263
  private baseUrl;
@@ -272,7 +270,7 @@ declare class HumanBehaviorAPI {
272
270
  endUserId: any;
273
271
  }>;
274
272
  sendEvents(events: any[], sessionId: string, userId: string): Promise<void>;
275
- sendEventsChunked(events: any[], sessionId: string): Promise<any[]>;
273
+ sendEventsChunked(events: any[], sessionId: string, userId?: string): Promise<any[]>;
276
274
  sendUserData(userId: string, userData: Record<string, any>, sessionId: string): Promise<any>;
277
275
  sendUserAuth(userId: string, userData: Record<string, any>, sessionId: string, authFields: string[]): Promise<any>;
278
276
  sendBeaconEvents(events: any[], sessionId: string): boolean;
@@ -316,5 +314,5 @@ declare const logWarn: (message: string, ...args: any[]) => void;
316
314
  declare const logInfo: (message: string, ...args: any[]) => void;
317
315
  declare const logDebug: (message: string, ...args: any[]) => void;
318
316
 
319
- export { HumanBehaviorAPI, HumanBehaviorTracker, LogLevel, MAX_CHUNK_SIZE_BYTES, RedactionManager, HumanBehaviorTracker as default, isChunkSizeExceeded, logDebug, logError, logInfo, logWarn, logger, redactionManager, validateSingleEventSize };
317
+ export { HumanBehaviorAPI, HumanBehaviorTracker, LogLevel, MAX_CHUNK_SIZE_BYTES, RedactionManager, HumanBehaviorTracker as default, isChunkSizeExceeded, logDebug, logError, logInfo, logWarn, logger, redactionManager, splitLargeEvent, validateSingleEventSize };
320
318
  export type { LoggerConfig, RedactionOptions };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "humanbehavior-js",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "SDK for HumanBehavior session and event recording",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
7
7
  "module": "./dist/esm/index.js",
8
8
  "types": "./dist/types/index.d.ts",
9
9
  "unpkg": "dist/index.min.js",
10
+ "homepage": "https://documentation.humanbehavior.co",
10
11
  "exports": {
11
12
  ".": {
12
13
  "import": "./dist/esm/index.js",
package/readme.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A simplified session recording and analytics SDK that maintains session continuity across page navigations.
4
4
 
5
+ 📖 **[Full Documentation](https://documentation.humanbehavior.co)** - Complete API reference, examples, and integration guides
6
+
5
7
  ## Quick Start
6
8
 
7
9
  ### Single Page Application (Recommended)
package/simple-spa.html CHANGED
@@ -280,10 +280,21 @@
280
280
 
281
281
  console.log('Tracker created, waiting for initialization...');
282
282
 
283
- // Wait for tracker to initialize
283
+ // Wait for tracker to initialize with timeout
284
284
  if (tracker.initializationPromise) {
285
- await tracker.initializationPromise;
286
- console.log('Tracker initialized successfully');
285
+ try {
286
+ await Promise.race([
287
+ tracker.initializationPromise,
288
+ new Promise((_, reject) =>
289
+ setTimeout(() => reject(new Error('Initialization timeout')), 10000)
290
+ )
291
+ ]);
292
+ console.log('Tracker initialized successfully');
293
+ } catch (initError) {
294
+ console.warn('Tracker initialization failed or timed out:', initError);
295
+ addLog('Tracker initialization warning: ' + initError.message);
296
+ // Continue anyway - tracker might still work
297
+ }
287
298
  } else {
288
299
  console.log('No initialization promise, continuing...');
289
300
  }
@@ -415,30 +426,35 @@
415
426
  const sessionIdElements = document.querySelectorAll('#sessionId, #sessionId-about, #sessionId-status');
416
427
  const sessionId = tracker.getSessionId ? tracker.getSessionId() : 'Not available';
417
428
  sessionIdElements.forEach(el => {
418
- el.textContent = sessionId;
429
+ if (el) el.textContent = sessionId;
419
430
  });
420
431
 
421
432
  const endUserIdElements = document.querySelectorAll('#endUserId, #endUserId-status');
422
433
  const endUserId = tracker.endUserId || 'Not set';
423
434
  endUserIdElements.forEach(el => {
424
- el.textContent = endUserId;
435
+ if (el) el.textContent = endUserId;
425
436
  });
426
437
 
427
438
  // Update user type (preexisting vs new)
428
439
  const userTypeElements = document.querySelectorAll('#userType, #userType-about, #userType-status');
429
440
  let userType = 'Unknown';
430
441
  if (tracker.isPreexistingUser) {
431
- const isPreexisting = tracker.isPreexistingUser();
432
- userType = isPreexisting ? '🔄 Preexisting User' : '🆕 New User';
442
+ try {
443
+ const isPreexisting = tracker.isPreexistingUser();
444
+ userType = isPreexisting ? '🔄 Preexisting User' : '🆕 New User';
445
+ } catch (error) {
446
+ console.warn('Error checking preexisting user:', error);
447
+ userType = 'Error';
448
+ }
433
449
  }
434
450
  userTypeElements.forEach(el => {
435
- el.textContent = userType;
451
+ if (el) el.textContent = userType;
436
452
  });
437
453
 
438
454
  const currentUrlElements = document.querySelectorAll('#currentUrl, #currentUrl-status');
439
455
  const currentUrl = window.location.href;
440
456
  currentUrlElements.forEach(el => {
441
- el.textContent = currentUrl;
457
+ if (el) el.textContent = currentUrl;
442
458
  });
443
459
 
444
460
  const trackerInitialized = document.getElementById('trackerInitialized');
@@ -448,8 +464,13 @@
448
464
 
449
465
  const connectionStatus = document.getElementById('connectionStatus');
450
466
  if (connectionStatus && tracker.getConnectionStatus) {
451
- const status = tracker.getConnectionStatus();
452
- connectionStatus.textContent = status.blocked ? 'Blocked' : 'Connected';
467
+ try {
468
+ const status = tracker.getConnectionStatus();
469
+ connectionStatus.textContent = status.blocked ? 'Blocked' : 'Connected';
470
+ } catch (error) {
471
+ console.warn('Error getting connection status:', error);
472
+ connectionStatus.textContent = 'Error';
473
+ }
453
474
  }
454
475
 
455
476
  const pageLoadTime = document.getElementById('pageLoadTime');
@@ -458,6 +479,7 @@
458
479
  }
459
480
  } catch (error) {
460
481
  console.error('Error updating status:', error);
482
+ // Don't let status update errors crash the app
461
483
  }
462
484
  }
463
485
 
@@ -589,6 +611,48 @@
589
611
 
590
612
  // Initialize the app when the page loads
591
613
  document.addEventListener('DOMContentLoaded', initApp);
614
+
615
+ // Add global error handler to prevent page from going blank
616
+ window.addEventListener('error', function(event) {
617
+ console.error('Global error caught:', event.error);
618
+ addLog('Global error: ' + event.error.message);
619
+
620
+ // Prevent the page from going blank by showing error content
621
+ const contentDiv = document.getElementById('content');
622
+ if (contentDiv && contentDiv.innerHTML.trim() === '') {
623
+ contentDiv.innerHTML = `
624
+ <h2>Application Error</h2>
625
+ <p>An error occurred but the application is still running:</p>
626
+ <div class="status">
627
+ <strong>Error:</strong> ${event.error.message}<br>
628
+ <strong>File:</strong> ${event.filename}<br>
629
+ <strong>Line:</strong> ${event.lineno}
630
+ </div>
631
+ <button class="button" onclick="loadPage('home')">Go to Home</button>
632
+ <button class="button secondary" onclick="location.reload()">Reload Page</button>
633
+ `;
634
+ }
635
+ });
636
+
637
+ // Add unhandled promise rejection handler
638
+ window.addEventListener('unhandledrejection', function(event) {
639
+ console.error('Unhandled promise rejection:', event.reason);
640
+ addLog('Unhandled promise rejection: ' + event.reason);
641
+ event.preventDefault(); // Prevent the default browser behavior
642
+ });
643
+
644
+ // Add periodic health check
645
+ setInterval(() => {
646
+ try {
647
+ const contentDiv = document.getElementById('content');
648
+ if (contentDiv && contentDiv.innerHTML.trim() === '') {
649
+ console.warn('Content div is empty, attempting to restore...');
650
+ loadPage('home');
651
+ }
652
+ } catch (error) {
653
+ console.error('Health check error:', error);
654
+ }
655
+ }, 5000); // Check every 5 seconds
592
656
  </script>
593
657
  </body>
594
658
  </html>
package/src/api.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { logError, logInfo } from './utils/logger';
2
2
 
3
- export const MAX_CHUNK_SIZE_BYTES = 1024 * 1024 * 10; // 10MB chunk size
3
+ export const MAX_CHUNK_SIZE_BYTES = 1024 * 1024; // 1MB chunk size - more conservative
4
4
 
5
5
  export function isChunkSizeExceeded(currentChunk: any[], newEvent: any, sessionId: string): boolean {
6
6
  const nextChunkSize = new TextEncoder().encode(JSON.stringify({
@@ -18,10 +18,62 @@ export function validateSingleEventSize(event: any, sessionId: string): void {
18
18
  })).length;
19
19
 
20
20
  if (singleEventSize > MAX_CHUNK_SIZE_BYTES) {
21
- throw new Error(`Single event size (${singleEventSize} bytes) exceeds maximum chunk size (${MAX_CHUNK_SIZE_BYTES} bytes)`);
21
+ // Instead of throwing, log a warning and suggest reducing event size
22
+ console.warn(`Single event size (${singleEventSize} bytes) exceeds maximum chunk size (${MAX_CHUNK_SIZE_BYTES} bytes). Consider reducing event data size.`);
22
23
  }
23
24
  }
24
25
 
26
+ export function splitLargeEvent(event: any, sessionId: string): any[] {
27
+ const eventSize = new TextEncoder().encode(JSON.stringify({
28
+ sessionId,
29
+ events: [event]
30
+ })).length;
31
+
32
+ if (eventSize <= MAX_CHUNK_SIZE_BYTES) {
33
+ return [event];
34
+ }
35
+
36
+ // If event is too large, try to split it by removing large properties
37
+ const simplifiedEvent = { ...event };
38
+
39
+ // Remove potentially large properties
40
+ const largeProperties = ['screenshot', 'html', 'dom', 'fullText', 'innerHTML', 'outerHTML'];
41
+ largeProperties.forEach(prop => {
42
+ if (simplifiedEvent[prop]) {
43
+ delete simplifiedEvent[prop];
44
+ }
45
+ });
46
+
47
+ // Check if simplified event is now small enough
48
+ const simplifiedSize = new TextEncoder().encode(JSON.stringify({
49
+ sessionId,
50
+ events: [simplifiedEvent]
51
+ })).length;
52
+
53
+ if (simplifiedSize <= MAX_CHUNK_SIZE_BYTES) {
54
+ return [simplifiedEvent];
55
+ }
56
+
57
+ // If still too large, create a minimal event
58
+ const minimalEvent = {
59
+ type: event.type,
60
+ timestamp: event.timestamp,
61
+ url: event.url,
62
+ pathname: event.pathname,
63
+ // Keep only essential properties
64
+ ...Object.fromEntries(
65
+ Object.entries(event).filter(([key, value]) =>
66
+ !largeProperties.includes(key) &&
67
+ typeof value !== 'object' &&
68
+ typeof value !== 'string' ||
69
+ (typeof value === 'string' && value.length < 1000)
70
+ )
71
+ )
72
+ };
73
+
74
+ return [minimalEvent];
75
+ }
76
+
25
77
  export class HumanBehaviorAPI {
26
78
  private apiKey: string;
27
79
  private baseUrl: string;
@@ -98,7 +150,7 @@ export class HumanBehaviorAPI {
98
150
  }
99
151
  }
100
152
 
101
- async sendEventsChunked(events: any[], sessionId: string) {
153
+ async sendEventsChunked(events: any[], sessionId: string, userId?: string) {
102
154
  try {
103
155
  const results = [];
104
156
  let currentChunk: any[] = [];
@@ -115,7 +167,8 @@ export class HumanBehaviorAPI {
115
167
  },
116
168
  body: JSON.stringify({
117
169
  sessionId,
118
- events: currentChunk
170
+ events: currentChunk,
171
+ endUserId: userId
119
172
  })
120
173
  });
121
174
 
@@ -127,11 +180,11 @@ export class HumanBehaviorAPI {
127
180
  currentChunk = [];
128
181
  }
129
182
 
130
- // Validate single event size
131
- validateSingleEventSize(event, sessionId);
183
+ // Handle large events by splitting them
184
+ const splitEvents = splitLargeEvent(event, sessionId);
132
185
 
133
- // Start new chunk with this event
134
- currentChunk = [event];
186
+ // Start new chunk with the split events
187
+ currentChunk = splitEvents;
135
188
  } else {
136
189
  // Add event to current chunk
137
190
  currentChunk.push(event);
@@ -148,7 +201,8 @@ export class HumanBehaviorAPI {
148
201
  },
149
202
  body: JSON.stringify({
150
203
  sessionId,
151
- events: currentChunk
204
+ events: currentChunk,
205
+ endUserId: userId
152
206
  })
153
207
  });
154
208
 
@@ -256,7 +310,7 @@ export class HumanBehaviorAPI {
256
310
  });
257
311
 
258
312
  if (!response.ok) {
259
- throw new Error(`Failed to send custom event: ${response.statusText}`);
313
+ throw new Error(`Failed to send custom event: ${response.status} ${response.statusText}`);
260
314
  }
261
315
 
262
316
  return await response.json();
@@ -114,7 +114,6 @@ export const HumanBehaviorProvider = ({ apiKey, client, children, options }: Hum
114
114
 
115
115
  // Wait for initialization to complete
116
116
  tracker.initializationPromise?.then(async () => {
117
- // Start the tracker to enable event flushing and recording
118
117
  await tracker.start();
119
118
  setIsInitialized(true);
120
119
 
package/src/tracker.ts CHANGED
@@ -18,8 +18,6 @@ declare global {
18
18
  export class HumanBehaviorTracker {
19
19
  private eventIngestionQueue: any[] = [];
20
20
  private queueSizeBytes: number = 0;
21
- private rejectedEvents: any[] = [];
22
- private isProcessingRejectedEvents: boolean = false;
23
21
 
24
22
  private sessionId!: string;
25
23
  private userProperties: Record<string, any> = {};
@@ -183,7 +181,6 @@ export class HumanBehaviorTracker {
183
181
  if (isBrowser) {
184
182
  this.setupPageUnloadHandler();
185
183
  this.setupNavigationTracking();
186
- this.processRejectedEvents();
187
184
  } else {
188
185
  logWarn('HumanBehaviorTracker initialized in a non-browser environment. Session tracking is disabled.');
189
186
  }
@@ -349,10 +346,21 @@ export class HumanBehaviorTracker {
349
346
  await this.api.sendCustomEvent(this.sessionId, eventName, properties);
350
347
 
351
348
  logDebug(`Custom event tracked: ${eventName}`, properties);
352
- } catch (error) {
349
+ } catch (error: any) {
353
350
  logError('Failed to track custom event:', error);
354
351
 
355
- // Fallback: add to event stream if direct API call fails
352
+ // Handle specific error types - check for any custom event failure
353
+ if (error.message?.includes('500') ||
354
+ error.message?.includes('Internal Server Error') ||
355
+ error.message?.includes('Failed to send custom event')) {
356
+ logWarn('Custom event endpoint failed, using fallback');
357
+ } else if (error.message?.includes('ERR_BLOCKED_BY_CLIENT')) {
358
+ logWarn('Custom event request blocked by ad blocker, using fallback');
359
+ } else if (error.message?.includes('Failed to fetch')) {
360
+ logWarn('Custom event network error, using fallback');
361
+ }
362
+
363
+ // Always try fallback for any custom event error
356
364
  try {
357
365
  const customEventData = {
358
366
  eventName: eventName,
@@ -792,33 +800,6 @@ export class HumanBehaviorTracker {
792
800
  this.queueSizeBytes += eventSize;
793
801
  }
794
802
 
795
- private async processRejectedEvents() {
796
- if (this.isProcessingRejectedEvents || this.rejectedEvents.length === 0) return;
797
-
798
- this.isProcessingRejectedEvents = true;
799
- try {
800
- // Create a new session ID for rejected events
801
- const newSessionId = uuidv1();
802
- if (isBrowser) {
803
- localStorage.setItem('human_behavior_session_id', newSessionId);
804
- }
805
-
806
- // Try to send rejected events with new session ID using beacon
807
- // sendBeacon returns true if the request was queued successfully
808
- this.api.sendBeaconEvents(this.rejectedEvents, newSessionId);
809
-
810
- // Clear rejected events and update session ID
811
- // Note: We can't verify if the beacon data was actually sent,
812
- // but we clear the events to prevent duplicate sending attempts
813
- this.rejectedEvents = [];
814
- this.sessionId = newSessionId;
815
- } catch (error) {
816
- logError('Failed to process rejected events:', error);
817
- } finally {
818
- this.isProcessingRejectedEvents = false;
819
- }
820
- }
821
-
822
803
  private async flush() {
823
804
  // Prevent concurrent flushes
824
805
  if (this.isProcessing || !this.initialized) {
@@ -835,20 +816,18 @@ export class HumanBehaviorTracker {
835
816
  if (eventsToProcess.length > 0) {
836
817
  logDebug('Flushing events:', eventsToProcess);
837
818
  try {
838
- await this.api.sendEvents(eventsToProcess, this.sessionId, this.endUserId!);
819
+ // Use chunked sending to handle large payloads
820
+ await this.api.sendEventsChunked(eventsToProcess, this.sessionId, this.endUserId!);
839
821
  } catch (error: any) {
840
- // If we get a 400 error, store events for retry
822
+ // Handle specific error types with graceful degradation
841
823
  if (error.message?.includes('ERROR: Session already completed')) {
842
- logInfo('Session expired, storing events for retry');
843
- this.rejectedEvents.push(...eventsToProcess);
844
- this.processRejectedEvents();
824
+ logWarn('Session expired, events will be lost');
825
+ } else if (error.message?.includes('413') || error.message?.includes('Content Too Large')) {
826
+ logWarn('Payload too large, events will be lost');
845
827
  } else if (error.message?.includes('ERR_BLOCKED_BY_CLIENT') ||
846
828
  error.message?.includes('Failed to fetch') ||
847
829
  error.message?.includes('NetworkError')) {
848
- // Handle ad blocker or network issues gracefully
849
- logWarn('Request blocked by ad blocker or network issue, storing events for retry');
850
- this.rejectedEvents.push(...eventsToProcess);
851
- // Don't process rejected events immediately to avoid spam
830
+ logWarn('Request blocked by ad blocker or network issue, events will be lost');
852
831
  } else {
853
832
  throw error;
854
833
  }
@@ -1005,8 +984,8 @@ export class HumanBehaviorTracker {
1005
984
  const recommendations: string[] = [];
1006
985
  let blocked = false;
1007
986
 
1008
- // Check if we have rejected events (might indicate blocking)
1009
- if (this.rejectedEvents.length > 0) {
987
+ // Check if we have queued events (might indicate blocking)
988
+ if (this.eventIngestionQueue.length > 0) {
1010
989
  blocked = true;
1011
990
  recommendations.push('Some requests may be blocked by ad blockers');
1012
991
  }