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.
- package/dist/cjs/angular/index.cjs +817 -19
- package/dist/cjs/angular/index.cjs.map +1 -1
- package/dist/cjs/index.cjs +833 -19
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/react/index.cjs +818 -20
- package/dist/cjs/react/index.cjs.map +1 -1
- package/dist/cjs/remix/index.cjs +818 -20
- package/dist/cjs/remix/index.cjs.map +1 -1
- package/dist/cjs/svelte/index.cjs +817 -19
- package/dist/cjs/svelte/index.cjs.map +1 -1
- package/dist/cjs/vue/index.cjs +817 -19
- package/dist/cjs/vue/index.cjs.map +1 -1
- package/dist/esm/angular/index.js +817 -19
- package/dist/esm/angular/index.js.map +1 -1
- package/dist/esm/index.js +825 -20
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/react/index.js +818 -20
- package/dist/esm/react/index.js.map +1 -1
- package/dist/esm/remix/index.js +818 -20
- package/dist/esm/remix/index.js.map +1 -1
- package/dist/esm/svelte/index.js +817 -19
- package/dist/esm/svelte/index.js.map +1 -1
- package/dist/esm/vue/index.js +817 -19
- package/dist/esm/vue/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 +193 -17
- 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/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;
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
...
|
|
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,
|
|
470
|
+
await this.api.sendCustomEvent(this.sessionId, eventName, enhancedProperties);
|
|
405
471
|
|
|
406
|
-
logDebug(`Custom event tracked: ${eventName}`,
|
|
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:
|
|
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
|
-
//
|
|
796
|
-
|
|
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 -
|
|
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
|
+
}
|