humanbehavior-js 0.4.19 → 0.4.21

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 (66) hide show
  1. package/dist/cjs/angular/index.cjs +799 -13
  2. package/dist/cjs/angular/index.cjs.map +1 -1
  3. package/dist/cjs/index.cjs +815 -13
  4. package/dist/cjs/index.cjs.map +1 -1
  5. package/dist/cjs/install-wizard.cjs +56 -10
  6. package/dist/cjs/install-wizard.cjs.map +1 -1
  7. package/dist/cjs/react/index.cjs +800 -14
  8. package/dist/cjs/react/index.cjs.map +1 -1
  9. package/dist/cjs/remix/index.cjs +800 -14
  10. package/dist/cjs/remix/index.cjs.map +1 -1
  11. package/dist/cjs/svelte/index.cjs +799 -13
  12. package/dist/cjs/svelte/index.cjs.map +1 -1
  13. package/dist/cjs/vue/index.cjs +799 -13
  14. package/dist/cjs/vue/index.cjs.map +1 -1
  15. package/dist/cjs/wizard/index.cjs +56 -10
  16. package/dist/cjs/wizard/index.cjs.map +1 -1
  17. package/dist/cli/ai-auto-install.js +56 -10
  18. package/dist/cli/ai-auto-install.js.map +1 -1
  19. package/dist/cli/auto-install.js +56 -10
  20. package/dist/cli/auto-install.js.map +1 -1
  21. package/dist/esm/angular/index.js +799 -13
  22. package/dist/esm/angular/index.js.map +1 -1
  23. package/dist/esm/index.js +807 -14
  24. package/dist/esm/index.js.map +1 -1
  25. package/dist/esm/install-wizard.js +56 -10
  26. package/dist/esm/install-wizard.js.map +1 -1
  27. package/dist/esm/react/index.js +800 -14
  28. package/dist/esm/react/index.js.map +1 -1
  29. package/dist/esm/remix/index.js +800 -14
  30. package/dist/esm/remix/index.js.map +1 -1
  31. package/dist/esm/svelte/index.js +799 -13
  32. package/dist/esm/svelte/index.js.map +1 -1
  33. package/dist/esm/vue/index.js +799 -13
  34. package/dist/esm/vue/index.js.map +1 -1
  35. package/dist/esm/wizard/index.js +56 -10
  36. package/dist/esm/wizard/index.js.map +1 -1
  37. package/dist/index.min.js +1 -1
  38. package/dist/index.min.js.map +1 -1
  39. package/dist/types/angular/index.d.ts +60 -1
  40. package/dist/types/index.d.ts +258 -3
  41. package/dist/types/react/index.d.ts +60 -1
  42. package/dist/types/remix/index.d.ts +60 -1
  43. package/dist/types/svelte/index.d.ts +60 -1
  44. package/package/canvas-recording-demo.html +1 -1
  45. package/package/simple-spa.html +1 -1
  46. package/package/src/angular/index.ts +3 -3
  47. package/package/src/react/index.tsx +2 -2
  48. package/package/src/svelte/index.ts +1 -1
  49. package/package/src/tracker.ts +2 -2
  50. package/package/src/vue/index.ts +1 -1
  51. package/package.json +1 -1
  52. package/simple-spa.html +164 -2
  53. package/src/angular/index.ts +3 -3
  54. package/src/api.ts +40 -0
  55. package/src/index.ts +7 -0
  56. package/src/react/index.tsx +2 -2
  57. package/src/svelte/index.ts +1 -1
  58. package/src/tracker.ts +175 -11
  59. package/src/utils/ip-detector.ts +158 -0
  60. package/src/utils/property-detector.ts +345 -0
  61. package/src/utils/property-manager.ts +274 -0
  62. package/src/vue/index.ts +1 -1
  63. package/src/wizard/core/install-wizard.ts +60 -10
  64. package/canvas-recording-demo.html +0 -143
  65. package/clean-console-demo.html +0 -39
  66. 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;
@@ -138,7 +142,10 @@ export class HumanBehaviorTracker {
138
142
  }
139
143
 
140
144
  // Create new tracker instance
141
- const tracker = new HumanBehaviorTracker(apiKey, options?.ingestionUrl);
145
+ const tracker = new HumanBehaviorTracker(apiKey, options?.ingestionUrl, {
146
+ enableAutomaticProperties: options?.enableAutomaticProperties,
147
+ propertyDenylist: options?.propertyDenylist
148
+ });
142
149
 
143
150
  // Store canvas recording preference
144
151
  tracker.recordCanvas = options?.recordCanvas ?? false;
@@ -161,7 +168,10 @@ export class HumanBehaviorTracker {
161
168
  return tracker;
162
169
  }
163
170
 
164
- constructor(apiKey: string | undefined, ingestionUrl?: string) {
171
+ constructor(apiKey: string | undefined, ingestionUrl?: string, options?: {
172
+ enableAutomaticProperties?: boolean;
173
+ propertyDenylist?: string[];
174
+ }) {
165
175
  if (!apiKey) {
166
176
  throw new Error('Human Behavior API Key is required');
167
177
  }
@@ -176,6 +186,12 @@ export class HumanBehaviorTracker {
176
186
  });
177
187
  this.apiKey = apiKey;
178
188
  this.redactionManager = new RedactionManager();
189
+
190
+ // Initialize property manager
191
+ this.propertyManager = new PropertyManager({
192
+ enableAutomaticProperties: options?.enableAutomaticProperties !== false,
193
+ propertyDenylist: options?.propertyDenylist || []
194
+ });
179
195
 
180
196
  // Handle session restoration with improved continuity
181
197
  if (isBrowser) {
@@ -217,7 +233,31 @@ export class HumanBehaviorTracker {
217
233
  const userId = this.getCookie(`human_behavior_end_user_id_${this.apiKey}`);
218
234
  logDebug(`Initializing with sessionId: ${this.sessionId}, userId: ${userId}`);
219
235
 
220
- const { sessionId, endUserId } = await this.api.init(this.sessionId, userId);
236
+ // Get automatic properties for init
237
+ const automaticProperties = this.propertyManager.getAutomaticProperties();
238
+
239
+ // Create a custom init request with automatic properties
240
+ const initResponse = await fetch(`${this.api['baseUrl']}/api/ingestion/init`, {
241
+ method: 'POST',
242
+ headers: {
243
+ 'Content-Type': 'application/json',
244
+ 'Authorization': `Bearer ${this.apiKey}`,
245
+ 'Referer': document.referrer || ''
246
+ },
247
+ body: JSON.stringify({
248
+ sessionId: this.sessionId,
249
+ endUserId: userId,
250
+ entryURL: window.location.href,
251
+ referrer: document.referrer,
252
+ automaticProperties: automaticProperties
253
+ })
254
+ });
255
+
256
+ if (!initResponse.ok) {
257
+ throw new Error(`Failed to initialize: ${initResponse.statusText}`);
258
+ }
259
+
260
+ const { sessionId, endUserId } = await initResponse.json();
221
261
 
222
262
  // Check if server returned a different session ID (for session continuity)
223
263
  if (sessionId !== this.sessionId) {
@@ -232,6 +272,11 @@ export class HumanBehaviorTracker {
232
272
  this.endUserId = endUserId;
233
273
  this.setCookie(`human_behavior_end_user_id_${this.apiKey}`, endUserId, 365);
234
274
 
275
+ // Send IP information after successful initialization
276
+ this.api.sendIPInfo(this.sessionId).catch(error => {
277
+ logWarn('Failed to send IP info:', error);
278
+ });
279
+
235
280
  // Only setup browser-specific handlers when in browser environment
236
281
  if (isBrowser) {
237
282
  this.setupPageUnloadHandler();
@@ -368,6 +413,9 @@ export class HumanBehaviorTracker {
368
413
  public async trackPageView(url?: string): Promise<void> {
369
414
  if (!this.initialized) return;
370
415
 
416
+ // Update automatic properties for new page
417
+ this.propertyManager.updateAutomaticProperties();
418
+
371
419
  try {
372
420
  const pageViewData = {
373
421
  url: url || window.location.href,
@@ -378,13 +426,16 @@ export class HumanBehaviorTracker {
378
426
  timestamp: new Date().toISOString()
379
427
  };
380
428
 
429
+ // Get enhanced properties with automatic properties
430
+ const enhancedProperties = this.propertyManager.getEventProperties(pageViewData);
431
+
381
432
  // Add pageview event to the main event stream
382
433
  await this.addEvent({
383
434
  type: 5, // Custom event type
384
435
  data: {
385
436
  payload: {
386
437
  eventType: 'pageview',
387
- ...pageViewData
438
+ ...enhancedProperties
388
439
  }
389
440
  },
390
441
  timestamp: Date.now()
@@ -399,11 +450,14 @@ export class HumanBehaviorTracker {
399
450
  public async customEvent(eventName: string, properties?: Record<string, any>): Promise<void> {
400
451
  if (!this.initialized) return;
401
452
 
453
+ // Get enhanced properties with automatic properties
454
+ const enhancedProperties = this.propertyManager.getEventProperties(properties);
455
+
402
456
  try {
403
457
  // Send custom event directly to the API
404
- await this.api.sendCustomEvent(this.sessionId, eventName, properties);
458
+ await this.api.sendCustomEvent(this.sessionId, eventName, enhancedProperties);
405
459
 
406
- logDebug(`Custom event tracked: ${eventName}`, properties);
460
+ logDebug(`Custom event tracked: ${eventName}`, enhancedProperties);
407
461
  } catch (error: any) {
408
462
  logError('Failed to track custom event:', error);
409
463
 
@@ -422,7 +476,7 @@ export class HumanBehaviorTracker {
422
476
  try {
423
477
  const customEventData = {
424
478
  eventName: eventName,
425
- properties: properties || {},
479
+ properties: enhancedProperties || {},
426
480
  timestamp: new Date().toISOString(),
427
481
  url: window.location.href,
428
482
  pathname: window.location.pathname
@@ -792,8 +846,27 @@ export class HumanBehaviorTracker {
792
846
 
793
847
  logDebug('Identifying user:', { userProperties, originalEndUserId, sessionId: this.sessionId });
794
848
 
795
- // Send user data with the original endUserId
796
- await this.api.sendUserData(originalEndUserId!, userProperties, this.sessionId);
849
+ // Get automatic properties and send with user data
850
+ const automaticProperties = this.propertyManager.getAutomaticProperties();
851
+
852
+ // Create a custom user request with automatic properties
853
+ const userResponse = await fetch(`${this.api['baseUrl']}/api/ingestion/user`, {
854
+ method: 'POST',
855
+ headers: {
856
+ 'Content-Type': 'application/json',
857
+ 'Authorization': `Bearer ${this.apiKey}`
858
+ },
859
+ body: JSON.stringify({
860
+ userId: originalEndUserId,
861
+ userAttributes: userProperties,
862
+ sessionId: this.sessionId,
863
+ automaticProperties: automaticProperties
864
+ })
865
+ });
866
+
867
+ if (!userResponse.ok) {
868
+ throw new Error(`Failed to identify user: ${userResponse.statusText}`);
869
+ }
797
870
 
798
871
  // Don't update endUserId - keep it as the original UUID
799
872
 
@@ -847,7 +920,7 @@ export class HumanBehaviorTracker {
847
920
  inlineStylesheet: true, // Keep styles for proper session replay
848
921
  recordCrossOriginIframes: false, // Prevent cross-origin iframe errors
849
922
 
850
- // ✅ CANVAS RECORDING - PostHog-style protection against overwhelm
923
+ // ✅ CANVAS RECORDING - protection against overwhelm
851
924
  recordCanvas: this.recordCanvas, // Opt-in only
852
925
  sampling: this.recordCanvas ? { canvas: 4 } : undefined, // 4 FPS throttle
853
926
  dataURLOptions: this.recordCanvas ? {
@@ -1309,6 +1382,97 @@ export class HumanBehaviorTracker {
1309
1382
  initialized: this.initialized
1310
1383
  };
1311
1384
  }
1385
+
1386
+ // ===== PROPERTY MANAGEMENT METHODS =====
1387
+
1388
+ /**
1389
+ * Set a session property that will be included in all events for this session
1390
+ */
1391
+ public setSessionProperty(key: string, value: any): void {
1392
+ this.propertyManager.setSessionProperty(key, value);
1393
+ }
1394
+
1395
+ /**
1396
+ * Set multiple session properties
1397
+ */
1398
+ public setSessionProperties(properties: Record<string, any>): void {
1399
+ this.propertyManager.setSessionProperties(properties);
1400
+ }
1401
+
1402
+ /**
1403
+ * Get a session property
1404
+ */
1405
+ public getSessionProperty(key: string): any {
1406
+ return this.propertyManager.getSessionProperty(key);
1407
+ }
1408
+
1409
+ /**
1410
+ * Remove a session property
1411
+ */
1412
+ public removeSessionProperty(key: string): void {
1413
+ this.propertyManager.removeSessionProperty(key);
1414
+ }
1415
+
1416
+ /**
1417
+ * Set a user property that will be included in all events
1418
+ */
1419
+ public setUserProperty(key: string, value: any): void {
1420
+ this.propertyManager.setUserProperty(key, value);
1421
+ }
1422
+
1423
+ /**
1424
+ * Set multiple user properties
1425
+ */
1426
+ public setUserProperties(properties: Record<string, any>): void {
1427
+ this.propertyManager.setUserProperties(properties);
1428
+ }
1429
+
1430
+ /**
1431
+ * Get a user property
1432
+ */
1433
+ public getUserProperty(key: string): any {
1434
+ return this.propertyManager.getUserProperty(key);
1435
+ }
1436
+
1437
+ /**
1438
+ * Remove a user property
1439
+ */
1440
+ public removeUserProperty(key: string): void {
1441
+ this.propertyManager.removeUserProperty(key);
1442
+ }
1443
+
1444
+ /**
1445
+ * Set a property only if it hasn't been set before
1446
+ */
1447
+ public setOnce(key: string, value: any, scope: 'session' | 'user' = 'user'): void {
1448
+ this.propertyManager.setOnce(key, value, scope);
1449
+ }
1450
+
1451
+ /**
1452
+ * Clear all session properties
1453
+ */
1454
+ public clearSessionProperties(): void {
1455
+ this.propertyManager.clearSessionProperties();
1456
+ }
1457
+
1458
+ /**
1459
+ * Clear all user properties
1460
+ */
1461
+ public clearUserProperties(): void {
1462
+ this.propertyManager.clearUserProperties();
1463
+ }
1464
+
1465
+ /**
1466
+ * Get all properties for debugging
1467
+ */
1468
+ public getAllProperties(): {
1469
+ automatic: Record<string, any>;
1470
+ session: Record<string, any>;
1471
+ user: Record<string, any>;
1472
+ initial: Record<string, any>;
1473
+ } {
1474
+ return this.propertyManager.getAllProperties();
1475
+ }
1312
1476
  }
1313
1477
 
1314
1478
  // 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
+ }