user-analytics-tracker 2.2.0 → 3.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 +38 -0
- package/README.md +15 -0
- package/dist/index.cjs.js +364 -19
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.cts +68 -5
- package/dist/index.d.ts +68 -5
- package/dist/index.esm.js +359 -20
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
# [3.0.0](https://github.com/switch-org/analytics-tracker/compare/v2.2.0...v3.0.0) (2025-12-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* optimize essential mode with deduplication and reduced deviceInfo fields ([b64eefa](https://github.com/switch-org/analytics-tracker/commit/b64eefa4992e0570b1011fa029d0db8de89c321b))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### BREAKING CHANGES
|
|
10
|
+
|
|
11
|
+
* Essential mode deviceInfo now stores only 8 fields instead of 15
|
|
12
|
+
|
|
13
|
+
## Changes
|
|
14
|
+
|
|
15
|
+
### Core Optimizations
|
|
16
|
+
- Reduce deviceInfo essential fields from 15 to 8
|
|
17
|
+
- Implement automatic deduplication between location and customData.ipLocation
|
|
18
|
+
- Remove duplicate fields from location object
|
|
19
|
+
- Add null/undefined value removal from filtered objects
|
|
20
|
+
- Improve nested object handling
|
|
21
|
+
|
|
22
|
+
### Field Storage Improvements
|
|
23
|
+
- Add generic field storage transformer for all data types
|
|
24
|
+
- Support essential/all/custom modes for all data types
|
|
25
|
+
- Implement recursive null value cleaning
|
|
26
|
+
- Add mode-aware deduplication logic
|
|
27
|
+
|
|
28
|
+
### Documentation
|
|
29
|
+
- Merge and consolidate essential mode documentation
|
|
30
|
+
- Add comprehensive essential-mode-guide.md
|
|
31
|
+
- Update documentation index
|
|
32
|
+
- Add example payload JSON
|
|
33
|
+
|
|
34
|
+
## Impact
|
|
35
|
+
- Significantly smaller payloads (~30-40% reduction)
|
|
36
|
+
- No duplicate data storage
|
|
37
|
+
- All crucial information preserved
|
|
38
|
+
|
|
1
39
|
# [2.2.0](https://github.com/switch-org/analytics-tracker/compare/v2.1.0...v2.2.0) (2025-12-30)
|
|
2
40
|
|
|
3
41
|
|
package/README.md
CHANGED
|
@@ -93,6 +93,21 @@ const analytics = useAnalytics({
|
|
|
93
93
|
|
|
94
94
|
// Metrics configuration
|
|
95
95
|
enableMetrics: false, // Enable metrics collection (default: false)
|
|
96
|
+
|
|
97
|
+
// Field storage configuration (optional) - control which fields are stored
|
|
98
|
+
fieldStorage: {
|
|
99
|
+
ipLocation: { mode: 'essential' }, // IP location fields
|
|
100
|
+
deviceInfo: { mode: 'essential' }, // Device info fields
|
|
101
|
+
networkInfo: { mode: 'essential' }, // Network info fields
|
|
102
|
+
location: { mode: 'essential' }, // Location fields
|
|
103
|
+
attribution: { mode: 'essential' }, // Attribution fields
|
|
104
|
+
// Each can be: 'essential' (default) | 'all' | 'custom'
|
|
105
|
+
// For 'custom': specify fields array
|
|
106
|
+
// For 'all': specify exclude array
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Legacy: IP Location storage (backward compatible)
|
|
110
|
+
ipLocationFields: { mode: 'essential' },
|
|
96
111
|
},
|
|
97
112
|
});
|
|
98
113
|
```
|
package/dist/index.cjs.js
CHANGED
|
@@ -4,6 +4,97 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
4
4
|
|
|
5
5
|
var react = require('react');
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Core types for the analytics tracker package
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Default essential fields for IP location storage
|
|
12
|
+
* These fields are stored when mode is 'essential' (default)
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_ESSENTIAL_IP_FIELDS = [
|
|
15
|
+
// Core identification
|
|
16
|
+
'ip',
|
|
17
|
+
'country',
|
|
18
|
+
'countryCode',
|
|
19
|
+
'region',
|
|
20
|
+
'city',
|
|
21
|
+
// Geographic coordinates (stored here, not duplicated in location)
|
|
22
|
+
'lat',
|
|
23
|
+
'lon',
|
|
24
|
+
// Additional geographic info
|
|
25
|
+
'continent',
|
|
26
|
+
'continentCode',
|
|
27
|
+
// Network info
|
|
28
|
+
'type',
|
|
29
|
+
'isEu',
|
|
30
|
+
'isp',
|
|
31
|
+
'connection',
|
|
32
|
+
'connection.asn',
|
|
33
|
+
'connection.org',
|
|
34
|
+
'connection.isp',
|
|
35
|
+
'connection.domain',
|
|
36
|
+
// Timezone (stored here, not duplicated in location)
|
|
37
|
+
'timezone',
|
|
38
|
+
'timezoneDetails',
|
|
39
|
+
'timezoneDetails.id',
|
|
40
|
+
'timezoneDetails.abbr',
|
|
41
|
+
'timezoneDetails.utc',
|
|
42
|
+
// Flag (only emoji in essential mode)
|
|
43
|
+
'flag.emoji',
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Default essential fields for Device Info storage
|
|
47
|
+
*/
|
|
48
|
+
const DEFAULT_ESSENTIAL_DEVICE_FIELDS = [
|
|
49
|
+
'type',
|
|
50
|
+
'os',
|
|
51
|
+
'osVersion',
|
|
52
|
+
'browser',
|
|
53
|
+
'browserVersion',
|
|
54
|
+
'deviceModel',
|
|
55
|
+
'deviceBrand',
|
|
56
|
+
'userAgent',
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Default essential fields for Network Info storage
|
|
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
|
+
|
|
7
98
|
/**
|
|
8
99
|
* Network Type Detector
|
|
9
100
|
* Detects WiFi, Mobile Data (Cellular), Hotspot, Ethernet, or Unknown
|
|
@@ -2010,18 +2101,206 @@ class MetricsCollector {
|
|
|
2010
2101
|
// Global metrics collector instance
|
|
2011
2102
|
const metricsCollector = new MetricsCollector();
|
|
2012
2103
|
|
|
2104
|
+
/**
|
|
2105
|
+
* Generic field storage transformer
|
|
2106
|
+
* Filters object fields based on storage configuration
|
|
2107
|
+
*/
|
|
2108
|
+
/**
|
|
2109
|
+
* Filter object fields based on storage configuration
|
|
2110
|
+
*
|
|
2111
|
+
* @param data - The data object to filter
|
|
2112
|
+
* @param config - Storage configuration
|
|
2113
|
+
* @param defaultEssentialFields - Default essential fields for this data type
|
|
2114
|
+
* @returns Filtered data object with only configured fields
|
|
2115
|
+
*/
|
|
2116
|
+
function filterFieldsByConfig(data, config, defaultEssentialFields) {
|
|
2117
|
+
if (!data) {
|
|
2118
|
+
return null;
|
|
2119
|
+
}
|
|
2120
|
+
const mode = config?.mode || 'essential';
|
|
2121
|
+
let fieldsToInclude = [];
|
|
2122
|
+
if (mode === 'essential') {
|
|
2123
|
+
// Use default essential fields
|
|
2124
|
+
fieldsToInclude = [...defaultEssentialFields];
|
|
2125
|
+
}
|
|
2126
|
+
else if (mode === 'all') {
|
|
2127
|
+
// Include all fields, then exclude specified ones
|
|
2128
|
+
fieldsToInclude = ['*']; // Special marker for "all fields"
|
|
2129
|
+
}
|
|
2130
|
+
else if (mode === 'custom' && config) {
|
|
2131
|
+
// Use custom field list
|
|
2132
|
+
fieldsToInclude = config.fields || [];
|
|
2133
|
+
}
|
|
2134
|
+
// If mode is 'all', just exclude specified fields
|
|
2135
|
+
if (mode === 'all') {
|
|
2136
|
+
const filtered = { ...data };
|
|
2137
|
+
if (config?.exclude && config.exclude.length > 0) {
|
|
2138
|
+
const excludeSet = new Set(config.exclude);
|
|
2139
|
+
Object.keys(filtered).forEach(key => {
|
|
2140
|
+
if (excludeSet.has(key)) {
|
|
2141
|
+
delete filtered[key];
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
// Handle nested exclusions
|
|
2145
|
+
if (filtered.connection && excludeSet.has('connection')) {
|
|
2146
|
+
delete filtered.connection;
|
|
2147
|
+
}
|
|
2148
|
+
if (filtered.timezoneDetails && excludeSet.has('timezoneDetails')) {
|
|
2149
|
+
delete filtered.timezoneDetails;
|
|
2150
|
+
}
|
|
2151
|
+
if (filtered.flag && excludeSet.has('flag')) {
|
|
2152
|
+
delete filtered.flag;
|
|
2153
|
+
}
|
|
2154
|
+
if (filtered.firstTouch && excludeSet.has('firstTouch')) {
|
|
2155
|
+
delete filtered.firstTouch;
|
|
2156
|
+
}
|
|
2157
|
+
if (filtered.lastTouch && excludeSet.has('lastTouch')) {
|
|
2158
|
+
delete filtered.lastTouch;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return filtered;
|
|
2162
|
+
}
|
|
2163
|
+
// For 'essential' or 'custom' mode, only include specified fields
|
|
2164
|
+
const filtered = {};
|
|
2165
|
+
const includeSet = new Set(fieldsToInclude);
|
|
2166
|
+
// Helper to check if a field path should be included
|
|
2167
|
+
const shouldInclude = (fieldPath) => {
|
|
2168
|
+
// Direct match - most specific
|
|
2169
|
+
if (includeSet.has(fieldPath))
|
|
2170
|
+
return true;
|
|
2171
|
+
// For nested fields (e.g., 'flag.emoji'), only include if explicitly listed
|
|
2172
|
+
// Don't auto-include all children just because parent is included
|
|
2173
|
+
const parts = fieldPath.split('.');
|
|
2174
|
+
if (parts.length > 1) {
|
|
2175
|
+
// For nested fields, require explicit inclusion
|
|
2176
|
+
// This prevents 'flag' from including all 'flag.*' fields
|
|
2177
|
+
return includeSet.has(fieldPath);
|
|
2178
|
+
}
|
|
2179
|
+
// For top-level fields only, check if parent path is included
|
|
2180
|
+
// This allows 'connection' to work when all connection.* fields are listed
|
|
2181
|
+
return false;
|
|
2182
|
+
};
|
|
2183
|
+
// Helper to check if a parent object should be created (for nested objects)
|
|
2184
|
+
const shouldIncludeParent = (parentPath) => {
|
|
2185
|
+
// Check if any child of this parent is included
|
|
2186
|
+
for (const field of fieldsToInclude) {
|
|
2187
|
+
if (field.startsWith(parentPath + '.')) {
|
|
2188
|
+
return true;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
// Also check if parent itself is explicitly included
|
|
2192
|
+
return includeSet.has(parentPath);
|
|
2193
|
+
};
|
|
2194
|
+
// Filter top-level fields
|
|
2195
|
+
Object.keys(data).forEach(key => {
|
|
2196
|
+
if (shouldInclude(key)) {
|
|
2197
|
+
filtered[key] = data[key];
|
|
2198
|
+
}
|
|
2199
|
+
});
|
|
2200
|
+
// Handle nested objects - only create if at least one child field is included
|
|
2201
|
+
if (data.connection && shouldIncludeParent('connection')) {
|
|
2202
|
+
filtered.connection = {};
|
|
2203
|
+
if (shouldInclude('connection.asn'))
|
|
2204
|
+
filtered.connection.asn = data.connection.asn;
|
|
2205
|
+
if (shouldInclude('connection.org'))
|
|
2206
|
+
filtered.connection.org = data.connection.org;
|
|
2207
|
+
if (shouldInclude('connection.isp'))
|
|
2208
|
+
filtered.connection.isp = data.connection.isp;
|
|
2209
|
+
if (shouldInclude('connection.domain'))
|
|
2210
|
+
filtered.connection.domain = data.connection.domain;
|
|
2211
|
+
// If no connection fields were included, remove the object
|
|
2212
|
+
if (Object.keys(filtered.connection).length === 0) {
|
|
2213
|
+
delete filtered.connection;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
if (data.timezoneDetails && shouldIncludeParent('timezoneDetails')) {
|
|
2217
|
+
filtered.timezoneDetails = {};
|
|
2218
|
+
if (shouldInclude('timezoneDetails.id'))
|
|
2219
|
+
filtered.timezoneDetails.id = data.timezoneDetails.id;
|
|
2220
|
+
if (shouldInclude('timezoneDetails.abbr'))
|
|
2221
|
+
filtered.timezoneDetails.abbr = data.timezoneDetails.abbr;
|
|
2222
|
+
if (shouldInclude('timezoneDetails.utc'))
|
|
2223
|
+
filtered.timezoneDetails.utc = data.timezoneDetails.utc;
|
|
2224
|
+
if (shouldInclude('timezoneDetails.isDst'))
|
|
2225
|
+
filtered.timezoneDetails.isDst = data.timezoneDetails.isDst;
|
|
2226
|
+
if (shouldInclude('timezoneDetails.offset'))
|
|
2227
|
+
filtered.timezoneDetails.offset = data.timezoneDetails.offset;
|
|
2228
|
+
if (shouldInclude('timezoneDetails.currentTime'))
|
|
2229
|
+
filtered.timezoneDetails.currentTime = data.timezoneDetails.currentTime;
|
|
2230
|
+
// If no timezoneDetails fields were included, remove the object
|
|
2231
|
+
if (Object.keys(filtered.timezoneDetails).length === 0) {
|
|
2232
|
+
delete filtered.timezoneDetails;
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
if (data.flag && shouldIncludeParent('flag')) {
|
|
2236
|
+
filtered.flag = {};
|
|
2237
|
+
// Only include specific flag fields if they're explicitly in the include list
|
|
2238
|
+
if (shouldInclude('flag.emoji'))
|
|
2239
|
+
filtered.flag.emoji = data.flag.emoji;
|
|
2240
|
+
if (shouldInclude('flag.img'))
|
|
2241
|
+
filtered.flag.img = data.flag.img;
|
|
2242
|
+
if (shouldInclude('flag.emojiUnicode'))
|
|
2243
|
+
filtered.flag.emojiUnicode = data.flag.emojiUnicode;
|
|
2244
|
+
// If no specific flag fields are included, don't add the flag object
|
|
2245
|
+
if (Object.keys(filtered.flag).length === 0) {
|
|
2246
|
+
delete filtered.flag;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
if (data.firstTouch && shouldInclude('firstTouch')) {
|
|
2250
|
+
filtered.firstTouch = data.firstTouch;
|
|
2251
|
+
}
|
|
2252
|
+
if (data.lastTouch && shouldInclude('lastTouch')) {
|
|
2253
|
+
filtered.lastTouch = data.lastTouch;
|
|
2254
|
+
}
|
|
2255
|
+
// Remove null and undefined values to reduce payload size
|
|
2256
|
+
const cleanValue = (val) => {
|
|
2257
|
+
if (val === null || val === undefined) {
|
|
2258
|
+
return undefined; // Will be filtered out
|
|
2259
|
+
}
|
|
2260
|
+
// For objects, recursively clean nested null/undefined values
|
|
2261
|
+
if (typeof val === 'object' && !Array.isArray(val) && val !== null) {
|
|
2262
|
+
const cleaned = {};
|
|
2263
|
+
let hasAnyValue = false;
|
|
2264
|
+
Object.keys(val).forEach(key => {
|
|
2265
|
+
const cleanedChild = cleanValue(val[key]);
|
|
2266
|
+
if (cleanedChild !== undefined) {
|
|
2267
|
+
cleaned[key] = cleanedChild;
|
|
2268
|
+
hasAnyValue = true;
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
return hasAnyValue ? cleaned : undefined;
|
|
2272
|
+
}
|
|
2273
|
+
// For arrays, clean each element
|
|
2274
|
+
if (Array.isArray(val)) {
|
|
2275
|
+
const cleaned = val.map(cleanValue).filter(item => item !== undefined);
|
|
2276
|
+
return cleaned.length > 0 ? cleaned : undefined;
|
|
2277
|
+
}
|
|
2278
|
+
return val;
|
|
2279
|
+
};
|
|
2280
|
+
const cleaned = {};
|
|
2281
|
+
Object.keys(filtered).forEach(key => {
|
|
2282
|
+
const cleanedValue = cleanValue(filtered[key]);
|
|
2283
|
+
if (cleanedValue !== undefined) {
|
|
2284
|
+
cleaned[key] = cleanedValue;
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
return cleaned;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2013
2290
|
/**
|
|
2014
2291
|
* Transform IP location data from API format (snake_case) to backend-expected format (camelCase)
|
|
2015
|
-
*
|
|
2292
|
+
* Supports configurable field storage to optimize storage capacity
|
|
2016
2293
|
*
|
|
2017
2294
|
* @param ipLocation - Raw IP location data from ipwho.is API
|
|
2018
|
-
* @
|
|
2295
|
+
* @param config - Optional configuration for which fields to store
|
|
2296
|
+
* @returns Transformed IP location data matching backend schema (only includes configured fields)
|
|
2019
2297
|
*/
|
|
2020
|
-
function transformIPLocationForBackend(ipLocation) {
|
|
2298
|
+
function transformIPLocationForBackend(ipLocation, config) {
|
|
2021
2299
|
if (!ipLocation) {
|
|
2022
2300
|
return null;
|
|
2023
2301
|
}
|
|
2024
2302
|
// Transform to match backend expected format (camelCase)
|
|
2303
|
+
// Build complete object first, then filter based on configuration
|
|
2025
2304
|
const transformed = {
|
|
2026
2305
|
// Basic fields
|
|
2027
2306
|
ip: ipLocation.ip,
|
|
@@ -2056,9 +2335,11 @@ function transformIPLocationForBackend(ipLocation) {
|
|
|
2056
2335
|
timezoneDetails: ipLocation.timezone && typeof ipLocation.timezone === 'object' ? {
|
|
2057
2336
|
id: ipLocation.timezone.id,
|
|
2058
2337
|
abbr: ipLocation.timezone.abbr,
|
|
2338
|
+
utc: ipLocation.timezone.utc,
|
|
2339
|
+
// Exclude these in essential mode: isDst, offset, currentTime
|
|
2340
|
+
// They will be filtered out by filterFieldsByConfig if not in essential fields
|
|
2059
2341
|
isDst: ipLocation.timezone.is_dst,
|
|
2060
2342
|
offset: ipLocation.timezone.offset,
|
|
2061
|
-
utc: ipLocation.timezone.utc,
|
|
2062
2343
|
currentTime: ipLocation.timezone.current_time,
|
|
2063
2344
|
} : undefined,
|
|
2064
2345
|
// Flag - transform to camelCase
|
|
@@ -2074,7 +2355,8 @@ function transformIPLocationForBackend(ipLocation) {
|
|
|
2074
2355
|
delete transformed[key];
|
|
2075
2356
|
}
|
|
2076
2357
|
});
|
|
2077
|
-
|
|
2358
|
+
// Filter fields based on configuration using generic filter
|
|
2359
|
+
return filterFieldsByConfig(transformed, config, DEFAULT_ESSENTIAL_IP_FIELDS);
|
|
2078
2360
|
}
|
|
2079
2361
|
|
|
2080
2362
|
/**
|
|
@@ -2340,20 +2622,49 @@ class AnalyticsService {
|
|
|
2340
2622
|
* Track user journey with full context
|
|
2341
2623
|
*/
|
|
2342
2624
|
static async trackUserJourney({ sessionId, pageUrl, networkInfo, deviceInfo, location, attribution, ipLocation, userId, customData, pageVisits = 1, interactions = 0, }) {
|
|
2343
|
-
//
|
|
2344
|
-
const
|
|
2625
|
+
// Get field storage config (support both new and legacy format)
|
|
2626
|
+
const fieldStorage = this.config.fieldStorage || {};
|
|
2627
|
+
const ipLocationConfig = fieldStorage.ipLocation || this.config.ipLocationFields;
|
|
2628
|
+
// Transform and filter all data types based on configuration
|
|
2629
|
+
const transformedIPLocation = transformIPLocationForBackend(ipLocation, ipLocationConfig);
|
|
2630
|
+
const filteredDeviceInfo = filterFieldsByConfig(deviceInfo, fieldStorage.deviceInfo, DEFAULT_ESSENTIAL_DEVICE_FIELDS);
|
|
2631
|
+
const filteredNetworkInfo = filterFieldsByConfig(networkInfo, fieldStorage.networkInfo, DEFAULT_ESSENTIAL_NETWORK_FIELDS);
|
|
2632
|
+
// For location: In essential mode, remove duplicate fields that are already in customData.ipLocation
|
|
2633
|
+
// This prevents storing the same data twice (e.g., ip, country, city, region, timezone)
|
|
2634
|
+
const locationConfig = fieldStorage.location;
|
|
2635
|
+
const locationMode = locationConfig?.mode || 'essential';
|
|
2636
|
+
let filteredLocation = filterFieldsByConfig(location, locationConfig, DEFAULT_ESSENTIAL_LOCATION_FIELDS);
|
|
2637
|
+
// In essential mode, if we have IP location data, remove duplicate fields from location
|
|
2638
|
+
// to avoid storing the same data twice
|
|
2639
|
+
if (locationMode === 'essential' && transformedIPLocation && filteredLocation) {
|
|
2640
|
+
// Remove fields that are duplicated in customData.ipLocation
|
|
2641
|
+
const duplicateFields = ['ip', 'country', 'countryCode', 'city', 'region', 'timezone'];
|
|
2642
|
+
const minimalLocation = { ...filteredLocation };
|
|
2643
|
+
duplicateFields.forEach(field => {
|
|
2644
|
+
delete minimalLocation[field];
|
|
2645
|
+
});
|
|
2646
|
+
// Only keep essential location fields: lat, lon, source, ts
|
|
2647
|
+
filteredLocation = {
|
|
2648
|
+
lat: minimalLocation.lat,
|
|
2649
|
+
lon: minimalLocation.lon,
|
|
2650
|
+
source: minimalLocation.source,
|
|
2651
|
+
ts: minimalLocation.ts,
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
const filteredAttribution = filterFieldsByConfig(attribution, fieldStorage.attribution, DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS);
|
|
2345
2655
|
await this.trackEvent({
|
|
2346
2656
|
sessionId,
|
|
2347
2657
|
pageUrl,
|
|
2348
|
-
networkInfo,
|
|
2349
|
-
deviceInfo,
|
|
2350
|
-
location,
|
|
2351
|
-
attribution,
|
|
2352
|
-
ipLocation
|
|
2658
|
+
networkInfo: filteredNetworkInfo || undefined,
|
|
2659
|
+
deviceInfo: filteredDeviceInfo || undefined,
|
|
2660
|
+
location: filteredLocation || undefined,
|
|
2661
|
+
attribution: filteredAttribution || undefined,
|
|
2662
|
+
// Don't include raw ipLocation - we have the filtered/transformed version in customData
|
|
2663
|
+
ipLocation: undefined,
|
|
2353
2664
|
userId: userId ?? sessionId,
|
|
2354
2665
|
customData: {
|
|
2355
2666
|
...customData,
|
|
2356
|
-
// Store transformed IP location in customData for backend integration
|
|
2667
|
+
// Store transformed and filtered IP location in customData for backend integration
|
|
2357
2668
|
...(transformedIPLocation && { ipLocation: transformedIPLocation }),
|
|
2358
2669
|
},
|
|
2359
2670
|
eventName: 'page_view', // Auto-tracked as page view
|
|
@@ -2430,15 +2741,41 @@ class AnalyticsService {
|
|
|
2430
2741
|
const ipLocationData = locationData && typeof locationData === 'object'
|
|
2431
2742
|
? locationData?.ipLocationData
|
|
2432
2743
|
: undefined;
|
|
2433
|
-
//
|
|
2434
|
-
const
|
|
2744
|
+
// Get field storage config (support both new and legacy format)
|
|
2745
|
+
const fieldStorage = this.config.fieldStorage || {};
|
|
2746
|
+
const ipLocationConfig = fieldStorage.ipLocation || this.config.ipLocationFields;
|
|
2747
|
+
// Transform and filter all data types based on configuration
|
|
2748
|
+
const transformedIPLocation = transformIPLocationForBackend(ipLocationData, ipLocationConfig);
|
|
2749
|
+
const filteredDeviceInfo = filterFieldsByConfig(context?.deviceInfo || autoContext?.deviceInfo, fieldStorage.deviceInfo, DEFAULT_ESSENTIAL_DEVICE_FIELDS);
|
|
2750
|
+
const filteredNetworkInfo = filterFieldsByConfig(context?.networkInfo || autoContext?.networkInfo, fieldStorage.networkInfo, DEFAULT_ESSENTIAL_NETWORK_FIELDS);
|
|
2751
|
+
// For location: In essential mode, remove duplicate fields that are already in customData.ipLocation
|
|
2752
|
+
const locationConfig = fieldStorage.location;
|
|
2753
|
+
const locationMode = locationConfig?.mode || 'essential';
|
|
2754
|
+
let filteredLocation = filterFieldsByConfig((context?.location || autoContext?.location), locationConfig, DEFAULT_ESSENTIAL_LOCATION_FIELDS);
|
|
2755
|
+
// In essential mode, if we have IP location data, remove duplicate fields from location
|
|
2756
|
+
if (locationMode === 'essential' && transformedIPLocation && filteredLocation) {
|
|
2757
|
+
// Remove fields that are duplicated in customData.ipLocation
|
|
2758
|
+
const duplicateFields = ['ip', 'country', 'countryCode', 'city', 'region', 'timezone'];
|
|
2759
|
+
const minimalLocation = { ...filteredLocation };
|
|
2760
|
+
duplicateFields.forEach(field => {
|
|
2761
|
+
delete minimalLocation[field];
|
|
2762
|
+
});
|
|
2763
|
+
// Only keep essential location fields: lat, lon, source, ts
|
|
2764
|
+
filteredLocation = {
|
|
2765
|
+
lat: minimalLocation.lat,
|
|
2766
|
+
lon: minimalLocation.lon,
|
|
2767
|
+
source: minimalLocation.source,
|
|
2768
|
+
ts: minimalLocation.ts,
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
const filteredAttribution = filterFieldsByConfig(context?.attribution || autoContext?.attribution, fieldStorage.attribution, DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS);
|
|
2435
2772
|
await this.trackEvent({
|
|
2436
2773
|
sessionId: finalSessionId,
|
|
2437
2774
|
pageUrl: finalPageUrl,
|
|
2438
|
-
networkInfo:
|
|
2439
|
-
deviceInfo:
|
|
2440
|
-
location:
|
|
2441
|
-
attribution:
|
|
2775
|
+
networkInfo: filteredNetworkInfo || undefined,
|
|
2776
|
+
deviceInfo: filteredDeviceInfo || undefined,
|
|
2777
|
+
location: filteredLocation || undefined,
|
|
2778
|
+
attribution: filteredAttribution || undefined,
|
|
2442
2779
|
userId: context?.userId || finalSessionId,
|
|
2443
2780
|
eventName,
|
|
2444
2781
|
eventParameters: parameters || {},
|
|
@@ -2624,6 +2961,8 @@ function useAnalytics(options = {}) {
|
|
|
2624
2961
|
logLevel: config.logLevel,
|
|
2625
2962
|
enableMetrics: config.enableMetrics,
|
|
2626
2963
|
sessionTimeout: config.sessionTimeout,
|
|
2964
|
+
fieldStorage: config.fieldStorage,
|
|
2965
|
+
ipLocationFields: config.ipLocationFields, // Legacy support
|
|
2627
2966
|
});
|
|
2628
2967
|
}
|
|
2629
2968
|
}, [
|
|
@@ -2911,6 +3250,11 @@ function useAnalytics(options = {}) {
|
|
|
2911
3250
|
|
|
2912
3251
|
exports.AnalyticsService = AnalyticsService;
|
|
2913
3252
|
exports.AttributionDetector = AttributionDetector;
|
|
3253
|
+
exports.DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS = DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS;
|
|
3254
|
+
exports.DEFAULT_ESSENTIAL_DEVICE_FIELDS = DEFAULT_ESSENTIAL_DEVICE_FIELDS;
|
|
3255
|
+
exports.DEFAULT_ESSENTIAL_IP_FIELDS = DEFAULT_ESSENTIAL_IP_FIELDS;
|
|
3256
|
+
exports.DEFAULT_ESSENTIAL_LOCATION_FIELDS = DEFAULT_ESSENTIAL_LOCATION_FIELDS;
|
|
3257
|
+
exports.DEFAULT_ESSENTIAL_NETWORK_FIELDS = DEFAULT_ESSENTIAL_NETWORK_FIELDS;
|
|
2914
3258
|
exports.DeviceDetector = DeviceDetector;
|
|
2915
3259
|
exports.LocationDetector = LocationDetector;
|
|
2916
3260
|
exports.NetworkDetector = NetworkDetector;
|
|
@@ -2919,6 +3263,7 @@ exports.checkAndSetLocationConsent = checkAndSetLocationConsent;
|
|
|
2919
3263
|
exports.clearLocationConsent = clearLocationConsent;
|
|
2920
3264
|
exports.clearSession = clearSession;
|
|
2921
3265
|
exports.default = useAnalytics;
|
|
3266
|
+
exports.filterFieldsByConfig = filterFieldsByConfig;
|
|
2922
3267
|
exports.getCompleteIPLocation = getCompleteIPLocation;
|
|
2923
3268
|
exports.getIPFromRequest = getIPFromRequest;
|
|
2924
3269
|
exports.getIPLocation = getIPLocation;
|