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

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.
@@ -1,61 +1,10 @@
1
1
  import { useEffect } from 'react';
2
2
  import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
3
- import { NetworkEventMap } from '../types/network';
4
-
5
- let requestCounter = 0;
6
-
7
- function generateRequestId() {
8
- return (++requestCounter).toString();
9
- }
10
-
11
- // Helper function to convert Headers object to plain object
12
- function headersToObject(headers: HeadersInit | Record<string, string>): Record<string, string> {
13
- if (headers instanceof Headers) {
14
- const obj: Record<string, string> = {};
15
- headers.forEach((value: string, key: string) => {
16
- obj[key] = value;
17
- });
18
- return obj;
19
- } else if (Array.isArray(headers)) {
20
- const obj: Record<string, string> = {};
21
- headers.forEach(([key, value]) => {
22
- obj[key] = value;
23
- });
24
- return obj;
25
- }
26
- return headers as Record<string, string>;
27
- }
28
-
29
- // Helper function to get request info from fetch input
30
- function getRequestInfo(input: RequestInfo | URL, init?: RequestInit) {
31
- let url: string;
32
- let method: string;
33
- let headers: Record<string, string>;
34
- let body: unknown;
35
-
36
- if (typeof input === 'string') {
37
- url = input;
38
- method = init?.method || 'GET';
39
- headers = headersToObject(init?.headers || {});
40
- body = init?.body || undefined;
41
- } else if (input instanceof Request) {
42
- url = input.url;
43
- method = input.method;
44
- headers = headersToObject(input.headers);
45
- body = input.body || undefined;
46
- } else {
47
- // URL object
48
- url = input.toString();
49
- method = init?.method || 'GET';
50
- headers = headersToObject(init?.headers || {});
51
- body = init?.body || undefined;
52
- }
53
-
54
- return { url, method, headers, body };
55
- }
3
+ import { getNetworkInspector } from './network-inspector';
4
+ import { NetworkActivityEventMap } from '../types/client';
56
5
 
57
6
  export const useNetworkActivityDevTools = () => {
58
- const client = useRozeniteDevToolsClient<NetworkEventMap>({
7
+ const client = useRozeniteDevToolsClient<NetworkActivityEventMap>({
59
8
  pluginId: '@rozenite/network-activity-plugin',
60
9
  });
61
10
 
@@ -64,172 +13,12 @@ export const useNetworkActivityDevTools = () => {
64
13
  return;
65
14
  }
66
15
 
67
- const originalFetch = window.fetch.bind(window);
68
-
69
- const sendCdpEvent = (method: keyof NetworkEventMap, params: NetworkEventMap[keyof NetworkEventMap]) => {
70
- client.send(method, params);
71
- };
72
-
73
- window.fetch = async function(input: RequestInfo | URL, init?: RequestInit) {
74
- const requestId = generateRequestId();
75
- const requestInfo = getRequestInfo(input, init);
76
- const timestamp = Date.now() / 1000;
77
- const wallTime = timestamp;
78
-
79
- sendCdpEvent('Network.requestWillBeSent', {
80
- requestId,
81
- loaderId: requestId,
82
- documentURL: '',
83
- request: {
84
- url: requestInfo.url,
85
- method: requestInfo.method,
86
- headers: requestInfo.headers,
87
- postData: requestInfo.body,
88
- hasPostData: !!requestInfo.body
89
- },
90
- timestamp,
91
- wallTime,
92
- initiator: {
93
- type: 'script',
94
- stack: {
95
- callFrames: [
96
- {
97
- functionName: 'fetch',
98
- scriptId: '1',
99
- url: '',
100
- lineNumber: 1,
101
- columnNumber: 1
102
- }
103
- ]
104
- }
105
- },
106
- redirectHasExtraInfo: false,
107
- redirectResponse: null,
108
- referrerPolicy: 'no-referrer',
109
- type: 'Fetch',
110
- frameId: '1',
111
- hasUserGesture: false
112
- });
113
-
114
- sendCdpEvent('Network.requestWillBeSentExtraInfo', {
115
- requestId,
116
- blockedCookies: [],
117
- headers: requestInfo.headers,
118
- connectTiming: {
119
- requestTime: timestamp
120
- },
121
- clientSecurityState: {
122
- initiatorIsSecureContext: false,
123
- initiatorIPAddressSpace: 'Public',
124
- privateNetworkRequestPolicy: 'Allow'
125
- },
126
- siteHasCookieInOtherPartition: false
127
- });
128
-
129
- let response;
130
- try {
131
- response = await originalFetch(input, init);
132
-
133
- // Get response headers
134
- const responseHeaders: Record<string, string> = {};
135
- response.headers.forEach((value: string, key: string) => {
136
- responseHeaders[key] = value;
137
- });
138
-
139
- const contentType = response.headers.get('content-type') || '';
140
- const contentLength = response.headers.get('content-length');
141
- const encodedDataLength = contentLength ? parseInt(contentLength, 10) : 0;
142
-
143
- // Get decoded body size (actual response size)
144
- let decodedBodySize = encodedDataLength;
145
- try {
146
- const responseClone = response.clone();
147
- const arrayBuffer = await responseClone.arrayBuffer();
148
- decodedBodySize = arrayBuffer.byteLength;
149
- } catch {
150
- // Fallback to content-length if we can't read the response
151
- decodedBodySize = encodedDataLength;
152
- }
153
-
154
- sendCdpEvent('Network.responseReceived', {
155
- requestId,
156
- loaderId: requestId,
157
- timestamp: Date.now() / 1000,
158
- type: 'Fetch',
159
- response: {
160
- url: requestInfo.url,
161
- status: response.status,
162
- statusText: response.statusText,
163
- headers: responseHeaders,
164
- mimeType: contentType,
165
- requestHeaders: requestInfo.headers,
166
- requestHeadersText: undefined,
167
- connectionReused: false,
168
- connectionId: 0,
169
- remoteIPAddress: undefined,
170
- remotePort: undefined,
171
- protocol: undefined,
172
- securityState: 'unknown',
173
- encodedDataLength,
174
- timing: {
175
- requestTime: timestamp,
176
- proxyStart: -1,
177
- proxyEnd: -1,
178
- dnsStart: -1,
179
- dnsEnd: -1,
180
- connectStart: -1,
181
- connectEnd: -1,
182
- sslStart: -1,
183
- sslEnd: -1,
184
- workerStart: -1,
185
- workerReadyStart: -1,
186
- workerReadyEnd: -1,
187
- sendStart: timestamp,
188
- sendEnd: timestamp,
189
- pushStart: -1,
190
- pushEnd: -1,
191
- receiveHeadersEnd: Date.now() / 1000
192
- },
193
- responseTime: Date.now() / 1000,
194
- fromDiskCache: false,
195
- fromServiceWorker: false,
196
- fromPrefetchCache: false,
197
- encodedBodySize: encodedDataLength,
198
- decodedBodySize: decodedBodySize,
199
- headersText: undefined,
200
- serviceWorkerResponseSource: undefined,
201
- responseSource: 'network',
202
- statusCode: response.status
203
- },
204
- hasExtraInfo: false
205
- });
206
-
207
- sendCdpEvent('Network.loadingFinished', {
208
- requestId,
209
- timestamp: Date.now() / 1000,
210
- encodedDataLength,
211
- shouldReportCorbBlocking: false
212
- });
213
-
214
- return response;
215
- } catch (err) {
216
- sendCdpEvent('Network.loadingFailed', {
217
- requestId,
218
- timestamp: Date.now() / 1000,
219
- type: 'Fetch',
220
- errorText: err instanceof Error ? err.message : 'Unknown error',
221
- canceled: false,
222
- blockedReason: undefined
223
- });
224
-
225
- throw err;
226
- }
227
- };
16
+ const networkInspector = getNetworkInspector(client);
228
17
 
229
18
  return () => {
230
- window.fetch = originalFetch;
19
+ networkInspector.dispose();
231
20
  };
232
21
  }, [client]);
233
22
 
234
23
  return client;
235
- };
24
+ };
@@ -0,0 +1,211 @@
1
+ /* eslint-disable prefer-rest-params */
2
+
3
+ /**
4
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ *
9
+ * Source: https://github.com/facebook/react-native/blob/2c683c5787dd03ac15d2aad45dcc53650529ee7f/packages/react-native/src/private/devsupport/devmenu/elementinspector/XHRInterceptor.js
10
+ */
11
+
12
+ const originalXHROpen = XMLHttpRequest.prototype.open;
13
+ const originalXHRSend = XMLHttpRequest.prototype.send;
14
+ const originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
15
+
16
+ type XHRInterceptorOpenCallback = (
17
+ method: string,
18
+ url: string,
19
+ request: XMLHttpRequest
20
+ ) => void;
21
+
22
+ type XHRInterceptorSendCallback = (
23
+ data: string,
24
+ request: XMLHttpRequest
25
+ ) => void;
26
+
27
+ type XHRInterceptorRequestHeaderCallback = (
28
+ header: string,
29
+ value: string,
30
+ request: XMLHttpRequest
31
+ ) => void;
32
+
33
+ type XHRInterceptorHeaderReceivedCallback = (
34
+ responseContentType: string | void,
35
+ responseSize: number | void,
36
+ allHeaders: string,
37
+ request: XMLHttpRequest
38
+ ) => void;
39
+
40
+ type XHRInterceptorResponseCallback = (
41
+ status: number,
42
+ timeout: number,
43
+ response: string,
44
+ responseURL: string,
45
+ responseType: string,
46
+ request: XMLHttpRequest
47
+ ) => void;
48
+
49
+ let openCallback: XHRInterceptorOpenCallback | null;
50
+ let sendCallback: XHRInterceptorSendCallback | null;
51
+ let requestHeaderCallback: XHRInterceptorRequestHeaderCallback | null;
52
+ let headerReceivedCallback: XHRInterceptorHeaderReceivedCallback | null;
53
+ let responseCallback: XHRInterceptorResponseCallback | null;
54
+
55
+ let isInterceptorEnabled = false;
56
+
57
+ /**
58
+ * A network interceptor which monkey-patches XMLHttpRequest methods
59
+ * to gather all network requests/responses, in order to show their
60
+ * information in the React Native inspector development tool.
61
+ * This supports interception with XMLHttpRequest API, including Fetch API
62
+ * and any other third party libraries that depend on XMLHttpRequest.
63
+ */
64
+ export const XHRInterceptor = {
65
+ /**
66
+ * Invoked before XMLHttpRequest.open(...) is called.
67
+ */
68
+ setOpenCallback(callback: XHRInterceptorOpenCallback) {
69
+ openCallback = callback;
70
+ },
71
+
72
+ /**
73
+ * Invoked before XMLHttpRequest.send(...) is called.
74
+ */
75
+ setSendCallback(callback: XHRInterceptorSendCallback) {
76
+ sendCallback = callback;
77
+ },
78
+
79
+ /**
80
+ * Invoked after xhr's readyState becomes xhr.HEADERS_RECEIVED.
81
+ */
82
+ setHeaderReceivedCallback(callback: XHRInterceptorHeaderReceivedCallback) {
83
+ headerReceivedCallback = callback;
84
+ },
85
+
86
+ /**
87
+ * Invoked after xhr's readyState becomes xhr.DONE.
88
+ */
89
+ setResponseCallback(callback: XHRInterceptorResponseCallback) {
90
+ responseCallback = callback;
91
+ },
92
+
93
+ /**
94
+ * Invoked before XMLHttpRequest.setRequestHeader(...) is called.
95
+ */
96
+ setRequestHeaderCallback(callback: XHRInterceptorRequestHeaderCallback) {
97
+ requestHeaderCallback = callback;
98
+ },
99
+
100
+ isInterceptorEnabled(): boolean {
101
+ return isInterceptorEnabled;
102
+ },
103
+
104
+ enableInterception() {
105
+ if (isInterceptorEnabled) {
106
+ return;
107
+ }
108
+ // Override `open` method for all XHR requests to intercept the request
109
+ // method and url, then pass them through the `openCallback`.
110
+ // $FlowFixMe[cannot-write]
111
+ // $FlowFixMe[missing-this-annot]
112
+ XMLHttpRequest.prototype.open = function (method: string, url: string) {
113
+ if (openCallback) {
114
+ openCallback(method, url, this);
115
+ }
116
+ // @ts-expect-error - Needed for the originalXHROpen to work
117
+ originalXHROpen.apply(this, arguments);
118
+ };
119
+
120
+ // Override `setRequestHeader` method for all XHR requests to intercept
121
+ // the request headers, then pass them through the `requestHeaderCallback`.
122
+ // $FlowFixMe[cannot-write]
123
+ // $FlowFixMe[missing-this-annot]
124
+ XMLHttpRequest.prototype.setRequestHeader = function (
125
+ header: string,
126
+ value: string
127
+ ) {
128
+ if (requestHeaderCallback) {
129
+ requestHeaderCallback(header, value, this);
130
+ }
131
+ // @ts-expect-error - Needed for the originalXHRSetRequestHeader to work
132
+ originalXHRSetRequestHeader.apply(this, arguments);
133
+ };
134
+
135
+ // Override `send` method of all XHR requests to intercept the data sent,
136
+ // register listeners to intercept the response, and invoke the callbacks.
137
+ // $FlowFixMe[cannot-write]
138
+ // $FlowFixMe[missing-this-annot]
139
+ XMLHttpRequest.prototype.send = function (data: string) {
140
+ if (sendCallback) {
141
+ sendCallback(data, this);
142
+ }
143
+ if (this.addEventListener) {
144
+ this.addEventListener(
145
+ 'readystatechange',
146
+ () => {
147
+ if (!isInterceptorEnabled) {
148
+ return;
149
+ }
150
+ if (this.readyState === this.HEADERS_RECEIVED) {
151
+ const contentTypeString = this.getResponseHeader('Content-Type');
152
+ const contentLengthString =
153
+ this.getResponseHeader('Content-Length');
154
+ let responseContentType, responseSize;
155
+ if (contentTypeString) {
156
+ responseContentType = contentTypeString.split(';')[0];
157
+ }
158
+ if (contentLengthString) {
159
+ responseSize = parseInt(contentLengthString, 10);
160
+ }
161
+ if (headerReceivedCallback) {
162
+ headerReceivedCallback(
163
+ responseContentType,
164
+ responseSize,
165
+ this.getAllResponseHeaders(),
166
+ this
167
+ );
168
+ }
169
+ }
170
+ if (this.readyState === this.DONE) {
171
+ if (responseCallback) {
172
+ responseCallback(
173
+ this.status,
174
+ this.timeout,
175
+ this.response,
176
+ this.responseURL,
177
+ this.responseType,
178
+ this
179
+ );
180
+ }
181
+ }
182
+ },
183
+ false
184
+ );
185
+ }
186
+
187
+ // @ts-expect-error - Needed for the originalXHRSend to work
188
+ originalXHRSend.apply(this, arguments);
189
+ };
190
+ isInterceptorEnabled = true;
191
+ },
192
+
193
+ // Unpatch XMLHttpRequest methods and remove the callbacks.
194
+ disableInterception() {
195
+ if (!isInterceptorEnabled) {
196
+ return;
197
+ }
198
+ isInterceptorEnabled = false;
199
+ // $FlowFixMe[cannot-write]
200
+ XMLHttpRequest.prototype.send = originalXHRSend;
201
+ // $FlowFixMe[cannot-write]
202
+ XMLHttpRequest.prototype.open = originalXHROpen;
203
+ // $FlowFixMe[cannot-write]
204
+ XMLHttpRequest.prototype.setRequestHeader = originalXHRSetRequestHeader;
205
+ responseCallback = null;
206
+ openCallback = null;
207
+ sendCallback = null;
208
+ headerReceivedCallback = null;
209
+ requestHeaderCallback = null;
210
+ },
211
+ };
@@ -0,0 +1,23 @@
1
+ declare global {
2
+ interface XMLHttpRequest {
3
+ _requestId?: number;
4
+ _subscriptions: Array<EventSubscription>;
5
+ _aborted: boolean;
6
+ _cachedResponse: Response;
7
+ _hasError: boolean;
8
+ _headers: { [key: string]: string };
9
+ _lowerCaseResponseHeaders: { [key: string]: string };
10
+ _method?: string | null;
11
+ _perfKey?: string | null;
12
+ _responseType: ResponseType;
13
+ _response: string;
14
+ _sent: boolean;
15
+ _url?: string | null;
16
+ _timedOut: boolean;
17
+ _trackingName?: string;
18
+ _incrementalEvents: boolean;
19
+ _startTime?: number | null;
20
+ }
21
+ }
22
+
23
+ export {};
@@ -0,0 +1,111 @@
1
+ import { RozeniteDevToolsClient } from '@rozenite/plugin-bridge';
2
+ import { NetworkEntry } from './network';
3
+
4
+ // CDP Network types based on Chrome DevTools Protocol - limited to XMLHttpRequest capturable properties
5
+ export type NetworkRequestId = string;
6
+ export type NetworkLoaderId = string;
7
+ export type NetworkTimeSinceEpoch = number;
8
+ export type NetworkMonotonicTime = number;
9
+
10
+ export type NetworkRequest = {
11
+ url: string;
12
+ method: string;
13
+ headers: Record<string, string>;
14
+ postData?: string;
15
+ hasPostData?: boolean;
16
+ };
17
+
18
+ export type NetworkResponse = {
19
+ url: string;
20
+ status: number;
21
+ statusText: string;
22
+ headers: Record<string, string>;
23
+ mimeType: string;
24
+ encodedDataLength: number;
25
+ responseTime: NetworkTimeSinceEpoch;
26
+ };
27
+
28
+ export type NetworkInitiator = {
29
+ type: string;
30
+ url?: string;
31
+ lineNumber?: number;
32
+ columnNumber?: number;
33
+ };
34
+
35
+ export type NetworkResourceType =
36
+ | 'Document'
37
+ | 'Stylesheet'
38
+ | 'Image'
39
+ | 'Media'
40
+ | 'Font'
41
+ | 'Script'
42
+ | 'XHR'
43
+ | 'Fetch'
44
+ | 'EventSource'
45
+ | 'WebSocket'
46
+ | 'Manifest'
47
+ | 'Other'
48
+ | 'Ping'
49
+ | 'CSPViolationReport'
50
+ | 'Preflight'
51
+ | 'Subresource';
52
+
53
+ export type NetworkActivityEventMap = {
54
+ // Control events
55
+ 'network-enable': unknown;
56
+ 'network-disable': unknown;
57
+
58
+ // CDP Network events - limited to XMLHttpRequest capturable properties
59
+ 'Network.requestWillBeSent': {
60
+ requestId: NetworkRequestId;
61
+ loaderId: NetworkLoaderId;
62
+ documentURL: string;
63
+ request: NetworkRequest;
64
+ timestamp: NetworkMonotonicTime;
65
+ wallTime: NetworkTimeSinceEpoch;
66
+ initiator: NetworkInitiator;
67
+ type?: NetworkResourceType;
68
+ };
69
+
70
+ 'Network.responseReceived': {
71
+ requestId: NetworkRequestId;
72
+ loaderId: NetworkLoaderId;
73
+ timestamp: NetworkMonotonicTime;
74
+ type: NetworkResourceType;
75
+ response: NetworkResponse;
76
+ };
77
+
78
+ 'Network.dataReceived': {
79
+ requestId: NetworkRequestId;
80
+ timestamp: NetworkMonotonicTime;
81
+ dataLength: number;
82
+ encodedDataLength: number;
83
+ };
84
+
85
+ 'Network.loadingFinished': {
86
+ requestId: NetworkRequestId;
87
+ timestamp: NetworkMonotonicTime;
88
+ encodedDataLength: number;
89
+ };
90
+
91
+ 'Network.loadingFailed': {
92
+ requestId: NetworkRequestId;
93
+ timestamp: NetworkMonotonicTime;
94
+ type: NetworkResourceType;
95
+ errorText: string;
96
+ canceled?: boolean;
97
+ };
98
+
99
+ 'Network.getResponseBody': {
100
+ requestId: NetworkRequestId;
101
+ };
102
+
103
+ 'Network.responseBodyReceived': {
104
+ requestId: NetworkRequestId;
105
+ body: string;
106
+ base64Encoded: boolean;
107
+ };
108
+ };
109
+
110
+ export type NetworkActivityDevToolsClient =
111
+ RozeniteDevToolsClient<NetworkActivityEventMap>;