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.
- package/dist/cjs/angular/index.cjs +799 -13
- package/dist/cjs/angular/index.cjs.map +1 -1
- package/dist/cjs/index.cjs +815 -13
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/install-wizard.cjs +56 -10
- package/dist/cjs/install-wizard.cjs.map +1 -1
- package/dist/cjs/react/index.cjs +800 -14
- package/dist/cjs/react/index.cjs.map +1 -1
- package/dist/cjs/remix/index.cjs +800 -14
- package/dist/cjs/remix/index.cjs.map +1 -1
- package/dist/cjs/svelte/index.cjs +799 -13
- package/dist/cjs/svelte/index.cjs.map +1 -1
- package/dist/cjs/vue/index.cjs +799 -13
- package/dist/cjs/vue/index.cjs.map +1 -1
- package/dist/cjs/wizard/index.cjs +56 -10
- package/dist/cjs/wizard/index.cjs.map +1 -1
- package/dist/cli/ai-auto-install.js +56 -10
- package/dist/cli/ai-auto-install.js.map +1 -1
- package/dist/cli/auto-install.js +56 -10
- package/dist/cli/auto-install.js.map +1 -1
- package/dist/esm/angular/index.js +799 -13
- package/dist/esm/angular/index.js.map +1 -1
- package/dist/esm/index.js +807 -14
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/install-wizard.js +56 -10
- package/dist/esm/install-wizard.js.map +1 -1
- package/dist/esm/react/index.js +800 -14
- package/dist/esm/react/index.js.map +1 -1
- package/dist/esm/remix/index.js +800 -14
- package/dist/esm/remix/index.js.map +1 -1
- package/dist/esm/svelte/index.js +799 -13
- package/dist/esm/svelte/index.js.map +1 -1
- package/dist/esm/vue/index.js +799 -13
- package/dist/esm/vue/index.js.map +1 -1
- package/dist/esm/wizard/index.js +56 -10
- package/dist/esm/wizard/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/types/angular/index.d.ts +60 -1
- package/dist/types/index.d.ts +258 -3
- package/dist/types/react/index.d.ts +60 -1
- package/dist/types/remix/index.d.ts +60 -1
- package/dist/types/svelte/index.d.ts +60 -1
- package/package/canvas-recording-demo.html +1 -1
- package/package/simple-spa.html +1 -1
- package/package/src/angular/index.ts +3 -3
- package/package/src/react/index.tsx +2 -2
- package/package/src/svelte/index.ts +1 -1
- package/package/src/tracker.ts +2 -2
- package/package/src/vue/index.ts +1 -1
- package/package.json +1 -1
- package/simple-spa.html +164 -2
- package/src/angular/index.ts +3 -3
- package/src/api.ts +40 -0
- package/src/index.ts +7 -0
- package/src/react/index.tsx +2 -2
- package/src/svelte/index.ts +1 -1
- package/src/tracker.ts +175 -11
- package/src/utils/ip-detector.ts +158 -0
- package/src/utils/property-detector.ts +345 -0
- package/src/utils/property-manager.ts +274 -0
- package/src/vue/index.ts +1 -1
- package/src/wizard/core/install-wizard.ts +60 -10
- package/canvas-recording-demo.html +0 -143
- package/clean-console-demo.html +0 -39
- 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
|
|
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
|
-
|
|
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
|
-
...
|
|
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,
|
|
458
|
+
await this.api.sendCustomEvent(this.sessionId, eventName, enhancedProperties);
|
|
405
459
|
|
|
406
|
-
logDebug(`Custom event tracked: ${eventName}`,
|
|
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:
|
|
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
|
-
//
|
|
796
|
-
|
|
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 -
|
|
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
|
+
}
|