user-analytics-tracker 2.2.0 → 4.0.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
@@ -1,5 +1,100 @@
1
1
  import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
2
2
 
3
+ /**
4
+ * Core types for the analytics tracker package
5
+ */
6
+ /**
7
+ * Default essential fields for IP location storage
8
+ * These fields are stored when mode is 'essential' (default)
9
+ */
10
+ const DEFAULT_ESSENTIAL_IP_FIELDS = [
11
+ // Core identification
12
+ 'ip',
13
+ 'country',
14
+ 'countryCode',
15
+ 'region',
16
+ 'city',
17
+ // Geographic coordinates (stored here, not duplicated in location)
18
+ 'lat',
19
+ 'lon',
20
+ // Additional geographic info
21
+ 'continent',
22
+ 'continentCode',
23
+ // Network info
24
+ 'type',
25
+ 'isEu',
26
+ 'isp',
27
+ 'connection',
28
+ 'connection.asn',
29
+ 'connection.org',
30
+ 'connection.isp',
31
+ 'connection.domain',
32
+ // Timezone (stored here, not duplicated in location)
33
+ 'timezone',
34
+ 'timezoneDetails',
35
+ 'timezoneDetails.id',
36
+ 'timezoneDetails.abbr',
37
+ 'timezoneDetails.utc',
38
+ // Flag (only emoji in essential mode)
39
+ 'flag.emoji',
40
+ ];
41
+ /**
42
+ * Default essential fields for Device Info storage
43
+ */
44
+ const DEFAULT_ESSENTIAL_DEVICE_FIELDS = [
45
+ 'type',
46
+ 'os',
47
+ 'osVersion',
48
+ 'browser',
49
+ 'browserVersion',
50
+ 'deviceModel',
51
+ 'deviceBrand',
52
+ 'userAgent',
53
+ ];
54
+ /**
55
+ * Default essential fields for Network Info storage
56
+ *
57
+ * NOTE: In essential mode, networkInfo is not stored.
58
+ * Connection data from ipwho.is API (in customData.ipLocation.connection) is used instead,
59
+ * as it provides more accurate network/ISP information.
60
+ */
61
+ const DEFAULT_ESSENTIAL_NETWORK_FIELDS = [
62
+ 'type',
63
+ 'effectiveType',
64
+ 'downlink',
65
+ 'rtt',
66
+ 'saveData',
67
+ ];
68
+ /**
69
+ * Default essential fields for Location Info storage
70
+ */
71
+ const DEFAULT_ESSENTIAL_LOCATION_FIELDS = [
72
+ // Minimal location fields - only coordinates and source
73
+ // All IP-related data (ip, country, city, etc.) is stored in customData.ipLocation to avoid duplication
74
+ 'lat',
75
+ 'lon',
76
+ 'source',
77
+ 'ts',
78
+ ];
79
+ /**
80
+ * Default essential fields for Attribution Info storage
81
+ */
82
+ const DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS = [
83
+ 'landingUrl',
84
+ 'path',
85
+ 'hostname',
86
+ 'referrerUrl',
87
+ 'referrerDomain',
88
+ 'navigationType',
89
+ 'isReload',
90
+ 'isBackForward',
91
+ 'utm_source',
92
+ 'utm_medium',
93
+ 'utm_campaign',
94
+ 'utm_term',
95
+ 'utm_content',
96
+ ];
97
+
3
98
  /**
4
99
  * Network Type Detector
5
100
  * Detects WiFi, Mobile Data (Cellular), Hotspot, Ethernet, or Unknown
@@ -135,11 +230,6 @@ class NetworkDetector {
135
230
  }
136
231
  }
137
232
 
138
- var networkDetector = /*#__PURE__*/Object.freeze({
139
- __proto__: null,
140
- NetworkDetector: NetworkDetector
141
- });
142
-
143
233
  /**
144
234
  * Device Information Detector
145
235
  * Detects device type, OS, browser, and hardware specs
@@ -2006,18 +2096,206 @@ class MetricsCollector {
2006
2096
  // Global metrics collector instance
2007
2097
  const metricsCollector = new MetricsCollector();
2008
2098
 
2099
+ /**
2100
+ * Generic field storage transformer
2101
+ * Filters object fields based on storage configuration
2102
+ */
2103
+ /**
2104
+ * Filter object fields based on storage configuration
2105
+ *
2106
+ * @param data - The data object to filter
2107
+ * @param config - Storage configuration
2108
+ * @param defaultEssentialFields - Default essential fields for this data type
2109
+ * @returns Filtered data object with only configured fields
2110
+ */
2111
+ function filterFieldsByConfig(data, config, defaultEssentialFields) {
2112
+ if (!data) {
2113
+ return null;
2114
+ }
2115
+ const mode = config?.mode || 'essential';
2116
+ let fieldsToInclude = [];
2117
+ if (mode === 'essential') {
2118
+ // Use default essential fields
2119
+ fieldsToInclude = [...defaultEssentialFields];
2120
+ }
2121
+ else if (mode === 'all') {
2122
+ // Include all fields, then exclude specified ones
2123
+ fieldsToInclude = ['*']; // Special marker for "all fields"
2124
+ }
2125
+ else if (mode === 'custom' && config) {
2126
+ // Use custom field list
2127
+ fieldsToInclude = config.fields || [];
2128
+ }
2129
+ // If mode is 'all', just exclude specified fields
2130
+ if (mode === 'all') {
2131
+ const filtered = { ...data };
2132
+ if (config?.exclude && config.exclude.length > 0) {
2133
+ const excludeSet = new Set(config.exclude);
2134
+ Object.keys(filtered).forEach(key => {
2135
+ if (excludeSet.has(key)) {
2136
+ delete filtered[key];
2137
+ }
2138
+ });
2139
+ // Handle nested exclusions
2140
+ if (filtered.connection && excludeSet.has('connection')) {
2141
+ delete filtered.connection;
2142
+ }
2143
+ if (filtered.timezoneDetails && excludeSet.has('timezoneDetails')) {
2144
+ delete filtered.timezoneDetails;
2145
+ }
2146
+ if (filtered.flag && excludeSet.has('flag')) {
2147
+ delete filtered.flag;
2148
+ }
2149
+ if (filtered.firstTouch && excludeSet.has('firstTouch')) {
2150
+ delete filtered.firstTouch;
2151
+ }
2152
+ if (filtered.lastTouch && excludeSet.has('lastTouch')) {
2153
+ delete filtered.lastTouch;
2154
+ }
2155
+ }
2156
+ return filtered;
2157
+ }
2158
+ // For 'essential' or 'custom' mode, only include specified fields
2159
+ const filtered = {};
2160
+ const includeSet = new Set(fieldsToInclude);
2161
+ // Helper to check if a field path should be included
2162
+ const shouldInclude = (fieldPath) => {
2163
+ // Direct match - most specific
2164
+ if (includeSet.has(fieldPath))
2165
+ return true;
2166
+ // For nested fields (e.g., 'flag.emoji'), only include if explicitly listed
2167
+ // Don't auto-include all children just because parent is included
2168
+ const parts = fieldPath.split('.');
2169
+ if (parts.length > 1) {
2170
+ // For nested fields, require explicit inclusion
2171
+ // This prevents 'flag' from including all 'flag.*' fields
2172
+ return includeSet.has(fieldPath);
2173
+ }
2174
+ // For top-level fields only, check if parent path is included
2175
+ // This allows 'connection' to work when all connection.* fields are listed
2176
+ return false;
2177
+ };
2178
+ // Helper to check if a parent object should be created (for nested objects)
2179
+ const shouldIncludeParent = (parentPath) => {
2180
+ // Check if any child of this parent is included
2181
+ for (const field of fieldsToInclude) {
2182
+ if (field.startsWith(parentPath + '.')) {
2183
+ return true;
2184
+ }
2185
+ }
2186
+ // Also check if parent itself is explicitly included
2187
+ return includeSet.has(parentPath);
2188
+ };
2189
+ // Filter top-level fields
2190
+ Object.keys(data).forEach(key => {
2191
+ if (shouldInclude(key)) {
2192
+ filtered[key] = data[key];
2193
+ }
2194
+ });
2195
+ // Handle nested objects - only create if at least one child field is included
2196
+ if (data.connection && shouldIncludeParent('connection')) {
2197
+ filtered.connection = {};
2198
+ if (shouldInclude('connection.asn'))
2199
+ filtered.connection.asn = data.connection.asn;
2200
+ if (shouldInclude('connection.org'))
2201
+ filtered.connection.org = data.connection.org;
2202
+ if (shouldInclude('connection.isp'))
2203
+ filtered.connection.isp = data.connection.isp;
2204
+ if (shouldInclude('connection.domain'))
2205
+ filtered.connection.domain = data.connection.domain;
2206
+ // If no connection fields were included, remove the object
2207
+ if (Object.keys(filtered.connection).length === 0) {
2208
+ delete filtered.connection;
2209
+ }
2210
+ }
2211
+ if (data.timezoneDetails && shouldIncludeParent('timezoneDetails')) {
2212
+ filtered.timezoneDetails = {};
2213
+ if (shouldInclude('timezoneDetails.id'))
2214
+ filtered.timezoneDetails.id = data.timezoneDetails.id;
2215
+ if (shouldInclude('timezoneDetails.abbr'))
2216
+ filtered.timezoneDetails.abbr = data.timezoneDetails.abbr;
2217
+ if (shouldInclude('timezoneDetails.utc'))
2218
+ filtered.timezoneDetails.utc = data.timezoneDetails.utc;
2219
+ if (shouldInclude('timezoneDetails.isDst'))
2220
+ filtered.timezoneDetails.isDst = data.timezoneDetails.isDst;
2221
+ if (shouldInclude('timezoneDetails.offset'))
2222
+ filtered.timezoneDetails.offset = data.timezoneDetails.offset;
2223
+ if (shouldInclude('timezoneDetails.currentTime'))
2224
+ filtered.timezoneDetails.currentTime = data.timezoneDetails.currentTime;
2225
+ // If no timezoneDetails fields were included, remove the object
2226
+ if (Object.keys(filtered.timezoneDetails).length === 0) {
2227
+ delete filtered.timezoneDetails;
2228
+ }
2229
+ }
2230
+ if (data.flag && shouldIncludeParent('flag')) {
2231
+ filtered.flag = {};
2232
+ // Only include specific flag fields if they're explicitly in the include list
2233
+ if (shouldInclude('flag.emoji'))
2234
+ filtered.flag.emoji = data.flag.emoji;
2235
+ if (shouldInclude('flag.img'))
2236
+ filtered.flag.img = data.flag.img;
2237
+ if (shouldInclude('flag.emojiUnicode'))
2238
+ filtered.flag.emojiUnicode = data.flag.emojiUnicode;
2239
+ // If no specific flag fields are included, don't add the flag object
2240
+ if (Object.keys(filtered.flag).length === 0) {
2241
+ delete filtered.flag;
2242
+ }
2243
+ }
2244
+ if (data.firstTouch && shouldInclude('firstTouch')) {
2245
+ filtered.firstTouch = data.firstTouch;
2246
+ }
2247
+ if (data.lastTouch && shouldInclude('lastTouch')) {
2248
+ filtered.lastTouch = data.lastTouch;
2249
+ }
2250
+ // Remove null and undefined values to reduce payload size
2251
+ const cleanValue = (val) => {
2252
+ if (val === null || val === undefined) {
2253
+ return undefined; // Will be filtered out
2254
+ }
2255
+ // For objects, recursively clean nested null/undefined values
2256
+ if (typeof val === 'object' && !Array.isArray(val) && val !== null) {
2257
+ const cleaned = {};
2258
+ let hasAnyValue = false;
2259
+ Object.keys(val).forEach(key => {
2260
+ const cleanedChild = cleanValue(val[key]);
2261
+ if (cleanedChild !== undefined) {
2262
+ cleaned[key] = cleanedChild;
2263
+ hasAnyValue = true;
2264
+ }
2265
+ });
2266
+ return hasAnyValue ? cleaned : undefined;
2267
+ }
2268
+ // For arrays, clean each element
2269
+ if (Array.isArray(val)) {
2270
+ const cleaned = val.map(cleanValue).filter(item => item !== undefined);
2271
+ return cleaned.length > 0 ? cleaned : undefined;
2272
+ }
2273
+ return val;
2274
+ };
2275
+ const cleaned = {};
2276
+ Object.keys(filtered).forEach(key => {
2277
+ const cleanedValue = cleanValue(filtered[key]);
2278
+ if (cleanedValue !== undefined) {
2279
+ cleaned[key] = cleanedValue;
2280
+ }
2281
+ });
2282
+ return cleaned;
2283
+ }
2284
+
2009
2285
  /**
2010
2286
  * Transform IP location data from API format (snake_case) to backend-expected format (camelCase)
2011
- * This ensures compatibility with the analytics backend integration
2287
+ * Supports configurable field storage to optimize storage capacity
2012
2288
  *
2013
2289
  * @param ipLocation - Raw IP location data from ipwho.is API
2014
- * @returns Transformed IP location data matching backend schema
2290
+ * @param config - Optional configuration for which fields to store
2291
+ * @returns Transformed IP location data matching backend schema (only includes configured fields)
2015
2292
  */
2016
- function transformIPLocationForBackend(ipLocation) {
2293
+ function transformIPLocationForBackend(ipLocation, config) {
2017
2294
  if (!ipLocation) {
2018
2295
  return null;
2019
2296
  }
2020
2297
  // Transform to match backend expected format (camelCase)
2298
+ // Build complete object first, then filter based on configuration
2021
2299
  const transformed = {
2022
2300
  // Basic fields
2023
2301
  ip: ipLocation.ip,
@@ -2052,9 +2330,11 @@ function transformIPLocationForBackend(ipLocation) {
2052
2330
  timezoneDetails: ipLocation.timezone && typeof ipLocation.timezone === 'object' ? {
2053
2331
  id: ipLocation.timezone.id,
2054
2332
  abbr: ipLocation.timezone.abbr,
2333
+ utc: ipLocation.timezone.utc,
2334
+ // Exclude these in essential mode: isDst, offset, currentTime
2335
+ // They will be filtered out by filterFieldsByConfig if not in essential fields
2055
2336
  isDst: ipLocation.timezone.is_dst,
2056
2337
  offset: ipLocation.timezone.offset,
2057
- utc: ipLocation.timezone.utc,
2058
2338
  currentTime: ipLocation.timezone.current_time,
2059
2339
  } : undefined,
2060
2340
  // Flag - transform to camelCase
@@ -2070,7 +2350,8 @@ function transformIPLocationForBackend(ipLocation) {
2070
2350
  delete transformed[key];
2071
2351
  }
2072
2352
  });
2073
- return transformed;
2353
+ // Filter fields based on configuration using generic filter
2354
+ return filterFieldsByConfig(transformed, config, DEFAULT_ESSENTIAL_IP_FIELDS);
2074
2355
  }
2075
2356
 
2076
2357
  /**
@@ -2336,20 +2617,56 @@ class AnalyticsService {
2336
2617
  * Track user journey with full context
2337
2618
  */
2338
2619
  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);
2620
+ // Get field storage config (support both new and legacy format)
2621
+ const fieldStorage = this.config.fieldStorage || {};
2622
+ const ipLocationConfig = fieldStorage.ipLocation || this.config.ipLocationFields;
2623
+ // Transform and filter all data types based on configuration
2624
+ const transformedIPLocation = transformIPLocationForBackend(ipLocation, ipLocationConfig);
2625
+ const filteredDeviceInfo = filterFieldsByConfig(deviceInfo, fieldStorage.deviceInfo, DEFAULT_ESSENTIAL_DEVICE_FIELDS);
2626
+ // In essential mode, don't store browser-based networkInfo
2627
+ // Connection data from ipwho.is (in customData.ipLocation.connection) is more accurate
2628
+ const networkInfoConfig = fieldStorage.networkInfo;
2629
+ const networkInfoMode = networkInfoConfig?.mode || 'essential';
2630
+ // Skip networkInfo in essential mode - use connection from ipwho.is instead
2631
+ const filteredNetworkInfo = networkInfoMode === 'essential'
2632
+ ? undefined
2633
+ : filterFieldsByConfig(networkInfo, networkInfoConfig, DEFAULT_ESSENTIAL_NETWORK_FIELDS);
2634
+ // For location: In essential mode, remove duplicate fields that are already in customData.ipLocation
2635
+ // This prevents storing the same data twice (e.g., ip, country, city, region, timezone)
2636
+ const locationConfig = fieldStorage.location;
2637
+ const locationMode = locationConfig?.mode || 'essential';
2638
+ let filteredLocation = filterFieldsByConfig(location, locationConfig, DEFAULT_ESSENTIAL_LOCATION_FIELDS);
2639
+ // In essential mode, if we have IP location data, remove duplicate fields from location
2640
+ // to avoid storing the same data twice
2641
+ if (locationMode === 'essential' && transformedIPLocation && filteredLocation) {
2642
+ // Remove fields that are duplicated in customData.ipLocation
2643
+ const duplicateFields = ['ip', 'country', 'countryCode', 'city', 'region', 'timezone'];
2644
+ const minimalLocation = { ...filteredLocation };
2645
+ duplicateFields.forEach(field => {
2646
+ delete minimalLocation[field];
2647
+ });
2648
+ // Only keep essential location fields: lat, lon, source, ts
2649
+ filteredLocation = {
2650
+ lat: minimalLocation.lat,
2651
+ lon: minimalLocation.lon,
2652
+ source: minimalLocation.source,
2653
+ ts: minimalLocation.ts,
2654
+ };
2655
+ }
2656
+ const filteredAttribution = filterFieldsByConfig(attribution, fieldStorage.attribution, DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS);
2341
2657
  await this.trackEvent({
2342
2658
  sessionId,
2343
2659
  pageUrl,
2344
- networkInfo,
2345
- deviceInfo,
2346
- location,
2347
- attribution,
2348
- ipLocation,
2660
+ networkInfo: filteredNetworkInfo || undefined,
2661
+ deviceInfo: filteredDeviceInfo || undefined,
2662
+ location: filteredLocation || undefined,
2663
+ attribution: filteredAttribution || undefined,
2664
+ // Don't include raw ipLocation - we have the filtered/transformed version in customData
2665
+ ipLocation: undefined,
2349
2666
  userId: userId ?? sessionId,
2350
2667
  customData: {
2351
2668
  ...customData,
2352
- // Store transformed IP location in customData for backend integration
2669
+ // Store transformed and filtered IP location in customData for backend integration
2353
2670
  ...(transformedIPLocation && { ipLocation: transformedIPLocation }),
2354
2671
  },
2355
2672
  eventName: 'page_view', // Auto-tracked as page view
@@ -2389,14 +2706,14 @@ class AnalyticsService {
2389
2706
  try {
2390
2707
  // Import dynamically to avoid circular dependencies
2391
2708
  const { getOrCreateUserId } = await Promise.resolve().then(function () { return storage; });
2392
- const { NetworkDetector } = await Promise.resolve().then(function () { return networkDetector; });
2393
2709
  const { DeviceDetector } = await Promise.resolve().then(function () { return deviceDetector; });
2394
2710
  const { LocationDetector } = await Promise.resolve().then(function () { return locationDetector; });
2395
2711
  const { AttributionDetector } = await Promise.resolve().then(function () { return attributionDetector; });
2712
+ // Don't collect networkInfo - use connection from ipwho.is instead (more accurate)
2396
2713
  autoContext = {
2397
2714
  sessionId: getOrCreateUserId(),
2398
2715
  pageUrl: window.location.href,
2399
- networkInfo: NetworkDetector.detect(),
2716
+ // networkInfo removed - use customData.ipLocation.connection from ipwho.is instead
2400
2717
  deviceInfo: await DeviceDetector.detect(),
2401
2718
  location: await LocationDetector.detect().catch(() => undefined),
2402
2719
  attribution: AttributionDetector.detect(),
@@ -2426,15 +2743,48 @@ class AnalyticsService {
2426
2743
  const ipLocationData = locationData && typeof locationData === 'object'
2427
2744
  ? locationData?.ipLocationData
2428
2745
  : undefined;
2429
- // Transform IP location data to match backend expected format
2430
- const transformedIPLocation = transformIPLocationForBackend(ipLocationData);
2746
+ // Get field storage config (support both new and legacy format)
2747
+ const fieldStorage = this.config.fieldStorage || {};
2748
+ const ipLocationConfig = fieldStorage.ipLocation || this.config.ipLocationFields;
2749
+ // Transform and filter all data types based on configuration
2750
+ const transformedIPLocation = transformIPLocationForBackend(ipLocationData, ipLocationConfig);
2751
+ const filteredDeviceInfo = filterFieldsByConfig(context?.deviceInfo || autoContext?.deviceInfo, fieldStorage.deviceInfo, DEFAULT_ESSENTIAL_DEVICE_FIELDS);
2752
+ // In essential mode, don't store browser-based networkInfo
2753
+ // Connection data from ipwho.is (in customData.ipLocation.connection) is more accurate
2754
+ const networkInfoConfig = fieldStorage.networkInfo;
2755
+ const networkInfoMode = networkInfoConfig?.mode || 'essential';
2756
+ // Skip networkInfo in essential mode - use connection from ipwho.is instead
2757
+ const filteredNetworkInfo = networkInfoMode === 'essential'
2758
+ ? undefined
2759
+ : filterFieldsByConfig(context?.networkInfo || autoContext?.networkInfo, networkInfoConfig, DEFAULT_ESSENTIAL_NETWORK_FIELDS);
2760
+ // For location: In essential mode, remove duplicate fields that are already in customData.ipLocation
2761
+ const locationConfig = fieldStorage.location;
2762
+ const locationMode = locationConfig?.mode || 'essential';
2763
+ let filteredLocation = filterFieldsByConfig((context?.location || autoContext?.location), locationConfig, DEFAULT_ESSENTIAL_LOCATION_FIELDS);
2764
+ // In essential mode, if we have IP location data, remove duplicate fields from location
2765
+ if (locationMode === 'essential' && transformedIPLocation && filteredLocation) {
2766
+ // Remove fields that are duplicated in customData.ipLocation
2767
+ const duplicateFields = ['ip', 'country', 'countryCode', 'city', 'region', 'timezone'];
2768
+ const minimalLocation = { ...filteredLocation };
2769
+ duplicateFields.forEach(field => {
2770
+ delete minimalLocation[field];
2771
+ });
2772
+ // Only keep essential location fields: lat, lon, source, ts
2773
+ filteredLocation = {
2774
+ lat: minimalLocation.lat,
2775
+ lon: minimalLocation.lon,
2776
+ source: minimalLocation.source,
2777
+ ts: minimalLocation.ts,
2778
+ };
2779
+ }
2780
+ const filteredAttribution = filterFieldsByConfig(context?.attribution || autoContext?.attribution, fieldStorage.attribution, DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS);
2431
2781
  await this.trackEvent({
2432
2782
  sessionId: finalSessionId,
2433
2783
  pageUrl: finalPageUrl,
2434
- networkInfo: context?.networkInfo || autoContext?.networkInfo,
2435
- deviceInfo: context?.deviceInfo || autoContext?.deviceInfo,
2436
- location: context?.location || autoContext?.location,
2437
- attribution: context?.attribution || autoContext?.attribution,
2784
+ networkInfo: filteredNetworkInfo || undefined,
2785
+ deviceInfo: filteredDeviceInfo || undefined,
2786
+ location: filteredLocation || undefined,
2787
+ attribution: filteredAttribution || undefined,
2438
2788
  userId: context?.userId || finalSessionId,
2439
2789
  eventName,
2440
2790
  eventParameters: parameters || {},
@@ -2620,6 +2970,8 @@ function useAnalytics(options = {}) {
2620
2970
  logLevel: config.logLevel,
2621
2971
  enableMetrics: config.enableMetrics,
2622
2972
  sessionTimeout: config.sessionTimeout,
2973
+ fieldStorage: config.fieldStorage,
2974
+ ipLocationFields: config.ipLocationFields, // Legacy support
2623
2975
  });
2624
2976
  }
2625
2977
  }, [
@@ -2662,7 +3014,8 @@ function useAnalytics(options = {}) {
2662
3014
  initDebug();
2663
3015
  }, []);
2664
3016
  const refresh = useCallback(async () => {
2665
- const net = NetworkDetector.detect();
3017
+ // Don't collect networkInfo - use connection from ipwho.is instead (more accurate)
3018
+ // const net = NetworkDetector.detect(); // Removed - use ipwho.is connection instead
2666
3019
  const dev = await DeviceDetector.detect();
2667
3020
  const attr = AttributionDetector.detect();
2668
3021
  const uid = getOrCreateUserId();
@@ -2705,7 +3058,8 @@ function useAnalytics(options = {}) {
2705
3058
  lastLocationRef.current = loc;
2706
3059
  }
2707
3060
  }
2708
- setNetworkInfo(net);
3061
+ // networkInfo removed - use connection from ipwho.is instead
3062
+ setNetworkInfo(null); // Set to null - connection data comes from ipwho.is
2709
3063
  setDeviceInfo(dev);
2710
3064
  setAttribution(attr);
2711
3065
  setSessionId(uid);
@@ -2715,13 +3069,14 @@ function useAnalytics(options = {}) {
2715
3069
  if (onReady && !sessionLoggedRef.current) {
2716
3070
  onReady({
2717
3071
  sessionId: uid,
2718
- networkInfo: net,
3072
+ networkInfo: null, // Use connection from ipwho.is instead (more accurate)
2719
3073
  deviceInfo: dev,
2720
3074
  location: loc,
2721
3075
  attribution: attr,
2722
3076
  });
2723
3077
  }
2724
- return { net, dev, attr, loc };
3078
+ // Return null for net - connection data comes from ipwho.is instead
3079
+ return { net: null, dev, attr, loc }; // net is null - use ipwho.is connection instead
2725
3080
  }, [onReady]);
2726
3081
  // Initialize on mount
2727
3082
  useEffect(() => {
@@ -2905,5 +3260,5 @@ function useAnalytics(options = {}) {
2905
3260
  ]);
2906
3261
  }
2907
3262
 
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 };
3263
+ export { AnalyticsService, AttributionDetector, DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS, DEFAULT_ESSENTIAL_DEVICE_FIELDS, DEFAULT_ESSENTIAL_IP_FIELDS, DEFAULT_ESSENTIAL_LOCATION_FIELDS, DEFAULT_ESSENTIAL_NETWORK_FIELDS, DeviceDetector, LocationDetector, NetworkDetector, QueueManager, checkAndSetLocationConsent, clearLocationConsent, clearSession, useAnalytics as default, filterFieldsByConfig, getCompleteIPLocation, getIPFromRequest, getIPLocation, getLocationConsentTimestamp, getOrCreateSession, getOrCreateUserId, getPublicIP, getSession, hasLocationConsent, initDebug, loadJSON, loadSessionJSON, logger, saveJSON, saveSessionJSON, setLocationConsentGranted, trackPageVisit, transformIPLocationForBackend, updateSessionActivity, useAnalytics };
2909
3264
  //# sourceMappingURL=index.esm.js.map