@rejourneyco/react-native 1.0.1 → 1.0.3
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/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +72 -391
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +11 -113
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +1 -15
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +1 -61
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +3 -1
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +1 -22
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +3 -26
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +0 -2
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +7 -93
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +5 -41
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +2 -58
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +4 -4
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +36 -7
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +7 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +9 -0
- package/ios/Capture/RJCaptureEngine.m +3 -34
- package/ios/Capture/RJVideoEncoder.m +0 -26
- package/ios/Capture/RJViewHierarchyScanner.m +68 -51
- package/ios/Core/RJLifecycleManager.m +0 -14
- package/ios/Core/Rejourney.mm +53 -129
- package/ios/Network/RJDeviceAuthManager.m +0 -2
- package/ios/Network/RJUploadManager.h +8 -0
- package/ios/Network/RJUploadManager.m +45 -0
- package/ios/Privacy/RJPrivacyMask.m +5 -31
- package/ios/Rejourney.h +0 -14
- package/ios/Touch/RJTouchInterceptor.m +21 -15
- package/ios/Utils/RJEventBuffer.m +57 -69
- package/ios/Utils/RJPerfTiming.m +0 -5
- package/ios/Utils/RJWindowUtils.m +87 -87
- package/lib/commonjs/components/Mask.js +1 -6
- package/lib/commonjs/index.js +46 -117
- package/lib/commonjs/sdk/autoTracking.js +39 -313
- package/lib/commonjs/sdk/constants.js +2 -13
- package/lib/commonjs/sdk/errorTracking.js +1 -29
- package/lib/commonjs/sdk/metricsTracking.js +3 -24
- package/lib/commonjs/sdk/navigation.js +3 -42
- package/lib/commonjs/sdk/networkInterceptor.js +7 -60
- package/lib/commonjs/sdk/utils.js +73 -19
- package/lib/module/components/Mask.js +1 -6
- package/lib/module/index.js +45 -121
- package/lib/module/sdk/autoTracking.js +39 -314
- package/lib/module/sdk/constants.js +2 -13
- package/lib/module/sdk/errorTracking.js +1 -29
- package/lib/module/sdk/index.js +0 -2
- package/lib/module/sdk/metricsTracking.js +3 -24
- package/lib/module/sdk/navigation.js +3 -42
- package/lib/module/sdk/networkInterceptor.js +7 -60
- package/lib/module/sdk/utils.js +73 -19
- package/lib/typescript/NativeRejourney.d.ts +1 -0
- package/lib/typescript/sdk/autoTracking.d.ts +4 -4
- package/lib/typescript/sdk/utils.d.ts +31 -1
- package/lib/typescript/types/index.d.ts +0 -1
- package/package.json +17 -11
- package/src/NativeRejourney.ts +2 -0
- package/src/components/Mask.tsx +0 -3
- package/src/index.ts +43 -92
- package/src/sdk/autoTracking.ts +51 -284
- package/src/sdk/constants.ts +13 -13
- package/src/sdk/errorTracking.ts +1 -17
- package/src/sdk/index.ts +0 -2
- package/src/sdk/metricsTracking.ts +5 -33
- package/src/sdk/navigation.ts +8 -29
- package/src/sdk/networkInterceptor.ts +9 -42
- package/src/sdk/utils.ts +76 -19
- package/src/types/index.ts +0 -29
|
@@ -14,35 +14,25 @@ const SDK_VERSION = exports.SDK_VERSION = '1.0.0';
|
|
|
14
14
|
const DEFAULT_CONFIG = exports.DEFAULT_CONFIG = {
|
|
15
15
|
enabled: true,
|
|
16
16
|
captureFPS: 0.5,
|
|
17
|
-
// Every 2 seconds (only used in timer mode)
|
|
18
17
|
captureOnEvents: true,
|
|
19
|
-
// Event-driven capture (not time-based)
|
|
20
18
|
maxSessionDuration: 10 * 60 * 1000,
|
|
21
|
-
// 10 minutes (project-level configurable, clamped 1–10)
|
|
22
19
|
maxStorageSize: 50 * 1024 * 1024,
|
|
23
|
-
// 50MB
|
|
24
20
|
autoScreenTracking: true,
|
|
25
21
|
autoGestureTracking: true,
|
|
26
22
|
privacyOcclusion: true,
|
|
27
23
|
enableCompression: true,
|
|
28
24
|
inactivityThreshold: 5000,
|
|
29
|
-
// 5 seconds
|
|
30
25
|
disableInDev: false,
|
|
31
26
|
detectRageTaps: true,
|
|
32
27
|
rageTapThreshold: 3,
|
|
33
28
|
rageTapTimeWindow: 1000,
|
|
34
|
-
// 1 second
|
|
35
29
|
debug: false,
|
|
36
30
|
autoStartRecording: true,
|
|
37
31
|
collectDeviceInfo: true,
|
|
38
|
-
// Collect detailed device information
|
|
39
32
|
collectGeoLocation: true,
|
|
40
|
-
// Collect IP address and geolocation
|
|
41
33
|
postNavigationDelay: 300,
|
|
42
|
-
// 300ms - allow navigation animations to complete
|
|
43
34
|
postGestureDelay: 200,
|
|
44
|
-
|
|
45
|
-
postModalDelay: 400 // 400ms - ensure modals/alerts are fully rendered
|
|
35
|
+
postModalDelay: 400
|
|
46
36
|
};
|
|
47
37
|
|
|
48
38
|
/** Event type constants */
|
|
@@ -78,8 +68,7 @@ const CAPTURE_SETTINGS = exports.CAPTURE_SETTINGS = {
|
|
|
78
68
|
MIN_FPS: 0.1,
|
|
79
69
|
MAX_FPS: 2,
|
|
80
70
|
CAPTURE_SCALE: 0.25,
|
|
81
|
-
|
|
82
|
-
MIN_CAPTURE_DELTA_TIME: 0.5 // Minimum 0.5s between captures (rate limiting)
|
|
71
|
+
MIN_CAPTURE_DELTA_TIME: 0.5
|
|
83
72
|
};
|
|
84
73
|
|
|
85
74
|
/** Memory management settings */
|
|
@@ -15,20 +15,11 @@ exports.setupErrorTracking = setupErrorTracking;
|
|
|
15
15
|
* Split from autoTracking.ts for better code organization.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
// Type declarations for browser globals (only used in hybrid apps where DOM is available)
|
|
19
|
-
|
|
20
|
-
// Cast globalThis to work with both RN and hybrid scenarios
|
|
21
18
|
const _globalThis = globalThis;
|
|
22
|
-
|
|
23
|
-
// Original error handlers (for restoration)
|
|
24
19
|
let originalErrorHandler;
|
|
25
20
|
let originalOnError = null;
|
|
26
21
|
let originalOnUnhandledRejection = null;
|
|
27
|
-
|
|
28
|
-
// Callbacks
|
|
29
22
|
let onErrorCallback = null;
|
|
30
|
-
|
|
31
|
-
// Metrics
|
|
32
23
|
let errorCount = 0;
|
|
33
24
|
|
|
34
25
|
/**
|
|
@@ -37,18 +28,12 @@ let errorCount = 0;
|
|
|
37
28
|
function setupErrorTracking(config, onError) {
|
|
38
29
|
onErrorCallback = onError;
|
|
39
30
|
errorCount = 0;
|
|
40
|
-
|
|
41
|
-
// Track React Native errors
|
|
42
31
|
if (config.trackReactNativeErrors !== false) {
|
|
43
32
|
setupReactNativeErrorHandler();
|
|
44
33
|
}
|
|
45
|
-
|
|
46
|
-
// Track JavaScript errors (only works in web/debug)
|
|
47
34
|
if (config.trackJSErrors !== false && typeof _globalThis !== 'undefined') {
|
|
48
35
|
setupJSErrorHandler();
|
|
49
36
|
}
|
|
50
|
-
|
|
51
|
-
// Track unhandled promise rejections
|
|
52
37
|
if (config.trackPromiseRejections !== false && typeof _globalThis !== 'undefined') {
|
|
53
38
|
setupPromiseRejectionHandler();
|
|
54
39
|
}
|
|
@@ -58,7 +43,6 @@ function setupErrorTracking(config, onError) {
|
|
|
58
43
|
* Cleanup error tracking and restore original handlers
|
|
59
44
|
*/
|
|
60
45
|
function cleanupErrorTracking() {
|
|
61
|
-
// Restore React Native handler
|
|
62
46
|
if (originalErrorHandler) {
|
|
63
47
|
try {
|
|
64
48
|
const ErrorUtils = _globalThis.ErrorUtils;
|
|
@@ -70,14 +54,10 @@ function cleanupErrorTracking() {
|
|
|
70
54
|
}
|
|
71
55
|
originalErrorHandler = undefined;
|
|
72
56
|
}
|
|
73
|
-
|
|
74
|
-
// Restore global onerror
|
|
75
57
|
if (originalOnError !== null) {
|
|
76
58
|
_globalThis.onerror = originalOnError;
|
|
77
59
|
originalOnError = null;
|
|
78
60
|
}
|
|
79
|
-
|
|
80
|
-
// Remove promise rejection handler
|
|
81
61
|
if (originalOnUnhandledRejection && typeof _globalThis.removeEventListener !== 'undefined') {
|
|
82
62
|
_globalThis.removeEventListener('unhandledrejection', originalOnUnhandledRejection);
|
|
83
63
|
originalOnUnhandledRejection = null;
|
|
@@ -129,11 +109,7 @@ function setupReactNativeErrorHandler() {
|
|
|
129
109
|
try {
|
|
130
110
|
const ErrorUtils = _globalThis.ErrorUtils;
|
|
131
111
|
if (!ErrorUtils) return;
|
|
132
|
-
|
|
133
|
-
// Store original handler
|
|
134
112
|
originalErrorHandler = ErrorUtils.getGlobalHandler();
|
|
135
|
-
|
|
136
|
-
// Set new handler
|
|
137
113
|
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
138
114
|
trackError({
|
|
139
115
|
type: 'error',
|
|
@@ -142,14 +118,12 @@ function setupReactNativeErrorHandler() {
|
|
|
142
118
|
stack: error.stack,
|
|
143
119
|
name: error.name || 'Error'
|
|
144
120
|
});
|
|
145
|
-
|
|
146
|
-
// Call original handler
|
|
147
121
|
if (originalErrorHandler) {
|
|
148
122
|
originalErrorHandler(error, isFatal);
|
|
149
123
|
}
|
|
150
124
|
});
|
|
151
125
|
} catch {
|
|
152
|
-
//
|
|
126
|
+
// Ignore
|
|
153
127
|
}
|
|
154
128
|
}
|
|
155
129
|
|
|
@@ -167,8 +141,6 @@ function setupJSErrorHandler() {
|
|
|
167
141
|
stack: error?.stack || `${source}:${lineno}:${colno}`,
|
|
168
142
|
name: error?.name || 'Error'
|
|
169
143
|
});
|
|
170
|
-
|
|
171
|
-
// Call original handler
|
|
172
144
|
if (originalOnError) {
|
|
173
145
|
return originalOnError(message, source, lineno, colno, error);
|
|
174
146
|
}
|
|
@@ -27,10 +27,9 @@ exports.trackAPIMetrics = trackAPIMetrics;
|
|
|
27
27
|
* Session metrics structure
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
// Metrics state
|
|
31
30
|
let metrics = createEmptyMetrics();
|
|
32
31
|
let sessionStartTime = 0;
|
|
33
|
-
let maxSessionDurationMs = 10 * 60 * 1000;
|
|
32
|
+
let maxSessionDurationMs = 10 * 60 * 1000;
|
|
34
33
|
|
|
35
34
|
/**
|
|
36
35
|
* Create empty metrics object
|
|
@@ -78,11 +77,8 @@ function initMetrics() {
|
|
|
78
77
|
* Get current session metrics with calculated scores
|
|
79
78
|
*/
|
|
80
79
|
function getSessionMetrics() {
|
|
81
|
-
// Calculate duration, clamped to max session duration
|
|
82
80
|
const rawDuration = Date.now() - sessionStartTime;
|
|
83
81
|
const durationMs = Math.min(rawDuration, maxSessionDurationMs);
|
|
84
|
-
|
|
85
|
-
// Calculate scores
|
|
86
82
|
const interactionScore = calculateInteractionScore(durationMs);
|
|
87
83
|
const explorationScore = calculateExplorationScore();
|
|
88
84
|
const uxScore = calculateUXScore();
|
|
@@ -104,9 +100,6 @@ function setMaxSessionDurationMinutes(minutes) {
|
|
|
104
100
|
maxSessionDurationMs = clampedMinutes * 60 * 1000;
|
|
105
101
|
}
|
|
106
102
|
}
|
|
107
|
-
|
|
108
|
-
// ==================== Metric Increment Methods ====================
|
|
109
|
-
|
|
110
103
|
function incrementTouchCount() {
|
|
111
104
|
metrics.touchCount++;
|
|
112
105
|
metrics.totalEvents++;
|
|
@@ -146,8 +139,6 @@ function trackAPIMetrics(success, durationMs = 0, responseBytes = 0) {
|
|
|
146
139
|
}
|
|
147
140
|
}
|
|
148
141
|
|
|
149
|
-
// ==================== Score Calculations ====================
|
|
150
|
-
|
|
151
142
|
/**
|
|
152
143
|
* Calculate interaction score based on engagement with app
|
|
153
144
|
* Higher = more engaged (more interactions per minute)
|
|
@@ -156,16 +147,12 @@ function calculateInteractionScore(durationMs) {
|
|
|
156
147
|
if (durationMs <= 0) return 100;
|
|
157
148
|
const durationMinutes = durationMs / 60000;
|
|
158
149
|
const interactionsPerMinute = metrics.touchCount / Math.max(0.5, durationMinutes);
|
|
159
|
-
|
|
160
|
-
// Ideal: 10-30 interactions per minute
|
|
161
|
-
// Low (< 5): user passive/confused
|
|
162
|
-
// Very high (> 60): rage tapping
|
|
163
150
|
if (interactionsPerMinute < 2) return 20;
|
|
164
151
|
if (interactionsPerMinute < 5) return 50;
|
|
165
152
|
if (interactionsPerMinute < 10) return 70;
|
|
166
153
|
if (interactionsPerMinute <= 30) return 100;
|
|
167
154
|
if (interactionsPerMinute <= 60) return 80;
|
|
168
|
-
return 50;
|
|
155
|
+
return 50;
|
|
169
156
|
}
|
|
170
157
|
|
|
171
158
|
/**
|
|
@@ -174,14 +161,12 @@ function calculateInteractionScore(durationMs) {
|
|
|
174
161
|
*/
|
|
175
162
|
function calculateExplorationScore() {
|
|
176
163
|
const uniqueScreens = metrics.uniqueScreensCount;
|
|
177
|
-
|
|
178
|
-
// More unique screens = better exploration
|
|
179
164
|
if (uniqueScreens >= 10) return 100;
|
|
180
165
|
if (uniqueScreens >= 7) return 90;
|
|
181
166
|
if (uniqueScreens >= 5) return 80;
|
|
182
167
|
if (uniqueScreens >= 3) return 60;
|
|
183
168
|
if (uniqueScreens >= 2) return 40;
|
|
184
|
-
return 20;
|
|
169
|
+
return 20;
|
|
185
170
|
}
|
|
186
171
|
|
|
187
172
|
/**
|
|
@@ -190,14 +175,8 @@ function calculateExplorationScore() {
|
|
|
190
175
|
*/
|
|
191
176
|
function calculateUXScore() {
|
|
192
177
|
let score = 100;
|
|
193
|
-
|
|
194
|
-
// Deduct for errors
|
|
195
178
|
score -= metrics.errorCount * 10;
|
|
196
|
-
|
|
197
|
-
// Deduct heavily for rage taps
|
|
198
179
|
score -= metrics.rageTapCount * 20;
|
|
199
|
-
|
|
200
|
-
// Deduct for API errors (less severe)
|
|
201
180
|
score -= metrics.apiErrorCount * 5;
|
|
202
181
|
return Math.max(0, Math.min(100, score));
|
|
203
182
|
}
|
|
@@ -26,46 +26,28 @@ exports.normalizeScreenName = normalizeScreenName;
|
|
|
26
26
|
function normalizeScreenName(raw) {
|
|
27
27
|
if (!raw) return 'Unknown';
|
|
28
28
|
let name = raw;
|
|
29
|
-
|
|
30
|
-
// 1. Remove non-printable characters and weird symbols (often from icons)
|
|
31
29
|
name = name.replace(/[^\x20-\x7E\s]/g, '');
|
|
32
|
-
|
|
33
|
-
// 2. Handle hyphens: my-recipes -> My Recipes
|
|
34
30
|
name = name.split(/[-_]/).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
|
|
35
|
-
|
|
36
|
-
// 3. Remove common suffixes
|
|
37
31
|
const suffixes = ['Screen', 'Page', 'View', 'Controller', 'ViewController', 'VC'];
|
|
38
32
|
for (const suffix of suffixes) {
|
|
39
33
|
if (name.endsWith(suffix) && name.length > suffix.length) {
|
|
40
34
|
name = name.slice(0, -suffix.length).trim();
|
|
41
35
|
}
|
|
42
36
|
}
|
|
43
|
-
|
|
44
|
-
// 4. Remove common prefixes (React Native internals)
|
|
45
37
|
const prefixes = ['RNS', 'RCT', 'RN', 'UI'];
|
|
46
38
|
for (const prefix of prefixes) {
|
|
47
39
|
if (name.startsWith(prefix) && name.length > prefix.length + 2) {
|
|
48
40
|
name = name.slice(prefix.length).trim();
|
|
49
41
|
}
|
|
50
42
|
}
|
|
51
|
-
|
|
52
|
-
// 5. Handle dynamic route params: [id] -> (omit), [userId] -> "User"
|
|
53
43
|
name = name.replace(/\[([a-zA-Z]+)(?:Id)?\]/g, (_, param) => {
|
|
54
44
|
const clean = param.replace(/Id$/i, '');
|
|
55
45
|
if (clean.length < 2) return '';
|
|
56
46
|
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
57
47
|
});
|
|
58
|
-
|
|
59
|
-
// 6. Remove leftover brackets
|
|
60
48
|
name = name.replace(/\[\]/g, '');
|
|
61
|
-
|
|
62
|
-
// 7. Convert camelCase/PascalCase to Title Case with spaces
|
|
63
49
|
name = name.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
64
|
-
|
|
65
|
-
// 8. Clean up multiple spaces and trim
|
|
66
50
|
name = name.replace(/\s+/g, ' ').trim();
|
|
67
|
-
|
|
68
|
-
// 9. Capitalize first letter
|
|
69
51
|
if (name.length > 0) {
|
|
70
52
|
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
71
53
|
}
|
|
@@ -80,23 +62,17 @@ function normalizeScreenName(raw) {
|
|
|
80
62
|
* @returns Human-readable screen name
|
|
81
63
|
*/
|
|
82
64
|
function getScreenNameFromPath(pathname, segments) {
|
|
83
|
-
// Use segments for cleaner names if available
|
|
84
65
|
if (segments.length > 0) {
|
|
85
|
-
// Filter out group markers like (tabs), (auth), etc.
|
|
86
66
|
const cleanSegments = segments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
87
67
|
if (cleanSegments.length > 0) {
|
|
88
|
-
// Process each segment
|
|
89
68
|
const processedSegments = cleanSegments.map(s => {
|
|
90
|
-
// Handle dynamic params like [id]
|
|
91
69
|
if (s.startsWith('[') && s.endsWith(']')) {
|
|
92
70
|
const param = s.slice(1, -1);
|
|
93
|
-
// Skip pure ID params, keep meaningful ones
|
|
94
71
|
if (param === 'id' || param === 'slug') return null;
|
|
95
|
-
|
|
72
|
+
if (param === 'id' || param === 'slug') return null;
|
|
96
73
|
const clean = param.replace(/Id$/i, '');
|
|
97
74
|
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
98
75
|
}
|
|
99
|
-
// Regular segment - capitalize
|
|
100
76
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
101
77
|
}).filter(Boolean);
|
|
102
78
|
if (processedSegments.length > 0) {
|
|
@@ -104,30 +80,17 @@ function getScreenNameFromPath(pathname, segments) {
|
|
|
104
80
|
}
|
|
105
81
|
}
|
|
106
82
|
}
|
|
107
|
-
|
|
108
|
-
// Fall back to pathname
|
|
109
83
|
if (!pathname || pathname === '/') {
|
|
110
84
|
return 'Home';
|
|
111
85
|
}
|
|
112
|
-
|
|
113
|
-
// Clean up the path
|
|
114
|
-
let cleanPath = pathname.replace(/^\/(tabs)?/, '') // Remove leading slash and (tabs)
|
|
115
|
-
.replace(/\([^)]+\)/g, '') // Remove all group markers like (settings)
|
|
116
|
-
.replace(/\[([^\]]+)\]/g, (_, param) => {
|
|
117
|
-
// Handle dynamic params in path
|
|
86
|
+
const cleanPath = pathname.replace(/^\/(tabs)?/, '').replace(/\([^)]+\)/g, '').replace(/\[([^\]]+)\]/g, (_, param) => {
|
|
118
87
|
if (param === 'id' || param === 'slug') return '';
|
|
119
88
|
const clean = param.replace(/Id$/i, '');
|
|
120
89
|
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
121
|
-
}).replace(/\/+/g, '/')
|
|
122
|
-
.replace(/^\//, '') // Remove leading slash
|
|
123
|
-
.replace(/\/$/, '') // Remove trailing slash
|
|
124
|
-
.replace(/\//g, ' > ') // Replace slashes with arrows
|
|
125
|
-
.trim();
|
|
90
|
+
}).replace(/\/+/g, '/').replace(/^\//, '').replace(/\/$/, '').replace(/\//g, ' > ').trim();
|
|
126
91
|
if (!cleanPath) {
|
|
127
92
|
return 'Home';
|
|
128
93
|
}
|
|
129
|
-
|
|
130
|
-
// Capitalize first letter of each word
|
|
131
94
|
return cleanPath.split(' > ').map(s => s.charAt(0).toUpperCase() + s.slice(1)).filter(s => s.length > 0).join(' > ') || 'Home';
|
|
132
95
|
}
|
|
133
96
|
|
|
@@ -141,8 +104,6 @@ function getCurrentRouteFromState(state) {
|
|
|
141
104
|
if (!state || !state.routes) return null;
|
|
142
105
|
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
143
106
|
if (!route) return null;
|
|
144
|
-
|
|
145
|
-
// If nested state, recurse
|
|
146
107
|
if (route.state) {
|
|
147
108
|
return getCurrentRouteFromState(route.state);
|
|
148
109
|
}
|
|
@@ -24,41 +24,26 @@ exports.restoreNetworkInterceptor = restoreNetworkInterceptor;
|
|
|
24
24
|
* - PII Scrubbing for query parameters
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
// Store original implementations
|
|
28
27
|
let originalFetch = null;
|
|
29
28
|
let originalXHROpen = null;
|
|
30
29
|
let originalXHRSend = null;
|
|
31
|
-
|
|
32
|
-
// Callback to log network requests (called asynchronously)
|
|
33
30
|
let logCallback = null;
|
|
34
|
-
|
|
35
|
-
// Pending requests buffer (circular buffer for memory efficiency)
|
|
36
31
|
const MAX_PENDING_REQUESTS = 100;
|
|
37
32
|
const pendingRequests = new Array(MAX_PENDING_REQUESTS).fill(null);
|
|
38
33
|
let pendingHead = 0;
|
|
39
34
|
let pendingTail = 0;
|
|
40
35
|
let pendingCount = 0;
|
|
41
|
-
|
|
42
|
-
// Flush timer
|
|
43
36
|
let flushTimer = null;
|
|
44
|
-
const FLUSH_INTERVAL = 500;
|
|
45
|
-
|
|
46
|
-
// Sampling for high-frequency endpoints
|
|
37
|
+
const FLUSH_INTERVAL = 500;
|
|
47
38
|
const endpointCounts = new Map();
|
|
48
|
-
const SAMPLE_WINDOW = 10000;
|
|
49
|
-
const MAX_PER_ENDPOINT = 20;
|
|
50
|
-
|
|
51
|
-
// Configuration
|
|
39
|
+
const SAMPLE_WINDOW = 10000;
|
|
40
|
+
const MAX_PER_ENDPOINT = 20;
|
|
52
41
|
const config = {
|
|
53
42
|
enabled: true,
|
|
54
43
|
ignorePatterns: [],
|
|
55
|
-
// Simple string patterns for fast matching
|
|
56
44
|
maxUrlLength: 300,
|
|
57
|
-
|
|
58
|
-
captureSizes: false // Disabled by default for performance
|
|
45
|
+
captureSizes: false
|
|
59
46
|
};
|
|
60
|
-
|
|
61
|
-
// PII Scrubbing - Sensitive keys to look for in query params
|
|
62
47
|
const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'access_token', 'api_key'];
|
|
63
48
|
|
|
64
49
|
/**
|
|
@@ -66,7 +51,6 @@ const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'access_to
|
|
|
66
51
|
*/
|
|
67
52
|
function scrubUrl(url) {
|
|
68
53
|
try {
|
|
69
|
-
// Fast check if URL might contain params
|
|
70
54
|
if (url.indexOf('?') === -1) return url;
|
|
71
55
|
const urlObj = new URL(url);
|
|
72
56
|
let modified = false;
|
|
@@ -76,13 +60,10 @@ function scrubUrl(url) {
|
|
|
76
60
|
modified = true;
|
|
77
61
|
}
|
|
78
62
|
});
|
|
79
|
-
|
|
80
|
-
// Also scan for partial matches (case-insensitive) if strict scrubbing needed
|
|
81
|
-
// But for performance, we stick to exact keys or common variations
|
|
82
|
-
|
|
83
63
|
return modified ? urlObj.toString() : url;
|
|
84
64
|
} catch {
|
|
85
|
-
//
|
|
65
|
+
// Ignore error, fallback to primitive scrubbing
|
|
66
|
+
|
|
86
67
|
let scrubbed = url;
|
|
87
68
|
SENSITIVE_KEYS.forEach(key => {
|
|
88
69
|
const regex = new RegExp(`([?&])${key}=[^&]*`, 'gi');
|
|
@@ -130,14 +111,10 @@ function queueRequest(request) {
|
|
|
130
111
|
pendingHead = (pendingHead + 1) % MAX_PENDING_REQUESTS;
|
|
131
112
|
pendingCount--;
|
|
132
113
|
}
|
|
133
|
-
|
|
134
|
-
// Scrub URL before queuing
|
|
135
114
|
request.url = scrubUrl(request.url);
|
|
136
115
|
pendingRequests[pendingTail] = request;
|
|
137
116
|
pendingTail = (pendingTail + 1) % MAX_PENDING_REQUESTS;
|
|
138
117
|
pendingCount++;
|
|
139
|
-
|
|
140
|
-
// Schedule flush if not already scheduled
|
|
141
118
|
if (!flushTimer) {
|
|
142
119
|
flushTimer = setTimeout(flushPendingRequests, FLUSH_INTERVAL);
|
|
143
120
|
}
|
|
@@ -149,8 +126,6 @@ function queueRequest(request) {
|
|
|
149
126
|
function flushPendingRequests() {
|
|
150
127
|
flushTimer = null;
|
|
151
128
|
if (!logCallback || pendingCount === 0) return;
|
|
152
|
-
|
|
153
|
-
// Process all pending requests
|
|
154
129
|
while (pendingCount > 0) {
|
|
155
130
|
const request = pendingRequests[pendingHead];
|
|
156
131
|
pendingRequests[pendingHead] = null; // Allow GC
|
|
@@ -160,7 +135,7 @@ function flushPendingRequests() {
|
|
|
160
135
|
try {
|
|
161
136
|
logCallback(request);
|
|
162
137
|
} catch {
|
|
163
|
-
// Ignore
|
|
138
|
+
// Ignore
|
|
164
139
|
}
|
|
165
140
|
}
|
|
166
141
|
}
|
|
@@ -173,12 +148,9 @@ function parseUrlFast(url) {
|
|
|
173
148
|
// Fast path for common patterns
|
|
174
149
|
let hostEnd = -1;
|
|
175
150
|
let pathStart = -1;
|
|
176
|
-
|
|
177
|
-
// Find ://
|
|
178
151
|
const protoEnd = url.indexOf('://');
|
|
179
152
|
if (protoEnd !== -1) {
|
|
180
153
|
const afterProto = protoEnd + 3;
|
|
181
|
-
// Find end of host (first / after ://)
|
|
182
154
|
const slashPos = url.indexOf('/', afterProto);
|
|
183
155
|
if (slashPos !== -1) {
|
|
184
156
|
hostEnd = slashPos;
|
|
@@ -192,8 +164,6 @@ function parseUrlFast(url) {
|
|
|
192
164
|
path: pathStart < url.length ? url.substring(pathStart) : '/'
|
|
193
165
|
};
|
|
194
166
|
}
|
|
195
|
-
|
|
196
|
-
// Relative URL
|
|
197
167
|
return {
|
|
198
168
|
host: '',
|
|
199
169
|
path: url
|
|
@@ -208,15 +178,10 @@ function interceptFetch() {
|
|
|
208
178
|
if (originalFetch) return;
|
|
209
179
|
originalFetch = globalThis.fetch;
|
|
210
180
|
globalThis.fetch = function optimizedFetch(input, init) {
|
|
211
|
-
// Fast path: if disabled or no callback, skip entirely
|
|
212
181
|
if (!config.enabled || !logCallback) {
|
|
213
182
|
return originalFetch(input, init);
|
|
214
183
|
}
|
|
215
|
-
|
|
216
|
-
// Extract URL string (minimal work)
|
|
217
184
|
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
218
|
-
|
|
219
|
-
// Fast ignore check
|
|
220
185
|
if (shouldIgnoreUrl(url)) {
|
|
221
186
|
return originalFetch(input, init);
|
|
222
187
|
}
|
|
@@ -228,14 +193,9 @@ function interceptFetch() {
|
|
|
228
193
|
if (!shouldSampleRequest(path)) {
|
|
229
194
|
return originalFetch(input, init);
|
|
230
195
|
}
|
|
231
|
-
|
|
232
|
-
// Capture start time (only synchronous work)
|
|
233
196
|
const startTime = Date.now();
|
|
234
197
|
const method = (init?.method || 'GET').toUpperCase();
|
|
235
|
-
|
|
236
|
-
// Call original fetch
|
|
237
198
|
return originalFetch(input, init).then(response => {
|
|
238
|
-
// Success - queue the log asynchronously
|
|
239
199
|
queueRequest({
|
|
240
200
|
requestId: `f${startTime}`,
|
|
241
201
|
method,
|
|
@@ -248,7 +208,6 @@ function interceptFetch() {
|
|
|
248
208
|
});
|
|
249
209
|
return response;
|
|
250
210
|
}, error => {
|
|
251
|
-
// Error - queue the log asynchronously
|
|
252
211
|
queueRequest({
|
|
253
212
|
requestId: `f${startTime}`,
|
|
254
213
|
method,
|
|
@@ -275,8 +234,6 @@ function interceptXHR() {
|
|
|
275
234
|
originalXHRSend = XMLHttpRequest.prototype.send;
|
|
276
235
|
XMLHttpRequest.prototype.open = function (method, url, async = true, username, password) {
|
|
277
236
|
const urlString = typeof url === 'string' ? url : url.toString();
|
|
278
|
-
|
|
279
|
-
// Store minimal info
|
|
280
237
|
this.__rj = {
|
|
281
238
|
m: method.toUpperCase(),
|
|
282
239
|
u: urlString,
|
|
@@ -289,8 +246,6 @@ function interceptXHR() {
|
|
|
289
246
|
if (!config.enabled || !logCallback || !data || shouldIgnoreUrl(data.u)) {
|
|
290
247
|
return originalXHRSend.call(this, body);
|
|
291
248
|
}
|
|
292
|
-
|
|
293
|
-
// Check sampling
|
|
294
249
|
const {
|
|
295
250
|
path
|
|
296
251
|
} = parseUrlFast(data.u);
|
|
@@ -312,9 +267,6 @@ function interceptXHR() {
|
|
|
312
267
|
errorMessage: this.status === 0 ? 'Network error' : undefined
|
|
313
268
|
});
|
|
314
269
|
};
|
|
315
|
-
|
|
316
|
-
// Use load/error events (more efficient than readystatechange)
|
|
317
|
-
// Note: We use basic addEventListener without options for RN compatibility
|
|
318
270
|
this.addEventListener('load', onComplete);
|
|
319
271
|
this.addEventListener('error', onComplete);
|
|
320
272
|
this.addEventListener('abort', onComplete);
|
|
@@ -327,11 +279,8 @@ function interceptXHR() {
|
|
|
327
279
|
*/
|
|
328
280
|
function initNetworkInterceptor(callback, options) {
|
|
329
281
|
logCallback = callback;
|
|
330
|
-
|
|
331
|
-
// Convert patterns to simple strings for fast matching
|
|
332
282
|
if (options?.ignoreUrls) {
|
|
333
283
|
config.ignorePatterns = options.ignoreUrls.filter(p => typeof p === 'string');
|
|
334
|
-
// Note: RegExp patterns are not supported in optimized version for performance
|
|
335
284
|
}
|
|
336
285
|
if (options?.captureSizes !== undefined) {
|
|
337
286
|
config.captureSizes = options.captureSizes;
|
|
@@ -345,8 +294,6 @@ function initNetworkInterceptor(callback, options) {
|
|
|
345
294
|
*/
|
|
346
295
|
function disableNetworkInterceptor() {
|
|
347
296
|
config.enabled = false;
|
|
348
|
-
|
|
349
|
-
// Flush any pending requests
|
|
350
297
|
if (flushTimer) {
|
|
351
298
|
clearTimeout(flushTimer);
|
|
352
299
|
flushTimer = null;
|