user-analytics-tracker 2.0.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
@@ -549,6 +549,277 @@ function checkAndSetLocationConsent(msisdn) {
549
549
  return false;
550
550
  }
551
551
 
552
+ /**
553
+ * IP Geolocation Service
554
+ * Fetches location data (country, region, city) from user's IP address
555
+ * Uses ipwho.is API (no API key required)
556
+ *
557
+ * Stores all keys dynamically from the API response, including nested objects
558
+ * This ensures we capture all available data and any new fields added by the API
559
+ */
560
+ /**
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
563
+ * No API key required
564
+ *
565
+ * @returns Promise<IPLocation | null> - Complete IP location data, or null if unavailable
566
+ *
567
+ * @example
568
+ * ```typescript
569
+ * const location = await getCompleteIPLocation();
570
+ * console.log('IP:', location?.ip);
571
+ * console.log('Country:', location?.country);
572
+ * console.log('ISP:', location?.connection?.isp);
573
+ * ```
574
+ */
575
+ async function getCompleteIPLocation() {
576
+ // Skip if we're in an environment without fetch (SSR)
577
+ if (typeof fetch === 'undefined') {
578
+ return null;
579
+ }
580
+ try {
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
583
+ const response = await fetch('https://ipwho.is/', {
584
+ method: 'GET',
585
+ headers: {
586
+ Accept: 'application/json',
587
+ },
588
+ // Add timeout to prevent hanging
589
+ signal: AbortSignal.timeout(5000),
590
+ });
591
+ if (!response.ok) {
592
+ return null;
593
+ }
594
+ const data = await response.json();
595
+ // ipwho.is returns success field
596
+ if (data.success === false) {
597
+ return null;
598
+ }
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;
637
+ }
638
+ catch (error) {
639
+ // Silently fail - don't break user experience
640
+ if (error.name !== 'AbortError') {
641
+ console.warn('[IP Geolocation] Error fetching complete IP location from ipwho.is:', error.message);
642
+ }
643
+ return null;
644
+ }
645
+ }
646
+ /**
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)
692
+ * Free tier: No API key required
693
+ *
694
+ * Stores all keys dynamically from the API response, including nested objects
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
698
+ */
699
+ async function getIPLocation(ip) {
700
+ // Skip localhost/private IPs (these can't be geolocated)
701
+ if (!ip ||
702
+ ip === '0.0.0.0' ||
703
+ ip === '::1' ||
704
+ ip.startsWith('127.') ||
705
+ ip.startsWith('192.168.') ||
706
+ ip.startsWith('10.') ||
707
+ ip.startsWith('172.') ||
708
+ ip.startsWith('::ffff:127.')) {
709
+ console.log(`[IP Geolocation] Skipping localhost/private IP: ${ip} (geolocation not available for local IPs)`);
710
+ return null;
711
+ }
712
+ try {
713
+ // Using ipwho.is API (no API key required)
714
+ const response = await fetch(`https://ipwho.is/${ip}`, {
715
+ method: 'GET',
716
+ headers: {
717
+ Accept: 'application/json',
718
+ },
719
+ // Add timeout to prevent hanging
720
+ signal: AbortSignal.timeout(5000),
721
+ });
722
+ if (!response.ok) {
723
+ console.warn(`[IP Geolocation] Failed to fetch location for IP ${ip}: ${response.status}`);
724
+ return null;
725
+ }
726
+ const data = await response.json();
727
+ // ipwho.is returns success field
728
+ if (data.success === false) {
729
+ console.warn(`[IP Geolocation] API error for IP ${ip}: ${data.message || 'Unknown error'}`);
730
+ return null;
731
+ }
732
+ // Store all keys dynamically from the response
733
+ // This ensures we capture all fields, including nested objects and any new fields
734
+ const locationData = {
735
+ ip: data.ip || ip,
736
+ // Map all fields from the API response dynamically
737
+ ...Object.keys(data).reduce((acc, key) => {
738
+ // Store all keys and their values, preserving nested objects
739
+ acc[key] = data[key];
740
+ return acc;
741
+ }, {}),
742
+ };
743
+ // Add backward compatibility mappings for existing code
744
+ if (data.latitude !== undefined) {
745
+ locationData.lat = data.latitude;
746
+ }
747
+ if (data.longitude !== undefined) {
748
+ locationData.lon = data.longitude;
749
+ }
750
+ if (data.country_code !== undefined) {
751
+ locationData.countryCode = data.country_code;
752
+ }
753
+ if (data.region !== undefined) {
754
+ locationData.regionName = data.region;
755
+ }
756
+ if (data.connection?.isp !== undefined) {
757
+ locationData.isp = data.connection.isp;
758
+ }
759
+ if (data.connection?.org !== undefined) {
760
+ locationData.org = data.connection.org;
761
+ }
762
+ if (data.connection?.asn !== undefined) {
763
+ locationData.as = `AS${data.connection.asn}`;
764
+ }
765
+ if (data.timezone?.id !== undefined) {
766
+ locationData.timezone = data.timezone.id;
767
+ }
768
+ locationData.query = data.ip || ip;
769
+ return locationData;
770
+ }
771
+ catch (error) {
772
+ // Silently fail - don't break user experience
773
+ if (error.name !== 'AbortError') {
774
+ console.warn(`[IP Geolocation] Error fetching location for IP ${ip}:`, error.message);
775
+ }
776
+ return null;
777
+ }
778
+ }
779
+ /**
780
+ * Get IP address from request headers
781
+ * Handles various proxy headers (x-forwarded-for, x-real-ip, etc.)
782
+ */
783
+ function getIPFromRequest(req) {
784
+ // Try various headers that proxies/load balancers use
785
+ const forwardedFor = req.headers?.get?.('x-forwarded-for') ||
786
+ req.headers?.['x-forwarded-for'] ||
787
+ req.headers?.['X-Forwarded-For'];
788
+ if (forwardedFor) {
789
+ // x-forwarded-for can contain multiple IPs, take the first one
790
+ const ips = forwardedFor.split(',').map((ip) => ip.trim());
791
+ const ip = ips[0];
792
+ if (ip && ip !== '0.0.0.0') {
793
+ return ip;
794
+ }
795
+ }
796
+ const realIP = req.headers?.get?.('x-real-ip') ||
797
+ req.headers?.['x-real-ip'] ||
798
+ req.headers?.['X-Real-IP'];
799
+ if (realIP && realIP !== '0.0.0.0') {
800
+ return realIP.trim();
801
+ }
802
+ // Try req.ip (from Express/Next.js)
803
+ if (req.ip && req.ip !== '0.0.0.0') {
804
+ return req.ip;
805
+ }
806
+ // For localhost, detect if we're running locally
807
+ if (typeof window === 'undefined') {
808
+ const hostname = req.headers?.get?.('host') || req.headers?.['host'];
809
+ if (hostname &&
810
+ (hostname.includes('localhost') ||
811
+ hostname.includes('127.0.0.1') ||
812
+ hostname.startsWith('192.168.'))) {
813
+ return '127.0.0.1'; // Localhost IP
814
+ }
815
+ }
816
+ // If no IP found and we're in development, return localhost
817
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') {
818
+ return '127.0.0.1'; // Localhost for development
819
+ }
820
+ return '0.0.0.0';
821
+ }
822
+
552
823
  /**
553
824
  * Location Detector
554
825
  * Detects GPS location with consent management, falls back to IP-based location API
@@ -813,7 +1084,8 @@ class LocationDetector {
813
1084
  /**
814
1085
  * Get location from IP-based public API (client-side)
815
1086
  * Works without user permission, good fallback when GPS is unavailable
816
- * Uses ip-api.com free tier (no API key required, 45 requests/minute)
1087
+ * Uses ipwho.is API (no API key required)
1088
+ * Stores all keys dynamically from the API response
817
1089
  */
818
1090
  static async getIPBasedLocation() {
819
1091
  // Return cached IP location if available
@@ -849,51 +1121,55 @@ class LocationDetector {
849
1121
  }
850
1122
  this.ipLocationFetchingRef.current = true;
851
1123
  try {
852
- // Call ip-api.com without IP parameter - it auto-detects user's IP
853
- // Using HTTPS endpoint for better security
854
- const response = await fetch('https://ip-api.com/json/?fields=status,message,country,countryCode,region,regionName,city,lat,lon,timezone,query', {
855
- method: 'GET',
856
- headers: {
857
- Accept: 'application/json',
858
- },
859
- // Add timeout to prevent hanging
860
- signal: AbortSignal.timeout(5000),
861
- });
862
- if (!response.ok) {
863
- throw new Error(`HTTP ${response.status}`);
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
+ }
864
1136
  }
865
- const data = await response.json();
866
- // ip-api.com returns status field
867
- if (data.status === 'fail') {
868
- console.warn(`[Location] IP API error: ${data.message}`);
869
- const fallback = {
870
- source: 'unknown',
871
- permission: 'granted',
872
- };
873
- this.lastIPLocationRef.current = fallback;
874
- return fallback;
1137
+ if (!ipLocation) {
1138
+ throw new Error('Could not fetch location data from ipwho.is');
875
1139
  }
876
1140
  // Convert IP location to LocationInfo format
1141
+ // Map all available fields from the IP location response
1142
+ // Handle timezone which can be either a string or an object
1143
+ const timezoneValue = typeof ipLocation.timezone === 'string'
1144
+ ? ipLocation.timezone
1145
+ : ipLocation.timezone?.id || undefined;
877
1146
  const locationResult = {
878
- lat: data.lat || null,
879
- lon: data.lon || null,
1147
+ lat: ipLocation.latitude ?? ipLocation.lat ?? null,
1148
+ lon: ipLocation.longitude ?? ipLocation.lon ?? null,
880
1149
  accuracy: null, // IP-based location has no accuracy metric
881
1150
  permission: 'granted', // IP location doesn't require permission
882
1151
  source: 'ip',
883
1152
  ts: new Date().toISOString(),
884
- ip: data.query || null, // Public IP address
885
- country: data.country || undefined,
886
- countryCode: data.countryCode || undefined,
887
- city: data.city || undefined,
888
- region: data.regionName || data.region || undefined,
889
- timezone: data.timezone || undefined,
1153
+ ip: ipLocation.ip || undefined,
1154
+ country: ipLocation.country || undefined,
1155
+ countryCode: ipLocation.country_code || ipLocation.countryCode || undefined,
1156
+ city: ipLocation.city || undefined,
1157
+ region: ipLocation.region || ipLocation.regionName || undefined,
1158
+ timezone: timezoneValue,
890
1159
  };
891
- console.log('[Location] IP-based location obtained:', {
1160
+ // Store the full IP location data in a custom field for access to all keys
1161
+ // This preserves all dynamic keys from the API response
1162
+ locationResult.ipLocationData = ipLocation;
1163
+ console.log('[Location] IP-based location obtained from ipwho.is:', {
892
1164
  ip: locationResult.ip,
893
1165
  lat: locationResult.lat,
894
1166
  lon: locationResult.lon,
895
1167
  city: locationResult.city,
896
1168
  country: locationResult.country,
1169
+ continent: ipLocation.continent,
1170
+ timezone: locationResult.timezone,
1171
+ isp: ipLocation.connection?.isp,
1172
+ connection: ipLocation.connection,
897
1173
  });
898
1174
  this.lastIPLocationRef.current = locationResult;
899
1175
  return locationResult;
@@ -1730,6 +2006,73 @@ class MetricsCollector {
1730
2006
  // Global metrics collector instance
1731
2007
  const metricsCollector = new MetricsCollector();
1732
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
+
1733
2076
  /**
1734
2077
  * Analytics Service
1735
2078
  * Sends analytics events to your backend API
@@ -1993,6 +2336,8 @@ class AnalyticsService {
1993
2336
  * Track user journey with full context
1994
2337
  */
1995
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);
1996
2341
  await this.trackEvent({
1997
2342
  sessionId,
1998
2343
  pageUrl,
@@ -2004,7 +2349,8 @@ class AnalyticsService {
2004
2349
  userId: userId ?? sessionId,
2005
2350
  customData: {
2006
2351
  ...customData,
2007
- ...(ipLocation && { ipLocation }),
2352
+ // Store transformed IP location in customData for backend integration
2353
+ ...(transformedIPLocation && { ipLocation: transformedIPLocation }),
2008
2354
  },
2009
2355
  eventName: 'page_view', // Auto-tracked as page view
2010
2356
  });
@@ -2075,6 +2421,13 @@ class AnalyticsService {
2075
2421
  }
2076
2422
  const finalSessionId = context?.sessionId || autoContext?.sessionId || 'unknown';
2077
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);
2078
2431
  await this.trackEvent({
2079
2432
  sessionId: finalSessionId,
2080
2433
  pageUrl: finalPageUrl,
@@ -2085,7 +2438,11 @@ class AnalyticsService {
2085
2438
  userId: context?.userId || finalSessionId,
2086
2439
  eventName,
2087
2440
  eventParameters: parameters || {},
2088
- customData: parameters || {},
2441
+ customData: {
2442
+ ...(parameters || {}),
2443
+ // Store transformed IP location in customData for backend integration
2444
+ ...(transformedIPLocation && { ipLocation: transformedIPLocation }),
2445
+ },
2089
2446
  });
2090
2447
  }
2091
2448
  /**
@@ -2376,6 +2733,8 @@ function useAnalytics(options = {}) {
2376
2733
  if (autoSend) {
2377
2734
  // Send after idle to not block paint
2378
2735
  const send = async () => {
2736
+ // Extract IP location data if available (stored in ipLocationData field)
2737
+ const ipLocationData = loc?.ipLocationData;
2379
2738
  await AnalyticsService.trackUserJourney({
2380
2739
  sessionId: getOrCreateUserId(),
2381
2740
  pageUrl: typeof window !== 'undefined' ? window.location.href : '',
@@ -2383,6 +2742,7 @@ function useAnalytics(options = {}) {
2383
2742
  deviceInfo: dev,
2384
2743
  location: loc,
2385
2744
  attribution: attr,
2745
+ ipLocation: ipLocationData,
2386
2746
  customData: config?.enableLocation ? { locationEnabled: true } : undefined,
2387
2747
  });
2388
2748
  };
@@ -2398,6 +2758,8 @@ function useAnalytics(options = {}) {
2398
2758
  const logEvent = useCallback(async (customData) => {
2399
2759
  if (!sessionId || !networkInfo || !deviceInfo)
2400
2760
  return;
2761
+ // Extract IP location data if available (stored in ipLocationData field)
2762
+ const ipLocationData = location ? location?.ipLocationData : undefined;
2401
2763
  await AnalyticsService.trackUserJourney({
2402
2764
  sessionId,
2403
2765
  pageUrl: typeof window !== 'undefined' ? window.location.href : '',
@@ -2405,6 +2767,7 @@ function useAnalytics(options = {}) {
2405
2767
  deviceInfo,
2406
2768
  location: location ?? undefined,
2407
2769
  attribution: attribution ?? undefined,
2770
+ ipLocation: ipLocationData,
2408
2771
  userId: sessionId,
2409
2772
  customData,
2410
2773
  });
@@ -2542,166 +2905,5 @@ function useAnalytics(options = {}) {
2542
2905
  ]);
2543
2906
  }
2544
2907
 
2545
- /**
2546
- * IP Geolocation Service
2547
- * Fetches location data (country, region, city) from user's IP address
2548
- * Uses free tier of ip-api.com (no API key required, 45 requests/minute)
2549
- */
2550
- /**
2551
- * Get public IP address using ip-api.com
2552
- * Free tier: 45 requests/minute, no API key required
2553
- *
2554
- * @returns Promise<string | null> - The public IP address, or null if unavailable
2555
- *
2556
- * @example
2557
- * ```typescript
2558
- * const ip = await getPublicIP();
2559
- * console.log('Your IP:', ip); // e.g., "203.0.113.42"
2560
- * ```
2561
- */
2562
- async function getPublicIP() {
2563
- // Skip if we're in an environment without fetch (SSR)
2564
- if (typeof fetch === 'undefined') {
2565
- return null;
2566
- }
2567
- try {
2568
- // Call ip-api.com without IP parameter - it auto-detects user's IP
2569
- // Using HTTPS endpoint for better security
2570
- const response = await fetch('https://ip-api.com/json/?fields=status,message,query', {
2571
- method: 'GET',
2572
- headers: {
2573
- Accept: 'application/json',
2574
- },
2575
- // Add timeout to prevent hanging
2576
- signal: AbortSignal.timeout(5000),
2577
- });
2578
- if (!response.ok) {
2579
- return null;
2580
- }
2581
- const data = await response.json();
2582
- // ip-api.com returns status field
2583
- if (data.status === 'fail') {
2584
- return null;
2585
- }
2586
- return data.query || null;
2587
- }
2588
- catch (error) {
2589
- // Silently fail - don't break user experience
2590
- if (error.name !== 'AbortError') {
2591
- console.warn('[IP Geolocation] Error fetching public IP:', error.message);
2592
- }
2593
- return null;
2594
- }
2595
- }
2596
- /**
2597
- * Get location from IP address using ip-api.com
2598
- * Free tier: 45 requests/minute, no API key required
2599
- *
2600
- * Alternative services:
2601
- * - ipapi.co (requires API key for production)
2602
- * - ipgeolocation.io (requires API key)
2603
- * - ip-api.com (free tier available)
2604
- */
2605
- async function getIPLocation(ip) {
2606
- // Skip localhost/private IPs (these can't be geolocated)
2607
- if (!ip ||
2608
- ip === '0.0.0.0' ||
2609
- ip === '::1' ||
2610
- ip.startsWith('127.') ||
2611
- ip.startsWith('192.168.') ||
2612
- ip.startsWith('10.') ||
2613
- ip.startsWith('172.') ||
2614
- ip.startsWith('::ffff:127.')) {
2615
- console.log(`[IP Geolocation] Skipping localhost/private IP: ${ip} (geolocation not available for local IPs)`);
2616
- return null;
2617
- }
2618
- try {
2619
- // Using ip-api.com free tier (JSON format)
2620
- const response = await fetch(`http://ip-api.com/json/${ip}?fields=status,message,country,countryCode,region,regionName,city,lat,lon,timezone,isp,org,as,query`, {
2621
- method: 'GET',
2622
- headers: {
2623
- Accept: 'application/json',
2624
- },
2625
- // Add timeout to prevent hanging
2626
- signal: AbortSignal.timeout(3000),
2627
- });
2628
- if (!response.ok) {
2629
- console.warn(`[IP Geolocation] Failed to fetch location for IP ${ip}: ${response.status}`);
2630
- return null;
2631
- }
2632
- const data = await response.json();
2633
- // ip-api.com returns status field
2634
- if (data.status === 'fail') {
2635
- console.warn(`[IP Geolocation] API error for IP ${ip}: ${data.message}`);
2636
- return null;
2637
- }
2638
- return {
2639
- ip: data.query || ip,
2640
- country: data.country || undefined,
2641
- countryCode: data.countryCode || undefined,
2642
- region: data.region || undefined,
2643
- regionName: data.regionName || undefined,
2644
- city: data.city || undefined,
2645
- lat: data.lat || undefined,
2646
- lon: data.lon || undefined,
2647
- timezone: data.timezone || undefined,
2648
- isp: data.isp || undefined,
2649
- org: data.org || undefined,
2650
- as: data.as || undefined,
2651
- query: data.query || ip,
2652
- };
2653
- }
2654
- catch (error) {
2655
- // Silently fail - don't break user experience
2656
- if (error.name !== 'AbortError') {
2657
- console.warn(`[IP Geolocation] Error fetching location for IP ${ip}:`, error.message);
2658
- }
2659
- return null;
2660
- }
2661
- }
2662
- /**
2663
- * Get IP address from request headers
2664
- * Handles various proxy headers (x-forwarded-for, x-real-ip, etc.)
2665
- */
2666
- function getIPFromRequest(req) {
2667
- // Try various headers that proxies/load balancers use
2668
- const forwardedFor = req.headers?.get?.('x-forwarded-for') ||
2669
- req.headers?.['x-forwarded-for'] ||
2670
- req.headers?.['X-Forwarded-For'];
2671
- if (forwardedFor) {
2672
- // x-forwarded-for can contain multiple IPs, take the first one
2673
- const ips = forwardedFor.split(',').map((ip) => ip.trim());
2674
- const ip = ips[0];
2675
- if (ip && ip !== '0.0.0.0') {
2676
- return ip;
2677
- }
2678
- }
2679
- const realIP = req.headers?.get?.('x-real-ip') ||
2680
- req.headers?.['x-real-ip'] ||
2681
- req.headers?.['X-Real-IP'];
2682
- if (realIP && realIP !== '0.0.0.0') {
2683
- return realIP.trim();
2684
- }
2685
- // Try req.ip (from Express/Next.js)
2686
- if (req.ip && req.ip !== '0.0.0.0') {
2687
- return req.ip;
2688
- }
2689
- // For localhost, detect if we're running locally
2690
- if (typeof window === 'undefined') {
2691
- const hostname = req.headers?.get?.('host') || req.headers?.['host'];
2692
- if (hostname &&
2693
- (hostname.includes('localhost') ||
2694
- hostname.includes('127.0.0.1') ||
2695
- hostname.startsWith('192.168.'))) {
2696
- return '127.0.0.1'; // Localhost IP
2697
- }
2698
- }
2699
- // If no IP found and we're in development, return localhost
2700
- if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') {
2701
- return '127.0.0.1'; // Localhost for development
2702
- }
2703
- return '0.0.0.0';
2704
- }
2705
-
2706
- 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 };
2707
2909
  //# sourceMappingURL=index.esm.js.map