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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [2.2.0](https://github.com/switch-org/analytics-tracker/compare/v2.1.0...v2.2.0) (2025-12-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* ip-location, connection update with ipwho-is api ([fa1b369](https://github.com/switch-org/analytics-tracker/commit/fa1b36997ecad6f71c758f738c768090529bb2b0))
|
|
7
|
+
|
|
1
8
|
# [2.1.0](https://github.com/switch-org/analytics-tracker/compare/v2.0.0...v2.1.0) (2025-12-29)
|
|
2
9
|
|
|
3
10
|
|
package/dist/index.cjs.js
CHANGED
|
@@ -562,25 +562,28 @@ function checkAndSetLocationConsent(msisdn) {
|
|
|
562
562
|
* This ensures we capture all available data and any new fields added by the API
|
|
563
563
|
*/
|
|
564
564
|
/**
|
|
565
|
-
* Get
|
|
565
|
+
* Get complete IP location data from ipwho.is API (HIGH PRIORITY)
|
|
566
|
+
* This is the primary method - gets IP, location, connection, and all data in one call
|
|
566
567
|
* No API key required
|
|
567
568
|
*
|
|
568
|
-
* @returns Promise<
|
|
569
|
+
* @returns Promise<IPLocation | null> - Complete IP location data, or null if unavailable
|
|
569
570
|
*
|
|
570
571
|
* @example
|
|
571
572
|
* ```typescript
|
|
572
|
-
* const
|
|
573
|
-
* console.log('
|
|
573
|
+
* const location = await getCompleteIPLocation();
|
|
574
|
+
* console.log('IP:', location?.ip);
|
|
575
|
+
* console.log('Country:', location?.country);
|
|
576
|
+
* console.log('ISP:', location?.connection?.isp);
|
|
574
577
|
* ```
|
|
575
578
|
*/
|
|
576
|
-
async function
|
|
579
|
+
async function getCompleteIPLocation() {
|
|
577
580
|
// Skip if we're in an environment without fetch (SSR)
|
|
578
581
|
if (typeof fetch === 'undefined') {
|
|
579
582
|
return null;
|
|
580
583
|
}
|
|
581
584
|
try {
|
|
582
|
-
// Call ipwho.is without IP parameter - it auto-detects user's IP
|
|
583
|
-
//
|
|
585
|
+
// Call ipwho.is without IP parameter - it auto-detects user's IP and returns everything
|
|
586
|
+
// This is the HIGH PRIORITY source - gets IP, location, connection, timezone, flag, etc. in one call
|
|
584
587
|
const response = await fetch('https://ipwho.is/', {
|
|
585
588
|
method: 'GET',
|
|
586
589
|
headers: {
|
|
@@ -597,22 +600,105 @@ async function getPublicIP() {
|
|
|
597
600
|
if (data.success === false) {
|
|
598
601
|
return null;
|
|
599
602
|
}
|
|
600
|
-
|
|
603
|
+
// Store all keys dynamically from the response
|
|
604
|
+
// This ensures we capture all fields, including nested objects and any new fields
|
|
605
|
+
const locationData = {
|
|
606
|
+
ip: data.ip,
|
|
607
|
+
// Map all fields from the API response dynamically
|
|
608
|
+
...Object.keys(data).reduce((acc, key) => {
|
|
609
|
+
// Store all keys and their values, preserving nested objects
|
|
610
|
+
acc[key] = data[key];
|
|
611
|
+
return acc;
|
|
612
|
+
}, {}),
|
|
613
|
+
};
|
|
614
|
+
// Add backward compatibility mappings for existing code
|
|
615
|
+
if (data.latitude !== undefined) {
|
|
616
|
+
locationData.lat = data.latitude;
|
|
617
|
+
}
|
|
618
|
+
if (data.longitude !== undefined) {
|
|
619
|
+
locationData.lon = data.longitude;
|
|
620
|
+
}
|
|
621
|
+
if (data.country_code !== undefined) {
|
|
622
|
+
locationData.countryCode = data.country_code;
|
|
623
|
+
}
|
|
624
|
+
if (data.region !== undefined) {
|
|
625
|
+
locationData.regionName = data.region;
|
|
626
|
+
}
|
|
627
|
+
if (data.connection?.isp !== undefined) {
|
|
628
|
+
locationData.isp = data.connection.isp;
|
|
629
|
+
}
|
|
630
|
+
if (data.connection?.org !== undefined) {
|
|
631
|
+
locationData.org = data.connection.org;
|
|
632
|
+
}
|
|
633
|
+
if (data.connection?.asn !== undefined) {
|
|
634
|
+
locationData.as = `AS${data.connection.asn}`;
|
|
635
|
+
}
|
|
636
|
+
if (data.timezone?.id !== undefined) {
|
|
637
|
+
locationData.timezone = data.timezone.id;
|
|
638
|
+
}
|
|
639
|
+
locationData.query = data.ip;
|
|
640
|
+
return locationData;
|
|
601
641
|
}
|
|
602
642
|
catch (error) {
|
|
603
643
|
// Silently fail - don't break user experience
|
|
604
644
|
if (error.name !== 'AbortError') {
|
|
605
|
-
console.warn('[IP Geolocation] Error fetching
|
|
645
|
+
console.warn('[IP Geolocation] Error fetching complete IP location from ipwho.is:', error.message);
|
|
606
646
|
}
|
|
607
647
|
return null;
|
|
608
648
|
}
|
|
609
649
|
}
|
|
610
650
|
/**
|
|
611
|
-
* Get
|
|
651
|
+
* Get public IP address using ipwho.is API (FALLBACK - lower priority)
|
|
652
|
+
* This is kept for backward compatibility and as a fallback
|
|
653
|
+
* Prefer getCompleteIPLocation() which gets everything in one call
|
|
654
|
+
*
|
|
655
|
+
* @returns Promise<string | null> - The public IP address, or null if unavailable
|
|
656
|
+
*
|
|
657
|
+
* @example
|
|
658
|
+
* ```typescript
|
|
659
|
+
* const ip = await getPublicIP();
|
|
660
|
+
* console.log('Your IP:', ip); // e.g., "203.0.113.42"
|
|
661
|
+
* ```
|
|
662
|
+
*/
|
|
663
|
+
async function getPublicIP() {
|
|
664
|
+
// Try to get complete location first (includes IP)
|
|
665
|
+
const completeLocation = await getCompleteIPLocation();
|
|
666
|
+
if (completeLocation?.ip) {
|
|
667
|
+
return completeLocation.ip;
|
|
668
|
+
}
|
|
669
|
+
// Fallback: try direct IP fetch (less efficient, lower priority)
|
|
670
|
+
try {
|
|
671
|
+
const response = await fetch('https://ipwho.is/', {
|
|
672
|
+
method: 'GET',
|
|
673
|
+
headers: {
|
|
674
|
+
Accept: 'application/json',
|
|
675
|
+
},
|
|
676
|
+
signal: AbortSignal.timeout(5000),
|
|
677
|
+
});
|
|
678
|
+
if (!response.ok) {
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
const data = await response.json();
|
|
682
|
+
if (data.success === false) {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
return data.ip || null;
|
|
686
|
+
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
if (error.name !== 'AbortError') {
|
|
689
|
+
console.warn('[IP Geolocation] Error fetching public IP (fallback):', error.message);
|
|
690
|
+
}
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Get location from IP address using ipwho.is API (HIGH PRIORITY)
|
|
612
696
|
* Free tier: No API key required
|
|
613
697
|
*
|
|
614
698
|
* Stores all keys dynamically from the API response, including nested objects
|
|
615
699
|
* This ensures we capture all available data and any new fields added by the API
|
|
700
|
+
*
|
|
701
|
+
* Note: If you don't have an IP yet, use getCompleteIPLocation() which gets everything in one call
|
|
616
702
|
*/
|
|
617
703
|
async function getIPLocation(ip) {
|
|
618
704
|
// Skip localhost/private IPs (these can't be geolocated)
|
|
@@ -1039,15 +1125,21 @@ class LocationDetector {
|
|
|
1039
1125
|
}
|
|
1040
1126
|
this.ipLocationFetchingRef.current = true;
|
|
1041
1127
|
try {
|
|
1042
|
-
// Get
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1128
|
+
// HIGH PRIORITY: Get complete IP location data from ipwho.is in one call
|
|
1129
|
+
// This gets IP, location, connection, timezone, flag, and all other data at once
|
|
1130
|
+
// More efficient than making separate calls
|
|
1131
|
+
let ipLocation = await getCompleteIPLocation();
|
|
1132
|
+
// If complete location fetch failed, try fallback: get IP first, then location
|
|
1133
|
+
if (!ipLocation) {
|
|
1134
|
+
console.log('[Location] Primary ipwho.is call failed, trying fallback...');
|
|
1135
|
+
const publicIP = await getPublicIP();
|
|
1136
|
+
if (publicIP) {
|
|
1137
|
+
// Fallback: Get location from IP using ipwho.is API
|
|
1138
|
+
ipLocation = await getIPLocation(publicIP);
|
|
1139
|
+
}
|
|
1046
1140
|
}
|
|
1047
|
-
// Get location from IP using ipwho.is API
|
|
1048
|
-
const ipLocation = await getIPLocation(publicIP);
|
|
1049
1141
|
if (!ipLocation) {
|
|
1050
|
-
throw new Error('Could not fetch location data');
|
|
1142
|
+
throw new Error('Could not fetch location data from ipwho.is');
|
|
1051
1143
|
}
|
|
1052
1144
|
// Convert IP location to LocationInfo format
|
|
1053
1145
|
// Map all available fields from the IP location response
|
|
@@ -1062,7 +1154,7 @@ class LocationDetector {
|
|
|
1062
1154
|
permission: 'granted', // IP location doesn't require permission
|
|
1063
1155
|
source: 'ip',
|
|
1064
1156
|
ts: new Date().toISOString(),
|
|
1065
|
-
ip: ipLocation.ip ||
|
|
1157
|
+
ip: ipLocation.ip || undefined,
|
|
1066
1158
|
country: ipLocation.country || undefined,
|
|
1067
1159
|
countryCode: ipLocation.country_code || ipLocation.countryCode || undefined,
|
|
1068
1160
|
city: ipLocation.city || undefined,
|
|
@@ -1072,7 +1164,7 @@ class LocationDetector {
|
|
|
1072
1164
|
// Store the full IP location data in a custom field for access to all keys
|
|
1073
1165
|
// This preserves all dynamic keys from the API response
|
|
1074
1166
|
locationResult.ipLocationData = ipLocation;
|
|
1075
|
-
console.log('[Location] IP-based location obtained:', {
|
|
1167
|
+
console.log('[Location] IP-based location obtained from ipwho.is:', {
|
|
1076
1168
|
ip: locationResult.ip,
|
|
1077
1169
|
lat: locationResult.lat,
|
|
1078
1170
|
lon: locationResult.lon,
|
|
@@ -1080,6 +1172,8 @@ class LocationDetector {
|
|
|
1080
1172
|
country: locationResult.country,
|
|
1081
1173
|
continent: ipLocation.continent,
|
|
1082
1174
|
timezone: locationResult.timezone,
|
|
1175
|
+
isp: ipLocation.connection?.isp,
|
|
1176
|
+
connection: ipLocation.connection,
|
|
1083
1177
|
});
|
|
1084
1178
|
this.lastIPLocationRef.current = locationResult;
|
|
1085
1179
|
return locationResult;
|
|
@@ -1916,6 +2010,73 @@ class MetricsCollector {
|
|
|
1916
2010
|
// Global metrics collector instance
|
|
1917
2011
|
const metricsCollector = new MetricsCollector();
|
|
1918
2012
|
|
|
2013
|
+
/**
|
|
2014
|
+
* Transform IP location data from API format (snake_case) to backend-expected format (camelCase)
|
|
2015
|
+
* This ensures compatibility with the analytics backend integration
|
|
2016
|
+
*
|
|
2017
|
+
* @param ipLocation - Raw IP location data from ipwho.is API
|
|
2018
|
+
* @returns Transformed IP location data matching backend schema
|
|
2019
|
+
*/
|
|
2020
|
+
function transformIPLocationForBackend(ipLocation) {
|
|
2021
|
+
if (!ipLocation) {
|
|
2022
|
+
return null;
|
|
2023
|
+
}
|
|
2024
|
+
// Transform to match backend expected format (camelCase)
|
|
2025
|
+
const transformed = {
|
|
2026
|
+
// Basic fields
|
|
2027
|
+
ip: ipLocation.ip,
|
|
2028
|
+
country: ipLocation.country,
|
|
2029
|
+
countryCode: ipLocation.country_code || ipLocation.countryCode,
|
|
2030
|
+
region: ipLocation.region || ipLocation.regionName,
|
|
2031
|
+
city: ipLocation.city,
|
|
2032
|
+
postal: ipLocation.postal,
|
|
2033
|
+
capital: ipLocation.capital,
|
|
2034
|
+
callingCode: ipLocation.calling_code || ipLocation.callingCode,
|
|
2035
|
+
// Geographic fields
|
|
2036
|
+
continent: ipLocation.continent,
|
|
2037
|
+
continentCode: ipLocation.continent_code || ipLocation.continentCode,
|
|
2038
|
+
lat: ipLocation.latitude ?? ipLocation.lat,
|
|
2039
|
+
lon: ipLocation.longitude ?? ipLocation.lon,
|
|
2040
|
+
borders: ipLocation.borders,
|
|
2041
|
+
// Network fields
|
|
2042
|
+
type: ipLocation.type,
|
|
2043
|
+
isEu: ipLocation.is_eu ?? ipLocation.isEu,
|
|
2044
|
+
// ISP/Connection - preserve connection object and also add top-level isp
|
|
2045
|
+
isp: ipLocation.connection?.isp || ipLocation.isp,
|
|
2046
|
+
connection: ipLocation.connection ? {
|
|
2047
|
+
asn: ipLocation.connection.asn,
|
|
2048
|
+
org: ipLocation.connection.org,
|
|
2049
|
+
isp: ipLocation.connection.isp,
|
|
2050
|
+
domain: ipLocation.connection.domain,
|
|
2051
|
+
} : undefined,
|
|
2052
|
+
// Timezone - store both simple string and full details object
|
|
2053
|
+
timezone: typeof ipLocation.timezone === 'string'
|
|
2054
|
+
? ipLocation.timezone
|
|
2055
|
+
: ipLocation.timezone?.id,
|
|
2056
|
+
timezoneDetails: ipLocation.timezone && typeof ipLocation.timezone === 'object' ? {
|
|
2057
|
+
id: ipLocation.timezone.id,
|
|
2058
|
+
abbr: ipLocation.timezone.abbr,
|
|
2059
|
+
isDst: ipLocation.timezone.is_dst,
|
|
2060
|
+
offset: ipLocation.timezone.offset,
|
|
2061
|
+
utc: ipLocation.timezone.utc,
|
|
2062
|
+
currentTime: ipLocation.timezone.current_time,
|
|
2063
|
+
} : undefined,
|
|
2064
|
+
// Flag - transform to camelCase
|
|
2065
|
+
flag: ipLocation.flag ? {
|
|
2066
|
+
img: ipLocation.flag.img,
|
|
2067
|
+
emoji: ipLocation.flag.emoji,
|
|
2068
|
+
emojiUnicode: ipLocation.flag.emoji_unicode,
|
|
2069
|
+
} : undefined,
|
|
2070
|
+
};
|
|
2071
|
+
// Remove undefined values to keep the payload clean
|
|
2072
|
+
Object.keys(transformed).forEach(key => {
|
|
2073
|
+
if (transformed[key] === undefined) {
|
|
2074
|
+
delete transformed[key];
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
return transformed;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
1919
2080
|
/**
|
|
1920
2081
|
* Analytics Service
|
|
1921
2082
|
* Sends analytics events to your backend API
|
|
@@ -2179,6 +2340,8 @@ class AnalyticsService {
|
|
|
2179
2340
|
* Track user journey with full context
|
|
2180
2341
|
*/
|
|
2181
2342
|
static async trackUserJourney({ sessionId, pageUrl, networkInfo, deviceInfo, location, attribution, ipLocation, userId, customData, pageVisits = 1, interactions = 0, }) {
|
|
2343
|
+
// Transform IP location data to match backend expected format (camelCase)
|
|
2344
|
+
const transformedIPLocation = transformIPLocationForBackend(ipLocation);
|
|
2182
2345
|
await this.trackEvent({
|
|
2183
2346
|
sessionId,
|
|
2184
2347
|
pageUrl,
|
|
@@ -2190,7 +2353,8 @@ class AnalyticsService {
|
|
|
2190
2353
|
userId: userId ?? sessionId,
|
|
2191
2354
|
customData: {
|
|
2192
2355
|
...customData,
|
|
2193
|
-
|
|
2356
|
+
// Store transformed IP location in customData for backend integration
|
|
2357
|
+
...(transformedIPLocation && { ipLocation: transformedIPLocation }),
|
|
2194
2358
|
},
|
|
2195
2359
|
eventName: 'page_view', // Auto-tracked as page view
|
|
2196
2360
|
});
|
|
@@ -2261,6 +2425,13 @@ class AnalyticsService {
|
|
|
2261
2425
|
}
|
|
2262
2426
|
const finalSessionId = context?.sessionId || autoContext?.sessionId || 'unknown';
|
|
2263
2427
|
const finalPageUrl = context?.pageUrl || autoContext?.pageUrl || '';
|
|
2428
|
+
// Extract IP location from location object if available
|
|
2429
|
+
const locationData = context?.location || autoContext?.location;
|
|
2430
|
+
const ipLocationData = locationData && typeof locationData === 'object'
|
|
2431
|
+
? locationData?.ipLocationData
|
|
2432
|
+
: undefined;
|
|
2433
|
+
// Transform IP location data to match backend expected format
|
|
2434
|
+
const transformedIPLocation = transformIPLocationForBackend(ipLocationData);
|
|
2264
2435
|
await this.trackEvent({
|
|
2265
2436
|
sessionId: finalSessionId,
|
|
2266
2437
|
pageUrl: finalPageUrl,
|
|
@@ -2271,7 +2442,11 @@ class AnalyticsService {
|
|
|
2271
2442
|
userId: context?.userId || finalSessionId,
|
|
2272
2443
|
eventName,
|
|
2273
2444
|
eventParameters: parameters || {},
|
|
2274
|
-
customData:
|
|
2445
|
+
customData: {
|
|
2446
|
+
...(parameters || {}),
|
|
2447
|
+
// Store transformed IP location in customData for backend integration
|
|
2448
|
+
...(transformedIPLocation && { ipLocation: transformedIPLocation }),
|
|
2449
|
+
},
|
|
2275
2450
|
});
|
|
2276
2451
|
}
|
|
2277
2452
|
/**
|
|
@@ -2744,6 +2919,7 @@ exports.checkAndSetLocationConsent = checkAndSetLocationConsent;
|
|
|
2744
2919
|
exports.clearLocationConsent = clearLocationConsent;
|
|
2745
2920
|
exports.clearSession = clearSession;
|
|
2746
2921
|
exports.default = useAnalytics;
|
|
2922
|
+
exports.getCompleteIPLocation = getCompleteIPLocation;
|
|
2747
2923
|
exports.getIPFromRequest = getIPFromRequest;
|
|
2748
2924
|
exports.getIPLocation = getIPLocation;
|
|
2749
2925
|
exports.getLocationConsentTimestamp = getLocationConsentTimestamp;
|
|
@@ -2760,6 +2936,7 @@ exports.saveJSON = saveJSON;
|
|
|
2760
2936
|
exports.saveSessionJSON = saveSessionJSON;
|
|
2761
2937
|
exports.setLocationConsentGranted = setLocationConsentGranted;
|
|
2762
2938
|
exports.trackPageVisit = trackPageVisit;
|
|
2939
|
+
exports.transformIPLocationForBackend = transformIPLocationForBackend;
|
|
2763
2940
|
exports.updateSessionActivity = updateSessionActivity;
|
|
2764
2941
|
exports.useAnalytics = useAnalytics;
|
|
2765
2942
|
//# sourceMappingURL=index.cjs.js.map
|