@rozenite/network-activity-plugin 1.0.0-alpha.1 → 1.0.0-alpha.4

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/project.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
3
+ "name": "@rozenite/network-activity-plugin",
4
+ "targets": {
5
+ "build": {
6
+ "cache": true,
7
+ "dependsOn": ["^build"],
8
+ "inputs": ["{projectRoot}/src/**/*"],
9
+ "outputs": ["{projectRoot}/dist"]
10
+ }
11
+ }
12
+ }
package/react-native.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export let useNetworkActivityDevTools: typeof import('./src/react-native/useNetworkActivityDevTools').useNetworkActivityDevTools;
2
2
 
3
3
  if (process.env.NODE_ENV !== 'production') {
4
- useNetworkActivityDevTools = require('./src/react-native/useNetworkActivityDevTools').useNetworkActivityDevTools;
4
+ useNetworkActivityDevTools =
5
+ require('./src/react-native/useNetworkActivityDevTools').useNetworkActivityDevTools;
5
6
  } else {
6
7
  useNetworkActivityDevTools = () => null;
7
8
  }
@@ -3,6 +3,6 @@ export default {
3
3
  {
4
4
  name: 'Network Activity',
5
5
  source: './src/ui/panel.tsx',
6
- }
6
+ },
7
7
  ],
8
8
  };
@@ -1,4 +1,4 @@
1
1
  declare module '*.module.css' {
2
2
  const classes: { [key: string]: string };
3
3
  export default classes;
4
- }
4
+ }
@@ -0,0 +1,391 @@
1
+ import { NetworkActivityDevToolsClient } from '../types/client';
2
+ import { getNetworkRequestsRegistry } from './network-requests-registry';
3
+ import { XHRInterceptor } from './xhr-interceptor';
4
+
5
+ const networkRequestsRegistry = getNetworkRequestsRegistry();
6
+
7
+ const mimeTypeFromResponseType = (responseType: string): string | undefined => {
8
+ switch (responseType) {
9
+ case 'arraybuffer':
10
+ case 'blob':
11
+ case 'base64':
12
+ return 'application/octet-stream';
13
+ case 'text':
14
+ case '':
15
+ return 'text/plain';
16
+ case 'json':
17
+ return 'application/json';
18
+ case 'document':
19
+ return 'text/html';
20
+ }
21
+
22
+ return undefined;
23
+ };
24
+
25
+ const parseHeaders = (headersString: string): Record<string, string> => {
26
+ const headers: Record<string, string> = {};
27
+ if (!headersString) return headers;
28
+
29
+ const lines = headersString.split('\r\n');
30
+ for (const line of lines) {
31
+ const colonIndex = line.indexOf(':');
32
+ if (colonIndex > 0) {
33
+ const key = line.substring(0, colonIndex).trim();
34
+ const value = line.substring(colonIndex + 1).trim();
35
+ headers[key] = value;
36
+ }
37
+ }
38
+ return headers;
39
+ };
40
+
41
+ const getResponseBody = async (
42
+ request: XMLHttpRequest
43
+ ): Promise<{ body: string; base64Encoded: boolean }> => {
44
+ try {
45
+ if (request.responseType === 'arraybuffer') {
46
+ const arrayBuffer = request.response as ArrayBuffer;
47
+ return {
48
+ body: btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))),
49
+ base64Encoded: true,
50
+ };
51
+ }
52
+
53
+ if (request.responseType === 'blob') {
54
+ const contentType = request.getResponseHeader('Content-Type') || '';
55
+
56
+ if (
57
+ contentType.startsWith('text/') ||
58
+ contentType.startsWith('application/json')
59
+ ) {
60
+ return new Promise((resolve) => {
61
+ const reader = new FileReader();
62
+ reader.onload = () => {
63
+ resolve({
64
+ body: reader.result as string,
65
+ base64Encoded: false,
66
+ });
67
+ };
68
+ reader.readAsText(request.response);
69
+ });
70
+ }
71
+ }
72
+
73
+ if (request.responseType === 'text') {
74
+ return {
75
+ body: request.responseText || request.response || '',
76
+ base64Encoded: false,
77
+ };
78
+ }
79
+
80
+ return {
81
+ body: request.responseText || request.response || '',
82
+ base64Encoded: false,
83
+ };
84
+ } catch (error) {
85
+ return {
86
+ body: `[Error reading response: ${error}]`,
87
+ base64Encoded: false,
88
+ };
89
+ }
90
+ };
91
+
92
+ const findRequestId = (request: XMLHttpRequest): string | null => {
93
+ const allRequests = networkRequestsRegistry.getAllEntries();
94
+ const entry = allRequests.find(({ request: req }) => req === request);
95
+ return entry?.id ?? null;
96
+ };
97
+
98
+ const getInitiatorFromStack = (): {
99
+ type: string;
100
+ url?: string;
101
+ lineNumber?: number;
102
+ columnNumber?: number;
103
+ } => {
104
+ try {
105
+ const stack = new Error().stack;
106
+ if (!stack) {
107
+ return { type: 'other' };
108
+ }
109
+
110
+ const line = stack.split('\n')[9];
111
+ const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
112
+ if (match) {
113
+ return {
114
+ type: 'script',
115
+ url: match[2],
116
+ lineNumber: parseInt(match[3]),
117
+ columnNumber: parseInt(match[4]),
118
+ };
119
+ }
120
+ } catch {
121
+ // Ignore stack parsing errors
122
+ }
123
+
124
+ return { type: 'other' };
125
+ };
126
+
127
+ export type NetworkInspector = {
128
+ enable: () => void;
129
+ disable: () => void;
130
+ isEnabled: () => boolean;
131
+ dispose: () => void;
132
+ };
133
+
134
+ export const getNetworkInspector = (
135
+ pluginClient: NetworkActivityDevToolsClient
136
+ ): NetworkInspector => {
137
+ const generateRequestId = (): string => {
138
+ return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
139
+ };
140
+
141
+ const generateLoaderId = (): string => {
142
+ return `loader_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
143
+ };
144
+
145
+ const enable = () => {
146
+ XHRInterceptor.disableInterception();
147
+
148
+ XHRInterceptor.setOpenCallback(
149
+ (method: string, url: string, request: XMLHttpRequest) => {
150
+ const requestId = generateRequestId();
151
+ const loaderId = generateLoaderId();
152
+ const startTime = Date.now();
153
+ const initiator = getInitiatorFromStack();
154
+
155
+ // Store request in registry with metadata
156
+ networkRequestsRegistry.addEntry(requestId, request, {
157
+ id: requestId,
158
+ loaderId,
159
+ documentURL:
160
+ typeof document !== 'undefined' ? document.URL : undefined,
161
+ method,
162
+ url,
163
+ headers: request?._headers || {},
164
+ startTime,
165
+ status: 'pending',
166
+ type: 'XHR',
167
+ initiator,
168
+ });
169
+ }
170
+ );
171
+
172
+ XHRInterceptor.setSendCallback((data: string, request: XMLHttpRequest) => {
173
+ const requestId = findRequestId(request);
174
+ if (!requestId) return;
175
+
176
+ const entry = networkRequestsRegistry.getEntry(requestId);
177
+ if (!entry) return;
178
+
179
+ const metadata = entry.metadata;
180
+
181
+ // Update metadata with post data
182
+ networkRequestsRegistry.updateEntry(requestId, {
183
+ postData: data,
184
+ hasPostData: !!data,
185
+ headers: request?._headers || {},
186
+ });
187
+
188
+ // Send Network.requestWillBeSent event
189
+ pluginClient.send('Network.requestWillBeSent', {
190
+ requestId,
191
+ loaderId: metadata.loaderId || '',
192
+ documentURL: metadata.documentURL || '',
193
+ request: {
194
+ url: metadata.url,
195
+ method: metadata.method,
196
+ headers: metadata.headers,
197
+ postData: data,
198
+ hasPostData: !!data,
199
+ },
200
+ timestamp: metadata.startTime,
201
+ wallTime: metadata.startTime,
202
+ initiator: metadata.initiator || { type: 'other' },
203
+ type: metadata.type,
204
+ });
205
+ });
206
+
207
+ XHRInterceptor.setHeaderReceivedCallback(
208
+ (
209
+ responseContentType: string | void,
210
+ responseSize: number | void,
211
+ allHeaders: string,
212
+ request: XMLHttpRequest
213
+ ) => {
214
+ const requestId = findRequestId(request);
215
+ if (!requestId) return;
216
+
217
+ const entry = networkRequestsRegistry.getEntry(requestId);
218
+ if (!entry) return;
219
+
220
+ const metadata = entry.metadata;
221
+ const headers = parseHeaders(allHeaders);
222
+ const mimeType =
223
+ responseContentType ||
224
+ mimeTypeFromResponseType(request.responseType) ||
225
+ 'text/plain';
226
+
227
+ // Update metadata with response info
228
+ networkRequestsRegistry.updateEntry(requestId, {
229
+ status: 'loading',
230
+ response: {
231
+ url: metadata.url,
232
+ status: request.status,
233
+ statusText: request.statusText,
234
+ headers,
235
+ mimeType,
236
+ encodedDataLength: responseSize || 0,
237
+ responseTime: Date.now(),
238
+ },
239
+ });
240
+
241
+ // Send Network.responseReceived event
242
+ pluginClient.send('Network.responseReceived', {
243
+ requestId,
244
+ loaderId: metadata.loaderId || '',
245
+ timestamp: Date.now(),
246
+ type: metadata.type || 'Other',
247
+ response: {
248
+ url: metadata.url,
249
+ status: request.status,
250
+ statusText: request.statusText,
251
+ headers,
252
+ mimeType,
253
+ encodedDataLength: responseSize || 0,
254
+ responseTime: Date.now(),
255
+ },
256
+ });
257
+ }
258
+ );
259
+
260
+ XHRInterceptor.setResponseCallback(
261
+ (
262
+ status: number,
263
+ timeout: number,
264
+ response: string,
265
+ responseURL: string,
266
+ responseType: string,
267
+ request: XMLHttpRequest
268
+ ) => {
269
+ const requestId = findRequestId(request);
270
+ if (!requestId) return;
271
+
272
+ const entry = networkRequestsRegistry.getEntry(requestId);
273
+ if (!entry) return;
274
+
275
+ const metadata = entry.metadata;
276
+ if (!metadata) return;
277
+
278
+ const endTime = Date.now();
279
+ const duration = endTime - metadata.startTime;
280
+ const dataLength = response ? response.length : 0;
281
+
282
+ // Update metadata with final data
283
+ networkRequestsRegistry.updateEntry(requestId, {
284
+ endTime,
285
+ duration,
286
+ dataLength,
287
+ encodedDataLength: dataLength,
288
+ });
289
+
290
+ // Check if request failed
291
+ if (status >= 400 || request.readyState === 0) {
292
+ const errorText = request.statusText || 'Request failed';
293
+ const canceled = request.readyState === 0;
294
+
295
+ // Update metadata
296
+ networkRequestsRegistry.updateEntry(requestId, {
297
+ status: 'failed',
298
+ errorText,
299
+ canceled,
300
+ });
301
+
302
+ // Send Network.loadingFailed event
303
+ pluginClient.send('Network.loadingFailed', {
304
+ requestId,
305
+ timestamp: endTime,
306
+ type: metadata.type || 'Other',
307
+ errorText,
308
+ canceled,
309
+ });
310
+ } else {
311
+ // Update metadata
312
+ networkRequestsRegistry.updateEntry(requestId, {
313
+ status: 'finished',
314
+ encodedDataLength: dataLength,
315
+ });
316
+
317
+ // Send Network.dataReceived event if there's data
318
+ if (dataLength > 0) {
319
+ pluginClient.send('Network.dataReceived', {
320
+ requestId,
321
+ timestamp: endTime,
322
+ dataLength,
323
+ encodedDataLength: dataLength,
324
+ });
325
+ }
326
+
327
+ // Send Network.loadingFinished event
328
+ pluginClient.send('Network.loadingFinished', {
329
+ requestId,
330
+ timestamp: endTime,
331
+ encodedDataLength: dataLength,
332
+ });
333
+ }
334
+ }
335
+ );
336
+
337
+ XHRInterceptor.enableInterception();
338
+ };
339
+
340
+ const disable = () => {
341
+ XHRInterceptor.disableInterception();
342
+ networkRequestsRegistry.clear();
343
+ };
344
+
345
+ const isEnabled = () => {
346
+ return XHRInterceptor.isInterceptorEnabled();
347
+ };
348
+
349
+ const enableSubscription = pluginClient.onMessage('network-enable', () => {
350
+ enable();
351
+ });
352
+
353
+ const disableSubscription = pluginClient.onMessage('network-disable', () => {
354
+ disable();
355
+ });
356
+
357
+ const handleBodySubscription = pluginClient.onMessage(
358
+ 'Network.getResponseBody',
359
+ async (payload) => {
360
+ const requestId = payload.requestId;
361
+ const entry = networkRequestsRegistry.getEntry(requestId);
362
+ if (!entry) {
363
+ return;
364
+ }
365
+
366
+ const { request } = entry;
367
+ const { body, base64Encoded } = await getResponseBody(request);
368
+
369
+ // Send Network.responseBodyReceived event
370
+ pluginClient.send('Network.responseBodyReceived', {
371
+ requestId,
372
+ body,
373
+ base64Encoded,
374
+ });
375
+ }
376
+ );
377
+
378
+ const dispose = () => {
379
+ disable();
380
+ enableSubscription.remove();
381
+ disableSubscription.remove();
382
+ handleBodySubscription.remove();
383
+ };
384
+
385
+ return {
386
+ enable,
387
+ disable,
388
+ isEnabled,
389
+ dispose,
390
+ };
391
+ };
@@ -0,0 +1,122 @@
1
+ import {
2
+ NetworkRequestId,
3
+ NetworkLoaderId,
4
+ NetworkResourceType,
5
+ NetworkRequest,
6
+ NetworkResponse,
7
+ NetworkInitiator,
8
+ } from '../types/client';
9
+
10
+ export type NetworkRequestMetadata = {
11
+ id: NetworkRequestId;
12
+ loaderId?: NetworkLoaderId;
13
+ documentURL?: string;
14
+ method: string;
15
+ url: string;
16
+ headers: Record<string, string>;
17
+ postData?: string;
18
+ hasPostData?: boolean;
19
+ type?: NetworkResourceType;
20
+ initiator?: NetworkInitiator;
21
+ startTime: number;
22
+ endTime?: number;
23
+ duration?: number;
24
+ status: 'pending' | 'loading' | 'finished' | 'failed';
25
+ response?: NetworkResponse;
26
+ errorText?: string;
27
+ canceled?: boolean;
28
+ encodedDataLength?: number;
29
+ dataLength?: number;
30
+ };
31
+
32
+ export type NetworkRegistryEntry = {
33
+ id: string;
34
+ request: XMLHttpRequest;
35
+ metadata: NetworkRequestMetadata;
36
+ sentAt: number;
37
+ };
38
+
39
+ export type NetworkRequestRegistry = {
40
+ addEntry: (
41
+ id: string,
42
+ request: XMLHttpRequest,
43
+ metadata: Partial<NetworkRequestMetadata>
44
+ ) => void;
45
+ getEntry: (id: string) => NetworkRegistryEntry | null;
46
+ updateEntry: (id: string, updates: Partial<NetworkRequestMetadata>) => void;
47
+ getAllEntries: () => Array<NetworkRegistryEntry>;
48
+ clear: () => void;
49
+ };
50
+
51
+ const REQUEST_TTL = 1000 * 60 * 5; // 5 minutes
52
+
53
+ export const getNetworkRequestsRegistry = (): NetworkRequestRegistry => {
54
+ const registry: Map<string, NetworkRegistryEntry> = new Map();
55
+
56
+ const trimRegistry = (): void => {
57
+ const now = Date.now();
58
+
59
+ registry.forEach((entry) => {
60
+ if (now - entry.sentAt < REQUEST_TTL) {
61
+ return;
62
+ }
63
+
64
+ registry.delete(entry.id);
65
+ });
66
+ };
67
+
68
+ const addEntry = (
69
+ id: string,
70
+ request: XMLHttpRequest,
71
+ metadata: Partial<NetworkRequestMetadata>
72
+ ) => {
73
+ trimRegistry();
74
+
75
+ const fullMetadata: NetworkRequestMetadata = {
76
+ id,
77
+ method: metadata.method || 'GET',
78
+ url: metadata.url || '',
79
+ headers: metadata.headers || {},
80
+ startTime: metadata.startTime || Date.now(),
81
+ status: metadata.status || 'pending',
82
+ ...metadata,
83
+ };
84
+
85
+ registry.set(id, {
86
+ id,
87
+ request,
88
+ metadata: fullMetadata,
89
+ sentAt: Date.now(),
90
+ });
91
+ };
92
+
93
+ const getEntry = (id: string) => {
94
+ return registry.get(id) ?? null;
95
+ };
96
+
97
+ const updateEntry = (
98
+ id: string,
99
+ updates: Partial<NetworkRequestMetadata>
100
+ ) => {
101
+ const entry = registry.get(id);
102
+ if (entry) {
103
+ entry.metadata = { ...entry.metadata, ...updates };
104
+ }
105
+ };
106
+
107
+ const getAllEntries = () => {
108
+ return Array.from(registry.values());
109
+ };
110
+
111
+ const clear = () => {
112
+ registry.clear();
113
+ };
114
+
115
+ return {
116
+ addEntry,
117
+ getEntry,
118
+ updateEntry,
119
+ getAllEntries,
120
+ clear,
121
+ };
122
+ };