@rejourneyco/react-native 1.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/android/build.gradle.kts +135 -0
- package/android/consumer-rules.pro +10 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
- package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
- package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
- package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
- package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
- package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
- package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
- package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
- package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
- package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
- package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
- package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
- package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
- package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
- package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
- package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
- package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
- package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Capture/RJANRHandler.h +42 -0
- package/ios/Capture/RJANRHandler.m +328 -0
- package/ios/Capture/RJCaptureEngine.h +275 -0
- package/ios/Capture/RJCaptureEngine.m +2062 -0
- package/ios/Capture/RJCaptureHeuristics.h +80 -0
- package/ios/Capture/RJCaptureHeuristics.m +903 -0
- package/ios/Capture/RJCrashHandler.h +46 -0
- package/ios/Capture/RJCrashHandler.m +313 -0
- package/ios/Capture/RJMotionEvent.h +183 -0
- package/ios/Capture/RJMotionEvent.m +183 -0
- package/ios/Capture/RJPerformanceManager.h +100 -0
- package/ios/Capture/RJPerformanceManager.m +373 -0
- package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
- package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
- package/ios/Capture/RJSegmentUploader.h +146 -0
- package/ios/Capture/RJSegmentUploader.m +778 -0
- package/ios/Capture/RJVideoEncoder.h +247 -0
- package/ios/Capture/RJVideoEncoder.m +1036 -0
- package/ios/Capture/RJViewControllerTracker.h +73 -0
- package/ios/Capture/RJViewControllerTracker.m +508 -0
- package/ios/Capture/RJViewHierarchyScanner.h +215 -0
- package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
- package/ios/Capture/RJViewSerializer.h +119 -0
- package/ios/Capture/RJViewSerializer.m +498 -0
- package/ios/Core/RJConstants.h +124 -0
- package/ios/Core/RJConstants.m +88 -0
- package/ios/Core/RJLifecycleManager.h +85 -0
- package/ios/Core/RJLifecycleManager.m +308 -0
- package/ios/Core/RJLogger.h +61 -0
- package/ios/Core/RJLogger.m +211 -0
- package/ios/Core/RJTypes.h +176 -0
- package/ios/Core/RJTypes.m +66 -0
- package/ios/Core/Rejourney.h +64 -0
- package/ios/Core/Rejourney.mm +2495 -0
- package/ios/Network/RJDeviceAuthManager.h +94 -0
- package/ios/Network/RJDeviceAuthManager.m +967 -0
- package/ios/Network/RJNetworkMonitor.h +68 -0
- package/ios/Network/RJNetworkMonitor.m +267 -0
- package/ios/Network/RJRetryManager.h +73 -0
- package/ios/Network/RJRetryManager.m +325 -0
- package/ios/Network/RJUploadManager.h +267 -0
- package/ios/Network/RJUploadManager.m +2296 -0
- package/ios/Privacy/RJPrivacyMask.h +163 -0
- package/ios/Privacy/RJPrivacyMask.m +922 -0
- package/ios/Rejourney.h +63 -0
- package/ios/Touch/RJGestureClassifier.h +130 -0
- package/ios/Touch/RJGestureClassifier.m +333 -0
- package/ios/Touch/RJTouchInterceptor.h +169 -0
- package/ios/Touch/RJTouchInterceptor.m +772 -0
- package/ios/Utils/RJEventBuffer.h +112 -0
- package/ios/Utils/RJEventBuffer.m +358 -0
- package/ios/Utils/RJGzipUtils.h +33 -0
- package/ios/Utils/RJGzipUtils.m +89 -0
- package/ios/Utils/RJKeychainManager.h +48 -0
- package/ios/Utils/RJKeychainManager.m +111 -0
- package/ios/Utils/RJPerfTiming.h +209 -0
- package/ios/Utils/RJPerfTiming.m +264 -0
- package/ios/Utils/RJTelemetry.h +92 -0
- package/ios/Utils/RJTelemetry.m +320 -0
- package/ios/Utils/RJWindowUtils.h +66 -0
- package/ios/Utils/RJWindowUtils.m +133 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +79 -0
- package/lib/commonjs/index.js +1381 -0
- package/lib/commonjs/sdk/autoTracking.js +1259 -0
- package/lib/commonjs/sdk/constants.js +151 -0
- package/lib/commonjs/sdk/errorTracking.js +199 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +204 -0
- package/lib/commonjs/sdk/navigation.js +151 -0
- package/lib/commonjs/sdk/networkInterceptor.js +412 -0
- package/lib/commonjs/sdk/utils.js +363 -0
- package/lib/commonjs/types/expo-router.d.js +2 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/module/NativeRejourney.js +38 -0
- package/lib/module/components/Mask.js +72 -0
- package/lib/module/index.js +1284 -0
- package/lib/module/sdk/autoTracking.js +1233 -0
- package/lib/module/sdk/constants.js +145 -0
- package/lib/module/sdk/errorTracking.js +189 -0
- package/lib/module/sdk/index.js +12 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +143 -0
- package/lib/module/sdk/networkInterceptor.js +401 -0
- package/lib/module/sdk/utils.js +342 -0
- package/lib/module/types/expo-router.d.js +2 -0
- package/lib/module/types/index.js +2 -0
- package/lib/typescript/NativeRejourney.d.ts +147 -0
- package/lib/typescript/components/Mask.d.ts +39 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +204 -0
- package/lib/typescript/sdk/constants.d.ts +120 -0
- package/lib/typescript/sdk/errorTracking.d.ts +32 -0
- package/lib/typescript/sdk/index.d.ts +9 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
- package/lib/typescript/sdk/navigation.d.ts +33 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
- package/lib/typescript/sdk/utils.d.ts +148 -0
- package/lib/typescript/types/index.d.ts +624 -0
- package/package.json +102 -0
- package/rejourney.podspec +21 -0
- package/src/NativeRejourney.ts +165 -0
- package/src/components/Mask.tsx +80 -0
- package/src/index.ts +1459 -0
- package/src/sdk/autoTracking.ts +1373 -0
- package/src/sdk/constants.ts +134 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/metricsTracking.ts +232 -0
- package/src/sdk/navigation.ts +157 -0
- package/src/sdk/networkInterceptor.ts +440 -0
- package/src/sdk/utils.ts +369 -0
- package/src/types/expo-router.d.ts +7 -0
- package/src/types/index.ts +739 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rejourney Navigation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for extracting human-readable screen names from
|
|
5
|
+
* React Navigation / Expo Router state.
|
|
6
|
+
*
|
|
7
|
+
* These functions are used internally by the SDK's automatic navigation
|
|
8
|
+
* detection. You don't need to import or use these directly.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a screen name to be human-readable
|
|
13
|
+
* Handles common patterns from React Native / Expo Router
|
|
14
|
+
*
|
|
15
|
+
* @param raw - Raw screen name to normalize
|
|
16
|
+
* @returns Cleaned, human-readable screen name
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeScreenName(raw: string): string {
|
|
19
|
+
if (!raw) return 'Unknown';
|
|
20
|
+
|
|
21
|
+
let name = raw;
|
|
22
|
+
|
|
23
|
+
// 1. Remove non-printable characters and weird symbols (often from icons)
|
|
24
|
+
name = name.replace(/[^\x20-\x7E\s]/g, '');
|
|
25
|
+
|
|
26
|
+
// 2. Handle hyphens: my-recipes -> My Recipes
|
|
27
|
+
name = name.split(/[-_]/).map(word =>
|
|
28
|
+
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
|
29
|
+
).join(' ');
|
|
30
|
+
|
|
31
|
+
// 3. Remove common suffixes
|
|
32
|
+
const suffixes = ['Screen', 'Page', 'View', 'Controller', 'ViewController', 'VC'];
|
|
33
|
+
for (const suffix of suffixes) {
|
|
34
|
+
if (name.endsWith(suffix) && name.length > suffix.length) {
|
|
35
|
+
name = name.slice(0, -suffix.length).trim();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 4. Remove common prefixes (React Native internals)
|
|
40
|
+
const prefixes = ['RNS', 'RCT', 'RN', 'UI'];
|
|
41
|
+
for (const prefix of prefixes) {
|
|
42
|
+
if (name.startsWith(prefix) && name.length > prefix.length + 2) {
|
|
43
|
+
name = name.slice(prefix.length).trim();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 5. Handle dynamic route params: [id] -> (omit), [userId] -> "User"
|
|
48
|
+
name = name.replace(/\[([a-zA-Z]+)(?:Id)?\]/g, (_, param) => {
|
|
49
|
+
const clean = param.replace(/Id$/i, '');
|
|
50
|
+
if (clean.length < 2) return '';
|
|
51
|
+
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// 6. Remove leftover brackets
|
|
55
|
+
name = name.replace(/\[\]/g, '');
|
|
56
|
+
|
|
57
|
+
// 7. Convert camelCase/PascalCase to Title Case with spaces
|
|
58
|
+
name = name.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
59
|
+
|
|
60
|
+
// 8. Clean up multiple spaces and trim
|
|
61
|
+
name = name.replace(/\s+/g, ' ').trim();
|
|
62
|
+
|
|
63
|
+
// 9. Capitalize first letter
|
|
64
|
+
if (name.length > 0) {
|
|
65
|
+
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return name || 'Unknown';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get a human-readable screen name from Expo Router path and segments
|
|
73
|
+
*
|
|
74
|
+
* @param pathname - The current route pathname
|
|
75
|
+
* @param segments - Route segments from useSegments()
|
|
76
|
+
* @returns Human-readable screen name
|
|
77
|
+
*/
|
|
78
|
+
export function getScreenNameFromPath(pathname: string, segments: string[]): string {
|
|
79
|
+
// Use segments for cleaner names if available
|
|
80
|
+
if (segments.length > 0) {
|
|
81
|
+
// Filter out group markers like (tabs), (auth), etc.
|
|
82
|
+
const cleanSegments = segments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
83
|
+
|
|
84
|
+
if (cleanSegments.length > 0) {
|
|
85
|
+
// Process each segment
|
|
86
|
+
const processedSegments = cleanSegments.map(s => {
|
|
87
|
+
// Handle dynamic params like [id]
|
|
88
|
+
if (s.startsWith('[') && s.endsWith(']')) {
|
|
89
|
+
const param = s.slice(1, -1);
|
|
90
|
+
// Skip pure ID params, keep meaningful ones
|
|
91
|
+
if (param === 'id' || param === 'slug') return null;
|
|
92
|
+
// Extract meaningful part: userId -> User
|
|
93
|
+
const clean = param.replace(/Id$/i, '');
|
|
94
|
+
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
95
|
+
}
|
|
96
|
+
// Regular segment - capitalize
|
|
97
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
98
|
+
}).filter(Boolean);
|
|
99
|
+
|
|
100
|
+
if (processedSegments.length > 0) {
|
|
101
|
+
return processedSegments.join(' > ');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Fall back to pathname
|
|
107
|
+
if (!pathname || pathname === '/') {
|
|
108
|
+
return 'Home';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Clean up the path
|
|
112
|
+
let cleanPath = pathname
|
|
113
|
+
.replace(/^\/(tabs)?/, '') // Remove leading slash and (tabs)
|
|
114
|
+
.replace(/\([^)]+\)/g, '') // Remove all group markers like (settings)
|
|
115
|
+
.replace(/\[([^\]]+)\]/g, (_, param) => {
|
|
116
|
+
// Handle dynamic params in path
|
|
117
|
+
if (param === 'id' || param === 'slug') return '';
|
|
118
|
+
const clean = param.replace(/Id$/i, '');
|
|
119
|
+
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
120
|
+
})
|
|
121
|
+
.replace(/\/+/g, '/') // Collapse multiple slashes
|
|
122
|
+
.replace(/^\//, '') // Remove leading slash
|
|
123
|
+
.replace(/\/$/, '') // Remove trailing slash
|
|
124
|
+
.replace(/\//g, ' > ') // Replace slashes with arrows
|
|
125
|
+
.trim();
|
|
126
|
+
|
|
127
|
+
if (!cleanPath) {
|
|
128
|
+
return 'Home';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Capitalize first letter of each word
|
|
132
|
+
return cleanPath
|
|
133
|
+
.split(' > ')
|
|
134
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
135
|
+
.filter(s => s.length > 0)
|
|
136
|
+
.join(' > ') || 'Home';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get the current route name from a navigation state object
|
|
141
|
+
*
|
|
142
|
+
* @param state - React Navigation state object
|
|
143
|
+
* @returns Current route name or null
|
|
144
|
+
*/
|
|
145
|
+
export function getCurrentRouteFromState(state: any): string | null {
|
|
146
|
+
if (!state || !state.routes) return null;
|
|
147
|
+
|
|
148
|
+
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
149
|
+
if (!route) return null;
|
|
150
|
+
|
|
151
|
+
// If nested state, recurse
|
|
152
|
+
if (route.state) {
|
|
153
|
+
return getCurrentRouteFromState(route.state);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return route.name || null;
|
|
157
|
+
}
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network Interceptor for Rejourney - Optimized Version
|
|
3
|
+
*
|
|
4
|
+
* Automatically intercepts fetch() and XMLHttpRequest to log API calls.
|
|
5
|
+
*
|
|
6
|
+
* PERFORMANCE OPTIMIZATIONS:
|
|
7
|
+
* - Minimal synchronous overhead (just captures timing, no processing)
|
|
8
|
+
* - Batched async logging (doesn't block requests)
|
|
9
|
+
* - Circular buffer with max size limit
|
|
10
|
+
* - Sampling for high-frequency endpoints
|
|
11
|
+
* - No string allocations in hot path
|
|
12
|
+
* - Lazy URL parsing
|
|
13
|
+
* - PII Scrubbing for query parameters
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { NetworkRequestParams } from '../types';
|
|
17
|
+
|
|
18
|
+
// Store original implementations
|
|
19
|
+
let originalFetch: typeof fetch | null = null;
|
|
20
|
+
let originalXHROpen: typeof XMLHttpRequest.prototype.open | null = null;
|
|
21
|
+
let originalXHRSend: typeof XMLHttpRequest.prototype.send | null = null;
|
|
22
|
+
|
|
23
|
+
// Callback to log network requests (called asynchronously)
|
|
24
|
+
let logCallback: ((request: NetworkRequestParams) => void) | null = null;
|
|
25
|
+
|
|
26
|
+
// Pending requests buffer (circular buffer for memory efficiency)
|
|
27
|
+
const MAX_PENDING_REQUESTS = 100;
|
|
28
|
+
const pendingRequests: (NetworkRequestParams | null)[] = new Array(MAX_PENDING_REQUESTS).fill(null);
|
|
29
|
+
let pendingHead = 0;
|
|
30
|
+
let pendingTail = 0;
|
|
31
|
+
let pendingCount = 0;
|
|
32
|
+
|
|
33
|
+
// Flush timer
|
|
34
|
+
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
35
|
+
const FLUSH_INTERVAL = 500; // Flush every 500ms
|
|
36
|
+
|
|
37
|
+
// Sampling for high-frequency endpoints
|
|
38
|
+
const endpointCounts = new Map<string, { count: number; lastReset: number }>();
|
|
39
|
+
const SAMPLE_WINDOW = 10000; // 10 second window
|
|
40
|
+
const MAX_PER_ENDPOINT = 20; // Max 20 requests per endpoint per window
|
|
41
|
+
|
|
42
|
+
// Configuration
|
|
43
|
+
const config = {
|
|
44
|
+
enabled: true,
|
|
45
|
+
ignorePatterns: [] as string[], // Simple string patterns for fast matching
|
|
46
|
+
maxUrlLength: 300, // Shorter for efficiency
|
|
47
|
+
captureSizes: false, // Disabled by default for performance
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// PII Scrubbing - Sensitive keys to look for in query params
|
|
51
|
+
const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'access_token', 'api_key'];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Scrub sensitive data from URL
|
|
55
|
+
*/
|
|
56
|
+
function scrubUrl(url: string): string {
|
|
57
|
+
try {
|
|
58
|
+
// Fast check if URL might contain params
|
|
59
|
+
if (url.indexOf('?') === -1) return url;
|
|
60
|
+
|
|
61
|
+
const urlObj = new URL(url);
|
|
62
|
+
let modified = false;
|
|
63
|
+
|
|
64
|
+
SENSITIVE_KEYS.forEach(key => {
|
|
65
|
+
if (urlObj.searchParams.has(key)) {
|
|
66
|
+
urlObj.searchParams.set(key, '[REDACTED]');
|
|
67
|
+
modified = true;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Also scan for partial matches (case-insensitive) if strict scrubbing needed
|
|
72
|
+
// But for performance, we stick to exact keys or common variations
|
|
73
|
+
|
|
74
|
+
return modified ? urlObj.toString() : url;
|
|
75
|
+
} catch {
|
|
76
|
+
// If URL parsing fails (relative URL?), try primitive replacement
|
|
77
|
+
let scrubbed = url;
|
|
78
|
+
SENSITIVE_KEYS.forEach(key => {
|
|
79
|
+
const regex = new RegExp(`([?&])${key}=[^&]*`, 'gi');
|
|
80
|
+
scrubbed = scrubbed.replace(regex, `$1${key}=[REDACTED]`);
|
|
81
|
+
});
|
|
82
|
+
return scrubbed;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Fast check if URL should be ignored (no regex for speed)
|
|
88
|
+
*/
|
|
89
|
+
function shouldIgnoreUrl(url: string): boolean {
|
|
90
|
+
const patterns = config.ignorePatterns;
|
|
91
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
92
|
+
const pattern = patterns[i];
|
|
93
|
+
if (pattern && url.indexOf(pattern) !== -1) return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if we should sample this request (rate limiting per endpoint)
|
|
100
|
+
*/
|
|
101
|
+
function shouldSampleRequest(urlPath: string): boolean {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
let entry = endpointCounts.get(urlPath);
|
|
104
|
+
|
|
105
|
+
if (!entry || now - entry.lastReset > SAMPLE_WINDOW) {
|
|
106
|
+
entry = { count: 0, lastReset: now };
|
|
107
|
+
endpointCounts.set(urlPath, entry);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
entry.count++;
|
|
111
|
+
return entry.count <= MAX_PER_ENDPOINT;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Add request to pending buffer (non-blocking)
|
|
116
|
+
*/
|
|
117
|
+
function queueRequest(request: NetworkRequestParams): void {
|
|
118
|
+
if (pendingCount >= MAX_PENDING_REQUESTS) {
|
|
119
|
+
// Buffer full, drop oldest
|
|
120
|
+
pendingHead = (pendingHead + 1) % MAX_PENDING_REQUESTS;
|
|
121
|
+
pendingCount--;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Scrub URL before queuing
|
|
125
|
+
request.url = scrubUrl(request.url);
|
|
126
|
+
|
|
127
|
+
pendingRequests[pendingTail] = request;
|
|
128
|
+
pendingTail = (pendingTail + 1) % MAX_PENDING_REQUESTS;
|
|
129
|
+
pendingCount++;
|
|
130
|
+
|
|
131
|
+
// Schedule flush if not already scheduled
|
|
132
|
+
if (!flushTimer) {
|
|
133
|
+
flushTimer = setTimeout(flushPendingRequests, FLUSH_INTERVAL);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Flush pending requests to callback
|
|
139
|
+
*/
|
|
140
|
+
function flushPendingRequests(): void {
|
|
141
|
+
flushTimer = null;
|
|
142
|
+
|
|
143
|
+
if (!logCallback || pendingCount === 0) return;
|
|
144
|
+
|
|
145
|
+
// Process all pending requests
|
|
146
|
+
while (pendingCount > 0) {
|
|
147
|
+
const request = pendingRequests[pendingHead];
|
|
148
|
+
pendingRequests[pendingHead] = null; // Allow GC
|
|
149
|
+
pendingHead = (pendingHead + 1) % MAX_PENDING_REQUESTS;
|
|
150
|
+
pendingCount--;
|
|
151
|
+
|
|
152
|
+
if (request) {
|
|
153
|
+
try {
|
|
154
|
+
logCallback(request);
|
|
155
|
+
} catch {
|
|
156
|
+
// Ignore logging errors
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Parse URL efficiently (only extract what we need)
|
|
164
|
+
*/
|
|
165
|
+
function parseUrlFast(url: string): { host: string; path: string } {
|
|
166
|
+
// Fast path for common patterns
|
|
167
|
+
let hostEnd = -1;
|
|
168
|
+
let pathStart = -1;
|
|
169
|
+
|
|
170
|
+
// Find ://
|
|
171
|
+
const protoEnd = url.indexOf('://');
|
|
172
|
+
if (protoEnd !== -1) {
|
|
173
|
+
const afterProto = protoEnd + 3;
|
|
174
|
+
// Find end of host (first / after ://)
|
|
175
|
+
const slashPos = url.indexOf('/', afterProto);
|
|
176
|
+
if (slashPos !== -1) {
|
|
177
|
+
hostEnd = slashPos;
|
|
178
|
+
pathStart = slashPos;
|
|
179
|
+
} else {
|
|
180
|
+
hostEnd = url.length;
|
|
181
|
+
pathStart = url.length;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
host: url.substring(afterProto, hostEnd),
|
|
186
|
+
path: pathStart < url.length ? url.substring(pathStart) : '/',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Relative URL
|
|
191
|
+
return { host: '', path: url };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Intercept fetch - minimal overhead version
|
|
196
|
+
*/
|
|
197
|
+
function interceptFetch(): void {
|
|
198
|
+
if (typeof globalThis.fetch === 'undefined') return;
|
|
199
|
+
if (originalFetch) return;
|
|
200
|
+
|
|
201
|
+
originalFetch = globalThis.fetch;
|
|
202
|
+
|
|
203
|
+
globalThis.fetch = function optimizedFetch(
|
|
204
|
+
input: RequestInfo | URL,
|
|
205
|
+
init?: RequestInit
|
|
206
|
+
): Promise<Response> {
|
|
207
|
+
// Fast path: if disabled or no callback, skip entirely
|
|
208
|
+
if (!config.enabled || !logCallback) {
|
|
209
|
+
return originalFetch!(input, init);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Extract URL string (minimal work)
|
|
213
|
+
const url = typeof input === 'string'
|
|
214
|
+
? input
|
|
215
|
+
: input instanceof URL
|
|
216
|
+
? input.href
|
|
217
|
+
: (input as Request).url;
|
|
218
|
+
|
|
219
|
+
// Fast ignore check
|
|
220
|
+
if (shouldIgnoreUrl(url)) {
|
|
221
|
+
return originalFetch!(input, init);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Parse URL and check sampling
|
|
225
|
+
const { path } = parseUrlFast(url);
|
|
226
|
+
if (!shouldSampleRequest(path)) {
|
|
227
|
+
return originalFetch!(input, init);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Capture start time (only synchronous work)
|
|
231
|
+
const startTime = Date.now();
|
|
232
|
+
const method = ((init?.method || 'GET').toUpperCase()) as NetworkRequestParams['method'];
|
|
233
|
+
|
|
234
|
+
// Call original fetch
|
|
235
|
+
return originalFetch!(input, init).then(
|
|
236
|
+
(response) => {
|
|
237
|
+
// Success - queue the log asynchronously
|
|
238
|
+
queueRequest({
|
|
239
|
+
requestId: `f${startTime}`,
|
|
240
|
+
method,
|
|
241
|
+
url: url.length > config.maxUrlLength ? url.substring(0, config.maxUrlLength) : url,
|
|
242
|
+
statusCode: response.status,
|
|
243
|
+
duration: Date.now() - startTime,
|
|
244
|
+
startTimestamp: startTime,
|
|
245
|
+
endTimestamp: Date.now(),
|
|
246
|
+
success: response.ok,
|
|
247
|
+
});
|
|
248
|
+
return response;
|
|
249
|
+
},
|
|
250
|
+
(error) => {
|
|
251
|
+
// Error - queue the log asynchronously
|
|
252
|
+
queueRequest({
|
|
253
|
+
requestId: `f${startTime}`,
|
|
254
|
+
method,
|
|
255
|
+
url: url.length > config.maxUrlLength ? url.substring(0, config.maxUrlLength) : url,
|
|
256
|
+
statusCode: 0,
|
|
257
|
+
duration: Date.now() - startTime,
|
|
258
|
+
startTimestamp: startTime,
|
|
259
|
+
endTimestamp: Date.now(),
|
|
260
|
+
success: false,
|
|
261
|
+
errorMessage: error?.message || 'Network error',
|
|
262
|
+
});
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Intercept XMLHttpRequest - minimal overhead version
|
|
271
|
+
*/
|
|
272
|
+
function interceptXHR(): void {
|
|
273
|
+
if (typeof XMLHttpRequest === 'undefined') return;
|
|
274
|
+
if (originalXHROpen) return;
|
|
275
|
+
|
|
276
|
+
originalXHROpen = XMLHttpRequest.prototype.open;
|
|
277
|
+
originalXHRSend = XMLHttpRequest.prototype.send;
|
|
278
|
+
|
|
279
|
+
XMLHttpRequest.prototype.open = function (
|
|
280
|
+
method: string,
|
|
281
|
+
url: string | URL,
|
|
282
|
+
async: boolean = true,
|
|
283
|
+
username?: string | null,
|
|
284
|
+
password?: string | null
|
|
285
|
+
): void {
|
|
286
|
+
const urlString = typeof url === 'string' ? url : url.toString();
|
|
287
|
+
|
|
288
|
+
// Store minimal info
|
|
289
|
+
(this as any).__rj = {
|
|
290
|
+
m: method.toUpperCase(),
|
|
291
|
+
u: urlString,
|
|
292
|
+
t: 0,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
return originalXHROpen!.call(this, method, urlString, async, username, password);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
XMLHttpRequest.prototype.send = function (body?: any): void {
|
|
299
|
+
const data = (this as any).__rj;
|
|
300
|
+
|
|
301
|
+
if (!config.enabled || !logCallback || !data || shouldIgnoreUrl(data.u)) {
|
|
302
|
+
return originalXHRSend!.call(this, body);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check sampling
|
|
306
|
+
const { path } = parseUrlFast(data.u);
|
|
307
|
+
if (!shouldSampleRequest(path)) {
|
|
308
|
+
return originalXHRSend!.call(this, body);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
data.t = Date.now();
|
|
312
|
+
|
|
313
|
+
const onComplete = () => {
|
|
314
|
+
const endTime = Date.now();
|
|
315
|
+
queueRequest({
|
|
316
|
+
requestId: `x${data.t}`,
|
|
317
|
+
method: data.m as NetworkRequestParams['method'],
|
|
318
|
+
url: data.u.length > config.maxUrlLength ? data.u.substring(0, config.maxUrlLength) : data.u,
|
|
319
|
+
statusCode: this.status,
|
|
320
|
+
duration: endTime - data.t,
|
|
321
|
+
startTimestamp: data.t,
|
|
322
|
+
endTimestamp: endTime,
|
|
323
|
+
success: this.status >= 200 && this.status < 400,
|
|
324
|
+
errorMessage: this.status === 0 ? 'Network error' : undefined,
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Use load/error events (more efficient than readystatechange)
|
|
329
|
+
// Note: We use basic addEventListener without options for RN compatibility
|
|
330
|
+
this.addEventListener('load', onComplete);
|
|
331
|
+
this.addEventListener('error', onComplete);
|
|
332
|
+
this.addEventListener('abort', onComplete);
|
|
333
|
+
|
|
334
|
+
return originalXHRSend!.call(this, body);
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Initialize network interception
|
|
340
|
+
*/
|
|
341
|
+
export function initNetworkInterceptor(
|
|
342
|
+
callback: (request: NetworkRequestParams) => void,
|
|
343
|
+
options?: {
|
|
344
|
+
ignoreUrls?: (string | RegExp)[];
|
|
345
|
+
captureSizes?: boolean;
|
|
346
|
+
}
|
|
347
|
+
): void {
|
|
348
|
+
logCallback = callback;
|
|
349
|
+
|
|
350
|
+
// Convert patterns to simple strings for fast matching
|
|
351
|
+
if (options?.ignoreUrls) {
|
|
352
|
+
config.ignorePatterns = options.ignoreUrls
|
|
353
|
+
.filter((p): p is string => typeof p === 'string');
|
|
354
|
+
// Note: RegExp patterns are not supported in optimized version for performance
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (options?.captureSizes !== undefined) {
|
|
358
|
+
config.captureSizes = options.captureSizes;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
interceptFetch();
|
|
362
|
+
interceptXHR();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Disable network interception
|
|
367
|
+
*/
|
|
368
|
+
export function disableNetworkInterceptor(): void {
|
|
369
|
+
config.enabled = false;
|
|
370
|
+
|
|
371
|
+
// Flush any pending requests
|
|
372
|
+
if (flushTimer) {
|
|
373
|
+
clearTimeout(flushTimer);
|
|
374
|
+
flushTimer = null;
|
|
375
|
+
}
|
|
376
|
+
flushPendingRequests();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Re-enable network interception
|
|
381
|
+
*/
|
|
382
|
+
export function enableNetworkInterceptor(): void {
|
|
383
|
+
config.enabled = true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Force flush pending requests (call before app termination)
|
|
388
|
+
*/
|
|
389
|
+
export function flushNetworkRequests(): void {
|
|
390
|
+
if (flushTimer) {
|
|
391
|
+
clearTimeout(flushTimer);
|
|
392
|
+
flushTimer = null;
|
|
393
|
+
}
|
|
394
|
+
flushPendingRequests();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Restore original fetch and XHR
|
|
399
|
+
*/
|
|
400
|
+
export function restoreNetworkInterceptor(): void {
|
|
401
|
+
if (originalFetch) {
|
|
402
|
+
globalThis.fetch = originalFetch;
|
|
403
|
+
originalFetch = null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (originalXHROpen && originalXHRSend) {
|
|
407
|
+
XMLHttpRequest.prototype.open = originalXHROpen;
|
|
408
|
+
XMLHttpRequest.prototype.send = originalXHRSend;
|
|
409
|
+
originalXHROpen = null;
|
|
410
|
+
originalXHRSend = null;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
logCallback = null;
|
|
414
|
+
|
|
415
|
+
// Clear state
|
|
416
|
+
pendingHead = 0;
|
|
417
|
+
pendingTail = 0;
|
|
418
|
+
pendingCount = 0;
|
|
419
|
+
endpointCounts.clear();
|
|
420
|
+
|
|
421
|
+
if (flushTimer) {
|
|
422
|
+
clearTimeout(flushTimer);
|
|
423
|
+
flushTimer = null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get stats for debugging
|
|
429
|
+
*/
|
|
430
|
+
export function getNetworkInterceptorStats(): {
|
|
431
|
+
pendingCount: number;
|
|
432
|
+
endpointCount: number;
|
|
433
|
+
enabled: boolean;
|
|
434
|
+
} {
|
|
435
|
+
return {
|
|
436
|
+
pendingCount,
|
|
437
|
+
endpointCount: endpointCounts.size,
|
|
438
|
+
enabled: config.enabled,
|
|
439
|
+
};
|
|
440
|
+
}
|