@rejourneyco/react-native 1.0.7
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/README.md +29 -0
- 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 +860 -0
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +290 -0
- package/android/src/main/java/com/rejourney/engine/DiagnosticLog.kt +385 -0
- package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +512 -0
- package/android/src/main/java/com/rejourney/platform/OEMDetector.kt +173 -0
- package/android/src/main/java/com/rejourney/platform/PerfTiming.kt +384 -0
- package/android/src/main/java/com/rejourney/platform/SessionLifecycleService.kt +160 -0
- package/android/src/main/java/com/rejourney/platform/Telemetry.kt +301 -0
- package/android/src/main/java/com/rejourney/platform/WindowUtils.kt +100 -0
- package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +129 -0
- package/android/src/main/java/com/rejourney/recording/EventBuffer.kt +330 -0
- package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +519 -0
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +740 -0
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +559 -0
- package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +238 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +633 -0
- package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +232 -0
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +474 -0
- package/android/src/main/java/com/rejourney/utility/DataCompression.kt +63 -0
- package/android/src/main/java/com/rejourney/utility/ImageBlur.kt +412 -0
- package/android/src/main/java/com/rejourney/utility/ViewIdentifier.kt +169 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +232 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +268 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Engine/DeviceRegistrar.swift +288 -0
- package/ios/Engine/DiagnosticLog.swift +387 -0
- package/ios/Engine/RejourneyImpl.swift +719 -0
- package/ios/Recording/AnrSentinel.swift +142 -0
- package/ios/Recording/EventBuffer.swift +326 -0
- package/ios/Recording/InteractionRecorder.swift +428 -0
- package/ios/Recording/ReplayOrchestrator.swift +624 -0
- package/ios/Recording/SegmentDispatcher.swift +492 -0
- package/ios/Recording/StabilityMonitor.swift +223 -0
- package/ios/Recording/TelemetryPipeline.swift +547 -0
- package/ios/Recording/ViewHierarchyScanner.swift +156 -0
- package/ios/Recording/VisualCapture.swift +675 -0
- package/ios/Rejourney.h +38 -0
- package/ios/Rejourney.mm +375 -0
- package/ios/Utility/DataCompression.swift +55 -0
- package/ios/Utility/ImageBlur.swift +89 -0
- package/ios/Utility/RuntimeMethodSwap.swift +41 -0
- package/ios/Utility/ViewIdentifier.swift +37 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +88 -0
- package/lib/commonjs/index.js +1443 -0
- package/lib/commonjs/sdk/autoTracking.js +1087 -0
- package/lib/commonjs/sdk/constants.js +166 -0
- package/lib/commonjs/sdk/errorTracking.js +187 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +205 -0
- package/lib/commonjs/sdk/navigation.js +128 -0
- package/lib/commonjs/sdk/networkInterceptor.js +375 -0
- package/lib/commonjs/sdk/utils.js +433 -0
- package/lib/commonjs/sdk/version.js +13 -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 +83 -0
- package/lib/module/index.js +1341 -0
- package/lib/module/sdk/autoTracking.js +1059 -0
- package/lib/module/sdk/constants.js +154 -0
- package/lib/module/sdk/errorTracking.js +177 -0
- package/lib/module/sdk/index.js +26 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +120 -0
- package/lib/module/sdk/networkInterceptor.js +364 -0
- package/lib/module/sdk/utils.js +412 -0
- package/lib/module/sdk/version.js +7 -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 +160 -0
- package/lib/typescript/components/Mask.d.ts +54 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +226 -0
- package/lib/typescript/sdk/constants.d.ts +138 -0
- package/lib/typescript/sdk/errorTracking.d.ts +47 -0
- package/lib/typescript/sdk/index.d.ts +24 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +75 -0
- package/lib/typescript/sdk/navigation.d.ts +48 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +62 -0
- package/lib/typescript/sdk/utils.d.ts +193 -0
- package/lib/typescript/sdk/version.d.ts +6 -0
- package/lib/typescript/types/index.d.ts +618 -0
- package/package.json +122 -0
- package/rejourney.podspec +23 -0
- package/src/NativeRejourney.ts +185 -0
- package/src/components/Mask.tsx +93 -0
- package/src/index.ts +1555 -0
- package/src/sdk/autoTracking.ts +1245 -0
- package/src/sdk/constants.ts +155 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +25 -0
- package/src/sdk/metricsTracking.ts +227 -0
- package/src/sdk/navigation.ts +152 -0
- package/src/sdk/networkInterceptor.ts +423 -0
- package/src/sdk/utils.ts +442 -0
- package/src/sdk/version.ts +6 -0
- package/src/types/expo-router.d.ts +7 -0
- package/src/types/index.ts +709 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Rejourney
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Rejourney Navigation Utilities
|
|
19
|
+
*
|
|
20
|
+
* Helper functions for extracting human-readable screen names from
|
|
21
|
+
* React Navigation / Expo Router state.
|
|
22
|
+
*
|
|
23
|
+
* These functions are used internally by the SDK's automatic navigation
|
|
24
|
+
* detection. You don't need to import or use these directly.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Normalize a screen name to be human-readable
|
|
29
|
+
* Handles common patterns from React Native / Expo Router
|
|
30
|
+
*
|
|
31
|
+
* @param raw - Raw screen name to normalize
|
|
32
|
+
* @returns Cleaned, human-readable screen name
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeScreenName(raw: string): string {
|
|
35
|
+
if (!raw) return 'Unknown';
|
|
36
|
+
|
|
37
|
+
let name = raw;
|
|
38
|
+
|
|
39
|
+
name = name.replace(/[^\x20-\x7E\s]/g, '');
|
|
40
|
+
|
|
41
|
+
name = name.split(/[-_]/).map(word =>
|
|
42
|
+
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
|
43
|
+
).join(' ');
|
|
44
|
+
|
|
45
|
+
const suffixes = ['Screen', 'Page', 'View', 'Controller', 'ViewController', 'VC'];
|
|
46
|
+
for (const suffix of suffixes) {
|
|
47
|
+
if (name.endsWith(suffix) && name.length > suffix.length) {
|
|
48
|
+
name = name.slice(0, -suffix.length).trim();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const prefixes = ['RNS', 'RCT', 'RN', 'UI'];
|
|
53
|
+
for (const prefix of prefixes) {
|
|
54
|
+
if (name.startsWith(prefix) && name.length > prefix.length + 2) {
|
|
55
|
+
name = name.slice(prefix.length).trim();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
name = name.replace(/\[([a-zA-Z]+)(?:Id)?\]/g, (_, param) => {
|
|
60
|
+
const clean = param.replace(/Id$/i, '');
|
|
61
|
+
if (clean.length < 2) return '';
|
|
62
|
+
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
name = name.replace(/\[\]/g, '');
|
|
66
|
+
|
|
67
|
+
name = name.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
68
|
+
|
|
69
|
+
name = name.replace(/\s+/g, ' ').trim();
|
|
70
|
+
|
|
71
|
+
if (name.length > 0) {
|
|
72
|
+
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return name || 'Unknown';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get a human-readable screen name from Expo Router path and segments
|
|
80
|
+
*
|
|
81
|
+
* @param pathname - The current route pathname
|
|
82
|
+
* @param segments - Route segments from useSegments()
|
|
83
|
+
* @returns Human-readable screen name
|
|
84
|
+
*/
|
|
85
|
+
export function getScreenNameFromPath(pathname: string, segments: string[]): string {
|
|
86
|
+
if (segments.length > 0) {
|
|
87
|
+
const cleanSegments = segments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
88
|
+
|
|
89
|
+
if (cleanSegments.length > 0) {
|
|
90
|
+
const processedSegments = cleanSegments.map(s => {
|
|
91
|
+
if (s.startsWith('[') && s.endsWith(']')) {
|
|
92
|
+
const param = s.slice(1, -1);
|
|
93
|
+
if (param === 'id' || param === 'slug') return null;
|
|
94
|
+
if (param === 'id' || param === 'slug') return null;
|
|
95
|
+
const clean = param.replace(/Id$/i, '');
|
|
96
|
+
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
97
|
+
}
|
|
98
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
99
|
+
}).filter(Boolean);
|
|
100
|
+
|
|
101
|
+
if (processedSegments.length > 0) {
|
|
102
|
+
return processedSegments.join(' > ');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!pathname || pathname === '/') {
|
|
108
|
+
return 'Home';
|
|
109
|
+
}
|
|
110
|
+
const cleanPath = pathname
|
|
111
|
+
.replace(/^\/(tabs)?/, '')
|
|
112
|
+
.replace(/\([^)]+\)/g, '')
|
|
113
|
+
.replace(/\[([^\]]+)\]/g, (_, param) => {
|
|
114
|
+
if (param === 'id' || param === 'slug') return '';
|
|
115
|
+
const clean = param.replace(/Id$/i, '');
|
|
116
|
+
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
117
|
+
})
|
|
118
|
+
.replace(/\/+/g, '/')
|
|
119
|
+
.replace(/^\//, '')
|
|
120
|
+
.replace(/\/$/, '')
|
|
121
|
+
.replace(/\//g, ' > ')
|
|
122
|
+
.trim();
|
|
123
|
+
|
|
124
|
+
if (!cleanPath) {
|
|
125
|
+
return 'Home';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return cleanPath
|
|
129
|
+
.split(' > ')
|
|
130
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
131
|
+
.filter(s => s.length > 0)
|
|
132
|
+
.join(' > ') || 'Home';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get the current route name from a navigation state object
|
|
137
|
+
*
|
|
138
|
+
* @param state - React Navigation state object
|
|
139
|
+
* @returns Current route name or null
|
|
140
|
+
*/
|
|
141
|
+
export function getCurrentRouteFromState(state: any): string | null {
|
|
142
|
+
if (!state || !state.routes) return null;
|
|
143
|
+
|
|
144
|
+
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
145
|
+
if (!route) return null;
|
|
146
|
+
|
|
147
|
+
if (route.state) {
|
|
148
|
+
return getCurrentRouteFromState(route.state);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return route.name || null;
|
|
152
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Rejourney
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Network Interceptor for Rejourney - Optimized Version
|
|
19
|
+
*
|
|
20
|
+
* Automatically intercepts fetch() and XMLHttpRequest to log API calls.
|
|
21
|
+
*
|
|
22
|
+
* PERFORMANCE OPTIMIZATIONS:
|
|
23
|
+
* - Minimal synchronous overhead (just captures timing, no processing)
|
|
24
|
+
* - Batched async logging (doesn't block requests)
|
|
25
|
+
* - Circular buffer with max size limit
|
|
26
|
+
* - Sampling for high-frequency endpoints
|
|
27
|
+
* - No string allocations in hot path
|
|
28
|
+
* - Lazy URL parsing
|
|
29
|
+
* - PII Scrubbing for query parameters
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { NetworkRequestParams } from '../types';
|
|
33
|
+
|
|
34
|
+
let originalFetch: typeof fetch | null = null;
|
|
35
|
+
let originalXHROpen: typeof XMLHttpRequest.prototype.open | null = null;
|
|
36
|
+
let originalXHRSend: typeof XMLHttpRequest.prototype.send | null = null;
|
|
37
|
+
|
|
38
|
+
let logCallback: ((request: NetworkRequestParams) => void) | null = null;
|
|
39
|
+
|
|
40
|
+
const MAX_PENDING_REQUESTS = 100;
|
|
41
|
+
const pendingRequests: (NetworkRequestParams | null)[] = new Array(MAX_PENDING_REQUESTS).fill(null);
|
|
42
|
+
let pendingHead = 0;
|
|
43
|
+
let pendingTail = 0;
|
|
44
|
+
let pendingCount = 0;
|
|
45
|
+
|
|
46
|
+
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
47
|
+
const FLUSH_INTERVAL = 500;
|
|
48
|
+
|
|
49
|
+
const endpointCounts = new Map<string, { count: number; lastReset: number }>();
|
|
50
|
+
const SAMPLE_WINDOW = 10000;
|
|
51
|
+
const MAX_PER_ENDPOINT = 20;
|
|
52
|
+
|
|
53
|
+
const config = {
|
|
54
|
+
enabled: true,
|
|
55
|
+
ignorePatterns: [] as string[],
|
|
56
|
+
maxUrlLength: 300,
|
|
57
|
+
captureSizes: false,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'access_token', 'api_key'];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Scrub sensitive data from URL
|
|
64
|
+
*/
|
|
65
|
+
function scrubUrl(url: string): string {
|
|
66
|
+
try {
|
|
67
|
+
if (url.indexOf('?') === -1) return url;
|
|
68
|
+
|
|
69
|
+
const urlObj = new URL(url);
|
|
70
|
+
let modified = false;
|
|
71
|
+
|
|
72
|
+
SENSITIVE_KEYS.forEach(key => {
|
|
73
|
+
if (urlObj.searchParams.has(key)) {
|
|
74
|
+
urlObj.searchParams.set(key, '[REDACTED]');
|
|
75
|
+
modified = true;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return modified ? urlObj.toString() : url;
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore error, fallback to primitive scrubbing
|
|
82
|
+
|
|
83
|
+
let scrubbed = url;
|
|
84
|
+
SENSITIVE_KEYS.forEach(key => {
|
|
85
|
+
const regex = new RegExp(`([?&])${key}=[^&]*`, 'gi');
|
|
86
|
+
scrubbed = scrubbed.replace(regex, `$1${key}=[REDACTED]`);
|
|
87
|
+
});
|
|
88
|
+
return scrubbed;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Fast check if URL should be ignored (no regex for speed)
|
|
94
|
+
*/
|
|
95
|
+
function shouldIgnoreUrl(url: string): boolean {
|
|
96
|
+
const patterns = config.ignorePatterns;
|
|
97
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
98
|
+
const pattern = patterns[i];
|
|
99
|
+
if (pattern && url.indexOf(pattern) !== -1) return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if we should sample this request (rate limiting per endpoint)
|
|
106
|
+
*/
|
|
107
|
+
function shouldSampleRequest(urlPath: string): boolean {
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
let entry = endpointCounts.get(urlPath);
|
|
110
|
+
|
|
111
|
+
if (!entry || now - entry.lastReset > SAMPLE_WINDOW) {
|
|
112
|
+
entry = { count: 0, lastReset: now };
|
|
113
|
+
endpointCounts.set(urlPath, entry);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
entry.count++;
|
|
117
|
+
return entry.count <= MAX_PER_ENDPOINT;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Add request to pending buffer (non-blocking)
|
|
122
|
+
*/
|
|
123
|
+
function queueRequest(request: NetworkRequestParams): void {
|
|
124
|
+
if (pendingCount >= MAX_PENDING_REQUESTS) {
|
|
125
|
+
// Buffer full, drop oldest
|
|
126
|
+
pendingHead = (pendingHead + 1) % MAX_PENDING_REQUESTS;
|
|
127
|
+
pendingCount--;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
request.url = scrubUrl(request.url);
|
|
131
|
+
|
|
132
|
+
pendingRequests[pendingTail] = request;
|
|
133
|
+
pendingTail = (pendingTail + 1) % MAX_PENDING_REQUESTS;
|
|
134
|
+
pendingCount++;
|
|
135
|
+
|
|
136
|
+
if (!flushTimer) {
|
|
137
|
+
flushTimer = setTimeout(flushPendingRequests, FLUSH_INTERVAL);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Flush pending requests to callback
|
|
143
|
+
*/
|
|
144
|
+
function flushPendingRequests(): void {
|
|
145
|
+
flushTimer = null;
|
|
146
|
+
|
|
147
|
+
if (!logCallback || pendingCount === 0) return;
|
|
148
|
+
while (pendingCount > 0) {
|
|
149
|
+
const request = pendingRequests[pendingHead];
|
|
150
|
+
pendingRequests[pendingHead] = null; // Allow GC
|
|
151
|
+
pendingHead = (pendingHead + 1) % MAX_PENDING_REQUESTS;
|
|
152
|
+
pendingCount--;
|
|
153
|
+
|
|
154
|
+
if (request) {
|
|
155
|
+
try {
|
|
156
|
+
logCallback(request);
|
|
157
|
+
} catch {
|
|
158
|
+
// Ignore
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse URL efficiently (only extract what we need)
|
|
166
|
+
*/
|
|
167
|
+
function parseUrlFast(url: string): { host: string; path: string } {
|
|
168
|
+
// Fast path for common patterns
|
|
169
|
+
let hostEnd = -1;
|
|
170
|
+
let pathStart = -1;
|
|
171
|
+
|
|
172
|
+
const protoEnd = url.indexOf('://');
|
|
173
|
+
if (protoEnd !== -1) {
|
|
174
|
+
const afterProto = protoEnd + 3;
|
|
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
|
+
return { host: '', path: url };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Intercept fetch - minimal overhead version
|
|
195
|
+
*/
|
|
196
|
+
function interceptFetch(): void {
|
|
197
|
+
if (typeof globalThis.fetch === 'undefined') return;
|
|
198
|
+
if (originalFetch) return;
|
|
199
|
+
|
|
200
|
+
originalFetch = globalThis.fetch;
|
|
201
|
+
|
|
202
|
+
globalThis.fetch = function optimizedFetch(
|
|
203
|
+
input: RequestInfo | URL,
|
|
204
|
+
init?: RequestInit
|
|
205
|
+
): Promise<Response> {
|
|
206
|
+
if (!config.enabled || !logCallback) {
|
|
207
|
+
return originalFetch!(input, init);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const url = typeof input === 'string'
|
|
211
|
+
? input
|
|
212
|
+
: input instanceof URL
|
|
213
|
+
? input.href
|
|
214
|
+
: (input as Request).url;
|
|
215
|
+
|
|
216
|
+
if (shouldIgnoreUrl(url)) {
|
|
217
|
+
return originalFetch!(input, init);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Parse URL and check sampling
|
|
221
|
+
const { path } = parseUrlFast(url);
|
|
222
|
+
if (!shouldSampleRequest(path)) {
|
|
223
|
+
return originalFetch!(input, init);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const startTime = Date.now();
|
|
227
|
+
const method = ((init?.method || 'GET').toUpperCase()) as NetworkRequestParams['method'];
|
|
228
|
+
return originalFetch!(input, init).then(
|
|
229
|
+
(response) => {
|
|
230
|
+
queueRequest({
|
|
231
|
+
requestId: `f${startTime}`,
|
|
232
|
+
method,
|
|
233
|
+
url: url.length > config.maxUrlLength ? url.substring(0, config.maxUrlLength) : url,
|
|
234
|
+
statusCode: response.status,
|
|
235
|
+
duration: Date.now() - startTime,
|
|
236
|
+
startTimestamp: startTime,
|
|
237
|
+
endTimestamp: Date.now(),
|
|
238
|
+
success: response.ok,
|
|
239
|
+
});
|
|
240
|
+
return response;
|
|
241
|
+
},
|
|
242
|
+
(error) => {
|
|
243
|
+
queueRequest({
|
|
244
|
+
requestId: `f${startTime}`,
|
|
245
|
+
method,
|
|
246
|
+
url: url.length > config.maxUrlLength ? url.substring(0, config.maxUrlLength) : url,
|
|
247
|
+
statusCode: 0,
|
|
248
|
+
duration: Date.now() - startTime,
|
|
249
|
+
startTimestamp: startTime,
|
|
250
|
+
endTimestamp: Date.now(),
|
|
251
|
+
success: false,
|
|
252
|
+
errorMessage: error?.message || 'Network error',
|
|
253
|
+
});
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Intercept XMLHttpRequest - minimal overhead version
|
|
262
|
+
*/
|
|
263
|
+
function interceptXHR(): void {
|
|
264
|
+
if (typeof XMLHttpRequest === 'undefined') return;
|
|
265
|
+
if (originalXHROpen) return;
|
|
266
|
+
|
|
267
|
+
originalXHROpen = XMLHttpRequest.prototype.open;
|
|
268
|
+
originalXHRSend = XMLHttpRequest.prototype.send;
|
|
269
|
+
|
|
270
|
+
XMLHttpRequest.prototype.open = function (
|
|
271
|
+
method: string,
|
|
272
|
+
url: string | URL,
|
|
273
|
+
async: boolean = true,
|
|
274
|
+
username?: string | null,
|
|
275
|
+
password?: string | null
|
|
276
|
+
): void {
|
|
277
|
+
const urlString = typeof url === 'string' ? url : url.toString();
|
|
278
|
+
|
|
279
|
+
(this as any).__rj = {
|
|
280
|
+
m: method.toUpperCase(),
|
|
281
|
+
u: urlString,
|
|
282
|
+
t: 0,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
return originalXHROpen!.call(this, method, urlString, async, username, password);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
XMLHttpRequest.prototype.send = function (body?: any): void {
|
|
289
|
+
const data = (this as any).__rj;
|
|
290
|
+
|
|
291
|
+
if (!config.enabled || !logCallback || !data || shouldIgnoreUrl(data.u)) {
|
|
292
|
+
return originalXHRSend!.call(this, body);
|
|
293
|
+
}
|
|
294
|
+
const { path } = parseUrlFast(data.u);
|
|
295
|
+
if (!shouldSampleRequest(path)) {
|
|
296
|
+
return originalXHRSend!.call(this, body);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
data.t = Date.now();
|
|
300
|
+
|
|
301
|
+
const onComplete = () => {
|
|
302
|
+
const endTime = Date.now();
|
|
303
|
+
queueRequest({
|
|
304
|
+
requestId: `x${data.t}`,
|
|
305
|
+
method: data.m as NetworkRequestParams['method'],
|
|
306
|
+
url: data.u.length > config.maxUrlLength ? data.u.substring(0, config.maxUrlLength) : data.u,
|
|
307
|
+
statusCode: this.status,
|
|
308
|
+
duration: endTime - data.t,
|
|
309
|
+
startTimestamp: data.t,
|
|
310
|
+
endTimestamp: endTime,
|
|
311
|
+
success: this.status >= 200 && this.status < 400,
|
|
312
|
+
errorMessage: this.status === 0 ? 'Network error' : undefined,
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
this.addEventListener('load', onComplete);
|
|
317
|
+
this.addEventListener('error', onComplete);
|
|
318
|
+
this.addEventListener('abort', onComplete);
|
|
319
|
+
|
|
320
|
+
return originalXHRSend!.call(this, body);
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Initialize network interception
|
|
326
|
+
*/
|
|
327
|
+
export function initNetworkInterceptor(
|
|
328
|
+
callback: (request: NetworkRequestParams) => void,
|
|
329
|
+
options?: {
|
|
330
|
+
ignoreUrls?: (string | RegExp)[];
|
|
331
|
+
captureSizes?: boolean;
|
|
332
|
+
}
|
|
333
|
+
): void {
|
|
334
|
+
logCallback = callback;
|
|
335
|
+
|
|
336
|
+
if (options?.ignoreUrls) {
|
|
337
|
+
config.ignorePatterns = options.ignoreUrls
|
|
338
|
+
.filter((p): p is string => typeof p === 'string');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (options?.captureSizes !== undefined) {
|
|
342
|
+
config.captureSizes = options.captureSizes;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
interceptFetch();
|
|
346
|
+
interceptXHR();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Disable network interception
|
|
351
|
+
*/
|
|
352
|
+
export function disableNetworkInterceptor(): void {
|
|
353
|
+
config.enabled = false;
|
|
354
|
+
|
|
355
|
+
if (flushTimer) {
|
|
356
|
+
clearTimeout(flushTimer);
|
|
357
|
+
flushTimer = null;
|
|
358
|
+
}
|
|
359
|
+
flushPendingRequests();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Re-enable network interception
|
|
364
|
+
*/
|
|
365
|
+
export function enableNetworkInterceptor(): void {
|
|
366
|
+
config.enabled = true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Force flush pending requests (call before app termination)
|
|
371
|
+
*/
|
|
372
|
+
export function flushNetworkRequests(): void {
|
|
373
|
+
if (flushTimer) {
|
|
374
|
+
clearTimeout(flushTimer);
|
|
375
|
+
flushTimer = null;
|
|
376
|
+
}
|
|
377
|
+
flushPendingRequests();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Restore original fetch and XHR
|
|
382
|
+
*/
|
|
383
|
+
export function restoreNetworkInterceptor(): void {
|
|
384
|
+
if (originalFetch) {
|
|
385
|
+
globalThis.fetch = originalFetch;
|
|
386
|
+
originalFetch = null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (originalXHROpen && originalXHRSend) {
|
|
390
|
+
XMLHttpRequest.prototype.open = originalXHROpen;
|
|
391
|
+
XMLHttpRequest.prototype.send = originalXHRSend;
|
|
392
|
+
originalXHROpen = null;
|
|
393
|
+
originalXHRSend = null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
logCallback = null;
|
|
397
|
+
|
|
398
|
+
// Clear state
|
|
399
|
+
pendingHead = 0;
|
|
400
|
+
pendingTail = 0;
|
|
401
|
+
pendingCount = 0;
|
|
402
|
+
endpointCounts.clear();
|
|
403
|
+
|
|
404
|
+
if (flushTimer) {
|
|
405
|
+
clearTimeout(flushTimer);
|
|
406
|
+
flushTimer = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Get stats for debugging
|
|
412
|
+
*/
|
|
413
|
+
export function getNetworkInterceptorStats(): {
|
|
414
|
+
pendingCount: number;
|
|
415
|
+
endpointCount: number;
|
|
416
|
+
enabled: boolean;
|
|
417
|
+
} {
|
|
418
|
+
return {
|
|
419
|
+
pendingCount,
|
|
420
|
+
endpointCount: endpointCounts.size,
|
|
421
|
+
enabled: config.enabled,
|
|
422
|
+
};
|
|
423
|
+
}
|