user-analytics-tracker 4.1.2 → 4.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
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
2
2
 
3
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
4
  /**
4
5
  * Core types for the analytics tracker package
5
6
  */
@@ -10,16 +11,11 @@ import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
10
11
  const DEFAULT_ESSENTIAL_IP_FIELDS = [
11
12
  // Core identification
12
13
  'ip',
13
- 'country',
14
14
  'countryCode',
15
- 'region',
16
15
  'city',
17
16
  // Geographic coordinates (stored here, not duplicated in location)
18
17
  'lat',
19
18
  'lon',
20
- // Additional geographic info
21
- 'continent',
22
- 'continentCode',
23
19
  // Network info
24
20
  'type',
25
21
  'isEu',
@@ -29,27 +25,14 @@ const DEFAULT_ESSENTIAL_IP_FIELDS = [
29
25
  'connection.org',
30
26
  'connection.isp',
31
27
  '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
28
  ];
41
29
  /**
42
30
  * Default essential fields for Device Info storage
31
+ * In essential mode, only OS and browser are stored
43
32
  */
44
33
  const DEFAULT_ESSENTIAL_DEVICE_FIELDS = [
45
- 'type',
46
34
  'os',
47
- 'osVersion',
48
35
  'browser',
49
- 'browserVersion',
50
- 'deviceModel',
51
- 'deviceBrand',
52
- 'userAgent',
53
36
  ];
54
37
  /**
55
38
  * Default essential fields for Network Info storage
@@ -82,17 +65,6 @@ const DEFAULT_ESSENTIAL_LOCATION_FIELDS = [
82
65
  const DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS = [
83
66
  'landingUrl',
84
67
  '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
68
  ];
97
69
 
98
70
  /**
@@ -145,7 +117,7 @@ class NetworkDetector {
145
117
  const downlink = c.downlink || 0;
146
118
  const rtt = c.rtt || 0;
147
119
  const saveData = c.saveData || false;
148
- c.effectiveType?.toLowerCase() || '';
120
+ // const effectiveType = c.effectiveType?.toLowerCase() || ''; // Reserved for future use
149
121
  const isMobileDevice = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
150
122
  const isDesktop = !isMobileDevice;
151
123
  // Data saver mode strongly suggests cellular
@@ -560,6 +532,7 @@ var deviceDetector = /*#__PURE__*/Object.freeze({
560
532
  DeviceDetector: DeviceDetector
561
533
  });
562
534
 
535
+ /* eslint-disable no-console */
563
536
  /**
564
537
  * Location Consent Manager
565
538
  * When user enters MSISDN, they implicitly consent to location tracking
@@ -639,44 +612,53 @@ function checkAndSetLocationConsent(msisdn) {
639
612
  return false;
640
613
  }
641
614
 
642
- /**
643
- * IP Geolocation Service
644
- * Fetches location data (country, region, city) from user's IP address
645
- * Uses ipwho.is API (no API key required)
646
- *
647
- * Stores all keys dynamically from the API response, including nested objects
648
- * This ensures we capture all available data and any new fields added by the API
649
- */
650
615
  /**
651
616
  * Get complete IP location data from ipwho.is API (HIGH PRIORITY)
652
617
  * This is the primary method - gets IP, location, connection, and all data in one call
653
- * No API key required
654
618
  *
619
+ * @param config - Optional configuration for API key and base URL
655
620
  * @returns Promise<IPLocation | null> - Complete IP location data, or null if unavailable
656
621
  *
657
622
  * @example
658
623
  * ```typescript
624
+ * // Without API key (free tier)
659
625
  * const location = await getCompleteIPLocation();
660
- * console.log('IP:', location?.ip);
661
- * console.log('Country:', location?.country);
662
- * console.log('ISP:', location?.connection?.isp);
626
+ *
627
+ * // With API key (for higher rate limits)
628
+ * const location = await getCompleteIPLocation({
629
+ * apiKey: '<your-api-key>',
630
+ * baseUrl: 'https://ipwho.is'
631
+ * });
663
632
  * ```
664
633
  */
665
- async function getCompleteIPLocation() {
634
+ async function getCompleteIPLocation(config) {
666
635
  // Skip if we're in an environment without fetch (SSR)
667
636
  if (typeof fetch === 'undefined') {
668
637
  return null;
669
638
  }
639
+ // Use provided config or defaults
640
+ const baseUrl = config?.baseUrl || 'https://ipwho.is';
641
+ const timeout = config?.timeout || 5000;
642
+ const apiKey = config?.apiKey;
670
643
  try {
644
+ // Build URL with optional API key
645
+ let url = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
646
+ // Add API key as query parameter if provided
647
+ if (apiKey) {
648
+ url += `?key=${encodeURIComponent(apiKey)}`;
649
+ }
650
+ else {
651
+ url += '/';
652
+ }
671
653
  // Call ipwho.is without IP parameter - it auto-detects user's IP and returns everything
672
654
  // This is the HIGH PRIORITY source - gets IP, location, connection, timezone, flag, etc. in one call
673
- const response = await fetch('https://ipwho.is/', {
655
+ const response = await fetch(url, {
674
656
  method: 'GET',
675
657
  headers: {
676
658
  Accept: 'application/json',
677
659
  },
678
660
  // Add timeout to prevent hanging
679
- signal: AbortSignal.timeout(5000),
661
+ signal: AbortSignal.timeout(timeout),
680
662
  });
681
663
  if (!response.ok) {
682
664
  return null;
@@ -738,6 +720,7 @@ async function getCompleteIPLocation() {
738
720
  * This is kept for backward compatibility and as a fallback
739
721
  * Prefer getCompleteIPLocation() which gets everything in one call
740
722
  *
723
+ * @param config - Optional configuration for API key and base URL
741
724
  * @returns Promise<string | null> - The public IP address, or null if unavailable
742
725
  *
743
726
  * @example
@@ -746,20 +729,31 @@ async function getCompleteIPLocation() {
746
729
  * console.log('Your IP:', ip); // e.g., "203.0.113.42"
747
730
  * ```
748
731
  */
749
- async function getPublicIP() {
732
+ async function getPublicIP(config) {
750
733
  // Try to get complete location first (includes IP)
751
- const completeLocation = await getCompleteIPLocation();
734
+ const completeLocation = await getCompleteIPLocation(config);
752
735
  if (completeLocation?.ip) {
753
736
  return completeLocation.ip;
754
737
  }
755
738
  // Fallback: try direct IP fetch (less efficient, lower priority)
756
739
  try {
757
- const response = await fetch('https://ipwho.is/', {
740
+ const baseUrl = config?.baseUrl || 'https://ipwho.is';
741
+ const timeout = config?.timeout || 5000;
742
+ const apiKey = config?.apiKey;
743
+ // Build URL with optional API key
744
+ let url = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
745
+ if (apiKey) {
746
+ url += `?key=${encodeURIComponent(apiKey)}`;
747
+ }
748
+ else {
749
+ url += '/';
750
+ }
751
+ const response = await fetch(url, {
758
752
  method: 'GET',
759
753
  headers: {
760
754
  Accept: 'application/json',
761
755
  },
762
- signal: AbortSignal.timeout(5000),
756
+ signal: AbortSignal.timeout(timeout),
763
757
  });
764
758
  if (!response.ok) {
765
759
  return null;
@@ -779,14 +773,28 @@ async function getPublicIP() {
779
773
  }
780
774
  /**
781
775
  * Get location from IP address using ipwho.is API (HIGH PRIORITY)
782
- * Free tier: No API key required
783
776
  *
784
777
  * Stores all keys dynamically from the API response, including nested objects
785
778
  * This ensures we capture all available data and any new fields added by the API
786
779
  *
780
+ * @param ip - IP address to geolocate
781
+ * @param config - Optional configuration for API key and base URL
782
+ * @returns Promise<IPLocation | null> - IP location data, or null if unavailable
783
+ *
784
+ * @example
785
+ * ```typescript
786
+ * // Without API key
787
+ * const location = await getIPLocation('203.0.113.42');
788
+ *
789
+ * // With API key
790
+ * const location = await getIPLocation('203.0.113.42', {
791
+ * apiKey: '<your-api-key>'
792
+ * });
793
+ * ```
794
+ *
787
795
  * Note: If you don't have an IP yet, use getCompleteIPLocation() which gets everything in one call
788
796
  */
789
- async function getIPLocation(ip) {
797
+ async function getIPLocation(ip, config) {
790
798
  // Skip localhost/private IPs (these can't be geolocated)
791
799
  if (!ip ||
792
800
  ip === '0.0.0.0' ||
@@ -800,14 +808,25 @@ async function getIPLocation(ip) {
800
808
  return null;
801
809
  }
802
810
  try {
803
- // Using ipwho.is API (no API key required)
804
- const response = await fetch(`https://ipwho.is/${ip}`, {
811
+ // Use provided config or defaults
812
+ const baseUrl = config?.baseUrl || 'https://ipwho.is';
813
+ const timeout = config?.timeout || 5000;
814
+ const apiKey = config?.apiKey;
815
+ // Build URL with IP and optional API key
816
+ let url = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
817
+ url += `/${ip}`;
818
+ // Add API key as query parameter if provided
819
+ if (apiKey) {
820
+ url += `?key=${encodeURIComponent(apiKey)}`;
821
+ }
822
+ // Using ipwho.is API
823
+ const response = await fetch(url, {
805
824
  method: 'GET',
806
825
  headers: {
807
826
  Accept: 'application/json',
808
827
  },
809
828
  // Add timeout to prevent hanging
810
- signal: AbortSignal.timeout(5000),
829
+ signal: AbortSignal.timeout(timeout),
811
830
  });
812
831
  if (!response.ok) {
813
832
  console.warn(`[IP Geolocation] Failed to fetch location for IP ${ip}: ${response.status}`);
@@ -916,6 +935,23 @@ function getIPFromRequest(req) {
916
935
  * IP-based location works automatically without user permission
917
936
  */
918
937
  class LocationDetector {
938
+ /**
939
+ * Configure IP geolocation settings (API key, base URL, timeout)
940
+ *
941
+ * @param config - IP geolocation configuration
942
+ *
943
+ * @example
944
+ * ```typescript
945
+ * LocationDetector.configureIPGeolocation({
946
+ * apiKey: '<your-ipwho-is-api-key>',
947
+ * baseUrl: 'https://ipwho.is',
948
+ * timeout: 5000
949
+ * });
950
+ * ```
951
+ */
952
+ static configureIPGeolocation(config) {
953
+ this.ipGeolocationConfig = config;
954
+ }
919
955
  /**
920
956
  * Detect location using IP-based API only (no GPS, no permission needed)
921
957
  * Fast and automatic - works immediately without user interaction
@@ -1214,14 +1250,15 @@ class LocationDetector {
1214
1250
  // HIGH PRIORITY: Get complete IP location data from ipwho.is in one call
1215
1251
  // This gets IP, location, connection, timezone, flag, and all other data at once
1216
1252
  // More efficient than making separate calls
1217
- let ipLocation = await getCompleteIPLocation();
1253
+ // Use configured API key and settings if available
1254
+ let ipLocation = await getCompleteIPLocation(this.ipGeolocationConfig || undefined);
1218
1255
  // If complete location fetch failed, try fallback: get IP first, then location
1219
1256
  if (!ipLocation) {
1220
1257
  console.log('[Location] Primary ipwho.is call failed, trying fallback...');
1221
- const publicIP = await getPublicIP();
1258
+ const publicIP = await getPublicIP(this.ipGeolocationConfig || undefined);
1222
1259
  if (publicIP) {
1223
1260
  // Fallback: Get location from IP using ipwho.is API
1224
- ipLocation = await getIPLocation(publicIP);
1261
+ ipLocation = await getIPLocation(publicIP, this.ipGeolocationConfig || undefined);
1225
1262
  }
1226
1263
  }
1227
1264
  if (!ipLocation) {
@@ -1297,6 +1334,7 @@ LocationDetector.lastLocationRef = { current: null };
1297
1334
  LocationDetector.locationConsentLoggedRef = { current: false };
1298
1335
  LocationDetector.ipLocationFetchingRef = { current: false };
1299
1336
  LocationDetector.lastIPLocationRef = { current: null };
1337
+ LocationDetector.ipGeolocationConfig = null;
1300
1338
 
1301
1339
  var locationDetector = /*#__PURE__*/Object.freeze({
1302
1340
  __proto__: null,
@@ -1407,7 +1445,7 @@ function getOrCreateSession(timeout = DEFAULT_SESSION_TIMEOUT) {
1407
1445
  return updated;
1408
1446
  }
1409
1447
  // Create new session
1410
- const sessionId = `session-${Date.now()}-${Math.random().toString(36).substring(7)}`;
1448
+ const sessionId = `session-${Date.now()}-${getSecureRandomString(16)}`;
1411
1449
  const newSession = {
1412
1450
  sessionId,
1413
1451
  startTime: now,
@@ -1451,6 +1489,25 @@ function clearSession() {
1451
1489
  // Silently fail
1452
1490
  }
1453
1491
  }
1492
+ /**
1493
+ * Generate a cryptographically secure random string.
1494
+ * Length is the number of bytes, which are hex-encoded (2 chars per byte).
1495
+ */
1496
+ function getSecureRandomString(length) {
1497
+ if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {
1498
+ const bytes = new Uint8Array(length);
1499
+ window.crypto.getRandomValues(bytes);
1500
+ let result = '';
1501
+ for (let i = 0; i < bytes.length; i++) {
1502
+ const hex = bytes[i].toString(16).padStart(2, '0');
1503
+ result += hex;
1504
+ }
1505
+ return result;
1506
+ }
1507
+ // Fallback to a less secure method only if crypto is unavailable.
1508
+ // This should be rare in modern browsers.
1509
+ return Math.random().toString(36).substring(2, 2 + length);
1510
+ }
1454
1511
 
1455
1512
  var storage = /*#__PURE__*/Object.freeze({
1456
1513
  __proto__: null,
@@ -1612,6 +1669,7 @@ var attributionDetector = /*#__PURE__*/Object.freeze({
1612
1669
  AttributionDetector: AttributionDetector
1613
1670
  });
1614
1671
 
1672
+ /* eslint-disable no-console, @typescript-eslint/no-explicit-any */
1615
1673
  /**
1616
1674
  * Logger utility for analytics tracker
1617
1675
  * Provides configurable logging levels for development and production
@@ -1662,6 +1720,7 @@ class Logger {
1662
1720
  }
1663
1721
  const logger = new Logger();
1664
1722
 
1723
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1665
1724
  /**
1666
1725
  * Queue Manager for Analytics Events
1667
1726
  * Handles batching, persistence, and offline support
@@ -1827,7 +1886,7 @@ class QueueManager {
1827
1886
  navigator.sendBeacon(this.getEndpointFromCallback(), blob);
1828
1887
  this.saveToStorage();
1829
1888
  }
1830
- catch (error) {
1889
+ catch {
1831
1890
  // Fallback: put events back in queue
1832
1891
  this.queue.unshift(...events.map((e) => ({
1833
1892
  event: e,
@@ -2096,6 +2155,7 @@ class MetricsCollector {
2096
2155
  // Global metrics collector instance
2097
2156
  const metricsCollector = new MetricsCollector();
2098
2157
 
2158
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2099
2159
  /**
2100
2160
  * Generic field storage transformer
2101
2161
  * Filters object fields based on storage configuration
@@ -2192,6 +2252,11 @@ function filterFieldsByConfig(data, config, defaultEssentialFields) {
2192
2252
  filtered[key] = data[key];
2193
2253
  }
2194
2254
  });
2255
+ // If no fields were included, return null (not empty object)
2256
+ // This helps distinguish between "no data" and "filtered out all fields"
2257
+ if (Object.keys(filtered).length === 0) {
2258
+ return null;
2259
+ }
2195
2260
  // Handle nested objects - only create if at least one child field is included
2196
2261
  if (data.connection && shouldIncludeParent('connection')) {
2197
2262
  filtered.connection = {};
@@ -2294,6 +2359,12 @@ function transformIPLocationForBackend(ipLocation, config) {
2294
2359
  if (!ipLocation) {
2295
2360
  return null;
2296
2361
  }
2362
+ // If ipLocation is a simple object with just ip, preserve it for validation
2363
+ // Skip transformation and filtering to ensure ip is always preserved
2364
+ if (ipLocation.ip && Object.keys(ipLocation).length === 1) {
2365
+ // Return object with ip directly (ip is essential field, always preserved)
2366
+ return { ip: ipLocation.ip };
2367
+ }
2297
2368
  // Transform to match backend expected format (camelCase)
2298
2369
  // Build complete object first, then filter based on configuration
2299
2370
  const transformed = {
@@ -2354,6 +2425,194 @@ function transformIPLocationForBackend(ipLocation, config) {
2354
2425
  return filterFieldsByConfig(transformed, config, DEFAULT_ESSENTIAL_IP_FIELDS);
2355
2426
  }
2356
2427
 
2428
+ /**
2429
+ * Extract required fields from analytics event
2430
+ */
2431
+ function extractRequiredFields(event) {
2432
+ // Extract IP from ipLocation or location or customData.ipLocation
2433
+ const ip = event.ipLocation?.ip ||
2434
+ event.location?.ip ||
2435
+ event.customData?.ipLocation?.ip ||
2436
+ null;
2437
+ // Extract lat/lon from location or ipLocation
2438
+ const lat = event.location?.lat || event.ipLocation?.latitude || event.ipLocation?.lat || null;
2439
+ const lon = event.location?.lon || event.ipLocation?.longitude || event.ipLocation?.lon || null;
2440
+ // Extract mobile status
2441
+ const mobile = event.deviceInfo?.type === 'mobile' ? true : event.deviceInfo?.type ? false : null;
2442
+ // Extract location (city - country removed in essential mode)
2443
+ const location = event.ipLocation?.city ||
2444
+ event.location?.city ||
2445
+ event.ipLocation?.country ||
2446
+ event.location?.country ||
2447
+ null;
2448
+ // Extract msisdn from customData
2449
+ const msisdn = event.customData?.msisdn || null;
2450
+ // Session ID
2451
+ const session = event.sessionId || null;
2452
+ // Extract operator/ISP
2453
+ const operators = event.ipLocation?.connection?.isp ||
2454
+ event.ipLocation?.connection?.org ||
2455
+ null;
2456
+ // Extract page name from URL
2457
+ const pageUrl = event.pageUrl || null;
2458
+ let page = null;
2459
+ if (pageUrl) {
2460
+ try {
2461
+ page = new URL(pageUrl).pathname;
2462
+ }
2463
+ catch {
2464
+ // If URL parsing fails, try to extract pathname manually
2465
+ const match = pageUrl.match(/^https?:\/\/[^/]+(\/.*)?$/);
2466
+ page = match ? (match[1] || '/') : null;
2467
+ }
2468
+ }
2469
+ // Event type
2470
+ const eventType = event.eventName || 'page_view';
2471
+ // Company name from customData
2472
+ const companyName = event.customData?.companyName || null;
2473
+ // Event ID
2474
+ const eventId = event.eventId || null;
2475
+ // Timestamp
2476
+ const timestamp = event.timestamp || null;
2477
+ // GPS source check
2478
+ const gps = event.location?.source === 'gps' ? true :
2479
+ (event.location?.source === 'ip' ? false : null);
2480
+ // OS
2481
+ const os = event.deviceInfo?.os || null;
2482
+ // Browser
2483
+ const browser = event.deviceInfo?.browser || null;
2484
+ // Service ID
2485
+ const serviceId = event.customData?.serviceId || null;
2486
+ return {
2487
+ ip,
2488
+ lat,
2489
+ lon,
2490
+ mobile,
2491
+ location,
2492
+ msisdn,
2493
+ session,
2494
+ operators,
2495
+ page,
2496
+ pageUrl,
2497
+ eventType,
2498
+ companyName,
2499
+ eventId,
2500
+ timestamp,
2501
+ gps,
2502
+ os,
2503
+ browser,
2504
+ serviceId,
2505
+ };
2506
+ }
2507
+ /**
2508
+ * Validate that event has minimum required fields
2509
+ * Returns true if valid, false if should be filtered out
2510
+ */
2511
+ function validateRequiredFields(fields) {
2512
+ // Must have at least one of: IP, or (lat AND lon)
2513
+ const hasLocation = fields.ip || (fields.lat !== null && fields.lon !== null);
2514
+ if (!hasLocation) {
2515
+ return false;
2516
+ }
2517
+ // Must have session (non-empty string, not 'unknown')
2518
+ if (!fields.session || fields.session === 'unknown' || fields.session.trim() === '') {
2519
+ return false;
2520
+ }
2521
+ // Must have pageUrl (non-empty string)
2522
+ if (!fields.pageUrl || fields.pageUrl.trim() === '') {
2523
+ return false;
2524
+ }
2525
+ // Must have eventType
2526
+ if (!fields.eventType) {
2527
+ return false;
2528
+ }
2529
+ // Must have eventId
2530
+ if (!fields.eventId) {
2531
+ return false;
2532
+ }
2533
+ // Must have timestamp
2534
+ if (!fields.timestamp) {
2535
+ return false;
2536
+ }
2537
+ // Must have at least one of: mobile, OS, or browser
2538
+ const hasDeviceInfo = fields.mobile !== null || fields.os || fields.browser;
2539
+ if (!hasDeviceInfo) {
2540
+ return false;
2541
+ }
2542
+ return true;
2543
+ }
2544
+ /**
2545
+ * Check if event has null values for critical fields
2546
+ * Returns true if should be filtered out (too many nulls)
2547
+ */
2548
+ function hasTooManyNulls(fields) {
2549
+ // Only count non-critical fields for null percentage
2550
+ // Critical fields (ip, lat, lon, session, pageUrl, eventType, eventId, timestamp) are already validated
2551
+ const nonCriticalFields = ['mobile', 'location', 'msisdn', 'operators', 'page', 'companyName', 'gps', 'os', 'browser', 'serviceId'];
2552
+ const nullCount = nonCriticalFields.filter(key => {
2553
+ const value = fields[key];
2554
+ return value === null || value === undefined;
2555
+ }).length;
2556
+ const totalNonCritical = nonCriticalFields.length;
2557
+ const nullPercentage = totalNonCritical > 0 ? nullCount / totalNonCritical : 0;
2558
+ // Filter out if more than 70% of non-critical fields are null (more lenient)
2559
+ return nullPercentage > 0.7;
2560
+ }
2561
+ /**
2562
+ * Validate and filter event
2563
+ * Returns null if event should be filtered out, otherwise returns the event
2564
+ */
2565
+ function validateEvent(event) {
2566
+ const fields = extractRequiredFields(event);
2567
+ // Check if has too many null values
2568
+ if (hasTooManyNulls(fields)) {
2569
+ logger.debug('Event filtered: too many null values', {
2570
+ fields,
2571
+ sessionId: event.sessionId,
2572
+ pageUrl: event.pageUrl,
2573
+ eventName: event.eventName,
2574
+ });
2575
+ return null;
2576
+ }
2577
+ // Check if has minimum required fields
2578
+ if (!validateRequiredFields(fields)) {
2579
+ logger.debug('Event filtered: missing required fields', {
2580
+ fields,
2581
+ sessionId: event.sessionId,
2582
+ pageUrl: event.pageUrl,
2583
+ eventName: event.eventName,
2584
+ hasIP: !!fields.ip,
2585
+ hasLatLon: fields.lat !== null && fields.lon !== null,
2586
+ hasSession: !!fields.session,
2587
+ hasPageUrl: !!fields.pageUrl,
2588
+ hasEventType: !!fields.eventType,
2589
+ hasEventId: !!fields.eventId,
2590
+ hasTimestamp: !!fields.timestamp,
2591
+ hasDeviceInfo: fields.mobile !== null || !!fields.os || !!fields.browser,
2592
+ });
2593
+ return null;
2594
+ }
2595
+ return event;
2596
+ }
2597
+ /**
2598
+ * Generate a unique key for deduplication
2599
+ * Based on: sessionId + pageUrl + eventName + timestamp (rounded to nearest second)
2600
+ */
2601
+ function generateDeduplicationKey(event) {
2602
+ const timestamp = event.timestamp instanceof Date
2603
+ ? event.timestamp.getTime()
2604
+ : new Date(event.timestamp).getTime();
2605
+ // Round timestamp to nearest second to handle rapid duplicate events
2606
+ const roundedTimestamp = Math.floor(timestamp / 1000) * 1000;
2607
+ const parts = [
2608
+ event.sessionId || '',
2609
+ event.pageUrl || '',
2610
+ event.eventName || 'page_view',
2611
+ roundedTimestamp.toString(),
2612
+ ];
2613
+ return parts.join('|');
2614
+ }
2615
+
2357
2616
  /**
2358
2617
  * Analytics Service
2359
2618
  * Sends analytics events to your backend API
@@ -2407,6 +2666,8 @@ class AnalyticsService {
2407
2666
  logLevel: 'warn',
2408
2667
  ...config,
2409
2668
  };
2669
+ // Clear seen event keys when reconfiguring (important for tests)
2670
+ this.seenEventKeys.clear();
2410
2671
  // Set log level
2411
2672
  if (this.config.logLevel) {
2412
2673
  logger.setLevel(this.config.logLevel);
@@ -2421,6 +2682,10 @@ class AnalyticsService {
2421
2682
  if (this.config.enableMetrics) {
2422
2683
  metricsCollector.reset();
2423
2684
  }
2685
+ // Configure IP geolocation if provided
2686
+ if (this.config.ipGeolocation) {
2687
+ LocationDetector.configureIPGeolocation(this.config.ipGeolocation);
2688
+ }
2424
2689
  this.isInitialized = true;
2425
2690
  }
2426
2691
  /**
@@ -2465,10 +2730,56 @@ class AnalyticsService {
2465
2730
  }
2466
2731
  /**
2467
2732
  * Send a batch of events with retry logic
2733
+ * Filters out invalid and duplicate events before sending
2468
2734
  */
2469
2735
  static async sendBatch(events) {
2470
- if (events.length === 0)
2736
+ // Validate and filter events
2737
+ const validatedEvents = [];
2738
+ const seenInBatch = new Set();
2739
+ for (const event of events) {
2740
+ // Validate event
2741
+ const validated = validateEvent(event);
2742
+ if (!validated) {
2743
+ logger.debug('Event filtered out in batch: missing required fields', {
2744
+ sessionId: event.sessionId,
2745
+ pageUrl: event.pageUrl,
2746
+ hasIPLocation: !!event.ipLocation,
2747
+ hasIP: !!(event.ipLocation?.ip),
2748
+ });
2749
+ continue;
2750
+ }
2751
+ // Check for duplicates in this batch only
2752
+ // Note: Events are already deduplicated when queued, so we only check within the batch
2753
+ const dedupeKey = generateDeduplicationKey(validated);
2754
+ if (seenInBatch.has(dedupeKey)) {
2755
+ logger.debug('Duplicate event filtered out in batch', { dedupeKey });
2756
+ continue;
2757
+ }
2758
+ // Add to seen events for this batch
2759
+ seenInBatch.add(dedupeKey);
2760
+ // Also add to global seen events cache (for cross-batch deduplication)
2761
+ // But don't filter out if already seen - events from queue are already validated
2762
+ if (!this.seenEventKeys.has(dedupeKey)) {
2763
+ this.seenEventKeys.add(dedupeKey);
2764
+ // Manage cache size
2765
+ if (this.seenEventKeys.size > this.DEDUPE_CACHE_SIZE) {
2766
+ const firstKey = this.seenEventKeys.values().next().value;
2767
+ if (firstKey !== undefined) {
2768
+ this.seenEventKeys.delete(firstKey);
2769
+ }
2770
+ }
2771
+ }
2772
+ validatedEvents.push(validated);
2773
+ }
2774
+ if (validatedEvents.length === 0) {
2775
+ logger.debug('All events in batch were filtered out');
2776
+ return;
2777
+ }
2778
+ // Use validated events
2779
+ if (validatedEvents.length === 0) {
2471
2780
  return;
2781
+ }
2782
+ events = validatedEvents;
2472
2783
  // Apply plugin transformations
2473
2784
  const transformedEvents = [];
2474
2785
  for (const event of events) {
@@ -2588,6 +2899,7 @@ class AnalyticsService {
2588
2899
  /**
2589
2900
  * Track user journey/analytics event
2590
2901
  * Events are automatically queued and batched
2902
+ * Duplicate events and events with null values are filtered out
2591
2903
  */
2592
2904
  static async trackEvent(event) {
2593
2905
  const payload = {
@@ -2595,9 +2907,39 @@ class AnalyticsService {
2595
2907
  timestamp: new Date(),
2596
2908
  eventId: this.generateEventId(),
2597
2909
  };
2910
+ // Validate event - filter out if missing required fields or too many nulls
2911
+ const validatedEvent = validateEvent(payload);
2912
+ if (!validatedEvent) {
2913
+ logger.debug('Event filtered out: missing required fields or too many null values', {
2914
+ sessionId: payload.sessionId,
2915
+ pageUrl: payload.pageUrl,
2916
+ eventName: payload.eventName,
2917
+ });
2918
+ return;
2919
+ }
2920
+ // Check for duplicates
2921
+ const dedupeKey = generateDeduplicationKey(validatedEvent);
2922
+ if (this.seenEventKeys.has(dedupeKey)) {
2923
+ logger.debug('Duplicate event filtered out', {
2924
+ dedupeKey,
2925
+ sessionId: validatedEvent.sessionId,
2926
+ pageUrl: validatedEvent.pageUrl,
2927
+ eventName: validatedEvent.eventName,
2928
+ });
2929
+ return;
2930
+ }
2931
+ // Add to seen events (with cache size limit)
2932
+ this.seenEventKeys.add(dedupeKey);
2933
+ if (this.seenEventKeys.size > this.DEDUPE_CACHE_SIZE) {
2934
+ // Remove oldest entries (simple FIFO - remove first entry)
2935
+ const firstKey = this.seenEventKeys.values().next().value;
2936
+ if (firstKey !== undefined) {
2937
+ this.seenEventKeys.delete(firstKey);
2938
+ }
2939
+ }
2598
2940
  // If queue is available, use it (browser environment)
2599
2941
  if (this.queueManager && typeof window !== 'undefined') {
2600
- this.queueManager.enqueue(payload);
2942
+ this.queueManager.enqueue(validatedEvent);
2601
2943
  // Record metrics
2602
2944
  if (this.config.enableMetrics) {
2603
2945
  metricsCollector.recordQueued();
@@ -2607,7 +2949,7 @@ class AnalyticsService {
2607
2949
  }
2608
2950
  // Fallback: send immediately (SSR or queue not initialized)
2609
2951
  try {
2610
- await this.sendBatch([payload]);
2952
+ await this.sendBatch([validatedEvent]);
2611
2953
  }
2612
2954
  catch (err) {
2613
2955
  logger.warn('Failed to send event:', err);
@@ -2616,12 +2958,17 @@ class AnalyticsService {
2616
2958
  /**
2617
2959
  * Track user journey with full context
2618
2960
  */
2619
- static async trackUserJourney({ sessionId, pageUrl, networkInfo, deviceInfo, location, attribution, ipLocation, userId, customData, pageVisits = 1, interactions = 0, }) {
2961
+ static async trackUserJourney({ sessionId, pageUrl, networkInfo, deviceInfo, location, attribution, ipLocation, userId, customData, pageVisits: _pageVisits = 1, interactions: _interactions = 0, }) {
2620
2962
  // Get field storage config (support both new and legacy format)
2621
2963
  const fieldStorage = this.config.fieldStorage || {};
2622
2964
  const ipLocationConfig = fieldStorage.ipLocation || this.config.ipLocationFields;
2623
2965
  // Transform and filter all data types based on configuration
2624
- const transformedIPLocation = transformIPLocationForBackend(ipLocation, ipLocationConfig);
2966
+ // Use raw ipLocation if transformation returns null (for validation purposes)
2967
+ let transformedIPLocation = transformIPLocationForBackend(ipLocation, ipLocationConfig);
2968
+ // Fallback to original ipLocation if transform returns null (needed for validation)
2969
+ if (!transformedIPLocation && ipLocation) {
2970
+ transformedIPLocation = ipLocation;
2971
+ }
2625
2972
  const filteredDeviceInfo = filterFieldsByConfig(deviceInfo, fieldStorage.deviceInfo, DEFAULT_ESSENTIAL_DEVICE_FIELDS);
2626
2973
  // In essential mode, don't store browser-based networkInfo
2627
2974
  // Connection data from ipwho.is (in customData.ipLocation.connection) is more accurate
@@ -2661,8 +3008,7 @@ class AnalyticsService {
2661
3008
  deviceInfo: filteredDeviceInfo || undefined,
2662
3009
  location: filteredLocation || undefined,
2663
3010
  attribution: filteredAttribution || undefined,
2664
- // Don't include raw ipLocation - we have the filtered/transformed version in customData
2665
- ipLocation: undefined,
3011
+ ipLocation: transformedIPLocation || ipLocation || undefined,
2666
3012
  userId: userId ?? sessionId,
2667
3013
  customData: {
2668
3014
  ...customData,
@@ -2719,7 +3065,7 @@ class AnalyticsService {
2719
3065
  attribution: AttributionDetector.detect(),
2720
3066
  };
2721
3067
  }
2722
- catch (error) {
3068
+ catch {
2723
3069
  // If auto-collection fails, use minimal context
2724
3070
  const { getOrCreateUserId } = await Promise.resolve().then(function () { return storage; });
2725
3071
  autoContext = {
@@ -2736,19 +3082,116 @@ class AnalyticsService {
2736
3082
  };
2737
3083
  }
2738
3084
  }
2739
- const finalSessionId = context?.sessionId || autoContext?.sessionId || 'unknown';
2740
- const finalPageUrl = context?.pageUrl || autoContext?.pageUrl || '';
2741
- // Extract IP location from location object if available
2742
- const locationData = context?.location || autoContext?.location;
2743
- const ipLocationData = locationData && typeof locationData === 'object'
2744
- ? locationData?.ipLocationData
2745
- : undefined;
3085
+ // Ensure sessionId is always a valid non-empty string for validation
3086
+ let finalSessionId = context?.sessionId || autoContext?.sessionId || 'unknown';
3087
+ if (finalSessionId === 'unknown' || finalSessionId.trim() === '') {
3088
+ // Try to get from storage if available
3089
+ if (typeof window !== 'undefined') {
3090
+ try {
3091
+ const { getOrCreateUserId } = await Promise.resolve().then(function () { return storage; });
3092
+ finalSessionId = getOrCreateUserId();
3093
+ }
3094
+ catch {
3095
+ // If storage is not available, generate a temporary session ID
3096
+ finalSessionId = `temp-${Date.now()}`;
3097
+ }
3098
+ }
3099
+ else {
3100
+ // SSR environment - generate a temporary session ID
3101
+ finalSessionId = `temp-${Date.now()}`;
3102
+ }
3103
+ }
3104
+ // Ensure pageUrl is always a valid non-empty string for validation
3105
+ let finalPageUrl = context?.pageUrl || autoContext?.pageUrl || '';
3106
+ // If pageUrl is empty, try to get it from window.location if available
3107
+ if (!finalPageUrl && typeof window !== 'undefined' && window.location) {
3108
+ finalPageUrl = window.location.href;
3109
+ }
3110
+ // If still empty, use a default value to pass validation
3111
+ if (!finalPageUrl || finalPageUrl.trim() === '') {
3112
+ finalPageUrl = 'https://unknown';
3113
+ }
3114
+ // Extract IP location from context.ipLocation, location object, or auto-collected location
3115
+ const ipLocationData = context?.ipLocation ||
3116
+ (context?.location && typeof context.location === 'object'
3117
+ ? context.location?.ipLocationData
3118
+ : undefined) ||
3119
+ (autoContext?.location && typeof autoContext.location === 'object'
3120
+ ? autoContext.location?.ipLocationData
3121
+ : undefined);
2746
3122
  // Get field storage config (support both new and legacy format)
2747
3123
  const fieldStorage = this.config.fieldStorage || {};
2748
3124
  const ipLocationConfig = fieldStorage.ipLocation || this.config.ipLocationFields;
2749
3125
  // 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);
3126
+ // Use raw ipLocationData if transformation returns null (for validation purposes)
3127
+ // Always prioritize context?.ipLocation if it has an IP (most direct and reliable source)
3128
+ const rawIPLocation = (context?.ipLocation && context.ipLocation.ip)
3129
+ ? context.ipLocation
3130
+ : (ipLocationData || undefined);
3131
+ // Preserve ip field for validation - always ensure ip is available
3132
+ const preserveIP = rawIPLocation?.ip;
3133
+ let transformedIPLocation = transformIPLocationForBackend(rawIPLocation, ipLocationConfig);
3134
+ // Critical: Ensure ip field is always preserved for validation
3135
+ // If transformation removed ip or returned null/empty, restore it from rawIPLocation
3136
+ if (preserveIP) {
3137
+ if (!transformedIPLocation) {
3138
+ // Transformation returned null, use rawIPLocation
3139
+ transformedIPLocation = rawIPLocation;
3140
+ }
3141
+ else if (Object.keys(transformedIPLocation).length === 0) {
3142
+ // Transformation returned empty object, use rawIPLocation
3143
+ transformedIPLocation = rawIPLocation;
3144
+ }
3145
+ else if (!transformedIPLocation.ip) {
3146
+ // Transformation returned object but without ip field, restore it
3147
+ transformedIPLocation = { ...transformedIPLocation, ip: preserveIP };
3148
+ }
3149
+ }
3150
+ else if (!transformedIPLocation && rawIPLocation) {
3151
+ // No ip to preserve, but use rawIPLocation if transformation failed
3152
+ transformedIPLocation = rawIPLocation;
3153
+ }
3154
+ const rawDeviceInfo = context?.deviceInfo || autoContext?.deviceInfo;
3155
+ const filteredDeviceInfo = filterFieldsByConfig(rawDeviceInfo, fieldStorage.deviceInfo, DEFAULT_ESSENTIAL_DEVICE_FIELDS);
3156
+ // Ensure deviceInfo has os and browser for validation (critical fields)
3157
+ // If filtering removed them, restore from rawDeviceInfo
3158
+ let finalDeviceInfo = filteredDeviceInfo;
3159
+ if (rawDeviceInfo) {
3160
+ if (!finalDeviceInfo) {
3161
+ // Filtering returned null, create minimal object with essential fields
3162
+ finalDeviceInfo = {};
3163
+ if (rawDeviceInfo.os)
3164
+ finalDeviceInfo.os = rawDeviceInfo.os;
3165
+ if (rawDeviceInfo.browser)
3166
+ finalDeviceInfo.browser = rawDeviceInfo.browser;
3167
+ }
3168
+ else {
3169
+ // Ensure os and browser are present for validation
3170
+ if (!finalDeviceInfo.os && rawDeviceInfo.os) {
3171
+ finalDeviceInfo = { ...finalDeviceInfo, os: rawDeviceInfo.os };
3172
+ }
3173
+ if (!finalDeviceInfo.browser && rawDeviceInfo.browser) {
3174
+ finalDeviceInfo = { ...finalDeviceInfo, browser: rawDeviceInfo.browser };
3175
+ }
3176
+ }
3177
+ // Only use finalDeviceInfo if it has at least os or browser
3178
+ // But if rawDeviceInfo has os or browser, ensure finalDeviceInfo has them
3179
+ if (rawDeviceInfo.os || rawDeviceInfo.browser) {
3180
+ if (!finalDeviceInfo) {
3181
+ finalDeviceInfo = {};
3182
+ }
3183
+ if (rawDeviceInfo.os && !finalDeviceInfo.os) {
3184
+ finalDeviceInfo.os = rawDeviceInfo.os;
3185
+ }
3186
+ if (rawDeviceInfo.browser && !finalDeviceInfo.browser) {
3187
+ finalDeviceInfo.browser = rawDeviceInfo.browser;
3188
+ }
3189
+ }
3190
+ else if (!finalDeviceInfo || (!finalDeviceInfo.os && !finalDeviceInfo.browser)) {
3191
+ // No os or browser in rawDeviceInfo, and finalDeviceInfo doesn't have them either
3192
+ finalDeviceInfo = null;
3193
+ }
3194
+ }
2752
3195
  // In essential mode, don't store browser-based networkInfo
2753
3196
  // Connection data from ipwho.is (in customData.ipLocation.connection) is more accurate
2754
3197
  const networkInfoConfig = fieldStorage.networkInfo;
@@ -2778,20 +3221,65 @@ class AnalyticsService {
2778
3221
  };
2779
3222
  }
2780
3223
  const filteredAttribution = filterFieldsByConfig(context?.attribution || autoContext?.attribution, fieldStorage.attribution, DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS);
3224
+ // Ensure ipLocation is available for validation
3225
+ // Always preserve ip field - critical for validation
3226
+ // Use transformedIPLocation if it has ip, otherwise fall back to rawIPLocation
3227
+ let finalIPLocation = undefined;
3228
+ // Priority 1: If we have rawIPLocation with IP, always use it (most reliable source)
3229
+ if (rawIPLocation && rawIPLocation.ip) {
3230
+ if (transformedIPLocation && transformedIPLocation.ip) {
3231
+ // Both have IP, prefer transformed (has filtering applied)
3232
+ finalIPLocation = transformedIPLocation;
3233
+ }
3234
+ else if (transformedIPLocation) {
3235
+ // Transformed exists but no IP, merge with rawIPLocation to preserve IP
3236
+ finalIPLocation = { ...transformedIPLocation, ip: rawIPLocation.ip };
3237
+ }
3238
+ else {
3239
+ // No transformation, use rawIPLocation directly
3240
+ finalIPLocation = rawIPLocation;
3241
+ }
3242
+ }
3243
+ else if (transformedIPLocation) {
3244
+ // No raw IP, but transformation succeeded
3245
+ finalIPLocation = transformedIPLocation;
3246
+ }
3247
+ else if (context?.ipLocation && context.ipLocation.ip) {
3248
+ // Fallback to context.ipLocation if available
3249
+ finalIPLocation = context.ipLocation;
3250
+ }
3251
+ // Final safety check: ensure IP is always present if we have it from any source
3252
+ if (!finalIPLocation || !finalIPLocation.ip) {
3253
+ if (rawIPLocation && rawIPLocation.ip) {
3254
+ finalIPLocation = rawIPLocation;
3255
+ }
3256
+ else if (context?.ipLocation && context.ipLocation.ip) {
3257
+ finalIPLocation = context.ipLocation;
3258
+ }
3259
+ }
3260
+ // Ultimate safeguard: if context.ipLocation has IP, always use it for validation
3261
+ // This ensures validation never fails due to missing IP when it's provided in context
3262
+ if (context?.ipLocation && context.ipLocation.ip) {
3263
+ if (!finalIPLocation || !finalIPLocation.ip) {
3264
+ finalIPLocation = context.ipLocation;
3265
+ }
3266
+ }
2781
3267
  await this.trackEvent({
2782
3268
  sessionId: finalSessionId,
2783
3269
  pageUrl: finalPageUrl,
2784
3270
  networkInfo: filteredNetworkInfo || undefined,
2785
- deviceInfo: filteredDeviceInfo || undefined,
3271
+ deviceInfo: (finalDeviceInfo && (finalDeviceInfo.os || finalDeviceInfo.browser)) ? finalDeviceInfo : undefined,
2786
3272
  location: filteredLocation || undefined,
2787
3273
  attribution: filteredAttribution || undefined,
3274
+ ipLocation: finalIPLocation,
2788
3275
  userId: context?.userId || finalSessionId,
2789
3276
  eventName,
2790
3277
  eventParameters: parameters || {},
2791
3278
  customData: {
2792
3279
  ...(parameters || {}),
2793
3280
  // Store transformed IP location in customData for backend integration
2794
- ...(transformedIPLocation && { ipLocation: transformedIPLocation }),
3281
+ // Use transformed if available, otherwise use raw (for validation)
3282
+ ...(finalIPLocation && { ipLocation: transformedIPLocation || finalIPLocation }),
2795
3283
  },
2796
3284
  });
2797
3285
  }
@@ -2814,13 +3302,13 @@ class AnalyticsService {
2814
3302
  * });
2815
3303
  * ```
2816
3304
  */
2817
- static async trackPageView(pageName, parameters) {
3305
+ static async trackPageView(pageName, parameters, context) {
2818
3306
  const page = pageName || (typeof window !== 'undefined' ? window.location.pathname : '');
2819
3307
  await this.logEvent('page_view', {
2820
3308
  page_name: page,
2821
3309
  page_title: typeof document !== 'undefined' ? document.title : undefined,
2822
3310
  ...parameters,
2823
- });
3311
+ }, context);
2824
3312
  }
2825
3313
  /**
2826
3314
  * Manually flush the event queue
@@ -2861,7 +3349,10 @@ AnalyticsService.apiEndpoint = '/api/analytics';
2861
3349
  AnalyticsService.queueManager = null;
2862
3350
  AnalyticsService.config = {};
2863
3351
  AnalyticsService.isInitialized = false;
3352
+ AnalyticsService.seenEventKeys = new Set();
3353
+ AnalyticsService.DEDUPE_CACHE_SIZE = 1000; // Keep last 1000 event keys
2864
3354
 
3355
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2865
3356
  /**
2866
3357
  * Debug utilities for analytics tracker
2867
3358
  * Provides debugging tools in development mode
@@ -2940,6 +3431,7 @@ function initDebug() {
2940
3431
  logger.info('Available methods: getQueue(), getQueueSize(), flushQueue(), clearQueue(), getStats(), setLogLevel()');
2941
3432
  }
2942
3433
 
3434
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2943
3435
  /**
2944
3436
  * React Hook for Analytics Tracking
2945
3437
  * Provides device, network, location, and attribution data
@@ -2984,6 +3476,8 @@ function useAnalytics(options = {}) {
2984
3476
  config?.logLevel,
2985
3477
  config?.enableMetrics,
2986
3478
  config?.sessionTimeout,
3479
+ config?.fieldStorage,
3480
+ config?.ipLocationFields,
2987
3481
  ]);
2988
3482
  const [networkInfo, setNetworkInfo] = useState(null);
2989
3483
  const [deviceInfo, setDeviceInfo] = useState(null);
@@ -3260,5 +3754,5 @@ function useAnalytics(options = {}) {
3260
3754
  ]);
3261
3755
  }
3262
3756
 
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 };
3757
+ 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, extractRequiredFields, filterFieldsByConfig, generateDeduplicationKey, getCompleteIPLocation, getIPFromRequest, getIPLocation, getLocationConsentTimestamp, getOrCreateSession, getOrCreateUserId, getPublicIP, getSession, hasLocationConsent, initDebug, loadJSON, loadSessionJSON, logger, saveJSON, saveSessionJSON, setLocationConsentGranted, trackPageVisit, transformIPLocationForBackend, updateSessionActivity, useAnalytics, validateEvent, validateRequiredFields };
3264
3758
  //# sourceMappingURL=index.esm.js.map