@rozenite/network-activity-plugin 1.0.0-alpha.9 → 1.1.0
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 +2 -0
- package/dist/App.html +2 -2
- package/dist/assets/{App-DoHQsY5s.css → App-BrSkOkws.css} +223 -2
- package/dist/assets/{App-CA1Fbh0I.js → App-Kyi7zHUX.js} +8188 -2671
- package/dist/react-native.cjs +4 -1
- package/dist/react-native.js +4 -1
- package/dist/rozenite.json +1 -1
- package/dist/src/react-native/config.d.ts +20 -0
- package/dist/src/react-native/http/overrides-registry.d.ts +6 -0
- package/dist/src/react-native/http/xhr-interceptor.d.ts +7 -1
- package/dist/src/react-native/sse/sse-interceptor.d.ts +2 -2
- package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -1
- package/dist/src/react-native/utils/getBlobName.d.ts +35 -0
- package/dist/src/react-native/utils/getFormDataEntries.d.ts +18 -0
- package/dist/src/shared/client.d.ts +55 -4
- package/dist/src/shared/sse-events.d.ts +4 -1
- package/dist/src/ui/components/Button.d.ts +2 -2
- package/dist/src/ui/components/CodeBlock.d.ts +3 -0
- package/dist/src/ui/components/CodeEditor.d.ts +5 -0
- package/dist/src/ui/components/CookieCard.d.ts +7 -0
- package/dist/src/ui/components/CopyRequestDropdown.d.ts +7 -0
- package/dist/src/ui/components/DropdownMenu.d.ts +27 -0
- package/dist/src/ui/components/FilterBar.d.ts +10 -0
- package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +1 -1
- package/dist/src/ui/components/KeyValueGrid.d.ts +13 -0
- package/dist/src/ui/components/OverrideResponse.d.ts +8 -0
- package/dist/src/ui/components/RequestBody.d.ts +6 -0
- package/dist/src/ui/components/RequestList.d.ts +9 -4
- package/dist/src/ui/components/ScrollArea.d.ts +3 -2
- package/dist/src/ui/components/Section.d.ts +8 -0
- package/dist/src/ui/components/Separator.d.ts +2 -1
- package/dist/src/ui/components/Tabs.d.ts +7 -0
- package/dist/src/ui/state/hooks.d.ts +4 -0
- package/dist/src/ui/state/model.d.ts +22 -7
- package/dist/src/ui/state/store.d.ts +27 -3
- package/dist/src/ui/utils/checkRequestBodyBinary.d.ts +2 -0
- package/dist/src/ui/utils/escapeShellArg.d.ts +1 -0
- package/dist/src/ui/utils/generateCurlCommand.d.ts +2 -0
- package/dist/src/ui/utils/generateFetchCall.d.ts +2 -0
- package/dist/src/ui/utils/generateMultipartBody.d.ts +4 -0
- package/dist/src/utils/applyReactNativeRequestHeadersLogic.d.ts +7 -0
- package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +9 -0
- package/dist/src/utils/cookieParser.d.ts +6 -0
- package/dist/src/utils/getContentTypeMimeType.d.ts +2 -0
- package/dist/src/utils/getHttpHeader.d.ts +5 -0
- package/dist/src/utils/getHttpHeaderValueAsString.d.ts +11 -0
- package/dist/src/utils/getStringSizeInBytes.d.ts +1 -0
- package/dist/src/utils/inferContentTypeFromPostData.d.ts +2 -0
- package/dist/src/utils/safeStringify.d.ts +1 -0
- package/dist/src/utils/typeChecks.d.ts +9 -0
- package/dist/useNetworkActivityDevTools.cjs +337 -24
- package/dist/useNetworkActivityDevTools.js +338 -25
- package/package.json +7 -4
- package/react-native.ts +6 -1
- package/src/react-native/config.ts +43 -0
- package/src/react-native/http/network-inspector.ts +190 -8
- package/src/react-native/http/overrides-registry.ts +32 -0
- package/src/react-native/http/xhr-interceptor.ts +19 -2
- package/src/react-native/sse/sse-inspector.ts +27 -5
- package/src/react-native/sse/sse-interceptor.ts +26 -8
- package/src/react-native/useNetworkActivityDevTools.ts +86 -8
- package/src/react-native/utils/getBlobName.ts +45 -0
- package/src/react-native/utils/getFormDataEntries.ts +32 -0
- package/src/react-native/utils.ts +3 -3
- package/src/shared/client.ts +81 -4
- package/src/shared/sse-events.ts +4 -1
- package/src/ui/components/Button.tsx +1 -0
- package/src/ui/components/CodeBlock.tsx +19 -0
- package/src/ui/components/CodeEditor.tsx +26 -0
- package/src/ui/components/CookieCard.tsx +64 -0
- package/src/ui/components/CopyRequestDropdown.tsx +95 -0
- package/src/ui/components/DropdownMenu.tsx +206 -0
- package/src/ui/components/FilterBar.tsx +117 -0
- package/src/ui/components/Input.tsx +1 -1
- package/src/ui/components/JsonTree.tsx +10 -3
- package/src/ui/components/JsonTreeCopyableItem.tsx +14 -10
- package/src/ui/components/KeyValueGrid.tsx +51 -0
- package/src/ui/components/OverrideResponse.tsx +132 -0
- package/src/ui/components/RequestBody.tsx +86 -0
- package/src/ui/components/RequestList.tsx +74 -14
- package/src/ui/components/ScrollArea.tsx +1 -0
- package/src/ui/components/Section.tsx +46 -0
- package/src/ui/components/SidePanel.tsx +15 -5
- package/src/ui/components/Toolbar.tsx +3 -2
- package/src/ui/globals.css +4 -0
- package/src/ui/hooks/useCopyToClipboard.ts +2 -2
- package/src/ui/state/derived.ts +2 -0
- package/src/ui/state/hooks.ts +8 -0
- package/src/ui/state/model.ts +28 -7
- package/src/ui/state/store.ts +640 -500
- package/src/ui/tabs/CookiesTab.tsx +60 -263
- package/src/ui/tabs/HeadersTab.tsx +78 -89
- package/src/ui/tabs/RequestTab.tsx +58 -46
- package/src/ui/tabs/ResponseTab.tsx +98 -67
- package/src/ui/tabs/SSEMessagesTab.tsx +50 -39
- package/src/ui/utils/checkRequestBodyBinary.ts +7 -0
- package/src/ui/utils/escapeShellArg.ts +12 -0
- package/src/ui/utils/generateCurlCommand.ts +83 -0
- package/src/ui/utils/generateFetchCall.ts +64 -0
- package/src/ui/utils/generateMultipartBody.ts +19 -0
- package/src/ui/views/InspectorView.tsx +15 -3
- package/src/utils/applyReactNativeRequestHeadersLogic.ts +30 -0
- package/src/utils/applyReactNativeResponseHeadersLogic.ts +28 -0
- package/src/utils/cookieParser.ts +126 -0
- package/src/utils/getContentTypeMimeType.ts +17 -0
- package/src/utils/getHttpHeader.ts +17 -0
- package/src/utils/getHttpHeaderValueAsString.ts +13 -0
- package/src/utils/getStringSizeInBytes.ts +3 -0
- package/src/utils/inferContentTypeFromPostData.ts +9 -0
- package/src/utils/safeStringify.ts +7 -0
- package/src/utils/typeChecks.ts +27 -0
- package/dist/src/ui/utils/getHttpHeaderValue.d.ts +0 -2
- package/src/ui/utils/getHttpHeaderValue.ts +0 -14
|
@@ -1,16 +1,121 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { safeStringify } from '../../utils/safeStringify';
|
|
2
|
+
import {
|
|
3
|
+
HttpMethod,
|
|
4
|
+
NetworkActivityDevToolsClient,
|
|
5
|
+
RequestPostData,
|
|
6
|
+
RequestTextPostData,
|
|
7
|
+
RequestBinaryPostData,
|
|
8
|
+
RequestFormDataPostData,
|
|
9
|
+
XHRPostData,
|
|
10
|
+
} from '../../shared/client';
|
|
2
11
|
import { getContentType } from '../utils';
|
|
3
12
|
import { getNetworkRequestsRegistry } from './network-requests-registry';
|
|
13
|
+
import { getBlobName } from '../utils/getBlobName';
|
|
14
|
+
import { getFormDataEntries } from '../utils/getFormDataEntries';
|
|
4
15
|
import { XHRInterceptor } from './xhr-interceptor';
|
|
16
|
+
import { getStringSizeInBytes } from '../../utils/getStringSizeInBytes';
|
|
17
|
+
import { applyReactNativeResponseHeadersLogic } from '../../utils/applyReactNativeResponseHeadersLogic';
|
|
18
|
+
import {
|
|
19
|
+
isBlob,
|
|
20
|
+
isArrayBuffer,
|
|
21
|
+
isFormData,
|
|
22
|
+
isNullOrUndefined,
|
|
23
|
+
} from '../../utils/typeChecks';
|
|
24
|
+
import { getOverridesRegistry } from './overrides-registry';
|
|
5
25
|
|
|
6
26
|
const networkRequestsRegistry = getNetworkRequestsRegistry();
|
|
27
|
+
const overridesRegistry = getOverridesRegistry();
|
|
28
|
+
|
|
29
|
+
const getBinaryPostData = (body: Blob): RequestBinaryPostData => ({
|
|
30
|
+
type: 'binary',
|
|
31
|
+
value: {
|
|
32
|
+
size: body.size,
|
|
33
|
+
type: body.type,
|
|
34
|
+
name: getBlobName(body),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const getArrayBufferPostData = (
|
|
39
|
+
body: ArrayBuffer | ArrayBufferView
|
|
40
|
+
): RequestBinaryPostData => ({
|
|
41
|
+
type: 'binary',
|
|
42
|
+
value: {
|
|
43
|
+
size: body.byteLength,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const getTextPostData = (body: unknown): RequestTextPostData => ({
|
|
48
|
+
type: 'text',
|
|
49
|
+
value: safeStringify(body),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const getFormDataPostData = (body: FormData): RequestFormDataPostData => ({
|
|
53
|
+
type: 'form-data',
|
|
54
|
+
value: getFormDataEntries(body).reduce<RequestFormDataPostData['value']>(
|
|
55
|
+
(acc, [key, value]) => {
|
|
56
|
+
if (isBlob(value)) {
|
|
57
|
+
acc[key] = getBinaryPostData(value);
|
|
58
|
+
} else if (isArrayBuffer(value)) {
|
|
59
|
+
acc[key] = getArrayBufferPostData(value);
|
|
60
|
+
} else {
|
|
61
|
+
acc[key] = getTextPostData(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return acc;
|
|
65
|
+
},
|
|
66
|
+
{}
|
|
67
|
+
),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const getRequestBody = (body: XHRPostData): RequestPostData => {
|
|
71
|
+
if (isNullOrUndefined(body)) {
|
|
72
|
+
return body;
|
|
73
|
+
}
|
|
7
74
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return request.response.size;
|
|
75
|
+
if (isBlob(body)) {
|
|
76
|
+
return getBinaryPostData(body);
|
|
11
77
|
}
|
|
12
78
|
|
|
13
|
-
|
|
79
|
+
if (isArrayBuffer(body)) {
|
|
80
|
+
return getArrayBufferPostData(body);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (isFormData(body)) {
|
|
84
|
+
return getFormDataPostData(body);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return getTextPostData(body);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const getResponseSize = (request: XMLHttpRequest): number | null => {
|
|
91
|
+
try {
|
|
92
|
+
const { responseType, response } = request;
|
|
93
|
+
|
|
94
|
+
// Handle a case of 204 where no-content was sent.
|
|
95
|
+
if (response === null) {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (responseType === '' || responseType === 'text') {
|
|
100
|
+
return getStringSizeInBytes(request.responseText);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (responseType === 'json') {
|
|
104
|
+
return getStringSizeInBytes(safeStringify(response));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (responseType === 'blob') {
|
|
108
|
+
return response.size;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (responseType === 'arraybuffer') {
|
|
112
|
+
return response.byteLength;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return 0;
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
14
119
|
};
|
|
15
120
|
|
|
16
121
|
const getResponseBody = async (
|
|
@@ -42,6 +147,10 @@ const getResponseBody = async (
|
|
|
42
147
|
}
|
|
43
148
|
}
|
|
44
149
|
|
|
150
|
+
if (responseType === 'json') {
|
|
151
|
+
return safeStringify(request.response);
|
|
152
|
+
}
|
|
153
|
+
|
|
45
154
|
return null;
|
|
46
155
|
};
|
|
47
156
|
|
|
@@ -90,7 +199,10 @@ export const getNetworkInspector = (
|
|
|
90
199
|
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
91
200
|
};
|
|
92
201
|
|
|
93
|
-
const handleRequestSend = (
|
|
202
|
+
const handleRequestSend = (
|
|
203
|
+
data: XHRPostData,
|
|
204
|
+
request: XMLHttpRequest
|
|
205
|
+
): void => {
|
|
94
206
|
const sendTime = Date.now();
|
|
95
207
|
|
|
96
208
|
const requestId = generateRequestId();
|
|
@@ -109,12 +221,22 @@ export const getNetworkInspector = (
|
|
|
109
221
|
url: request._url as string,
|
|
110
222
|
method: request._method as HttpMethod,
|
|
111
223
|
headers: request._headers,
|
|
112
|
-
postData: data,
|
|
224
|
+
postData: getRequestBody(data),
|
|
113
225
|
},
|
|
114
226
|
type: 'XHR',
|
|
115
227
|
initiator,
|
|
116
228
|
});
|
|
117
229
|
|
|
230
|
+
request.addEventListener('progress', (event) => {
|
|
231
|
+
pluginClient.send('request-progress', {
|
|
232
|
+
requestId: requestId,
|
|
233
|
+
timestamp: Date.now(),
|
|
234
|
+
loaded: event.loaded,
|
|
235
|
+
total: event.total,
|
|
236
|
+
lengthComputable: event.lengthComputable,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
118
240
|
request.addEventListener('readystatechange', () => {
|
|
119
241
|
if (request.readyState === READY_STATE_HEADERS_RECEIVED) {
|
|
120
242
|
ttfb = Date.now() - sendTime;
|
|
@@ -130,7 +252,9 @@ export const getNetworkInspector = (
|
|
|
130
252
|
url: request._url as string,
|
|
131
253
|
status: request.status,
|
|
132
254
|
statusText: request.statusText,
|
|
133
|
-
headers:
|
|
255
|
+
headers: applyReactNativeResponseHeadersLogic(
|
|
256
|
+
request.responseHeaders || {}
|
|
257
|
+
),
|
|
134
258
|
contentType: getContentType(request),
|
|
135
259
|
size: getResponseSize(request),
|
|
136
260
|
responseTime: Date.now(),
|
|
@@ -167,11 +291,69 @@ export const getNetworkInspector = (
|
|
|
167
291
|
canceled: true,
|
|
168
292
|
});
|
|
169
293
|
});
|
|
294
|
+
|
|
295
|
+
request.addEventListener('timeout', () => {
|
|
296
|
+
pluginClient.send('request-failed', {
|
|
297
|
+
requestId: requestId,
|
|
298
|
+
timestamp: Date.now(),
|
|
299
|
+
type: 'XHR',
|
|
300
|
+
error: 'Timeout',
|
|
301
|
+
canceled: false,
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const handleRequestOverride = (request: XMLHttpRequest): void => {
|
|
307
|
+
const override = overridesRegistry.getOverrideForUrl(
|
|
308
|
+
request._url as string
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
if (!override) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
request.addEventListener('readystatechange', () => {
|
|
316
|
+
if (override.body !== undefined) {
|
|
317
|
+
Object.defineProperty(request, 'responseType', {
|
|
318
|
+
writable: true,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
Object.defineProperty(request, 'response', {
|
|
322
|
+
writable: true,
|
|
323
|
+
});
|
|
324
|
+
Object.defineProperty(request, 'responseText', {
|
|
325
|
+
writable: true,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const contentType = getContentType(request);
|
|
329
|
+
|
|
330
|
+
if (contentType === 'application/json') {
|
|
331
|
+
request.responseType = 'json';
|
|
332
|
+
} else if (contentType === 'text/plain') {
|
|
333
|
+
request.responseType = 'text';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// @ts-expect-error - Mocking response
|
|
337
|
+
request.response = override.body;
|
|
338
|
+
// @ts-expect-error - Mocking responseText
|
|
339
|
+
request.responseText = override.body;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (override.status !== undefined) {
|
|
343
|
+
Object.defineProperty(request, 'status', {
|
|
344
|
+
writable: true,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// @ts-expect-error - Mocking status
|
|
348
|
+
request.status = override.status;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
170
351
|
};
|
|
171
352
|
|
|
172
353
|
const enable = () => {
|
|
173
354
|
XHRInterceptor.disableInterception();
|
|
174
355
|
XHRInterceptor.setSendCallback(handleRequestSend);
|
|
356
|
+
XHRInterceptor.setOverrideCallback(handleRequestOverride);
|
|
175
357
|
XHRInterceptor.enableInterception();
|
|
176
358
|
};
|
|
177
359
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { RequestOverride } from '../../shared/client';
|
|
2
|
+
|
|
3
|
+
export type OverridesRegistry = {
|
|
4
|
+
setOverrides: (newOverrides: [string, RequestOverride][]) => void;
|
|
5
|
+
getOverrideForUrl: (url: string) => RequestOverride | undefined;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const createOverridesRegistry = (): OverridesRegistry => {
|
|
9
|
+
let overrides = new Map<string, RequestOverride>();
|
|
10
|
+
|
|
11
|
+
const setOverrides = (newOverrides: [string, RequestOverride][]) => {
|
|
12
|
+
overrides = new Map(newOverrides);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const getOverrideForUrl = (url: string) => {
|
|
16
|
+
return overrides.get(url);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
setOverrides,
|
|
21
|
+
getOverrideForUrl,
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let registryInstance: OverridesRegistry | null = null;
|
|
26
|
+
|
|
27
|
+
export const getOverridesRegistry = (): OverridesRegistry => {
|
|
28
|
+
if (!registryInstance) {
|
|
29
|
+
registryInstance = createOverridesRegistry();
|
|
30
|
+
}
|
|
31
|
+
return registryInstance;
|
|
32
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* eslint-disable prefer-rest-params */
|
|
2
2
|
|
|
3
|
+
import { XHRPostData } from '../../shared/client';
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
5
7
|
*
|
|
@@ -9,6 +11,7 @@
|
|
|
9
11
|
* Source: https://github.com/facebook/react-native/blob/2c683c5787dd03ac15d2aad45dcc53650529ee7f/packages/react-native/src/private/devsupport/devmenu/elementinspector/XHRInterceptor.js
|
|
10
12
|
*/
|
|
11
13
|
|
|
14
|
+
const XMLHttpRequest = global.XMLHttpRequest || window.XMLHttpRequest;
|
|
12
15
|
const originalXHROpen = XMLHttpRequest.prototype.open;
|
|
13
16
|
const originalXHRSend = XMLHttpRequest.prototype.send;
|
|
14
17
|
const originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
@@ -20,7 +23,7 @@ type XHRInterceptorOpenCallback = (
|
|
|
20
23
|
) => void;
|
|
21
24
|
|
|
22
25
|
type XHRInterceptorSendCallback = (
|
|
23
|
-
data:
|
|
26
|
+
data: XHRPostData,
|
|
24
27
|
request: XMLHttpRequest
|
|
25
28
|
) => void;
|
|
26
29
|
|
|
@@ -46,11 +49,14 @@ type XHRInterceptorResponseCallback = (
|
|
|
46
49
|
request: XMLHttpRequest
|
|
47
50
|
) => void;
|
|
48
51
|
|
|
52
|
+
type XHRInterceptorOverrideCallback = (request: XMLHttpRequest) => void;
|
|
53
|
+
|
|
49
54
|
let openCallback: XHRInterceptorOpenCallback | null;
|
|
50
55
|
let sendCallback: XHRInterceptorSendCallback | null;
|
|
51
56
|
let requestHeaderCallback: XHRInterceptorRequestHeaderCallback | null;
|
|
52
57
|
let headerReceivedCallback: XHRInterceptorHeaderReceivedCallback | null;
|
|
53
58
|
let responseCallback: XHRInterceptorResponseCallback | null;
|
|
59
|
+
let overrideCallback: XHRInterceptorOverrideCallback | null;
|
|
54
60
|
|
|
55
61
|
let isInterceptorEnabled = false;
|
|
56
62
|
|
|
@@ -97,6 +103,13 @@ export const XHRInterceptor = {
|
|
|
97
103
|
requestHeaderCallback = callback;
|
|
98
104
|
},
|
|
99
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Invoked before XMLHttpRequest.send(...) is called.
|
|
108
|
+
*/
|
|
109
|
+
setOverrideCallback(callback: XHRInterceptorOverrideCallback) {
|
|
110
|
+
overrideCallback = callback;
|
|
111
|
+
},
|
|
112
|
+
|
|
100
113
|
isInterceptorEnabled(): boolean {
|
|
101
114
|
return isInterceptorEnabled;
|
|
102
115
|
},
|
|
@@ -136,10 +149,13 @@ export const XHRInterceptor = {
|
|
|
136
149
|
// register listeners to intercept the response, and invoke the callbacks.
|
|
137
150
|
// $FlowFixMe[cannot-write]
|
|
138
151
|
// $FlowFixMe[missing-this-annot]
|
|
139
|
-
XMLHttpRequest.prototype.send = function (data:
|
|
152
|
+
XMLHttpRequest.prototype.send = function (data: XHRPostData) {
|
|
140
153
|
if (sendCallback) {
|
|
141
154
|
sendCallback(data, this);
|
|
142
155
|
}
|
|
156
|
+
if (overrideCallback) {
|
|
157
|
+
overrideCallback(this);
|
|
158
|
+
}
|
|
143
159
|
if (this.addEventListener) {
|
|
144
160
|
this.addEventListener(
|
|
145
161
|
'readystatechange',
|
|
@@ -207,5 +223,6 @@ export const XHRInterceptor = {
|
|
|
207
223
|
sendCallback = null;
|
|
208
224
|
headerReceivedCallback = null;
|
|
209
225
|
requestHeaderCallback = null;
|
|
226
|
+
overrideCallback = null;
|
|
210
227
|
},
|
|
211
228
|
};
|
|
@@ -22,13 +22,14 @@ export type SSEInspector = {
|
|
|
22
22
|
export const getSSEInspector = (): SSEInspector => {
|
|
23
23
|
const eventEmitter = createNanoEvents<NanoEventsMap>();
|
|
24
24
|
|
|
25
|
-
const getRequestId = (
|
|
25
|
+
const getRequestId = (
|
|
26
|
+
eventSource: EventSourceWithInternals
|
|
27
|
+
): string | null => {
|
|
26
28
|
const requestId = eventSource._xhr?._rozeniteRequestId;
|
|
27
29
|
|
|
28
30
|
if (!requestId) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
);
|
|
31
|
+
// It means that the EventSource was created before the inspector was enabled.
|
|
32
|
+
return null;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
return requestId;
|
|
@@ -39,6 +40,11 @@ export const getSSEInspector = (): SSEInspector => {
|
|
|
39
40
|
SSEInterceptor.setOpenEventCallback((_, eventSource) => {
|
|
40
41
|
const sseEventSource = eventSource as EventSourceWithInternals;
|
|
41
42
|
const requestId = getRequestId(sseEventSource);
|
|
43
|
+
|
|
44
|
+
if (!requestId) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
const sseXhr = sseEventSource._xhr as XMLHttpRequest;
|
|
43
49
|
|
|
44
50
|
const event: SSEEvent = {
|
|
@@ -62,11 +68,18 @@ export const getSSEInspector = (): SSEInspector => {
|
|
|
62
68
|
const sseEventSource = eventSource as EventSourceWithInternals;
|
|
63
69
|
const requestId = getRequestId(sseEventSource);
|
|
64
70
|
|
|
71
|
+
if (!requestId) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
65
75
|
const event: SSEEvent = {
|
|
66
76
|
type: 'sse-message',
|
|
67
77
|
requestId,
|
|
68
78
|
timestamp: Date.now(),
|
|
69
|
-
|
|
79
|
+
payload: {
|
|
80
|
+
type: messageEvent.type,
|
|
81
|
+
data: messageEvent.data || '',
|
|
82
|
+
},
|
|
70
83
|
};
|
|
71
84
|
eventEmitter.emit('sse-message', event);
|
|
72
85
|
});
|
|
@@ -75,6 +88,10 @@ export const getSSEInspector = (): SSEInspector => {
|
|
|
75
88
|
const sseEventSource = eventSource as EventSourceWithInternals;
|
|
76
89
|
const requestId = getRequestId(sseEventSource);
|
|
77
90
|
|
|
91
|
+
if (!requestId) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
const event: SSEEvent = {
|
|
79
96
|
type: 'sse-error',
|
|
80
97
|
requestId,
|
|
@@ -92,6 +109,10 @@ export const getSSEInspector = (): SSEInspector => {
|
|
|
92
109
|
const sseEventSource = eventSource as EventSourceWithInternals;
|
|
93
110
|
const requestId = getRequestId(sseEventSource);
|
|
94
111
|
|
|
112
|
+
if (!requestId) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
95
116
|
const event: SSEEvent = {
|
|
96
117
|
type: 'sse-close',
|
|
97
118
|
requestId,
|
|
@@ -107,6 +128,7 @@ export const getSSEInspector = (): SSEInspector => {
|
|
|
107
128
|
},
|
|
108
129
|
isEnabled: () => SSEInterceptor.isInterceptorEnabled(),
|
|
109
130
|
dispose: () => {
|
|
131
|
+
SSEInterceptor.disableInterception();
|
|
110
132
|
eventEmitter.events = {};
|
|
111
133
|
},
|
|
112
134
|
on: <TEventType extends keyof SSEEventMap>(
|
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
CloseEvent,
|
|
7
7
|
TimeoutEvent,
|
|
8
8
|
ExceptionEvent,
|
|
9
|
+
EventSourceEvent,
|
|
10
|
+
CustomEvent,
|
|
9
11
|
} from 'react-native-sse';
|
|
10
12
|
import { EventSourceWithInternals } from './types';
|
|
11
13
|
import { getEventSource } from './event-source';
|
|
@@ -16,7 +18,7 @@ export type SSEInterceptorConnectCallback = (
|
|
|
16
18
|
) => void;
|
|
17
19
|
|
|
18
20
|
export type SSEInterceptorMessageCallback = (
|
|
19
|
-
event: MessageEvent
|
|
21
|
+
event: MessageEvent | CustomEvent<string>,
|
|
20
22
|
request: EventSource
|
|
21
23
|
) => void;
|
|
22
24
|
|
|
@@ -45,8 +47,12 @@ let isInterceptorEnabled = false;
|
|
|
45
47
|
|
|
46
48
|
const eventSourceClass = getEventSource();
|
|
47
49
|
|
|
48
|
-
// Store original EventSource
|
|
50
|
+
// Store original EventSource methods
|
|
49
51
|
const originalOpen = eventSourceClass.prototype.open;
|
|
52
|
+
const originalDispatch = eventSourceClass.prototype.dispatch;
|
|
53
|
+
|
|
54
|
+
// Built-in SSE event types that we don't want to capture as messages
|
|
55
|
+
const BUILT_IN_EVENT_TYPES = new Set(['open', 'error', 'close', 'done']);
|
|
50
56
|
|
|
51
57
|
/**
|
|
52
58
|
* A network interceptor which monkey-patches EventSource open method
|
|
@@ -114,12 +120,6 @@ export const SSEInterceptor = {
|
|
|
114
120
|
}
|
|
115
121
|
});
|
|
116
122
|
|
|
117
|
-
this.addEventListener('message', (event: MessageEvent) => {
|
|
118
|
-
if (messageCallback) {
|
|
119
|
-
messageCallback(event, this);
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
|
|
123
123
|
this.addEventListener(
|
|
124
124
|
'error',
|
|
125
125
|
(event: ErrorEvent | TimeoutEvent | ExceptionEvent) => {
|
|
@@ -139,6 +139,21 @@ export const SSEInterceptor = {
|
|
|
139
139
|
return originalOpen.call(this);
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
+
eventSourceClass.prototype.dispatch = function (
|
|
143
|
+
this: EventSourceWithInternals,
|
|
144
|
+
eventType: string,
|
|
145
|
+
data: EventSourceEvent<string>
|
|
146
|
+
) {
|
|
147
|
+
if (!BUILT_IN_EVENT_TYPES.has(eventType)) {
|
|
148
|
+
if (messageCallback) {
|
|
149
|
+
messageCallback(data, this);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Call original open method
|
|
154
|
+
return originalDispatch.call(this, eventType, data);
|
|
155
|
+
};
|
|
156
|
+
|
|
142
157
|
isInterceptorEnabled = true;
|
|
143
158
|
},
|
|
144
159
|
|
|
@@ -152,6 +167,9 @@ export const SSEInterceptor = {
|
|
|
152
167
|
// Restore original open method
|
|
153
168
|
eventSourceClass.prototype.open = originalOpen;
|
|
154
169
|
|
|
170
|
+
// Restore original dispatch method
|
|
171
|
+
eventSourceClass.prototype.dispatch = originalDispatch;
|
|
172
|
+
|
|
155
173
|
// Clear callbacks
|
|
156
174
|
connectCallback = null;
|
|
157
175
|
messageCallback = null;
|
|
@@ -1,32 +1,100 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
|
|
3
3
|
import { getNetworkInspector } from './http/network-inspector';
|
|
4
|
+
import { getOverridesRegistry } from './http/overrides-registry';
|
|
4
5
|
import { NetworkActivityEventMap } from '../shared/client';
|
|
5
6
|
import { getWebSocketInspector } from './websocket/websocket-inspector';
|
|
6
7
|
import { WebSocketEventMap } from '../shared/websocket-events';
|
|
7
8
|
import { UnionToTuple } from './utils';
|
|
8
9
|
import { getSSEInspector } from './sse/sse-inspector';
|
|
9
10
|
import { SSEEventMap } from '../shared/sse-events';
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_CONFIG,
|
|
13
|
+
NetworkActivityDevToolsConfig,
|
|
14
|
+
validateConfig,
|
|
15
|
+
} from './config';
|
|
16
|
+
|
|
17
|
+
const overridesRegistry = getOverridesRegistry();
|
|
18
|
+
|
|
19
|
+
export const useNetworkActivityDevTools = (
|
|
20
|
+
config: NetworkActivityDevToolsConfig = DEFAULT_CONFIG
|
|
21
|
+
) => {
|
|
22
|
+
const isRecordingEnabledRef = useRef(false);
|
|
12
23
|
const client = useRozeniteDevToolsClient<NetworkActivityEventMap>({
|
|
13
24
|
pluginId: '@rozenite/network-activity-plugin',
|
|
14
25
|
});
|
|
15
26
|
|
|
27
|
+
const isHttpInspectorEnabled = config.inspectors?.http ?? true;
|
|
28
|
+
const isWebSocketInspectorEnabled = config.inspectors?.websocket ?? true;
|
|
29
|
+
const isSSEInspectorEnabled = config.inspectors?.sse ?? true;
|
|
30
|
+
const showUrlAsName = config.clientUISettings?.showUrlAsName;
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!client) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
validateConfig(config);
|
|
38
|
+
}, [config]);
|
|
39
|
+
|
|
40
|
+
/** Persist the recording state across hot reloads */
|
|
16
41
|
useEffect(() => {
|
|
17
42
|
if (!client) {
|
|
18
43
|
return;
|
|
19
44
|
}
|
|
20
45
|
|
|
46
|
+
|
|
47
|
+
const sendClientUISettings = () => {
|
|
48
|
+
client.send('client-ui-settings', {
|
|
49
|
+
settings: {
|
|
50
|
+
showUrlAsName: showUrlAsName ?? DEFAULT_CONFIG.clientUISettings?.showUrlAsName,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const subscriptions = [
|
|
56
|
+
client.onMessage('network-enable', () => {
|
|
57
|
+
isRecordingEnabledRef.current = true;
|
|
58
|
+
}),
|
|
59
|
+
client.onMessage('network-disable', () => {
|
|
60
|
+
isRecordingEnabledRef.current = false;
|
|
61
|
+
}),
|
|
62
|
+
client.onMessage('set-overrides', (data) => {
|
|
63
|
+
overridesRegistry.setOverrides(data.overrides);
|
|
64
|
+
}),
|
|
65
|
+
|
|
66
|
+
client.onMessage('get-client-ui-settings', () => {
|
|
67
|
+
sendClientUISettings();
|
|
68
|
+
}),
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Send initial or changed values live
|
|
72
|
+
sendClientUISettings();
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
subscriptions.forEach((subscription) => subscription.remove());
|
|
76
|
+
};
|
|
77
|
+
}, [client, showUrlAsName]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!client || !isHttpInspectorEnabled) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
21
84
|
const networkInspector = getNetworkInspector(client);
|
|
22
85
|
|
|
86
|
+
// If recording was previously enabled, enable the inspector (hot reload)
|
|
87
|
+
if (isRecordingEnabledRef.current) {
|
|
88
|
+
networkInspector.enable();
|
|
89
|
+
}
|
|
90
|
+
|
|
23
91
|
return () => {
|
|
24
92
|
networkInspector.dispose();
|
|
25
93
|
};
|
|
26
|
-
}, [client]);
|
|
94
|
+
}, [client, isHttpInspectorEnabled]);
|
|
27
95
|
|
|
28
96
|
useEffect(() => {
|
|
29
|
-
if (!client) {
|
|
97
|
+
if (!client || !isWebSocketInspectorEnabled) {
|
|
30
98
|
return;
|
|
31
99
|
}
|
|
32
100
|
|
|
@@ -55,14 +123,19 @@ export const useNetworkActivityDevTools = () => {
|
|
|
55
123
|
websocketInspector.disable();
|
|
56
124
|
});
|
|
57
125
|
|
|
126
|
+
// If recording was previously enabled, enable the inspector (hot reload)
|
|
127
|
+
if (isRecordingEnabledRef.current) {
|
|
128
|
+
websocketInspector.enable();
|
|
129
|
+
}
|
|
130
|
+
|
|
58
131
|
return () => {
|
|
59
132
|
// Subscriptions will be disposed by the inspector
|
|
60
133
|
websocketInspector.dispose();
|
|
61
134
|
};
|
|
62
|
-
}, [client]);
|
|
135
|
+
}, [client, isWebSocketInspectorEnabled]);
|
|
63
136
|
|
|
64
137
|
useEffect(() => {
|
|
65
|
-
if (!client) {
|
|
138
|
+
if (!client || !isSSEInspectorEnabled) {
|
|
66
139
|
return;
|
|
67
140
|
}
|
|
68
141
|
|
|
@@ -88,11 +161,16 @@ export const useNetworkActivityDevTools = () => {
|
|
|
88
161
|
sseInspector.disable();
|
|
89
162
|
});
|
|
90
163
|
|
|
164
|
+
// If recording was previously enabled, enable the inspector (hot reload)
|
|
165
|
+
if (isRecordingEnabledRef.current) {
|
|
166
|
+
sseInspector.enable();
|
|
167
|
+
}
|
|
168
|
+
|
|
91
169
|
return () => {
|
|
92
170
|
// Subscriptions will be disposed by the inspector
|
|
93
171
|
sseInspector.dispose();
|
|
94
172
|
};
|
|
95
|
-
}, [client]);
|
|
173
|
+
}, [client, isSSEInspectorEnabled]);
|
|
96
174
|
|
|
97
175
|
return client;
|
|
98
176
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility function to get the name of a blob. Handles both the direct name property and the data object.
|
|
3
|
+
*
|
|
4
|
+
* ```
|
|
5
|
+
* // node_modules/react-native/Libraries/Blob/Blob.js
|
|
6
|
+
*
|
|
7
|
+
* export type BlobData = {
|
|
8
|
+
* blobId: string,
|
|
9
|
+
* offset: number,
|
|
10
|
+
* size: number,
|
|
11
|
+
* name?: string,
|
|
12
|
+
* type?: string,
|
|
13
|
+
* lastModified?: number,
|
|
14
|
+
* __collector?: ?BlobCollector,
|
|
15
|
+
* ...
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* get data(): BlobData {
|
|
19
|
+
* if (!this._data) {
|
|
20
|
+
* throw new Error('Blob has been closed and is no longer available');
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* return this._data;
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* get size(): number {
|
|
27
|
+
* return this.data.size;
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* get type(): string {
|
|
31
|
+
* return this.data.type || '';
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function getBlobName(blob: any): string | undefined {
|
|
36
|
+
if (typeof blob?.name === 'string') {
|
|
37
|
+
return blob.name;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (blob?.data && typeof blob.data.name === 'string') {
|
|
41
|
+
return blob.data.name;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|