@buoy-gg/network 1.7.2
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 +381 -0
- package/lib/commonjs/index.js +34 -0
- package/lib/commonjs/network/components/NetworkCopySettingsView.js +867 -0
- package/lib/commonjs/network/components/NetworkEventDetailView.js +837 -0
- package/lib/commonjs/network/components/NetworkEventItemCompact.js +323 -0
- package/lib/commonjs/network/components/NetworkFilterViewV3.js +297 -0
- package/lib/commonjs/network/components/NetworkModal.js +937 -0
- package/lib/commonjs/network/hooks/useNetworkEvents.js +320 -0
- package/lib/commonjs/network/hooks/useTickEveryMinute.js +34 -0
- package/lib/commonjs/network/index.js +102 -0
- package/lib/commonjs/network/types/index.js +1 -0
- package/lib/commonjs/network/utils/extractOperationName.js +80 -0
- package/lib/commonjs/network/utils/formatGraphQLVariables.js +219 -0
- package/lib/commonjs/network/utils/formatting.js +30 -0
- package/lib/commonjs/network/utils/networkEventStore.js +269 -0
- package/lib/commonjs/network/utils/networkListener.js +801 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +83 -0
- package/lib/module/index.js +7 -0
- package/lib/module/network/components/NetworkCopySettingsView.js +862 -0
- package/lib/module/network/components/NetworkEventDetailView.js +834 -0
- package/lib/module/network/components/NetworkEventItemCompact.js +320 -0
- package/lib/module/network/components/NetworkFilterViewV3.js +293 -0
- package/lib/module/network/components/NetworkModal.js +933 -0
- package/lib/module/network/hooks/useNetworkEvents.js +316 -0
- package/lib/module/network/hooks/useTickEveryMinute.js +29 -0
- package/lib/module/network/index.js +20 -0
- package/lib/module/network/types/index.js +1 -0
- package/lib/module/network/utils/extractOperationName.js +76 -0
- package/lib/module/network/utils/formatGraphQLVariables.js +213 -0
- package/lib/module/network/utils/formatting.js +9 -0
- package/lib/module/network/utils/networkEventStore.js +265 -0
- package/lib/module/network/utils/networkListener.js +791 -0
- package/lib/module/preset.js +79 -0
- package/lib/typescript/index.d.ts +3 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkCopySettingsView.d.ts +26 -0
- package/lib/typescript/network/components/NetworkCopySettingsView.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts +13 -0
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +12 -0
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkFilterViewV3.d.ts +22 -0
- package/lib/typescript/network/components/NetworkFilterViewV3.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkModal.d.ts +14 -0
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useNetworkEvents.d.ts +72 -0
- package/lib/typescript/network/hooks/useNetworkEvents.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useTickEveryMinute.d.ts +9 -0
- package/lib/typescript/network/hooks/useTickEveryMinute.d.ts.map +1 -0
- package/lib/typescript/network/index.d.ts +12 -0
- package/lib/typescript/network/index.d.ts.map +1 -0
- package/lib/typescript/network/types/index.d.ts +88 -0
- package/lib/typescript/network/types/index.d.ts.map +1 -0
- package/lib/typescript/network/utils/extractOperationName.d.ts +41 -0
- package/lib/typescript/network/utils/extractOperationName.d.ts.map +1 -0
- package/lib/typescript/network/utils/formatGraphQLVariables.d.ts +79 -0
- package/lib/typescript/network/utils/formatGraphQLVariables.d.ts.map +1 -0
- package/lib/typescript/network/utils/formatting.d.ts +6 -0
- package/lib/typescript/network/utils/formatting.d.ts.map +1 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts +81 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -0
- package/lib/typescript/network/utils/networkListener.d.ts +191 -0
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +76 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.stopNetworkListener = exports.startNetworkListener = exports.removeAllNetworkListeners = exports.networkListener = exports.isNetworkListening = exports.getNetworkListenerCount = exports.addNetworkListener = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Network listener using Reactotron-style event pattern
|
|
9
|
+
* Simple and reliable network interception for React Native
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Extended XMLHttpRequest interface for monkey-patching
|
|
13
|
+
|
|
14
|
+
// Event types for network operations
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Network traffic interceptor for React Native applications
|
|
18
|
+
*
|
|
19
|
+
* This class intercepts both fetch and XMLHttpRequest operations to provide
|
|
20
|
+
* comprehensive network monitoring capabilities. It uses method swizzling to
|
|
21
|
+
* wrap native networking APIs while preserving their original functionality.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // Start monitoring network traffic
|
|
26
|
+
* startNetworkListener();
|
|
27
|
+
*
|
|
28
|
+
* // Add a listener for network events
|
|
29
|
+
* const unsubscribe = addNetworkListener((event) => {
|
|
30
|
+
* if (event.type === 'response') {
|
|
31
|
+
* console.log(`${event.request.method} ${event.request.url}: ${event.response?.status}`);
|
|
32
|
+
* }
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* // Stop monitoring and cleanup
|
|
36
|
+
* unsubscribe();
|
|
37
|
+
* stopNetworkListener();
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @performance Uses lazy singleton pattern to minimize memory footprint
|
|
41
|
+
* @performance Includes URL filtering to ignore development traffic
|
|
42
|
+
*/
|
|
43
|
+
class NetworkListener {
|
|
44
|
+
listeners = [];
|
|
45
|
+
isListening = false;
|
|
46
|
+
requestCounter = 1000;
|
|
47
|
+
|
|
48
|
+
// URLs to ignore (Metro bundler, symbolicate, etc.)
|
|
49
|
+
ignoredUrls = [/\/symbolicate$/, /\/logs$/, /\/debugger-proxy/, /\/reload$/, /\/launch-js-devtools/, /localhost:8081/, /100\.64\.\d+\.\d+:8081/,
|
|
50
|
+
// iOS simulator
|
|
51
|
+
/10\.0\.\d+\.\d+:8081/ // Android emulator
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Store original methods
|
|
55
|
+
|
|
56
|
+
constructor() {
|
|
57
|
+
// Store original methods
|
|
58
|
+
this.originalFetch = globalThis.fetch.bind(globalThis);
|
|
59
|
+
this.originalXHROpen = XMLHttpRequest.prototype.open;
|
|
60
|
+
this.originalXHRSend = XMLHttpRequest.prototype.send;
|
|
61
|
+
this.originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if URL should be ignored from network monitoring
|
|
66
|
+
*
|
|
67
|
+
* Filters out development-related URLs like Metro bundler, debugger proxy,
|
|
68
|
+
* and symbolication requests to reduce noise in the network logs.
|
|
69
|
+
*
|
|
70
|
+
* @param url - The URL to check
|
|
71
|
+
* @returns True if the URL should be ignored
|
|
72
|
+
*/
|
|
73
|
+
shouldIgnoreUrl(url) {
|
|
74
|
+
return this.ignoredUrls.some(pattern => pattern.test(url));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Emit event to all listeners
|
|
78
|
+
emit(event) {
|
|
79
|
+
this.listeners.forEach(listener => {
|
|
80
|
+
try {
|
|
81
|
+
listener(event);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Error in event listener - continuing with others
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse URL to extract query parameters and clean URL
|
|
90
|
+
*
|
|
91
|
+
* @param url - The URL to parse
|
|
92
|
+
* @returns Object containing cleaned URL and parsed query parameters
|
|
93
|
+
*
|
|
94
|
+
* @performance Uses manual parsing instead of URL constructor for better performance
|
|
95
|
+
*/
|
|
96
|
+
parseUrl(url) {
|
|
97
|
+
let params = null;
|
|
98
|
+
const queryParamIdx = url.indexOf("?");
|
|
99
|
+
if (queryParamIdx > -1) {
|
|
100
|
+
params = {};
|
|
101
|
+
url.substring(queryParamIdx + 1).split("&").forEach(pair => {
|
|
102
|
+
const [key, value] = pair.split("=");
|
|
103
|
+
if (key && value !== undefined) {
|
|
104
|
+
params[key] = decodeURIComponent(value.replace(/\+/g, " "));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
url: queryParamIdx > -1 ? url.substring(0, queryParamIdx) : url,
|
|
110
|
+
params
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Process response body with size limits to prevent memory issues
|
|
116
|
+
* @param response - The Response object to process
|
|
117
|
+
* @param maxSize - Maximum body size in bytes (default: 1MB)
|
|
118
|
+
* @returns Object containing body, size, and truncation status
|
|
119
|
+
*/
|
|
120
|
+
async processResponseBody(response, maxSize = 1024 * 1024 // 1MB default
|
|
121
|
+
) {
|
|
122
|
+
try {
|
|
123
|
+
// Check Content-Length header first
|
|
124
|
+
const contentLength = response.headers.get("content-length");
|
|
125
|
+
if (contentLength) {
|
|
126
|
+
const size = parseInt(contentLength, 10);
|
|
127
|
+
if (!isNaN(size) && size > maxSize) {
|
|
128
|
+
return {
|
|
129
|
+
body: `[Response too large: ${this.formatBytes(size)}]`,
|
|
130
|
+
size,
|
|
131
|
+
truncated: true
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Read the response text
|
|
137
|
+
const text = await response.text();
|
|
138
|
+
const size = text.length;
|
|
139
|
+
|
|
140
|
+
// Check if text exceeds max size
|
|
141
|
+
if (size > maxSize) {
|
|
142
|
+
const preview = text.substring(0, maxSize);
|
|
143
|
+
const omitted = size - maxSize;
|
|
144
|
+
return {
|
|
145
|
+
body: `${preview}\n\n... [truncated, ${this.formatBytes(omitted)} omitted]`,
|
|
146
|
+
size,
|
|
147
|
+
truncated: true
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Try to parse as JSON
|
|
152
|
+
try {
|
|
153
|
+
return {
|
|
154
|
+
body: JSON.parse(text),
|
|
155
|
+
size,
|
|
156
|
+
truncated: false
|
|
157
|
+
};
|
|
158
|
+
} catch {
|
|
159
|
+
// Return as text if not JSON
|
|
160
|
+
return {
|
|
161
|
+
body: text,
|
|
162
|
+
size,
|
|
163
|
+
truncated: false
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return {
|
|
168
|
+
body: "~~~ unable to read body ~~~",
|
|
169
|
+
size: 0,
|
|
170
|
+
truncated: false
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Format bytes into human-readable format
|
|
177
|
+
* @param bytes - Number of bytes
|
|
178
|
+
* @returns Formatted string (e.g., "1.5 MB")
|
|
179
|
+
*/
|
|
180
|
+
formatBytes(bytes) {
|
|
181
|
+
if (bytes === 0) return "0 B";
|
|
182
|
+
const k = 1024;
|
|
183
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
184
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
185
|
+
return `${Math.round(bytes / Math.pow(k, i) * 100) / 100} ${sizes[i]}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Get response body size
|
|
189
|
+
getResponseSize(body) {
|
|
190
|
+
if (!body) return 0;
|
|
191
|
+
if (typeof body === "string") return body.length;
|
|
192
|
+
if (typeof body === "object") {
|
|
193
|
+
try {
|
|
194
|
+
return JSON.stringify(body).length;
|
|
195
|
+
} catch {
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Handle XMLHttpRequest response processing
|
|
204
|
+
* This method processes the response and emits appropriate events
|
|
205
|
+
*/
|
|
206
|
+
handleXHRResponse(xhr, requestId, cleanUrl, method, requestHeaders, requestData, params, startTime, isError = false, clientType = "axios") {
|
|
207
|
+
const duration = startTime ? Date.now() - startTime : 0;
|
|
208
|
+
if (isError || xhr.status === 0) {
|
|
209
|
+
// Network error or aborted request
|
|
210
|
+
this.emit({
|
|
211
|
+
type: "error",
|
|
212
|
+
timestamp: new Date(),
|
|
213
|
+
duration,
|
|
214
|
+
request: {
|
|
215
|
+
id: requestId || "unknown",
|
|
216
|
+
url: cleanUrl,
|
|
217
|
+
method,
|
|
218
|
+
headers: requestHeaders,
|
|
219
|
+
data: requestData,
|
|
220
|
+
params: params || undefined,
|
|
221
|
+
client: clientType
|
|
222
|
+
},
|
|
223
|
+
error: {
|
|
224
|
+
message: isError ? "Request failed" : "Network error or request aborted"
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Parse response
|
|
231
|
+
let body;
|
|
232
|
+
let responseSize = 0;
|
|
233
|
+
try {
|
|
234
|
+
const response = xhr.response;
|
|
235
|
+
|
|
236
|
+
// Debug logging to help diagnose responseType issues
|
|
237
|
+
|
|
238
|
+
// Try different ways to get response based on responseType
|
|
239
|
+
if (xhr.responseType === "json" && response) {
|
|
240
|
+
// Response is already parsed as JSON
|
|
241
|
+
body = response;
|
|
242
|
+
responseSize = JSON.stringify(response).length;
|
|
243
|
+
} else if (xhr.responseType === "" || xhr.responseType === "text") {
|
|
244
|
+
// Only access responseText when responseType allows it
|
|
245
|
+
if (xhr.responseText) {
|
|
246
|
+
responseSize = xhr.responseText.length;
|
|
247
|
+
try {
|
|
248
|
+
body = JSON.parse(xhr.responseText);
|
|
249
|
+
} catch {
|
|
250
|
+
body = xhr.responseText;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} else if (xhr.responseType === "arraybuffer") {
|
|
254
|
+
// For arraybuffer, try to decode as text and parse as JSON
|
|
255
|
+
// This is common for Axios requests that return JSON
|
|
256
|
+
if (response) {
|
|
257
|
+
try {
|
|
258
|
+
const text = new TextDecoder("utf-8").decode(response);
|
|
259
|
+
responseSize = text.length;
|
|
260
|
+
try {
|
|
261
|
+
body = JSON.parse(text);
|
|
262
|
+
} catch {
|
|
263
|
+
// Not JSON, return as text
|
|
264
|
+
body = text;
|
|
265
|
+
}
|
|
266
|
+
} catch (decodeError) {
|
|
267
|
+
// Failed to decode, show placeholder
|
|
268
|
+
body = `[arraybuffer response - ${response.byteLength || 0} bytes]`;
|
|
269
|
+
responseSize = response.byteLength || 0;
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
body = `[arraybuffer response - no data]`;
|
|
273
|
+
responseSize = 0;
|
|
274
|
+
}
|
|
275
|
+
} else if (xhr.responseType === "blob") {
|
|
276
|
+
// For blob responses, we can't synchronously read the content
|
|
277
|
+
// Note: In React Native, most JSON responses shouldn't be blobs
|
|
278
|
+
// but if they are, we show metadata
|
|
279
|
+
if (response instanceof Blob) {
|
|
280
|
+
body = `[blob response - ${response.size} bytes, type: ${response.type || "unknown"}]`;
|
|
281
|
+
responseSize = response.size;
|
|
282
|
+
} else if (response) {
|
|
283
|
+
// Sometimes response might not be a Blob object but still have data
|
|
284
|
+
// Try to handle it as an object
|
|
285
|
+
try {
|
|
286
|
+
body = typeof response === "string" ? JSON.parse(response) : response;
|
|
287
|
+
responseSize = JSON.stringify(body).length;
|
|
288
|
+
} catch {
|
|
289
|
+
body = `[blob response - unable to parse]`;
|
|
290
|
+
responseSize = 0;
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
body = `[blob response - no data]`;
|
|
294
|
+
responseSize = 0;
|
|
295
|
+
}
|
|
296
|
+
} else if (response) {
|
|
297
|
+
// Fallback: try to handle the response regardless of responseType
|
|
298
|
+
// This catches cases where Axios sets an unexpected responseType
|
|
299
|
+
if (typeof response === "string") {
|
|
300
|
+
body = response;
|
|
301
|
+
responseSize = response.length;
|
|
302
|
+
// Try to parse as JSON if it's a string
|
|
303
|
+
try {
|
|
304
|
+
body = JSON.parse(response);
|
|
305
|
+
} catch {
|
|
306
|
+
// Not JSON, keep as string
|
|
307
|
+
}
|
|
308
|
+
} else if (typeof response === "object") {
|
|
309
|
+
// Already an object, use it directly
|
|
310
|
+
body = response;
|
|
311
|
+
responseSize = JSON.stringify(response).length;
|
|
312
|
+
} else {
|
|
313
|
+
body = String(response);
|
|
314
|
+
responseSize = String(response).length;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
// Failed to parse response
|
|
319
|
+
body = "~~~ unable to read body ~~~";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Parse response headers
|
|
323
|
+
const responseHeaders = {};
|
|
324
|
+
try {
|
|
325
|
+
const headerString = xhr.getAllResponseHeaders();
|
|
326
|
+
if (headerString) {
|
|
327
|
+
headerString.split("\r\n").forEach(line => {
|
|
328
|
+
if (line) {
|
|
329
|
+
const colonIndex = line.indexOf(": ");
|
|
330
|
+
if (colonIndex > 0) {
|
|
331
|
+
const key = line.substring(0, colonIndex).toLowerCase();
|
|
332
|
+
const value = line.substring(colonIndex + 2);
|
|
333
|
+
responseHeaders[key] = value;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
// Ignore header parsing errors
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Emit response or error based on status
|
|
343
|
+
if (xhr.status >= 200 && xhr.status < 400) {
|
|
344
|
+
this.emit({
|
|
345
|
+
type: "response",
|
|
346
|
+
timestamp: new Date(),
|
|
347
|
+
duration,
|
|
348
|
+
request: {
|
|
349
|
+
id: requestId || "unknown",
|
|
350
|
+
url: cleanUrl,
|
|
351
|
+
method,
|
|
352
|
+
headers: requestHeaders,
|
|
353
|
+
data: requestData,
|
|
354
|
+
params: params || undefined,
|
|
355
|
+
client: clientType
|
|
356
|
+
},
|
|
357
|
+
response: {
|
|
358
|
+
status: xhr.status,
|
|
359
|
+
statusText: xhr.statusText,
|
|
360
|
+
headers: responseHeaders,
|
|
361
|
+
body,
|
|
362
|
+
size: responseSize
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
} else {
|
|
366
|
+
this.emit({
|
|
367
|
+
type: "error",
|
|
368
|
+
timestamp: new Date(),
|
|
369
|
+
duration,
|
|
370
|
+
request: {
|
|
371
|
+
id: requestId || "unknown",
|
|
372
|
+
url: cleanUrl,
|
|
373
|
+
method,
|
|
374
|
+
headers: requestHeaders,
|
|
375
|
+
data: requestData,
|
|
376
|
+
params: params || undefined,
|
|
377
|
+
client: clientType
|
|
378
|
+
},
|
|
379
|
+
response: {
|
|
380
|
+
status: xhr.status,
|
|
381
|
+
statusText: xhr.statusText,
|
|
382
|
+
headers: responseHeaders,
|
|
383
|
+
body,
|
|
384
|
+
size: responseSize
|
|
385
|
+
},
|
|
386
|
+
error: {
|
|
387
|
+
message: `HTTP ${xhr.status}: ${xhr.statusText}`
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Start intercepting network operations by swizzling fetch and XMLHttpRequest
|
|
395
|
+
*
|
|
396
|
+
* This method replaces the global fetch function and XMLHttpRequest methods
|
|
397
|
+
* with instrumented versions that emit events while preserving original functionality.
|
|
398
|
+
*
|
|
399
|
+
* @throws Will log warnings if already listening
|
|
400
|
+
*
|
|
401
|
+
* @performance Uses method swizzling for minimal runtime overhead
|
|
402
|
+
* @performance Includes request deduplication through ignored URL patterns
|
|
403
|
+
*/
|
|
404
|
+
startListening() {
|
|
405
|
+
if (this.isListening) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const self = this;
|
|
409
|
+
|
|
410
|
+
// Swizzle fetch
|
|
411
|
+
globalThis.fetch = async (input, init) => {
|
|
412
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
413
|
+
|
|
414
|
+
// Skip ignored URLs
|
|
415
|
+
if (self.shouldIgnoreUrl(url)) {
|
|
416
|
+
return self.originalFetch(input, init);
|
|
417
|
+
}
|
|
418
|
+
const startTime = Date.now();
|
|
419
|
+
const requestId = `fetch_${++self.requestCounter}`;
|
|
420
|
+
const method = init?.method || "GET";
|
|
421
|
+
const {
|
|
422
|
+
url: cleanUrl,
|
|
423
|
+
params
|
|
424
|
+
} = self.parseUrl(url);
|
|
425
|
+
|
|
426
|
+
// Parse request headers
|
|
427
|
+
let requestHeaders = {};
|
|
428
|
+
if (init?.headers) {
|
|
429
|
+
if (init.headers instanceof Headers) {
|
|
430
|
+
init.headers.forEach((value, key) => {
|
|
431
|
+
requestHeaders[key] = value;
|
|
432
|
+
});
|
|
433
|
+
} else if (Array.isArray(init.headers)) {
|
|
434
|
+
init.headers.forEach(([key, value]) => {
|
|
435
|
+
requestHeaders[key] = value;
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
requestHeaders = init.headers;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Parse request body
|
|
443
|
+
let requestData;
|
|
444
|
+
if (init?.body) {
|
|
445
|
+
if (typeof init.body === "string") {
|
|
446
|
+
try {
|
|
447
|
+
requestData = JSON.parse(init.body);
|
|
448
|
+
} catch {
|
|
449
|
+
requestData = init.body;
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
requestData = init.body;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Determine client type from X-Request-Client header or default to fetch
|
|
457
|
+
const clientType = requestHeaders["X-Request-Client"] || requestHeaders["x-request-client"] || "fetch";
|
|
458
|
+
|
|
459
|
+
// Emit request event
|
|
460
|
+
self.emit({
|
|
461
|
+
type: "request",
|
|
462
|
+
timestamp: new Date(),
|
|
463
|
+
request: {
|
|
464
|
+
id: requestId || "unknown",
|
|
465
|
+
url: cleanUrl,
|
|
466
|
+
method,
|
|
467
|
+
headers: requestHeaders,
|
|
468
|
+
data: requestData,
|
|
469
|
+
params: params || undefined,
|
|
470
|
+
client: clientType
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
try {
|
|
474
|
+
const response = await self.originalFetch(input, init);
|
|
475
|
+
const duration = Date.now() - startTime;
|
|
476
|
+
|
|
477
|
+
// Clone response to read body with size limits
|
|
478
|
+
const responseClone = response.clone();
|
|
479
|
+
const {
|
|
480
|
+
body,
|
|
481
|
+
size: responseSize,
|
|
482
|
+
truncated
|
|
483
|
+
} = await self.processResponseBody(responseClone);
|
|
484
|
+
|
|
485
|
+
// Parse response headers
|
|
486
|
+
const responseHeaders = {};
|
|
487
|
+
response.headers.forEach((value, key) => {
|
|
488
|
+
responseHeaders[key.toLowerCase()] = value;
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Emit response event
|
|
492
|
+
self.emit({
|
|
493
|
+
type: "response",
|
|
494
|
+
timestamp: new Date(),
|
|
495
|
+
duration,
|
|
496
|
+
request: {
|
|
497
|
+
id: requestId || "unknown",
|
|
498
|
+
url: cleanUrl,
|
|
499
|
+
method,
|
|
500
|
+
headers: requestHeaders,
|
|
501
|
+
data: requestData,
|
|
502
|
+
params: params || undefined,
|
|
503
|
+
client: clientType
|
|
504
|
+
},
|
|
505
|
+
response: {
|
|
506
|
+
status: response.status,
|
|
507
|
+
statusText: response.statusText,
|
|
508
|
+
headers: responseHeaders,
|
|
509
|
+
body,
|
|
510
|
+
size: responseSize
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
return response;
|
|
514
|
+
} catch (error) {
|
|
515
|
+
const duration = Date.now() - startTime;
|
|
516
|
+
|
|
517
|
+
// Emit error event
|
|
518
|
+
self.emit({
|
|
519
|
+
type: "error",
|
|
520
|
+
timestamp: new Date(),
|
|
521
|
+
duration,
|
|
522
|
+
request: {
|
|
523
|
+
id: requestId || "unknown",
|
|
524
|
+
url: cleanUrl,
|
|
525
|
+
method,
|
|
526
|
+
headers: requestHeaders,
|
|
527
|
+
data: requestData,
|
|
528
|
+
params: params || undefined,
|
|
529
|
+
client: clientType
|
|
530
|
+
},
|
|
531
|
+
error: {
|
|
532
|
+
message: error instanceof Error ? error.message : "Network error",
|
|
533
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// Swizzle XMLHttpRequest
|
|
541
|
+
XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
|
|
542
|
+
// Store request info on the xhr instance
|
|
543
|
+
const xhr = this;
|
|
544
|
+
xhr._requestId = `xhr_${++self.requestCounter}`;
|
|
545
|
+
xhr._method = method;
|
|
546
|
+
xhr._url = url;
|
|
547
|
+
xhr._startTime = Date.now();
|
|
548
|
+
xhr._requestHeaders = {};
|
|
549
|
+
return self.originalXHROpen.call(this, method, url, async, user, password);
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// Track request headers
|
|
553
|
+
XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
|
|
554
|
+
const xhr = this;
|
|
555
|
+
if (xhr._requestHeaders) {
|
|
556
|
+
xhr._requestHeaders[header] = value;
|
|
557
|
+
}
|
|
558
|
+
return self.originalXHRSetRequestHeader.call(this, header, value);
|
|
559
|
+
};
|
|
560
|
+
XMLHttpRequest.prototype.send = function (
|
|
561
|
+
// @ts-ignore - this does exist on native
|
|
562
|
+
data) {
|
|
563
|
+
const xhr = this;
|
|
564
|
+
const requestId = xhr._requestId;
|
|
565
|
+
const method = xhr._method || "GET";
|
|
566
|
+
const url = xhr._url || "";
|
|
567
|
+
const startTime = xhr._startTime;
|
|
568
|
+
const requestHeaders = xhr._requestHeaders || {};
|
|
569
|
+
|
|
570
|
+
// Skip ignored URLs
|
|
571
|
+
if (self.shouldIgnoreUrl(url)) {
|
|
572
|
+
return self.originalXHRSend.call(this, data);
|
|
573
|
+
}
|
|
574
|
+
const {
|
|
575
|
+
url: cleanUrl,
|
|
576
|
+
params
|
|
577
|
+
} = self.parseUrl(url);
|
|
578
|
+
|
|
579
|
+
// Parse request data
|
|
580
|
+
let requestData;
|
|
581
|
+
if (data) {
|
|
582
|
+
if (typeof data === "string") {
|
|
583
|
+
try {
|
|
584
|
+
requestData = JSON.parse(data);
|
|
585
|
+
} catch {
|
|
586
|
+
requestData = data;
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
requestData = data;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Determine client type from X-Request-Client header
|
|
594
|
+
const clientType = requestHeaders["X-Request-Client"] || requestHeaders["x-request-client"] || "axios";
|
|
595
|
+
|
|
596
|
+
// Emit request event
|
|
597
|
+
self.emit({
|
|
598
|
+
type: "request",
|
|
599
|
+
timestamp: new Date(),
|
|
600
|
+
request: {
|
|
601
|
+
id: requestId || "unknown",
|
|
602
|
+
url: cleanUrl,
|
|
603
|
+
method,
|
|
604
|
+
headers: requestHeaders,
|
|
605
|
+
data: requestData,
|
|
606
|
+
params: params || undefined,
|
|
607
|
+
client: clientType
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Track if we've already processed this request to avoid duplicate events
|
|
612
|
+
let processed = false;
|
|
613
|
+
const processResponse = (isError = false) => {
|
|
614
|
+
if (processed) return;
|
|
615
|
+
processed = true;
|
|
616
|
+
self.handleXHRResponse(this, requestId || "unknown", cleanUrl, method, requestHeaders, requestData, params, startTime || 0, isError, clientType);
|
|
617
|
+
// Clean up event listeners after processing to prevent memory leaks
|
|
618
|
+
cleanup();
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// Cleanup function to remove event listeners
|
|
622
|
+
const cleanup = () => {
|
|
623
|
+
this.removeEventListener("load", loadListener);
|
|
624
|
+
this.removeEventListener("error", errorListener);
|
|
625
|
+
this.removeEventListener("abort", abortListener);
|
|
626
|
+
this.removeEventListener("readystatechange", readyStateListener);
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// Use addEventListener to listen to events WITHOUT replacing user handlers
|
|
630
|
+
// This is critical because React Native's XMLHttpRequest uses EventTarget
|
|
631
|
+
// with getters/setters that shouldn't be overridden
|
|
632
|
+
|
|
633
|
+
const loadListener = () => {
|
|
634
|
+
processResponse(false);
|
|
635
|
+
};
|
|
636
|
+
const errorListener = () => {
|
|
637
|
+
processResponse(true);
|
|
638
|
+
};
|
|
639
|
+
const abortListener = () => {
|
|
640
|
+
processResponse(true);
|
|
641
|
+
};
|
|
642
|
+
const readyStateListener = () => {
|
|
643
|
+
if (this.readyState === 4 && !processed) {
|
|
644
|
+
processResponse(false);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// Add event listeners that will fire alongside user handlers
|
|
649
|
+
this.addEventListener("load", loadListener);
|
|
650
|
+
this.addEventListener("error", errorListener);
|
|
651
|
+
this.addEventListener("abort", abortListener);
|
|
652
|
+
this.addEventListener("readystatechange", readyStateListener);
|
|
653
|
+
return self.originalXHRSend.call(this, data);
|
|
654
|
+
};
|
|
655
|
+
this.isListening = true;
|
|
656
|
+
if (__DEV__) {
|
|
657
|
+
// Network listener has started monitoring fetch and XMLHttpRequest operations
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Stop listening and restore original networking methods
|
|
663
|
+
*
|
|
664
|
+
* This method restores the original fetch and XMLHttpRequest implementations,
|
|
665
|
+
* effectively disabling network monitoring.
|
|
666
|
+
*/
|
|
667
|
+
stopListening() {
|
|
668
|
+
if (!this.isListening) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Restore original methods
|
|
673
|
+
globalThis.fetch = this.originalFetch;
|
|
674
|
+
XMLHttpRequest.prototype.open = this.originalXHROpen;
|
|
675
|
+
XMLHttpRequest.prototype.send = this.originalXHRSend;
|
|
676
|
+
XMLHttpRequest.prototype.setRequestHeader = this.originalXHRSetRequestHeader;
|
|
677
|
+
this.isListening = false;
|
|
678
|
+
if (__DEV__) {
|
|
679
|
+
// Network listener has stopped monitoring and restored original methods
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Add a listener for network events
|
|
685
|
+
*
|
|
686
|
+
* @param listener - Callback function to handle network events
|
|
687
|
+
* @returns Unsubscribe function to remove the listener
|
|
688
|
+
*/
|
|
689
|
+
addListener(listener) {
|
|
690
|
+
this.listeners.push(listener);
|
|
691
|
+
|
|
692
|
+
// Return unsubscribe function
|
|
693
|
+
return () => {
|
|
694
|
+
const index = this.listeners.indexOf(listener);
|
|
695
|
+
if (index > -1) {
|
|
696
|
+
this.listeners.splice(index, 1);
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Remove all listeners
|
|
702
|
+
removeAllListeners() {
|
|
703
|
+
this.listeners = [];
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Check if currently listening
|
|
707
|
+
get isActive() {
|
|
708
|
+
return this.isListening;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Get number of active listeners
|
|
712
|
+
get listenerCount() {
|
|
713
|
+
return this.listeners.length;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Lazy singleton instance holder for NetworkListener
|
|
719
|
+
*
|
|
720
|
+
* This pattern ensures only one NetworkListener instance exists throughout
|
|
721
|
+
* the application lifecycle while deferring instantiation until first use.
|
|
722
|
+
*/
|
|
723
|
+
let _networkListener = null;
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Get or create the singleton NetworkListener instance
|
|
727
|
+
*
|
|
728
|
+
* @returns The singleton NetworkListener instance
|
|
729
|
+
*/
|
|
730
|
+
const getNetworkListener = () => {
|
|
731
|
+
if (!_networkListener) {
|
|
732
|
+
_networkListener = new NetworkListener();
|
|
733
|
+
}
|
|
734
|
+
return _networkListener;
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Access function for the singleton NetworkListener instance
|
|
739
|
+
*
|
|
740
|
+
* @returns Function that returns the NetworkListener instance
|
|
741
|
+
*/
|
|
742
|
+
const networkListener = exports.networkListener = getNetworkListener;
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Start network traffic monitoring
|
|
746
|
+
*
|
|
747
|
+
* @example
|
|
748
|
+
* ```typescript
|
|
749
|
+
* startNetworkListener();
|
|
750
|
+
* console.log('Network monitoring started');
|
|
751
|
+
* ```
|
|
752
|
+
*/
|
|
753
|
+
const startNetworkListener = () => getNetworkListener().startListening();
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Stop network traffic monitoring
|
|
757
|
+
*/
|
|
758
|
+
exports.startNetworkListener = startNetworkListener;
|
|
759
|
+
const stopNetworkListener = () => getNetworkListener().stopListening();
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Add a listener for network events
|
|
763
|
+
*
|
|
764
|
+
* @param listener - Callback function to handle network events
|
|
765
|
+
* @returns Unsubscribe function to remove the listener
|
|
766
|
+
*
|
|
767
|
+
* @example
|
|
768
|
+
* ```typescript
|
|
769
|
+
* const unsubscribe = addNetworkListener((event) => {
|
|
770
|
+
* console.log(`Network ${event.type}:`, event.request.url);
|
|
771
|
+
* });
|
|
772
|
+
*
|
|
773
|
+
* // Later...
|
|
774
|
+
* unsubscribe();
|
|
775
|
+
* ```
|
|
776
|
+
*/
|
|
777
|
+
exports.stopNetworkListener = stopNetworkListener;
|
|
778
|
+
const addNetworkListener = listener => getNetworkListener().addListener(listener);
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Remove all registered network event listeners
|
|
782
|
+
*/
|
|
783
|
+
exports.addNetworkListener = addNetworkListener;
|
|
784
|
+
const removeAllNetworkListeners = () => getNetworkListener().removeAllListeners();
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Check if network monitoring is currently active
|
|
788
|
+
*
|
|
789
|
+
* @returns True if currently intercepting network traffic
|
|
790
|
+
*/
|
|
791
|
+
exports.removeAllNetworkListeners = removeAllNetworkListeners;
|
|
792
|
+
const isNetworkListening = () => getNetworkListener().isActive;
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Get the number of registered network event listeners
|
|
796
|
+
*
|
|
797
|
+
* @returns Number of active listeners
|
|
798
|
+
*/
|
|
799
|
+
exports.isNetworkListening = isNetworkListening;
|
|
800
|
+
const getNetworkListenerCount = () => getNetworkListener().listenerCount;
|
|
801
|
+
exports.getNetworkListenerCount = getNetworkListenerCount;
|