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/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 public IP address using ipwho.is API
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<string | null> - The public IP address, or null if unavailable
565
+ * @returns Promise<IPLocation | null> - Complete IP location data, or null if unavailable
565
566
  *
566
567
  * @example
567
568
  * ```typescript
568
- * const ip = await getPublicIP();
569
- * console.log('Your IP:', ip); // e.g., "203.0.113.42"
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 getPublicIP() {
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
- // Using HTTPS endpoint for better security
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
- return data.ip || null;
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 public IP:', error.message);
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 location from IP address using ipwho.is API
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 public IP first, then get location
1039
- const publicIP = await getPublicIP();
1040
- if (!publicIP) {
1041
- throw new Error('Could not determine public IP address');
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 || publicIP,
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
- ...(ipLocation && { ipLocation }),
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: parameters || {},
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