@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.
Files changed (105) hide show
  1. package/README.md +29 -0
  2. package/android/build.gradle.kts +135 -0
  3. package/android/consumer-rules.pro +10 -0
  4. package/android/proguard-rules.pro +1 -0
  5. package/android/src/main/AndroidManifest.xml +15 -0
  6. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +860 -0
  7. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +290 -0
  8. package/android/src/main/java/com/rejourney/engine/DiagnosticLog.kt +385 -0
  9. package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +512 -0
  10. package/android/src/main/java/com/rejourney/platform/OEMDetector.kt +173 -0
  11. package/android/src/main/java/com/rejourney/platform/PerfTiming.kt +384 -0
  12. package/android/src/main/java/com/rejourney/platform/SessionLifecycleService.kt +160 -0
  13. package/android/src/main/java/com/rejourney/platform/Telemetry.kt +301 -0
  14. package/android/src/main/java/com/rejourney/platform/WindowUtils.kt +100 -0
  15. package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +129 -0
  16. package/android/src/main/java/com/rejourney/recording/EventBuffer.kt +330 -0
  17. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +519 -0
  18. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +740 -0
  19. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +559 -0
  20. package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +238 -0
  21. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +633 -0
  22. package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +232 -0
  23. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +474 -0
  24. package/android/src/main/java/com/rejourney/utility/DataCompression.kt +63 -0
  25. package/android/src/main/java/com/rejourney/utility/ImageBlur.kt +412 -0
  26. package/android/src/main/java/com/rejourney/utility/ViewIdentifier.kt +169 -0
  27. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +232 -0
  28. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  29. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +268 -0
  30. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  31. package/ios/Engine/DeviceRegistrar.swift +288 -0
  32. package/ios/Engine/DiagnosticLog.swift +387 -0
  33. package/ios/Engine/RejourneyImpl.swift +719 -0
  34. package/ios/Recording/AnrSentinel.swift +142 -0
  35. package/ios/Recording/EventBuffer.swift +326 -0
  36. package/ios/Recording/InteractionRecorder.swift +428 -0
  37. package/ios/Recording/ReplayOrchestrator.swift +624 -0
  38. package/ios/Recording/SegmentDispatcher.swift +492 -0
  39. package/ios/Recording/StabilityMonitor.swift +223 -0
  40. package/ios/Recording/TelemetryPipeline.swift +547 -0
  41. package/ios/Recording/ViewHierarchyScanner.swift +156 -0
  42. package/ios/Recording/VisualCapture.swift +675 -0
  43. package/ios/Rejourney.h +38 -0
  44. package/ios/Rejourney.mm +375 -0
  45. package/ios/Utility/DataCompression.swift +55 -0
  46. package/ios/Utility/ImageBlur.swift +89 -0
  47. package/ios/Utility/RuntimeMethodSwap.swift +41 -0
  48. package/ios/Utility/ViewIdentifier.swift +37 -0
  49. package/lib/commonjs/NativeRejourney.js +40 -0
  50. package/lib/commonjs/components/Mask.js +88 -0
  51. package/lib/commonjs/index.js +1443 -0
  52. package/lib/commonjs/sdk/autoTracking.js +1087 -0
  53. package/lib/commonjs/sdk/constants.js +166 -0
  54. package/lib/commonjs/sdk/errorTracking.js +187 -0
  55. package/lib/commonjs/sdk/index.js +50 -0
  56. package/lib/commonjs/sdk/metricsTracking.js +205 -0
  57. package/lib/commonjs/sdk/navigation.js +128 -0
  58. package/lib/commonjs/sdk/networkInterceptor.js +375 -0
  59. package/lib/commonjs/sdk/utils.js +433 -0
  60. package/lib/commonjs/sdk/version.js +13 -0
  61. package/lib/commonjs/types/expo-router.d.js +2 -0
  62. package/lib/commonjs/types/index.js +2 -0
  63. package/lib/module/NativeRejourney.js +38 -0
  64. package/lib/module/components/Mask.js +83 -0
  65. package/lib/module/index.js +1341 -0
  66. package/lib/module/sdk/autoTracking.js +1059 -0
  67. package/lib/module/sdk/constants.js +154 -0
  68. package/lib/module/sdk/errorTracking.js +177 -0
  69. package/lib/module/sdk/index.js +26 -0
  70. package/lib/module/sdk/metricsTracking.js +187 -0
  71. package/lib/module/sdk/navigation.js +120 -0
  72. package/lib/module/sdk/networkInterceptor.js +364 -0
  73. package/lib/module/sdk/utils.js +412 -0
  74. package/lib/module/sdk/version.js +7 -0
  75. package/lib/module/types/expo-router.d.js +2 -0
  76. package/lib/module/types/index.js +2 -0
  77. package/lib/typescript/NativeRejourney.d.ts +160 -0
  78. package/lib/typescript/components/Mask.d.ts +54 -0
  79. package/lib/typescript/index.d.ts +117 -0
  80. package/lib/typescript/sdk/autoTracking.d.ts +226 -0
  81. package/lib/typescript/sdk/constants.d.ts +138 -0
  82. package/lib/typescript/sdk/errorTracking.d.ts +47 -0
  83. package/lib/typescript/sdk/index.d.ts +24 -0
  84. package/lib/typescript/sdk/metricsTracking.d.ts +75 -0
  85. package/lib/typescript/sdk/navigation.d.ts +48 -0
  86. package/lib/typescript/sdk/networkInterceptor.d.ts +62 -0
  87. package/lib/typescript/sdk/utils.d.ts +193 -0
  88. package/lib/typescript/sdk/version.d.ts +6 -0
  89. package/lib/typescript/types/index.d.ts +618 -0
  90. package/package.json +122 -0
  91. package/rejourney.podspec +23 -0
  92. package/src/NativeRejourney.ts +185 -0
  93. package/src/components/Mask.tsx +93 -0
  94. package/src/index.ts +1555 -0
  95. package/src/sdk/autoTracking.ts +1245 -0
  96. package/src/sdk/constants.ts +155 -0
  97. package/src/sdk/errorTracking.ts +231 -0
  98. package/src/sdk/index.ts +25 -0
  99. package/src/sdk/metricsTracking.ts +227 -0
  100. package/src/sdk/navigation.ts +152 -0
  101. package/src/sdk/networkInterceptor.ts +423 -0
  102. package/src/sdk/utils.ts +442 -0
  103. package/src/sdk/version.ts +6 -0
  104. package/src/types/expo-router.d.ts +7 -0
  105. 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
+ }