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/CHANGELOG.md +50 -0
- package/README.md +24 -17
- package/dist/index.cjs.js +391 -30
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.cts +72 -5
- package/dist/index.d.ts +72 -5
- package/dist/index.esm.js +386 -31
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
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
|
-
*
|
|
2287
|
+
* Supports configurable field storage to optimize storage capacity
|
|
2012
2288
|
*
|
|
2013
2289
|
* @param ipLocation - Raw IP location data from ipwho.is API
|
|
2014
|
-
* @
|
|
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
|
-
|
|
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
|
-
//
|
|
2340
|
-
const
|
|
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
|
|
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
|
-
//
|
|
2430
|
-
const
|
|
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:
|
|
2435
|
-
deviceInfo:
|
|
2436
|
-
location:
|
|
2437
|
-
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|