user-analytics-tracker 2.1.0 → 2.2.0
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/CHANGELOG.md +7 -0
- package/dist/index.cjs.js +198 -21
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.cts +31 -3
- package/dist/index.d.ts +31 -3
- package/dist/index.esm.js +197 -22
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -558,25 +558,28 @@ function checkAndSetLocationConsent(msisdn) {
|
|
|
558
558
|
* This ensures we capture all available data and any new fields added by the API
|
|
559
559
|
*/
|
|
560
560
|
/**
|
|
561
|
-
* Get
|
|
561
|
+
* Get complete IP location data from ipwho.is API (HIGH PRIORITY)
|
|
562
|
+
* This is the primary method - gets IP, location, connection, and all data in one call
|
|
562
563
|
* No API key required
|
|
563
564
|
*
|
|
564
|
-
* @returns Promise<
|
|
565
|
+
* @returns Promise<IPLocation | null> - Complete IP location data, or null if unavailable
|
|
565
566
|
*
|
|
566
567
|
* @example
|
|
567
568
|
* ```typescript
|
|
568
|
-
* const
|
|
569
|
-
* console.log('
|
|
569
|
+
* const location = await getCompleteIPLocation();
|
|
570
|
+
* console.log('IP:', location?.ip);
|
|
571
|
+
* console.log('Country:', location?.country);
|
|
572
|
+
* console.log('ISP:', location?.connection?.isp);
|
|
570
573
|
* ```
|
|
571
574
|
*/
|
|
572
|
-
async function
|
|
575
|
+
async function getCompleteIPLocation() {
|
|
573
576
|
// Skip if we're in an environment without fetch (SSR)
|
|
574
577
|
if (typeof fetch === 'undefined') {
|
|
575
578
|
return null;
|
|
576
579
|
}
|
|
577
580
|
try {
|
|
578
|
-
// Call ipwho.is without IP parameter - it auto-detects user's IP
|
|
579
|
-
//
|
|
581
|
+
// Call ipwho.is without IP parameter - it auto-detects user's IP and returns everything
|
|
582
|
+
// This is the HIGH PRIORITY source - gets IP, location, connection, timezone, flag, etc. in one call
|
|
580
583
|
const response = await fetch('https://ipwho.is/', {
|
|
581
584
|
method: 'GET',
|
|
582
585
|
headers: {
|
|
@@ -593,22 +596,105 @@ async function getPublicIP() {
|
|
|
593
596
|
if (data.success === false) {
|
|
594
597
|
return null;
|
|
595
598
|
}
|
|
596
|
-
|
|
599
|
+
// Store all keys dynamically from the response
|
|
600
|
+
// This ensures we capture all fields, including nested objects and any new fields
|
|
601
|
+
const locationData = {
|
|
602
|
+
ip: data.ip,
|
|
603
|
+
// Map all fields from the API response dynamically
|
|
604
|
+
...Object.keys(data).reduce((acc, key) => {
|
|
605
|
+
// Store all keys and their values, preserving nested objects
|
|
606
|
+
acc[key] = data[key];
|
|
607
|
+
return acc;
|
|
608
|
+
}, {}),
|
|
609
|
+
};
|
|
610
|
+
// Add backward compatibility mappings for existing code
|
|
611
|
+
if (data.latitude !== undefined) {
|
|
612
|
+
locationData.lat = data.latitude;
|
|
613
|
+
}
|
|
614
|
+
if (data.longitude !== undefined) {
|
|
615
|
+
locationData.lon = data.longitude;
|
|
616
|
+
}
|
|
617
|
+
if (data.country_code !== undefined) {
|
|
618
|
+
locationData.countryCode = data.country_code;
|
|
619
|
+
}
|
|
620
|
+
if (data.region !== undefined) {
|
|
621
|
+
locationData.regionName = data.region;
|
|
622
|
+
}
|
|
623
|
+
if (data.connection?.isp !== undefined) {
|
|
624
|
+
locationData.isp = data.connection.isp;
|
|
625
|
+
}
|
|
626
|
+
if (data.connection?.org !== undefined) {
|
|
627
|
+
locationData.org = data.connection.org;
|
|
628
|
+
}
|
|
629
|
+
if (data.connection?.asn !== undefined) {
|
|
630
|
+
locationData.as = `AS${data.connection.asn}`;
|
|
631
|
+
}
|
|
632
|
+
if (data.timezone?.id !== undefined) {
|
|
633
|
+
locationData.timezone = data.timezone.id;
|
|
634
|
+
}
|
|
635
|
+
locationData.query = data.ip;
|
|
636
|
+
return locationData;
|
|
597
637
|
}
|
|
598
638
|
catch (error) {
|
|
599
639
|
// Silently fail - don't break user experience
|
|
600
640
|
if (error.name !== 'AbortError') {
|
|
601
|
-
console.warn('[IP Geolocation] Error fetching
|
|
641
|
+
console.warn('[IP Geolocation] Error fetching complete IP location from ipwho.is:', error.message);
|
|
602
642
|
}
|
|
603
643
|
return null;
|
|
604
644
|
}
|
|
605
645
|
}
|
|
606
646
|
/**
|
|
607
|
-
* Get
|
|
647
|
+
* Get public IP address using ipwho.is API (FALLBACK - lower priority)
|
|
648
|
+
* This is kept for backward compatibility and as a fallback
|
|
649
|
+
* Prefer getCompleteIPLocation() which gets everything in one call
|
|
650
|
+
*
|
|
651
|
+
* @returns Promise<string | null> - The public IP address, or null if unavailable
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```typescript
|
|
655
|
+
* const ip = await getPublicIP();
|
|
656
|
+
* console.log('Your IP:', ip); // e.g., "203.0.113.42"
|
|
657
|
+
* ```
|
|
658
|
+
*/
|
|
659
|
+
async function getPublicIP() {
|
|
660
|
+
// Try to get complete location first (includes IP)
|
|
661
|
+
const completeLocation = await getCompleteIPLocation();
|
|
662
|
+
if (completeLocation?.ip) {
|
|
663
|
+
return completeLocation.ip;
|
|
664
|
+
}
|
|
665
|
+
// Fallback: try direct IP fetch (less efficient, lower priority)
|
|
666
|
+
try {
|
|
667
|
+
const response = await fetch('https://ipwho.is/', {
|
|
668
|
+
method: 'GET',
|
|
669
|
+
headers: {
|
|
670
|
+
Accept: 'application/json',
|
|
671
|
+
},
|
|
672
|
+
signal: AbortSignal.timeout(5000),
|
|
673
|
+
});
|
|
674
|
+
if (!response.ok) {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
const data = await response.json();
|
|
678
|
+
if (data.success === false) {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
return data.ip || null;
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
if (error.name !== 'AbortError') {
|
|
685
|
+
console.warn('[IP Geolocation] Error fetching public IP (fallback):', error.message);
|
|
686
|
+
}
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Get location from IP address using ipwho.is API (HIGH PRIORITY)
|
|
608
692
|
* Free tier: No API key required
|
|
609
693
|
*
|
|
610
694
|
* Stores all keys dynamically from the API response, including nested objects
|
|
611
695
|
* This ensures we capture all available data and any new fields added by the API
|
|
696
|
+
*
|
|
697
|
+
* Note: If you don't have an IP yet, use getCompleteIPLocation() which gets everything in one call
|
|
612
698
|
*/
|
|
613
699
|
async function getIPLocation(ip) {
|
|
614
700
|
// Skip localhost/private IPs (these can't be geolocated)
|
|
@@ -1035,15 +1121,21 @@ class LocationDetector {
|
|
|
1035
1121
|
}
|
|
1036
1122
|
this.ipLocationFetchingRef.current = true;
|
|
1037
1123
|
try {
|
|
1038
|
-
// Get
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1124
|
+
// HIGH PRIORITY: Get complete IP location data from ipwho.is in one call
|
|
1125
|
+
// This gets IP, location, connection, timezone, flag, and all other data at once
|
|
1126
|
+
// More efficient than making separate calls
|
|
1127
|
+
let ipLocation = await getCompleteIPLocation();
|
|
1128
|
+
// If complete location fetch failed, try fallback: get IP first, then location
|
|
1129
|
+
if (!ipLocation) {
|
|
1130
|
+
console.log('[Location] Primary ipwho.is call failed, trying fallback...');
|
|
1131
|
+
const publicIP = await getPublicIP();
|
|
1132
|
+
if (publicIP) {
|
|
1133
|
+
// Fallback: Get location from IP using ipwho.is API
|
|
1134
|
+
ipLocation = await getIPLocation(publicIP);
|
|
1135
|
+
}
|
|
1042
1136
|
}
|
|
1043
|
-
// Get location from IP using ipwho.is API
|
|
1044
|
-
const ipLocation = await getIPLocation(publicIP);
|
|
1045
1137
|
if (!ipLocation) {
|
|
1046
|
-
throw new Error('Could not fetch location data');
|
|
1138
|
+
throw new Error('Could not fetch location data from ipwho.is');
|
|
1047
1139
|
}
|
|
1048
1140
|
// Convert IP location to LocationInfo format
|
|
1049
1141
|
// Map all available fields from the IP location response
|
|
@@ -1058,7 +1150,7 @@ class LocationDetector {
|
|
|
1058
1150
|
permission: 'granted', // IP location doesn't require permission
|
|
1059
1151
|
source: 'ip',
|
|
1060
1152
|
ts: new Date().toISOString(),
|
|
1061
|
-
ip: ipLocation.ip ||
|
|
1153
|
+
ip: ipLocation.ip || undefined,
|
|
1062
1154
|
country: ipLocation.country || undefined,
|
|
1063
1155
|
countryCode: ipLocation.country_code || ipLocation.countryCode || undefined,
|
|
1064
1156
|
city: ipLocation.city || undefined,
|
|
@@ -1068,7 +1160,7 @@ class LocationDetector {
|
|
|
1068
1160
|
// Store the full IP location data in a custom field for access to all keys
|
|
1069
1161
|
// This preserves all dynamic keys from the API response
|
|
1070
1162
|
locationResult.ipLocationData = ipLocation;
|
|
1071
|
-
console.log('[Location] IP-based location obtained:', {
|
|
1163
|
+
console.log('[Location] IP-based location obtained from ipwho.is:', {
|
|
1072
1164
|
ip: locationResult.ip,
|
|
1073
1165
|
lat: locationResult.lat,
|
|
1074
1166
|
lon: locationResult.lon,
|
|
@@ -1076,6 +1168,8 @@ class LocationDetector {
|
|
|
1076
1168
|
country: locationResult.country,
|
|
1077
1169
|
continent: ipLocation.continent,
|
|
1078
1170
|
timezone: locationResult.timezone,
|
|
1171
|
+
isp: ipLocation.connection?.isp,
|
|
1172
|
+
connection: ipLocation.connection,
|
|
1079
1173
|
});
|
|
1080
1174
|
this.lastIPLocationRef.current = locationResult;
|
|
1081
1175
|
return locationResult;
|
|
@@ -1912,6 +2006,73 @@ class MetricsCollector {
|
|
|
1912
2006
|
// Global metrics collector instance
|
|
1913
2007
|
const metricsCollector = new MetricsCollector();
|
|
1914
2008
|
|
|
2009
|
+
/**
|
|
2010
|
+
* Transform IP location data from API format (snake_case) to backend-expected format (camelCase)
|
|
2011
|
+
* This ensures compatibility with the analytics backend integration
|
|
2012
|
+
*
|
|
2013
|
+
* @param ipLocation - Raw IP location data from ipwho.is API
|
|
2014
|
+
* @returns Transformed IP location data matching backend schema
|
|
2015
|
+
*/
|
|
2016
|
+
function transformIPLocationForBackend(ipLocation) {
|
|
2017
|
+
if (!ipLocation) {
|
|
2018
|
+
return null;
|
|
2019
|
+
}
|
|
2020
|
+
// Transform to match backend expected format (camelCase)
|
|
2021
|
+
const transformed = {
|
|
2022
|
+
// Basic fields
|
|
2023
|
+
ip: ipLocation.ip,
|
|
2024
|
+
country: ipLocation.country,
|
|
2025
|
+
countryCode: ipLocation.country_code || ipLocation.countryCode,
|
|
2026
|
+
region: ipLocation.region || ipLocation.regionName,
|
|
2027
|
+
city: ipLocation.city,
|
|
2028
|
+
postal: ipLocation.postal,
|
|
2029
|
+
capital: ipLocation.capital,
|
|
2030
|
+
callingCode: ipLocation.calling_code || ipLocation.callingCode,
|
|
2031
|
+
// Geographic fields
|
|
2032
|
+
continent: ipLocation.continent,
|
|
2033
|
+
continentCode: ipLocation.continent_code || ipLocation.continentCode,
|
|
2034
|
+
lat: ipLocation.latitude ?? ipLocation.lat,
|
|
2035
|
+
lon: ipLocation.longitude ?? ipLocation.lon,
|
|
2036
|
+
borders: ipLocation.borders,
|
|
2037
|
+
// Network fields
|
|
2038
|
+
type: ipLocation.type,
|
|
2039
|
+
isEu: ipLocation.is_eu ?? ipLocation.isEu,
|
|
2040
|
+
// ISP/Connection - preserve connection object and also add top-level isp
|
|
2041
|
+
isp: ipLocation.connection?.isp || ipLocation.isp,
|
|
2042
|
+
connection: ipLocation.connection ? {
|
|
2043
|
+
asn: ipLocation.connection.asn,
|
|
2044
|
+
org: ipLocation.connection.org,
|
|
2045
|
+
isp: ipLocation.connection.isp,
|
|
2046
|
+
domain: ipLocation.connection.domain,
|
|
2047
|
+
} : undefined,
|
|
2048
|
+
// Timezone - store both simple string and full details object
|
|
2049
|
+
timezone: typeof ipLocation.timezone === 'string'
|
|
2050
|
+
? ipLocation.timezone
|
|
2051
|
+
: ipLocation.timezone?.id,
|
|
2052
|
+
timezoneDetails: ipLocation.timezone && typeof ipLocation.timezone === 'object' ? {
|
|
2053
|
+
id: ipLocation.timezone.id,
|
|
2054
|
+
abbr: ipLocation.timezone.abbr,
|
|
2055
|
+
isDst: ipLocation.timezone.is_dst,
|
|
2056
|
+
offset: ipLocation.timezone.offset,
|
|
2057
|
+
utc: ipLocation.timezone.utc,
|
|
2058
|
+
currentTime: ipLocation.timezone.current_time,
|
|
2059
|
+
} : undefined,
|
|
2060
|
+
// Flag - transform to camelCase
|
|
2061
|
+
flag: ipLocation.flag ? {
|
|
2062
|
+
img: ipLocation.flag.img,
|
|
2063
|
+
emoji: ipLocation.flag.emoji,
|
|
2064
|
+
emojiUnicode: ipLocation.flag.emoji_unicode,
|
|
2065
|
+
} : undefined,
|
|
2066
|
+
};
|
|
2067
|
+
// Remove undefined values to keep the payload clean
|
|
2068
|
+
Object.keys(transformed).forEach(key => {
|
|
2069
|
+
if (transformed[key] === undefined) {
|
|
2070
|
+
delete transformed[key];
|
|
2071
|
+
}
|
|
2072
|
+
});
|
|
2073
|
+
return transformed;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
1915
2076
|
/**
|
|
1916
2077
|
* Analytics Service
|
|
1917
2078
|
* Sends analytics events to your backend API
|
|
@@ -2175,6 +2336,8 @@ class AnalyticsService {
|
|
|
2175
2336
|
* Track user journey with full context
|
|
2176
2337
|
*/
|
|
2177
2338
|
static async trackUserJourney({ sessionId, pageUrl, networkInfo, deviceInfo, location, attribution, ipLocation, userId, customData, pageVisits = 1, interactions = 0, }) {
|
|
2339
|
+
// Transform IP location data to match backend expected format (camelCase)
|
|
2340
|
+
const transformedIPLocation = transformIPLocationForBackend(ipLocation);
|
|
2178
2341
|
await this.trackEvent({
|
|
2179
2342
|
sessionId,
|
|
2180
2343
|
pageUrl,
|
|
@@ -2186,7 +2349,8 @@ class AnalyticsService {
|
|
|
2186
2349
|
userId: userId ?? sessionId,
|
|
2187
2350
|
customData: {
|
|
2188
2351
|
...customData,
|
|
2189
|
-
|
|
2352
|
+
// Store transformed IP location in customData for backend integration
|
|
2353
|
+
...(transformedIPLocation && { ipLocation: transformedIPLocation }),
|
|
2190
2354
|
},
|
|
2191
2355
|
eventName: 'page_view', // Auto-tracked as page view
|
|
2192
2356
|
});
|
|
@@ -2257,6 +2421,13 @@ class AnalyticsService {
|
|
|
2257
2421
|
}
|
|
2258
2422
|
const finalSessionId = context?.sessionId || autoContext?.sessionId || 'unknown';
|
|
2259
2423
|
const finalPageUrl = context?.pageUrl || autoContext?.pageUrl || '';
|
|
2424
|
+
// Extract IP location from location object if available
|
|
2425
|
+
const locationData = context?.location || autoContext?.location;
|
|
2426
|
+
const ipLocationData = locationData && typeof locationData === 'object'
|
|
2427
|
+
? locationData?.ipLocationData
|
|
2428
|
+
: undefined;
|
|
2429
|
+
// Transform IP location data to match backend expected format
|
|
2430
|
+
const transformedIPLocation = transformIPLocationForBackend(ipLocationData);
|
|
2260
2431
|
await this.trackEvent({
|
|
2261
2432
|
sessionId: finalSessionId,
|
|
2262
2433
|
pageUrl: finalPageUrl,
|
|
@@ -2267,7 +2438,11 @@ class AnalyticsService {
|
|
|
2267
2438
|
userId: context?.userId || finalSessionId,
|
|
2268
2439
|
eventName,
|
|
2269
2440
|
eventParameters: parameters || {},
|
|
2270
|
-
customData:
|
|
2441
|
+
customData: {
|
|
2442
|
+
...(parameters || {}),
|
|
2443
|
+
// Store transformed IP location in customData for backend integration
|
|
2444
|
+
...(transformedIPLocation && { ipLocation: transformedIPLocation }),
|
|
2445
|
+
},
|
|
2271
2446
|
});
|
|
2272
2447
|
}
|
|
2273
2448
|
/**
|
|
@@ -2730,5 +2905,5 @@ function useAnalytics(options = {}) {
|
|
|
2730
2905
|
]);
|
|
2731
2906
|
}
|
|
2732
2907
|
|
|
2733
|
-
export { AnalyticsService, AttributionDetector, DeviceDetector, LocationDetector, NetworkDetector, QueueManager, checkAndSetLocationConsent, clearLocationConsent, clearSession, useAnalytics as default, getIPFromRequest, getIPLocation, getLocationConsentTimestamp, getOrCreateSession, getOrCreateUserId, getPublicIP, getSession, hasLocationConsent, initDebug, loadJSON, loadSessionJSON, logger, saveJSON, saveSessionJSON, setLocationConsentGranted, trackPageVisit, updateSessionActivity, useAnalytics };
|
|
2908
|
+
export { AnalyticsService, AttributionDetector, DeviceDetector, LocationDetector, NetworkDetector, QueueManager, checkAndSetLocationConsent, clearLocationConsent, clearSession, useAnalytics as default, getCompleteIPLocation, getIPFromRequest, getIPLocation, getLocationConsentTimestamp, getOrCreateSession, getOrCreateUserId, getPublicIP, getSession, hasLocationConsent, initDebug, loadJSON, loadSessionJSON, logger, saveJSON, saveSessionJSON, setLocationConsentGranted, trackPageVisit, transformIPLocationForBackend, updateSessionActivity, useAnalytics };
|
|
2734
2909
|
//# sourceMappingURL=index.esm.js.map
|