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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
|
+
# [4.0.0](https://github.com/switch-org/analytics-tracker/compare/v3.0.0...v4.0.0) (2025-12-30)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* optimize essential mode and use ipwho.is connection data ([0f830ea](https://github.com/switch-org/analytics-tracker/commit/0f830eae27009c1cfbfb4e50da4c13a28b497f5e))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### BREAKING CHANGES
|
|
10
|
+
|
|
11
|
+
* Essential mode deviceInfo now has 8 fields, networkInfo removed
|
|
12
|
+
|
|
13
|
+
# [3.0.0](https://github.com/switch-org/analytics-tracker/compare/v2.2.0...v3.0.0) (2025-12-30)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* optimize essential mode with deduplication and reduced deviceInfo fields ([b64eefa](https://github.com/switch-org/analytics-tracker/commit/b64eefa4992e0570b1011fa029d0db8de89c321b))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### BREAKING CHANGES
|
|
22
|
+
|
|
23
|
+
* Essential mode deviceInfo now stores only 8 fields instead of 15
|
|
24
|
+
|
|
25
|
+
## Changes
|
|
26
|
+
|
|
27
|
+
### Core Optimizations
|
|
28
|
+
- Reduce deviceInfo essential fields from 15 to 8
|
|
29
|
+
- Implement automatic deduplication between location and customData.ipLocation
|
|
30
|
+
- Remove duplicate fields from location object
|
|
31
|
+
- Add null/undefined value removal from filtered objects
|
|
32
|
+
- Improve nested object handling
|
|
33
|
+
|
|
34
|
+
### Field Storage Improvements
|
|
35
|
+
- Add generic field storage transformer for all data types
|
|
36
|
+
- Support essential/all/custom modes for all data types
|
|
37
|
+
- Implement recursive null value cleaning
|
|
38
|
+
- Add mode-aware deduplication logic
|
|
39
|
+
|
|
40
|
+
### Documentation
|
|
41
|
+
- Merge and consolidate essential mode documentation
|
|
42
|
+
- Add comprehensive essential-mode-guide.md
|
|
43
|
+
- Update documentation index
|
|
44
|
+
- Add example payload JSON
|
|
45
|
+
|
|
46
|
+
## Impact
|
|
47
|
+
- Significantly smaller payloads (~30-40% reduction)
|
|
48
|
+
- No duplicate data storage
|
|
49
|
+
- All crucial information preserved
|
|
50
|
+
|
|
1
51
|
# [2.2.0](https://github.com/switch-org/analytics-tracker/compare/v2.1.0...v2.2.0) (2025-12-30)
|
|
2
52
|
|
|
3
53
|
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A comprehensive, lightweight analytics tracking library for React applications.
|
|
|
11
11
|
## ✨ Features
|
|
12
12
|
|
|
13
13
|
- 🔍 **Device Detection**: Automatically detects device type, OS, browser, model, brand, and hardware specs using User-Agent Client Hints
|
|
14
|
-
- 🌐 **Network
|
|
14
|
+
- 🌐 **Network & Connection Info**: Accurate ISP and connection details from ipwho.is API (ASN, organization, domain)
|
|
15
15
|
- 📍 **Location Tracking**:
|
|
16
16
|
- **IP-based location** - Requires user consent (privacy-compliant)
|
|
17
17
|
- **GPS location** - Requires explicit user consent and browser permission
|
|
@@ -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 (includes connection data)
|
|
100
|
+
deviceInfo: { mode: 'essential' }, // Device info fields
|
|
101
|
+
// networkInfo: Not stored in essential mode - use connection from ipwho.is instead
|
|
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
|
```
|
|
@@ -211,7 +226,6 @@ import { useAnalytics } from 'user-analytics-tracker';
|
|
|
211
226
|
function MyApp() {
|
|
212
227
|
const {
|
|
213
228
|
sessionId,
|
|
214
|
-
networkInfo,
|
|
215
229
|
deviceInfo,
|
|
216
230
|
location,
|
|
217
231
|
trackEvent,
|
|
@@ -242,7 +256,7 @@ function MyApp() {
|
|
|
242
256
|
return (
|
|
243
257
|
<div>
|
|
244
258
|
<p>Device: {deviceInfo?.deviceBrand} {deviceInfo?.deviceModel}</p>
|
|
245
|
-
|
|
259
|
+
{/* Connection data from ipwho.is (in customData.ipLocation.connection) */}
|
|
246
260
|
<button onClick={handleButtonClick}>
|
|
247
261
|
Track Click
|
|
248
262
|
</button>
|
|
@@ -743,18 +757,11 @@ Detect and restrict hotspot users:
|
|
|
743
757
|
```tsx
|
|
744
758
|
import { useAnalytics } from 'user-analytics-tracker';
|
|
745
759
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
<div>
|
|
752
|
-
<h2>Hotspot Detected</h2>
|
|
753
|
-
<p>Please switch to mobile data or Wi-Fi.</p>
|
|
754
|
-
</div>
|
|
755
|
-
);
|
|
756
|
-
}
|
|
757
|
-
|
|
760
|
+
// Note: networkInfo is no longer available in essential mode
|
|
761
|
+
// Connection data is available in customData.ipLocation.connection from ipwho.is
|
|
762
|
+
function ConnectionInfo({ children }) {
|
|
763
|
+
// Connection info comes from ipwho.is API in analytics events
|
|
764
|
+
// Access via: customData.ipLocation.connection (asn, org, isp, domain)
|
|
758
765
|
return children;
|
|
759
766
|
}
|
|
760
767
|
```
|
|
@@ -973,8 +980,8 @@ MIT © [Switch Org](https://github.com/switch-org)
|
|
|
973
980
|
|
|
974
981
|
## 🙏 Acknowledgments
|
|
975
982
|
|
|
976
|
-
- Uses [ipwho.is](https://ipwho.is/) for free IP geolocation
|
|
977
|
-
- Built with modern web APIs (User-Agent Client Hints,
|
|
983
|
+
- Uses [ipwho.is](https://ipwho.is/) for free IP geolocation and accurate connection data
|
|
984
|
+
- Built with modern web APIs (User-Agent Client Hints, Geolocation API)
|
|
978
985
|
|
|
979
986
|
<!-- ## 📞 Support
|
|
980
987
|
|
package/dist/index.cjs.js
CHANGED
|
@@ -4,6 +4,101 @@ 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
|
+
* NOTE: In essential mode, networkInfo is not stored.
|
|
62
|
+
* Connection data from ipwho.is API (in customData.ipLocation.connection) is used instead,
|
|
63
|
+
* as it provides more accurate network/ISP information.
|
|
64
|
+
*/
|
|
65
|
+
const DEFAULT_ESSENTIAL_NETWORK_FIELDS = [
|
|
66
|
+
'type',
|
|
67
|
+
'effectiveType',
|
|
68
|
+
'downlink',
|
|
69
|
+
'rtt',
|
|
70
|
+
'saveData',
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Default essential fields for Location Info storage
|
|
74
|
+
*/
|
|
75
|
+
const DEFAULT_ESSENTIAL_LOCATION_FIELDS = [
|
|
76
|
+
// Minimal location fields - only coordinates and source
|
|
77
|
+
// All IP-related data (ip, country, city, etc.) is stored in customData.ipLocation to avoid duplication
|
|
78
|
+
'lat',
|
|
79
|
+
'lon',
|
|
80
|
+
'source',
|
|
81
|
+
'ts',
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* Default essential fields for Attribution Info storage
|
|
85
|
+
*/
|
|
86
|
+
const DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS = [
|
|
87
|
+
'landingUrl',
|
|
88
|
+
'path',
|
|
89
|
+
'hostname',
|
|
90
|
+
'referrerUrl',
|
|
91
|
+
'referrerDomain',
|
|
92
|
+
'navigationType',
|
|
93
|
+
'isReload',
|
|
94
|
+
'isBackForward',
|
|
95
|
+
'utm_source',
|
|
96
|
+
'utm_medium',
|
|
97
|
+
'utm_campaign',
|
|
98
|
+
'utm_term',
|
|
99
|
+
'utm_content',
|
|
100
|
+
];
|
|
101
|
+
|
|
7
102
|
/**
|
|
8
103
|
* Network Type Detector
|
|
9
104
|
* Detects WiFi, Mobile Data (Cellular), Hotspot, Ethernet, or Unknown
|
|
@@ -139,11 +234,6 @@ class NetworkDetector {
|
|
|
139
234
|
}
|
|
140
235
|
}
|
|
141
236
|
|
|
142
|
-
var networkDetector = /*#__PURE__*/Object.freeze({
|
|
143
|
-
__proto__: null,
|
|
144
|
-
NetworkDetector: NetworkDetector
|
|
145
|
-
});
|
|
146
|
-
|
|
147
237
|
/**
|
|
148
238
|
* Device Information Detector
|
|
149
239
|
* Detects device type, OS, browser, and hardware specs
|
|
@@ -2010,18 +2100,206 @@ class MetricsCollector {
|
|
|
2010
2100
|
// Global metrics collector instance
|
|
2011
2101
|
const metricsCollector = new MetricsCollector();
|
|
2012
2102
|
|
|
2103
|
+
/**
|
|
2104
|
+
* Generic field storage transformer
|
|
2105
|
+
* Filters object fields based on storage configuration
|
|
2106
|
+
*/
|
|
2107
|
+
/**
|
|
2108
|
+
* Filter object fields based on storage configuration
|
|
2109
|
+
*
|
|
2110
|
+
* @param data - The data object to filter
|
|
2111
|
+
* @param config - Storage configuration
|
|
2112
|
+
* @param defaultEssentialFields - Default essential fields for this data type
|
|
2113
|
+
* @returns Filtered data object with only configured fields
|
|
2114
|
+
*/
|
|
2115
|
+
function filterFieldsByConfig(data, config, defaultEssentialFields) {
|
|
2116
|
+
if (!data) {
|
|
2117
|
+
return null;
|
|
2118
|
+
}
|
|
2119
|
+
const mode = config?.mode || 'essential';
|
|
2120
|
+
let fieldsToInclude = [];
|
|
2121
|
+
if (mode === 'essential') {
|
|
2122
|
+
// Use default essential fields
|
|
2123
|
+
fieldsToInclude = [...defaultEssentialFields];
|
|
2124
|
+
}
|
|
2125
|
+
else if (mode === 'all') {
|
|
2126
|
+
// Include all fields, then exclude specified ones
|
|
2127
|
+
fieldsToInclude = ['*']; // Special marker for "all fields"
|
|
2128
|
+
}
|
|
2129
|
+
else if (mode === 'custom' && config) {
|
|
2130
|
+
// Use custom field list
|
|
2131
|
+
fieldsToInclude = config.fields || [];
|
|
2132
|
+
}
|
|
2133
|
+
// If mode is 'all', just exclude specified fields
|
|
2134
|
+
if (mode === 'all') {
|
|
2135
|
+
const filtered = { ...data };
|
|
2136
|
+
if (config?.exclude && config.exclude.length > 0) {
|
|
2137
|
+
const excludeSet = new Set(config.exclude);
|
|
2138
|
+
Object.keys(filtered).forEach(key => {
|
|
2139
|
+
if (excludeSet.has(key)) {
|
|
2140
|
+
delete filtered[key];
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
// Handle nested exclusions
|
|
2144
|
+
if (filtered.connection && excludeSet.has('connection')) {
|
|
2145
|
+
delete filtered.connection;
|
|
2146
|
+
}
|
|
2147
|
+
if (filtered.timezoneDetails && excludeSet.has('timezoneDetails')) {
|
|
2148
|
+
delete filtered.timezoneDetails;
|
|
2149
|
+
}
|
|
2150
|
+
if (filtered.flag && excludeSet.has('flag')) {
|
|
2151
|
+
delete filtered.flag;
|
|
2152
|
+
}
|
|
2153
|
+
if (filtered.firstTouch && excludeSet.has('firstTouch')) {
|
|
2154
|
+
delete filtered.firstTouch;
|
|
2155
|
+
}
|
|
2156
|
+
if (filtered.lastTouch && excludeSet.has('lastTouch')) {
|
|
2157
|
+
delete filtered.lastTouch;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
return filtered;
|
|
2161
|
+
}
|
|
2162
|
+
// For 'essential' or 'custom' mode, only include specified fields
|
|
2163
|
+
const filtered = {};
|
|
2164
|
+
const includeSet = new Set(fieldsToInclude);
|
|
2165
|
+
// Helper to check if a field path should be included
|
|
2166
|
+
const shouldInclude = (fieldPath) => {
|
|
2167
|
+
// Direct match - most specific
|
|
2168
|
+
if (includeSet.has(fieldPath))
|
|
2169
|
+
return true;
|
|
2170
|
+
// For nested fields (e.g., 'flag.emoji'), only include if explicitly listed
|
|
2171
|
+
// Don't auto-include all children just because parent is included
|
|
2172
|
+
const parts = fieldPath.split('.');
|
|
2173
|
+
if (parts.length > 1) {
|
|
2174
|
+
// For nested fields, require explicit inclusion
|
|
2175
|
+
// This prevents 'flag' from including all 'flag.*' fields
|
|
2176
|
+
return includeSet.has(fieldPath);
|
|
2177
|
+
}
|
|
2178
|
+
// For top-level fields only, check if parent path is included
|
|
2179
|
+
// This allows 'connection' to work when all connection.* fields are listed
|
|
2180
|
+
return false;
|
|
2181
|
+
};
|
|
2182
|
+
// Helper to check if a parent object should be created (for nested objects)
|
|
2183
|
+
const shouldIncludeParent = (parentPath) => {
|
|
2184
|
+
// Check if any child of this parent is included
|
|
2185
|
+
for (const field of fieldsToInclude) {
|
|
2186
|
+
if (field.startsWith(parentPath + '.')) {
|
|
2187
|
+
return true;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
// Also check if parent itself is explicitly included
|
|
2191
|
+
return includeSet.has(parentPath);
|
|
2192
|
+
};
|
|
2193
|
+
// Filter top-level fields
|
|
2194
|
+
Object.keys(data).forEach(key => {
|
|
2195
|
+
if (shouldInclude(key)) {
|
|
2196
|
+
filtered[key] = data[key];
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
// Handle nested objects - only create if at least one child field is included
|
|
2200
|
+
if (data.connection && shouldIncludeParent('connection')) {
|
|
2201
|
+
filtered.connection = {};
|
|
2202
|
+
if (shouldInclude('connection.asn'))
|
|
2203
|
+
filtered.connection.asn = data.connection.asn;
|
|
2204
|
+
if (shouldInclude('connection.org'))
|
|
2205
|
+
filtered.connection.org = data.connection.org;
|
|
2206
|
+
if (shouldInclude('connection.isp'))
|
|
2207
|
+
filtered.connection.isp = data.connection.isp;
|
|
2208
|
+
if (shouldInclude('connection.domain'))
|
|
2209
|
+
filtered.connection.domain = data.connection.domain;
|
|
2210
|
+
// If no connection fields were included, remove the object
|
|
2211
|
+
if (Object.keys(filtered.connection).length === 0) {
|
|
2212
|
+
delete filtered.connection;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
if (data.timezoneDetails && shouldIncludeParent('timezoneDetails')) {
|
|
2216
|
+
filtered.timezoneDetails = {};
|
|
2217
|
+
if (shouldInclude('timezoneDetails.id'))
|
|
2218
|
+
filtered.timezoneDetails.id = data.timezoneDetails.id;
|
|
2219
|
+
if (shouldInclude('timezoneDetails.abbr'))
|
|
2220
|
+
filtered.timezoneDetails.abbr = data.timezoneDetails.abbr;
|
|
2221
|
+
if (shouldInclude('timezoneDetails.utc'))
|
|
2222
|
+
filtered.timezoneDetails.utc = data.timezoneDetails.utc;
|
|
2223
|
+
if (shouldInclude('timezoneDetails.isDst'))
|
|
2224
|
+
filtered.timezoneDetails.isDst = data.timezoneDetails.isDst;
|
|
2225
|
+
if (shouldInclude('timezoneDetails.offset'))
|
|
2226
|
+
filtered.timezoneDetails.offset = data.timezoneDetails.offset;
|
|
2227
|
+
if (shouldInclude('timezoneDetails.currentTime'))
|
|
2228
|
+
filtered.timezoneDetails.currentTime = data.timezoneDetails.currentTime;
|
|
2229
|
+
// If no timezoneDetails fields were included, remove the object
|
|
2230
|
+
if (Object.keys(filtered.timezoneDetails).length === 0) {
|
|
2231
|
+
delete filtered.timezoneDetails;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
if (data.flag && shouldIncludeParent('flag')) {
|
|
2235
|
+
filtered.flag = {};
|
|
2236
|
+
// Only include specific flag fields if they're explicitly in the include list
|
|
2237
|
+
if (shouldInclude('flag.emoji'))
|
|
2238
|
+
filtered.flag.emoji = data.flag.emoji;
|
|
2239
|
+
if (shouldInclude('flag.img'))
|
|
2240
|
+
filtered.flag.img = data.flag.img;
|
|
2241
|
+
if (shouldInclude('flag.emojiUnicode'))
|
|
2242
|
+
filtered.flag.emojiUnicode = data.flag.emojiUnicode;
|
|
2243
|
+
// If no specific flag fields are included, don't add the flag object
|
|
2244
|
+
if (Object.keys(filtered.flag).length === 0) {
|
|
2245
|
+
delete filtered.flag;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
if (data.firstTouch && shouldInclude('firstTouch')) {
|
|
2249
|
+
filtered.firstTouch = data.firstTouch;
|
|
2250
|
+
}
|
|
2251
|
+
if (data.lastTouch && shouldInclude('lastTouch')) {
|
|
2252
|
+
filtered.lastTouch = data.lastTouch;
|
|
2253
|
+
}
|
|
2254
|
+
// Remove null and undefined values to reduce payload size
|
|
2255
|
+
const cleanValue = (val) => {
|
|
2256
|
+
if (val === null || val === undefined) {
|
|
2257
|
+
return undefined; // Will be filtered out
|
|
2258
|
+
}
|
|
2259
|
+
// For objects, recursively clean nested null/undefined values
|
|
2260
|
+
if (typeof val === 'object' && !Array.isArray(val) && val !== null) {
|
|
2261
|
+
const cleaned = {};
|
|
2262
|
+
let hasAnyValue = false;
|
|
2263
|
+
Object.keys(val).forEach(key => {
|
|
2264
|
+
const cleanedChild = cleanValue(val[key]);
|
|
2265
|
+
if (cleanedChild !== undefined) {
|
|
2266
|
+
cleaned[key] = cleanedChild;
|
|
2267
|
+
hasAnyValue = true;
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
return hasAnyValue ? cleaned : undefined;
|
|
2271
|
+
}
|
|
2272
|
+
// For arrays, clean each element
|
|
2273
|
+
if (Array.isArray(val)) {
|
|
2274
|
+
const cleaned = val.map(cleanValue).filter(item => item !== undefined);
|
|
2275
|
+
return cleaned.length > 0 ? cleaned : undefined;
|
|
2276
|
+
}
|
|
2277
|
+
return val;
|
|
2278
|
+
};
|
|
2279
|
+
const cleaned = {};
|
|
2280
|
+
Object.keys(filtered).forEach(key => {
|
|
2281
|
+
const cleanedValue = cleanValue(filtered[key]);
|
|
2282
|
+
if (cleanedValue !== undefined) {
|
|
2283
|
+
cleaned[key] = cleanedValue;
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
return cleaned;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2013
2289
|
/**
|
|
2014
2290
|
* Transform IP location data from API format (snake_case) to backend-expected format (camelCase)
|
|
2015
|
-
*
|
|
2291
|
+
* Supports configurable field storage to optimize storage capacity
|
|
2016
2292
|
*
|
|
2017
2293
|
* @param ipLocation - Raw IP location data from ipwho.is API
|
|
2018
|
-
* @
|
|
2294
|
+
* @param config - Optional configuration for which fields to store
|
|
2295
|
+
* @returns Transformed IP location data matching backend schema (only includes configured fields)
|
|
2019
2296
|
*/
|
|
2020
|
-
function transformIPLocationForBackend(ipLocation) {
|
|
2297
|
+
function transformIPLocationForBackend(ipLocation, config) {
|
|
2021
2298
|
if (!ipLocation) {
|
|
2022
2299
|
return null;
|
|
2023
2300
|
}
|
|
2024
2301
|
// Transform to match backend expected format (camelCase)
|
|
2302
|
+
// Build complete object first, then filter based on configuration
|
|
2025
2303
|
const transformed = {
|
|
2026
2304
|
// Basic fields
|
|
2027
2305
|
ip: ipLocation.ip,
|
|
@@ -2056,9 +2334,11 @@ function transformIPLocationForBackend(ipLocation) {
|
|
|
2056
2334
|
timezoneDetails: ipLocation.timezone && typeof ipLocation.timezone === 'object' ? {
|
|
2057
2335
|
id: ipLocation.timezone.id,
|
|
2058
2336
|
abbr: ipLocation.timezone.abbr,
|
|
2337
|
+
utc: ipLocation.timezone.utc,
|
|
2338
|
+
// Exclude these in essential mode: isDst, offset, currentTime
|
|
2339
|
+
// They will be filtered out by filterFieldsByConfig if not in essential fields
|
|
2059
2340
|
isDst: ipLocation.timezone.is_dst,
|
|
2060
2341
|
offset: ipLocation.timezone.offset,
|
|
2061
|
-
utc: ipLocation.timezone.utc,
|
|
2062
2342
|
currentTime: ipLocation.timezone.current_time,
|
|
2063
2343
|
} : undefined,
|
|
2064
2344
|
// Flag - transform to camelCase
|
|
@@ -2074,7 +2354,8 @@ function transformIPLocationForBackend(ipLocation) {
|
|
|
2074
2354
|
delete transformed[key];
|
|
2075
2355
|
}
|
|
2076
2356
|
});
|
|
2077
|
-
|
|
2357
|
+
// Filter fields based on configuration using generic filter
|
|
2358
|
+
return filterFieldsByConfig(transformed, config, DEFAULT_ESSENTIAL_IP_FIELDS);
|
|
2078
2359
|
}
|
|
2079
2360
|
|
|
2080
2361
|
/**
|
|
@@ -2340,20 +2621,56 @@ class AnalyticsService {
|
|
|
2340
2621
|
* Track user journey with full context
|
|
2341
2622
|
*/
|
|
2342
2623
|
static async trackUserJourney({ sessionId, pageUrl, networkInfo, deviceInfo, location, attribution, ipLocation, userId, customData, pageVisits = 1, interactions = 0, }) {
|
|
2343
|
-
//
|
|
2344
|
-
const
|
|
2624
|
+
// Get field storage config (support both new and legacy format)
|
|
2625
|
+
const fieldStorage = this.config.fieldStorage || {};
|
|
2626
|
+
const ipLocationConfig = fieldStorage.ipLocation || this.config.ipLocationFields;
|
|
2627
|
+
// Transform and filter all data types based on configuration
|
|
2628
|
+
const transformedIPLocation = transformIPLocationForBackend(ipLocation, ipLocationConfig);
|
|
2629
|
+
const filteredDeviceInfo = filterFieldsByConfig(deviceInfo, fieldStorage.deviceInfo, DEFAULT_ESSENTIAL_DEVICE_FIELDS);
|
|
2630
|
+
// In essential mode, don't store browser-based networkInfo
|
|
2631
|
+
// Connection data from ipwho.is (in customData.ipLocation.connection) is more accurate
|
|
2632
|
+
const networkInfoConfig = fieldStorage.networkInfo;
|
|
2633
|
+
const networkInfoMode = networkInfoConfig?.mode || 'essential';
|
|
2634
|
+
// Skip networkInfo in essential mode - use connection from ipwho.is instead
|
|
2635
|
+
const filteredNetworkInfo = networkInfoMode === 'essential'
|
|
2636
|
+
? undefined
|
|
2637
|
+
: filterFieldsByConfig(networkInfo, networkInfoConfig, DEFAULT_ESSENTIAL_NETWORK_FIELDS);
|
|
2638
|
+
// For location: In essential mode, remove duplicate fields that are already in customData.ipLocation
|
|
2639
|
+
// This prevents storing the same data twice (e.g., ip, country, city, region, timezone)
|
|
2640
|
+
const locationConfig = fieldStorage.location;
|
|
2641
|
+
const locationMode = locationConfig?.mode || 'essential';
|
|
2642
|
+
let filteredLocation = filterFieldsByConfig(location, locationConfig, DEFAULT_ESSENTIAL_LOCATION_FIELDS);
|
|
2643
|
+
// In essential mode, if we have IP location data, remove duplicate fields from location
|
|
2644
|
+
// to avoid storing the same data twice
|
|
2645
|
+
if (locationMode === 'essential' && transformedIPLocation && filteredLocation) {
|
|
2646
|
+
// Remove fields that are duplicated in customData.ipLocation
|
|
2647
|
+
const duplicateFields = ['ip', 'country', 'countryCode', 'city', 'region', 'timezone'];
|
|
2648
|
+
const minimalLocation = { ...filteredLocation };
|
|
2649
|
+
duplicateFields.forEach(field => {
|
|
2650
|
+
delete minimalLocation[field];
|
|
2651
|
+
});
|
|
2652
|
+
// Only keep essential location fields: lat, lon, source, ts
|
|
2653
|
+
filteredLocation = {
|
|
2654
|
+
lat: minimalLocation.lat,
|
|
2655
|
+
lon: minimalLocation.lon,
|
|
2656
|
+
source: minimalLocation.source,
|
|
2657
|
+
ts: minimalLocation.ts,
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
const filteredAttribution = filterFieldsByConfig(attribution, fieldStorage.attribution, DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS);
|
|
2345
2661
|
await this.trackEvent({
|
|
2346
2662
|
sessionId,
|
|
2347
2663
|
pageUrl,
|
|
2348
|
-
networkInfo,
|
|
2349
|
-
deviceInfo,
|
|
2350
|
-
location,
|
|
2351
|
-
attribution,
|
|
2352
|
-
ipLocation
|
|
2664
|
+
networkInfo: filteredNetworkInfo || undefined,
|
|
2665
|
+
deviceInfo: filteredDeviceInfo || undefined,
|
|
2666
|
+
location: filteredLocation || undefined,
|
|
2667
|
+
attribution: filteredAttribution || undefined,
|
|
2668
|
+
// Don't include raw ipLocation - we have the filtered/transformed version in customData
|
|
2669
|
+
ipLocation: undefined,
|
|
2353
2670
|
userId: userId ?? sessionId,
|
|
2354
2671
|
customData: {
|
|
2355
2672
|
...customData,
|
|
2356
|
-
// Store transformed IP location in customData for backend integration
|
|
2673
|
+
// Store transformed and filtered IP location in customData for backend integration
|
|
2357
2674
|
...(transformedIPLocation && { ipLocation: transformedIPLocation }),
|
|
2358
2675
|
},
|
|
2359
2676
|
eventName: 'page_view', // Auto-tracked as page view
|
|
@@ -2393,14 +2710,14 @@ class AnalyticsService {
|
|
|
2393
2710
|
try {
|
|
2394
2711
|
// Import dynamically to avoid circular dependencies
|
|
2395
2712
|
const { getOrCreateUserId } = await Promise.resolve().then(function () { return storage; });
|
|
2396
|
-
const { NetworkDetector } = await Promise.resolve().then(function () { return networkDetector; });
|
|
2397
2713
|
const { DeviceDetector } = await Promise.resolve().then(function () { return deviceDetector; });
|
|
2398
2714
|
const { LocationDetector } = await Promise.resolve().then(function () { return locationDetector; });
|
|
2399
2715
|
const { AttributionDetector } = await Promise.resolve().then(function () { return attributionDetector; });
|
|
2716
|
+
// Don't collect networkInfo - use connection from ipwho.is instead (more accurate)
|
|
2400
2717
|
autoContext = {
|
|
2401
2718
|
sessionId: getOrCreateUserId(),
|
|
2402
2719
|
pageUrl: window.location.href,
|
|
2403
|
-
networkInfo
|
|
2720
|
+
// networkInfo removed - use customData.ipLocation.connection from ipwho.is instead
|
|
2404
2721
|
deviceInfo: await DeviceDetector.detect(),
|
|
2405
2722
|
location: await LocationDetector.detect().catch(() => undefined),
|
|
2406
2723
|
attribution: AttributionDetector.detect(),
|
|
@@ -2430,15 +2747,48 @@ class AnalyticsService {
|
|
|
2430
2747
|
const ipLocationData = locationData && typeof locationData === 'object'
|
|
2431
2748
|
? locationData?.ipLocationData
|
|
2432
2749
|
: undefined;
|
|
2433
|
-
//
|
|
2434
|
-
const
|
|
2750
|
+
// Get field storage config (support both new and legacy format)
|
|
2751
|
+
const fieldStorage = this.config.fieldStorage || {};
|
|
2752
|
+
const ipLocationConfig = fieldStorage.ipLocation || this.config.ipLocationFields;
|
|
2753
|
+
// Transform and filter all data types based on configuration
|
|
2754
|
+
const transformedIPLocation = transformIPLocationForBackend(ipLocationData, ipLocationConfig);
|
|
2755
|
+
const filteredDeviceInfo = filterFieldsByConfig(context?.deviceInfo || autoContext?.deviceInfo, fieldStorage.deviceInfo, DEFAULT_ESSENTIAL_DEVICE_FIELDS);
|
|
2756
|
+
// In essential mode, don't store browser-based networkInfo
|
|
2757
|
+
// Connection data from ipwho.is (in customData.ipLocation.connection) is more accurate
|
|
2758
|
+
const networkInfoConfig = fieldStorage.networkInfo;
|
|
2759
|
+
const networkInfoMode = networkInfoConfig?.mode || 'essential';
|
|
2760
|
+
// Skip networkInfo in essential mode - use connection from ipwho.is instead
|
|
2761
|
+
const filteredNetworkInfo = networkInfoMode === 'essential'
|
|
2762
|
+
? undefined
|
|
2763
|
+
: filterFieldsByConfig(context?.networkInfo || autoContext?.networkInfo, networkInfoConfig, DEFAULT_ESSENTIAL_NETWORK_FIELDS);
|
|
2764
|
+
// For location: In essential mode, remove duplicate fields that are already in customData.ipLocation
|
|
2765
|
+
const locationConfig = fieldStorage.location;
|
|
2766
|
+
const locationMode = locationConfig?.mode || 'essential';
|
|
2767
|
+
let filteredLocation = filterFieldsByConfig((context?.location || autoContext?.location), locationConfig, DEFAULT_ESSENTIAL_LOCATION_FIELDS);
|
|
2768
|
+
// In essential mode, if we have IP location data, remove duplicate fields from location
|
|
2769
|
+
if (locationMode === 'essential' && transformedIPLocation && filteredLocation) {
|
|
2770
|
+
// Remove fields that are duplicated in customData.ipLocation
|
|
2771
|
+
const duplicateFields = ['ip', 'country', 'countryCode', 'city', 'region', 'timezone'];
|
|
2772
|
+
const minimalLocation = { ...filteredLocation };
|
|
2773
|
+
duplicateFields.forEach(field => {
|
|
2774
|
+
delete minimalLocation[field];
|
|
2775
|
+
});
|
|
2776
|
+
// Only keep essential location fields: lat, lon, source, ts
|
|
2777
|
+
filteredLocation = {
|
|
2778
|
+
lat: minimalLocation.lat,
|
|
2779
|
+
lon: minimalLocation.lon,
|
|
2780
|
+
source: minimalLocation.source,
|
|
2781
|
+
ts: minimalLocation.ts,
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
const filteredAttribution = filterFieldsByConfig(context?.attribution || autoContext?.attribution, fieldStorage.attribution, DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS);
|
|
2435
2785
|
await this.trackEvent({
|
|
2436
2786
|
sessionId: finalSessionId,
|
|
2437
2787
|
pageUrl: finalPageUrl,
|
|
2438
|
-
networkInfo:
|
|
2439
|
-
deviceInfo:
|
|
2440
|
-
location:
|
|
2441
|
-
attribution:
|
|
2788
|
+
networkInfo: filteredNetworkInfo || undefined,
|
|
2789
|
+
deviceInfo: filteredDeviceInfo || undefined,
|
|
2790
|
+
location: filteredLocation || undefined,
|
|
2791
|
+
attribution: filteredAttribution || undefined,
|
|
2442
2792
|
userId: context?.userId || finalSessionId,
|
|
2443
2793
|
eventName,
|
|
2444
2794
|
eventParameters: parameters || {},
|
|
@@ -2624,6 +2974,8 @@ function useAnalytics(options = {}) {
|
|
|
2624
2974
|
logLevel: config.logLevel,
|
|
2625
2975
|
enableMetrics: config.enableMetrics,
|
|
2626
2976
|
sessionTimeout: config.sessionTimeout,
|
|
2977
|
+
fieldStorage: config.fieldStorage,
|
|
2978
|
+
ipLocationFields: config.ipLocationFields, // Legacy support
|
|
2627
2979
|
});
|
|
2628
2980
|
}
|
|
2629
2981
|
}, [
|
|
@@ -2666,7 +3018,8 @@ function useAnalytics(options = {}) {
|
|
|
2666
3018
|
initDebug();
|
|
2667
3019
|
}, []);
|
|
2668
3020
|
const refresh = react.useCallback(async () => {
|
|
2669
|
-
|
|
3021
|
+
// Don't collect networkInfo - use connection from ipwho.is instead (more accurate)
|
|
3022
|
+
// const net = NetworkDetector.detect(); // Removed - use ipwho.is connection instead
|
|
2670
3023
|
const dev = await DeviceDetector.detect();
|
|
2671
3024
|
const attr = AttributionDetector.detect();
|
|
2672
3025
|
const uid = getOrCreateUserId();
|
|
@@ -2709,7 +3062,8 @@ function useAnalytics(options = {}) {
|
|
|
2709
3062
|
lastLocationRef.current = loc;
|
|
2710
3063
|
}
|
|
2711
3064
|
}
|
|
2712
|
-
|
|
3065
|
+
// networkInfo removed - use connection from ipwho.is instead
|
|
3066
|
+
setNetworkInfo(null); // Set to null - connection data comes from ipwho.is
|
|
2713
3067
|
setDeviceInfo(dev);
|
|
2714
3068
|
setAttribution(attr);
|
|
2715
3069
|
setSessionId(uid);
|
|
@@ -2719,13 +3073,14 @@ function useAnalytics(options = {}) {
|
|
|
2719
3073
|
if (onReady && !sessionLoggedRef.current) {
|
|
2720
3074
|
onReady({
|
|
2721
3075
|
sessionId: uid,
|
|
2722
|
-
networkInfo:
|
|
3076
|
+
networkInfo: null, // Use connection from ipwho.is instead (more accurate)
|
|
2723
3077
|
deviceInfo: dev,
|
|
2724
3078
|
location: loc,
|
|
2725
3079
|
attribution: attr,
|
|
2726
3080
|
});
|
|
2727
3081
|
}
|
|
2728
|
-
|
|
3082
|
+
// Return null for net - connection data comes from ipwho.is instead
|
|
3083
|
+
return { net: null, dev, attr, loc }; // net is null - use ipwho.is connection instead
|
|
2729
3084
|
}, [onReady]);
|
|
2730
3085
|
// Initialize on mount
|
|
2731
3086
|
react.useEffect(() => {
|
|
@@ -2911,6 +3266,11 @@ function useAnalytics(options = {}) {
|
|
|
2911
3266
|
|
|
2912
3267
|
exports.AnalyticsService = AnalyticsService;
|
|
2913
3268
|
exports.AttributionDetector = AttributionDetector;
|
|
3269
|
+
exports.DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS = DEFAULT_ESSENTIAL_ATTRIBUTION_FIELDS;
|
|
3270
|
+
exports.DEFAULT_ESSENTIAL_DEVICE_FIELDS = DEFAULT_ESSENTIAL_DEVICE_FIELDS;
|
|
3271
|
+
exports.DEFAULT_ESSENTIAL_IP_FIELDS = DEFAULT_ESSENTIAL_IP_FIELDS;
|
|
3272
|
+
exports.DEFAULT_ESSENTIAL_LOCATION_FIELDS = DEFAULT_ESSENTIAL_LOCATION_FIELDS;
|
|
3273
|
+
exports.DEFAULT_ESSENTIAL_NETWORK_FIELDS = DEFAULT_ESSENTIAL_NETWORK_FIELDS;
|
|
2914
3274
|
exports.DeviceDetector = DeviceDetector;
|
|
2915
3275
|
exports.LocationDetector = LocationDetector;
|
|
2916
3276
|
exports.NetworkDetector = NetworkDetector;
|
|
@@ -2919,6 +3279,7 @@ exports.checkAndSetLocationConsent = checkAndSetLocationConsent;
|
|
|
2919
3279
|
exports.clearLocationConsent = clearLocationConsent;
|
|
2920
3280
|
exports.clearSession = clearSession;
|
|
2921
3281
|
exports.default = useAnalytics;
|
|
3282
|
+
exports.filterFieldsByConfig = filterFieldsByConfig;
|
|
2922
3283
|
exports.getCompleteIPLocation = getCompleteIPLocation;
|
|
2923
3284
|
exports.getIPFromRequest = getIPFromRequest;
|
|
2924
3285
|
exports.getIPLocation = getIPLocation;
|