humanbehavior-js 0.4.20 → 0.4.22

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.
Files changed (53) hide show
  1. package/dist/cjs/angular/index.cjs +817 -19
  2. package/dist/cjs/angular/index.cjs.map +1 -1
  3. package/dist/cjs/index.cjs +833 -19
  4. package/dist/cjs/index.cjs.map +1 -1
  5. package/dist/cjs/react/index.cjs +818 -20
  6. package/dist/cjs/react/index.cjs.map +1 -1
  7. package/dist/cjs/remix/index.cjs +818 -20
  8. package/dist/cjs/remix/index.cjs.map +1 -1
  9. package/dist/cjs/svelte/index.cjs +817 -19
  10. package/dist/cjs/svelte/index.cjs.map +1 -1
  11. package/dist/cjs/vue/index.cjs +817 -19
  12. package/dist/cjs/vue/index.cjs.map +1 -1
  13. package/dist/esm/angular/index.js +817 -19
  14. package/dist/esm/angular/index.js.map +1 -1
  15. package/dist/esm/index.js +825 -20
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/react/index.js +818 -20
  18. package/dist/esm/react/index.js.map +1 -1
  19. package/dist/esm/remix/index.js +818 -20
  20. package/dist/esm/remix/index.js.map +1 -1
  21. package/dist/esm/svelte/index.js +817 -19
  22. package/dist/esm/svelte/index.js.map +1 -1
  23. package/dist/esm/vue/index.js +817 -19
  24. package/dist/esm/vue/index.js.map +1 -1
  25. package/dist/index.min.js +1 -1
  26. package/dist/index.min.js.map +1 -1
  27. package/dist/types/angular/index.d.ts +60 -1
  28. package/dist/types/index.d.ts +258 -3
  29. package/dist/types/react/index.d.ts +60 -1
  30. package/dist/types/remix/index.d.ts +60 -1
  31. package/dist/types/svelte/index.d.ts +60 -1
  32. package/package/canvas-recording-demo.html +1 -1
  33. package/package/simple-spa.html +1 -1
  34. package/package/src/angular/index.ts +3 -3
  35. package/package/src/react/index.tsx +2 -2
  36. package/package/src/svelte/index.ts +1 -1
  37. package/package/src/tracker.ts +2 -2
  38. package/package/src/vue/index.ts +1 -1
  39. package/package.json +1 -1
  40. package/simple-spa.html +164 -2
  41. package/src/angular/index.ts +3 -3
  42. package/src/api.ts +40 -0
  43. package/src/index.ts +7 -0
  44. package/src/react/index.tsx +2 -2
  45. package/src/svelte/index.ts +1 -1
  46. package/src/tracker.ts +193 -17
  47. package/src/utils/ip-detector.ts +158 -0
  48. package/src/utils/property-detector.ts +345 -0
  49. package/src/utils/property-manager.ts +274 -0
  50. package/src/vue/index.ts +1 -1
  51. package/canvas-recording-demo.html +0 -143
  52. package/clean-console-demo.html +0 -39
  53. package/simple-demo.html +0 -26
package/src/tracker.ts CHANGED
@@ -4,6 +4,7 @@ import { v1 as uuidv1 } from 'uuid';
4
4
  import { HumanBehaviorAPI } from './api';
5
5
  import { RedactionManager, RedactionOptions } from './redact';
6
6
  import { logger, logError, logWarn, logInfo, logDebug } from './utils/logger';
7
+ import { PropertyManager, Properties } from './utils/property-manager';
7
8
 
8
9
  // Check if we're in a browser environment
9
10
  const isBrowser = typeof window !== 'undefined';
@@ -29,6 +30,7 @@ export class HumanBehaviorTracker {
29
30
  private initialized: boolean = false;
30
31
  public initializationPromise: Promise<void> | null = null;
31
32
  private redactionManager!: RedactionManager;
33
+ private propertyManager!: PropertyManager;
32
34
 
33
35
  // Console tracking properties
34
36
  private originalConsole: {
@@ -62,7 +64,9 @@ export class HumanBehaviorTracker {
62
64
  redactFields?: string[];
63
65
  enableAutomaticTracking?: boolean;
64
66
  suppressConsoleErrors?: boolean; // New option to control error suppression
65
- recordCanvas?: boolean; // Enable canvas recording with PostHog-style protection
67
+ recordCanvas?: boolean; // Enable canvas recording with protection
68
+ enableAutomaticProperties?: boolean; // Enable automatic property detection
69
+ propertyDenylist?: string[]; // Properties to exclude from tracking
66
70
  automaticTrackingOptions?: {
67
71
  trackButtons?: boolean;
68
72
  trackLinks?: boolean;
@@ -73,7 +77,7 @@ export class HumanBehaviorTracker {
73
77
  }): HumanBehaviorTracker {
74
78
  // ✅ SUPPRESS COMMON RRWEB ERRORS FOR CLEAN CONSOLE
75
79
  if (isBrowser && options?.suppressConsoleErrors !== false) {
76
- // Suppress canvas security errors
80
+ // Suppress canvas security errors and network errors
77
81
  const originalConsoleError = console.error;
78
82
  console.error = (...args: any[]) => {
79
83
  const message = args.join(' ');
@@ -85,9 +89,15 @@ export class HumanBehaviorTracker {
85
89
  message.includes('CORS') ||
86
90
  message.includes('Access-Control-Allow-Origin') ||
87
91
  message.includes('Failed to load resource') ||
88
- message.includes('net::ERR_BLOCKED_BY_CLIENT')
92
+ message.includes('net::ERR_BLOCKED_BY_CLIENT') ||
93
+ message.includes('NetworkError when attempting to fetch resource') ||
94
+ message.includes('Failed to fetch') ||
95
+ message.includes('TypeError: NetworkError') ||
96
+ message.includes('HumanBehavior ERROR') ||
97
+ message.includes('Failed to track custom event') ||
98
+ message.includes('Error sending custom event')
89
99
  ) {
90
- // Silently suppress these common rrweb errors
100
+ // Silently suppress these common errors
91
101
  return;
92
102
  }
93
103
  originalConsoleError.apply(console, args);
@@ -103,9 +113,13 @@ export class HumanBehaviorTracker {
103
113
  message.includes('CORS') ||
104
114
  message.includes('Access-Control-Allow-Origin') ||
105
115
  message.includes('Failed to load resource') ||
106
- message.includes('net::ERR_BLOCKED_BY_CLIENT')
116
+ message.includes('net::ERR_BLOCKED_BY_CLIENT') ||
117
+ message.includes('NetworkError when attempting to fetch resource') ||
118
+ message.includes('Failed to fetch') ||
119
+ message.includes('Custom event network error') ||
120
+ message.includes('Request blocked by ad blocker')
107
121
  ) {
108
- // Silently suppress these common rrweb warnings
122
+ // Silently suppress these common warnings
109
123
  return;
110
124
  }
111
125
  originalConsoleWarn.apply(console, args);
@@ -119,7 +133,9 @@ export class HumanBehaviorTracker {
119
133
  message.includes('Tainted canvases') ||
120
134
  message.includes('toDataURL') ||
121
135
  message.includes('Cross-Origin') ||
122
- message.includes('CORS')
136
+ message.includes('CORS') ||
137
+ message.includes('NetworkError') ||
138
+ message.includes('Failed to fetch')
123
139
  ) {
124
140
  event.preventDefault();
125
141
  return false;
@@ -138,7 +154,10 @@ export class HumanBehaviorTracker {
138
154
  }
139
155
 
140
156
  // Create new tracker instance
141
- const tracker = new HumanBehaviorTracker(apiKey, options?.ingestionUrl);
157
+ const tracker = new HumanBehaviorTracker(apiKey, options?.ingestionUrl, {
158
+ enableAutomaticProperties: options?.enableAutomaticProperties,
159
+ propertyDenylist: options?.propertyDenylist
160
+ });
142
161
 
143
162
  // Store canvas recording preference
144
163
  tracker.recordCanvas = options?.recordCanvas ?? false;
@@ -161,7 +180,10 @@ export class HumanBehaviorTracker {
161
180
  return tracker;
162
181
  }
163
182
 
164
- constructor(apiKey: string | undefined, ingestionUrl?: string) {
183
+ constructor(apiKey: string | undefined, ingestionUrl?: string, options?: {
184
+ enableAutomaticProperties?: boolean;
185
+ propertyDenylist?: string[];
186
+ }) {
165
187
  if (!apiKey) {
166
188
  throw new Error('Human Behavior API Key is required');
167
189
  }
@@ -176,6 +198,12 @@ export class HumanBehaviorTracker {
176
198
  });
177
199
  this.apiKey = apiKey;
178
200
  this.redactionManager = new RedactionManager();
201
+
202
+ // Initialize property manager
203
+ this.propertyManager = new PropertyManager({
204
+ enableAutomaticProperties: options?.enableAutomaticProperties !== false,
205
+ propertyDenylist: options?.propertyDenylist || []
206
+ });
179
207
 
180
208
  // Handle session restoration with improved continuity
181
209
  if (isBrowser) {
@@ -217,7 +245,31 @@ export class HumanBehaviorTracker {
217
245
  const userId = this.getCookie(`human_behavior_end_user_id_${this.apiKey}`);
218
246
  logDebug(`Initializing with sessionId: ${this.sessionId}, userId: ${userId}`);
219
247
 
220
- const { sessionId, endUserId } = await this.api.init(this.sessionId, userId);
248
+ // Get automatic properties for init
249
+ const automaticProperties = this.propertyManager.getAutomaticProperties();
250
+
251
+ // Create a custom init request with automatic properties
252
+ const initResponse = await fetch(`${this.api['baseUrl']}/api/ingestion/init`, {
253
+ method: 'POST',
254
+ headers: {
255
+ 'Content-Type': 'application/json',
256
+ 'Authorization': `Bearer ${this.apiKey}`,
257
+ 'Referer': document.referrer || ''
258
+ },
259
+ body: JSON.stringify({
260
+ sessionId: this.sessionId,
261
+ endUserId: userId,
262
+ entryURL: window.location.href,
263
+ referrer: document.referrer,
264
+ automaticProperties: automaticProperties
265
+ })
266
+ });
267
+
268
+ if (!initResponse.ok) {
269
+ throw new Error(`Failed to initialize: ${initResponse.statusText}`);
270
+ }
271
+
272
+ const { sessionId, endUserId } = await initResponse.json();
221
273
 
222
274
  // Check if server returned a different session ID (for session continuity)
223
275
  if (sessionId !== this.sessionId) {
@@ -232,6 +284,11 @@ export class HumanBehaviorTracker {
232
284
  this.endUserId = endUserId;
233
285
  this.setCookie(`human_behavior_end_user_id_${this.apiKey}`, endUserId, 365);
234
286
 
287
+ // Send IP information after successful initialization
288
+ this.api.sendIPInfo(this.sessionId).catch(error => {
289
+ logWarn('Failed to send IP info:', error);
290
+ });
291
+
235
292
  // Only setup browser-specific handlers when in browser environment
236
293
  if (isBrowser) {
237
294
  this.setupPageUnloadHandler();
@@ -368,6 +425,9 @@ export class HumanBehaviorTracker {
368
425
  public async trackPageView(url?: string): Promise<void> {
369
426
  if (!this.initialized) return;
370
427
 
428
+ // Update automatic properties for new page
429
+ this.propertyManager.updateAutomaticProperties();
430
+
371
431
  try {
372
432
  const pageViewData = {
373
433
  url: url || window.location.href,
@@ -378,13 +438,16 @@ export class HumanBehaviorTracker {
378
438
  timestamp: new Date().toISOString()
379
439
  };
380
440
 
441
+ // Get enhanced properties with automatic properties
442
+ const enhancedProperties = this.propertyManager.getEventProperties(pageViewData);
443
+
381
444
  // Add pageview event to the main event stream
382
445
  await this.addEvent({
383
446
  type: 5, // Custom event type
384
447
  data: {
385
448
  payload: {
386
449
  eventType: 'pageview',
387
- ...pageViewData
450
+ ...enhancedProperties
388
451
  }
389
452
  },
390
453
  timestamp: Date.now()
@@ -399,11 +462,14 @@ export class HumanBehaviorTracker {
399
462
  public async customEvent(eventName: string, properties?: Record<string, any>): Promise<void> {
400
463
  if (!this.initialized) return;
401
464
 
465
+ // Get enhanced properties with automatic properties
466
+ const enhancedProperties = this.propertyManager.getEventProperties(properties);
467
+
402
468
  try {
403
469
  // Send custom event directly to the API
404
- await this.api.sendCustomEvent(this.sessionId, eventName, properties);
470
+ await this.api.sendCustomEvent(this.sessionId, eventName, enhancedProperties);
405
471
 
406
- logDebug(`Custom event tracked: ${eventName}`, properties);
472
+ logDebug(`Custom event tracked: ${eventName}`, enhancedProperties);
407
473
  } catch (error: any) {
408
474
  logError('Failed to track custom event:', error);
409
475
 
@@ -422,7 +488,7 @@ export class HumanBehaviorTracker {
422
488
  try {
423
489
  const customEventData = {
424
490
  eventName: eventName,
425
- properties: properties || {},
491
+ properties: enhancedProperties || {},
426
492
  timestamp: new Date().toISOString(),
427
493
  url: window.location.href,
428
494
  pathname: window.location.pathname
@@ -792,8 +858,27 @@ export class HumanBehaviorTracker {
792
858
 
793
859
  logDebug('Identifying user:', { userProperties, originalEndUserId, sessionId: this.sessionId });
794
860
 
795
- // Send user data with the original endUserId
796
- await this.api.sendUserData(originalEndUserId!, userProperties, this.sessionId);
861
+ // Get automatic properties and send with user data
862
+ const automaticProperties = this.propertyManager.getAutomaticProperties();
863
+
864
+ // Create a custom user request with automatic properties
865
+ const userResponse = await fetch(`${this.api['baseUrl']}/api/ingestion/user`, {
866
+ method: 'POST',
867
+ headers: {
868
+ 'Content-Type': 'application/json',
869
+ 'Authorization': `Bearer ${this.apiKey}`
870
+ },
871
+ body: JSON.stringify({
872
+ userId: originalEndUserId,
873
+ userAttributes: userProperties,
874
+ sessionId: this.sessionId,
875
+ automaticProperties: automaticProperties
876
+ })
877
+ });
878
+
879
+ if (!userResponse.ok) {
880
+ throw new Error(`Failed to identify user: ${userResponse.statusText}`);
881
+ }
797
882
 
798
883
  // Don't update endUserId - keep it as the original UUID
799
884
 
@@ -847,7 +932,7 @@ export class HumanBehaviorTracker {
847
932
  inlineStylesheet: true, // Keep styles for proper session replay
848
933
  recordCrossOriginIframes: false, // Prevent cross-origin iframe errors
849
934
 
850
- // ✅ CANVAS RECORDING - PostHog-style protection against overwhelm
935
+ // ✅ CANVAS RECORDING - protection against overwhelm
851
936
  recordCanvas: this.recordCanvas, // Opt-in only
852
937
  sampling: this.recordCanvas ? { canvas: 4 } : undefined, // 4 FPS throttle
853
938
  dataURLOptions: this.recordCanvas ? {
@@ -1309,6 +1394,97 @@ export class HumanBehaviorTracker {
1309
1394
  initialized: this.initialized
1310
1395
  };
1311
1396
  }
1397
+
1398
+ // ===== PROPERTY MANAGEMENT METHODS =====
1399
+
1400
+ /**
1401
+ * Set a session property that will be included in all events for this session
1402
+ */
1403
+ public setSessionProperty(key: string, value: any): void {
1404
+ this.propertyManager.setSessionProperty(key, value);
1405
+ }
1406
+
1407
+ /**
1408
+ * Set multiple session properties
1409
+ */
1410
+ public setSessionProperties(properties: Record<string, any>): void {
1411
+ this.propertyManager.setSessionProperties(properties);
1412
+ }
1413
+
1414
+ /**
1415
+ * Get a session property
1416
+ */
1417
+ public getSessionProperty(key: string): any {
1418
+ return this.propertyManager.getSessionProperty(key);
1419
+ }
1420
+
1421
+ /**
1422
+ * Remove a session property
1423
+ */
1424
+ public removeSessionProperty(key: string): void {
1425
+ this.propertyManager.removeSessionProperty(key);
1426
+ }
1427
+
1428
+ /**
1429
+ * Set a user property that will be included in all events
1430
+ */
1431
+ public setUserProperty(key: string, value: any): void {
1432
+ this.propertyManager.setUserProperty(key, value);
1433
+ }
1434
+
1435
+ /**
1436
+ * Set multiple user properties
1437
+ */
1438
+ public setUserProperties(properties: Record<string, any>): void {
1439
+ this.propertyManager.setUserProperties(properties);
1440
+ }
1441
+
1442
+ /**
1443
+ * Get a user property
1444
+ */
1445
+ public getUserProperty(key: string): any {
1446
+ return this.propertyManager.getUserProperty(key);
1447
+ }
1448
+
1449
+ /**
1450
+ * Remove a user property
1451
+ */
1452
+ public removeUserProperty(key: string): void {
1453
+ this.propertyManager.removeUserProperty(key);
1454
+ }
1455
+
1456
+ /**
1457
+ * Set a property only if it hasn't been set before
1458
+ */
1459
+ public setOnce(key: string, value: any, scope: 'session' | 'user' = 'user'): void {
1460
+ this.propertyManager.setOnce(key, value, scope);
1461
+ }
1462
+
1463
+ /**
1464
+ * Clear all session properties
1465
+ */
1466
+ public clearSessionProperties(): void {
1467
+ this.propertyManager.clearSessionProperties();
1468
+ }
1469
+
1470
+ /**
1471
+ * Clear all user properties
1472
+ */
1473
+ public clearUserProperties(): void {
1474
+ this.propertyManager.clearUserProperties();
1475
+ }
1476
+
1477
+ /**
1478
+ * Get all properties for debugging
1479
+ */
1480
+ public getAllProperties(): {
1481
+ automatic: Record<string, any>;
1482
+ session: Record<string, any>;
1483
+ user: Record<string, any>;
1484
+ initial: Record<string, any>;
1485
+ } {
1486
+ return this.propertyManager.getAllProperties();
1487
+ }
1312
1488
  }
1313
1489
 
1314
1490
  // Only expose to window object in browser environments
@@ -0,0 +1,158 @@
1
+ /**
2
+ * IP Address Detection Utility
3
+ * Attempts to get the client's public IP address using multiple methods
4
+ */
5
+
6
+ export interface IPInfo {
7
+ ip: string;
8
+ method: 'stun' | 'public-service' | 'fallback';
9
+ timestamp: number;
10
+ }
11
+
12
+ /**
13
+ * Get IP address using STUN server (most reliable)
14
+ */
15
+ async function getIPFromSTUN(): Promise<string | null> {
16
+ try {
17
+ const pc = new RTCPeerConnection({
18
+ iceServers: [
19
+ { urls: 'stun:stun.l.google.com:19302' },
20
+ { urls: 'stun:stun1.l.google.com:19302' },
21
+ { urls: 'stun:stun2.l.google.com:19302' }
22
+ ]
23
+ });
24
+
25
+ return new Promise((resolve) => {
26
+ const timeout = setTimeout(() => {
27
+ pc.close();
28
+ resolve(null);
29
+ }, 5000);
30
+
31
+ pc.createDataChannel('');
32
+ pc.createOffer()
33
+ .then(offer => pc.setLocalDescription(offer))
34
+ .catch(() => resolve(null));
35
+
36
+ pc.onicecandidate = (event) => {
37
+ if (event.candidate) {
38
+ const candidate = event.candidate.candidate;
39
+ const match = candidate.match(/([0-9]{1,3}(\.[0-9]{1,3}){3})/);
40
+ if (match) {
41
+ clearTimeout(timeout);
42
+ pc.close();
43
+ resolve(match[1]);
44
+ }
45
+ }
46
+ };
47
+ });
48
+ } catch (error) {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Get IP address using public IP service (fallback)
55
+ */
56
+ async function getIPFromPublicService(): Promise<string | null> {
57
+ try {
58
+ const response = await fetch('https://api.ipify.org?format=json', {
59
+ method: 'GET'
60
+ });
61
+
62
+ if (response.ok) {
63
+ const data = await response.json();
64
+ return data.ip;
65
+ }
66
+ } catch (error) {
67
+ // Try alternative service
68
+ try {
69
+ const response = await fetch('https://httpbin.org/ip', {
70
+ method: 'GET'
71
+ });
72
+
73
+ if (response.ok) {
74
+ const data = await response.json();
75
+ return data.origin;
76
+ }
77
+ } catch (fallbackError) {
78
+ // Last resort
79
+ try {
80
+ const response = await fetch('https://api.myip.com', {
81
+ method: 'GET'
82
+ });
83
+
84
+ if (response.ok) {
85
+ const data = await response.json();
86
+ return data.ip;
87
+ }
88
+ } catch (lastError) {
89
+ return null;
90
+ }
91
+ }
92
+ }
93
+
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Get client's public IP address
99
+ * Tries STUN first (most reliable), then falls back to public services
100
+ */
101
+ export async function getClientIP(): Promise<IPInfo | null> {
102
+ const startTime = Date.now();
103
+
104
+ // Try STUN first (most reliable and privacy-friendly)
105
+ const stunIP = await getIPFromSTUN();
106
+ if (stunIP) {
107
+ return {
108
+ ip: stunIP,
109
+ method: 'stun',
110
+ timestamp: startTime
111
+ };
112
+ }
113
+
114
+ // Fallback to public IP service
115
+ const publicIP = await getIPFromPublicService();
116
+ if (publicIP) {
117
+ return {
118
+ ip: publicIP,
119
+ method: 'public-service',
120
+ timestamp: startTime
121
+ };
122
+ }
123
+
124
+ return null;
125
+ }
126
+
127
+ /**
128
+ * Get IP address with caching to avoid repeated requests
129
+ */
130
+ let cachedIP: IPInfo | null = null;
131
+ let cacheTimestamp = 0;
132
+ const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
133
+
134
+ export async function getCachedIP(): Promise<IPInfo | null> {
135
+ const now = Date.now();
136
+
137
+ // Return cached IP if still valid
138
+ if (cachedIP && (now - cacheTimestamp) < CACHE_DURATION) {
139
+ return cachedIP;
140
+ }
141
+
142
+ // Get fresh IP
143
+ const ipInfo = await getClientIP();
144
+ if (ipInfo) {
145
+ cachedIP = ipInfo;
146
+ cacheTimestamp = now;
147
+ }
148
+
149
+ return ipInfo;
150
+ }
151
+
152
+ /**
153
+ * Clear IP cache (useful for testing or when network changes)
154
+ */
155
+ export function clearIPCache(): void {
156
+ cachedIP = null;
157
+ cacheTimestamp = 0;
158
+ }