@rozenite/network-activity-plugin 1.0.0-alpha.11 → 1.0.0-alpha.12
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-Ct73Yrm6.css → App-DCuHdq4D.css} +17 -0
- package/dist/assets/{App-BKBLGSeM.js → App-JuOeT_VQ.js} +2693 -2642
- package/dist/rozenite.json +1 -1
- package/dist/src/react-native/config.d.ts +13 -0
- package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -1
- package/dist/src/shared/client.d.ts +15 -3
- package/dist/src/ui/components/Button.d.ts +1 -1
- package/dist/src/ui/components/CodeBlock.d.ts +3 -0
- package/dist/src/ui/components/CookieCard.d.ts +7 -0
- package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +1 -1
- package/dist/src/ui/components/Section.d.ts +2 -1
- package/dist/src/ui/state/model.d.ts +4 -4
- package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +10 -0
- package/dist/src/utils/cookieParser.d.ts +6 -0
- package/dist/src/utils/getHttpHeader.d.ts +5 -0
- package/dist/src/utils/getStringSizeInBytes.d.ts +1 -0
- package/dist/src/utils/isNumber.d.ts +1 -0
- package/dist/useNetworkActivityDevTools.cjs +115 -19
- package/dist/useNetworkActivityDevTools.js +116 -20
- package/package.json +4 -4
- package/src/react-native/config.ts +33 -0
- package/src/react-native/http/network-inspector.ts +36 -10
- package/src/react-native/sse/sse-inspector.ts +1 -0
- package/src/react-native/useNetworkActivityDevTools.ts +63 -8
- package/src/shared/client.ts +17 -3
- package/src/ui/components/CodeBlock.tsx +19 -0
- package/src/ui/components/CookieCard.tsx +64 -0
- package/src/ui/components/JsonTree.tsx +10 -3
- package/src/ui/components/JsonTreeCopyableItem.tsx +14 -10
- package/src/ui/components/RequestList.tsx +15 -5
- package/src/ui/components/Section.tsx +31 -4
- package/src/ui/state/model.ts +4 -4
- package/src/ui/tabs/CookiesTab.tsx +64 -263
- package/src/ui/tabs/HeadersTab.tsx +26 -20
- package/src/ui/tabs/RequestTab.tsx +62 -47
- package/src/ui/tabs/ResponseTab.tsx +54 -69
- package/src/utils/applyReactNativeRequestHeadersLogic.ts +2 -2
- package/src/utils/applyReactNativeResponseHeadersLogic.ts +29 -0
- package/src/utils/cookieParser.ts +126 -0
- package/src/utils/getContentTypeMimeType.ts +10 -5
- package/src/utils/getHttpHeader.ts +17 -0
- package/src/utils/getStringSizeInBytes.ts +3 -0
- package/src/utils/isNumber.ts +3 -0
- package/src/utils/safeStringify.ts +1 -1
- package/dist/src/utils/getHttpHeaderValue.d.ts +0 -2
- package/src/utils/getHttpHeaderValue.ts +0 -14
|
@@ -2,12 +2,23 @@ import { useEffect, useRef } from 'react';
|
|
|
2
2
|
import { ScrollArea } from '../components/ScrollArea';
|
|
3
3
|
import { JsonTree } from '../components/JsonTree';
|
|
4
4
|
import { HttpNetworkEntry } from '../state/model';
|
|
5
|
+
import { Section } from '../components/Section';
|
|
6
|
+
import { KeyValueGrid } from '../components/KeyValueGrid';
|
|
7
|
+
import { CodeBlock } from '../components/CodeBlock';
|
|
5
8
|
|
|
6
9
|
export type ResponseTabProps = {
|
|
7
10
|
selectedRequest: HttpNetworkEntry;
|
|
8
11
|
onRequestResponseBody: (requestId: string) => void;
|
|
9
12
|
};
|
|
10
13
|
|
|
14
|
+
const renderResponseBodySection = (children: React.ReactNode) => {
|
|
15
|
+
return (
|
|
16
|
+
<Section title="Response Body" collapsible={false}>
|
|
17
|
+
<div className="space-y-4">{children}</div>
|
|
18
|
+
</Section>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
11
22
|
export const ResponseTab = ({
|
|
12
23
|
selectedRequest,
|
|
13
24
|
onRequestResponseBody,
|
|
@@ -24,10 +35,10 @@ export const ResponseTab = ({
|
|
|
24
35
|
}
|
|
25
36
|
}, [selectedRequest.id]);
|
|
26
37
|
|
|
27
|
-
const
|
|
28
|
-
const responseBody = selectedRequest.response?.body;
|
|
38
|
+
const responseBody = selectedRequest.response?.body;
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
const renderResponseBody = () => {
|
|
41
|
+
if (!responseBody || responseBody.data === null) {
|
|
31
42
|
return (
|
|
32
43
|
<div className="text-sm text-gray-400">
|
|
33
44
|
No response body available for this request
|
|
@@ -37,94 +48,68 @@ export const ResponseTab = ({
|
|
|
37
48
|
|
|
38
49
|
const { type, data } = responseBody;
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
const contentTypeGrid = (
|
|
52
|
+
<KeyValueGrid
|
|
53
|
+
items={[
|
|
54
|
+
{
|
|
55
|
+
key: 'Content-Type',
|
|
56
|
+
value: type,
|
|
57
|
+
valueClassName: 'text-blue-400',
|
|
58
|
+
},
|
|
59
|
+
]}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (type.startsWith('application/json')) {
|
|
64
|
+
let bodyContent;
|
|
48
65
|
|
|
49
|
-
// Handle JSON content
|
|
50
|
-
if (type === 'application/json') {
|
|
51
66
|
try {
|
|
52
67
|
const jsonData = JSON.parse(data);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
</div>
|
|
59
|
-
<div className="bg-gray-800 p-3 rounded border border-gray-700">
|
|
60
|
-
<JsonTree data={jsonData} />
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
68
|
+
|
|
69
|
+
bodyContent = (
|
|
70
|
+
<CodeBlock>
|
|
71
|
+
<JsonTree data={jsonData} />
|
|
72
|
+
</CodeBlock>
|
|
63
73
|
);
|
|
64
74
|
} catch {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<div className="text-
|
|
69
|
-
<span className="text-gray-400">Content-Type: </span>
|
|
70
|
-
<span className="text-blue-400">{type}</span>
|
|
71
|
-
</div>
|
|
72
|
-
<pre className="text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded border border-gray-700 overflow-x-auto">
|
|
73
|
-
{data}
|
|
74
|
-
</pre>
|
|
75
|
-
<div className="text-xs text-gray-500">
|
|
75
|
+
bodyContent = (
|
|
76
|
+
<>
|
|
77
|
+
<CodeBlock>{data}</CodeBlock>
|
|
78
|
+
<div className="text-xs text-gray-500 mt-1">
|
|
76
79
|
⚠️ Failed to parse as JSON, showing as raw text
|
|
77
80
|
</div>
|
|
78
|
-
|
|
81
|
+
</>
|
|
79
82
|
);
|
|
80
83
|
}
|
|
81
|
-
}
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<span className="text-gray-400">Content-Type: </span>
|
|
89
|
-
<span className="text-blue-400">{type}</span>
|
|
90
|
-
</div>
|
|
91
|
-
<pre className="text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded border border-gray-700 overflow-x-auto">
|
|
92
|
-
{data}
|
|
93
|
-
</pre>
|
|
94
|
-
</div>
|
|
85
|
+
return renderResponseBodySection(
|
|
86
|
+
<>
|
|
87
|
+
{contentTypeGrid}
|
|
88
|
+
{bodyContent}
|
|
89
|
+
</>
|
|
95
90
|
);
|
|
96
91
|
}
|
|
97
92
|
|
|
98
|
-
// Handle other text content types
|
|
99
93
|
if (
|
|
100
94
|
type.startsWith('text/') ||
|
|
101
|
-
type
|
|
102
|
-
type
|
|
95
|
+
type.startsWith('application/xml') ||
|
|
96
|
+
type.startsWith('application/javascript')
|
|
103
97
|
) {
|
|
104
|
-
return (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
</div>
|
|
110
|
-
<pre className="text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded border border-gray-700 overflow-x-auto">
|
|
111
|
-
{data}
|
|
112
|
-
</pre>
|
|
113
|
-
</div>
|
|
98
|
+
return renderResponseBodySection(
|
|
99
|
+
<>
|
|
100
|
+
{contentTypeGrid}
|
|
101
|
+
<CodeBlock>{data}</CodeBlock>
|
|
102
|
+
</>
|
|
114
103
|
);
|
|
115
104
|
}
|
|
116
105
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<div className="text-sm mb-2">
|
|
121
|
-
<span className="text-gray-400">Content-Type: </span>
|
|
122
|
-
<span className="text-blue-400">{type}</span>
|
|
123
|
-
</div>
|
|
106
|
+
return renderResponseBodySection(
|
|
107
|
+
<>
|
|
108
|
+
{contentTypeGrid}
|
|
124
109
|
<div className="text-sm text-gray-400">
|
|
125
110
|
Binary content not shown - {data.length} bytes
|
|
126
111
|
</div>
|
|
127
|
-
|
|
112
|
+
</>
|
|
128
113
|
);
|
|
129
114
|
};
|
|
130
115
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HttpHeaders, RequestPostData } from '../shared/client';
|
|
2
|
-
import {
|
|
2
|
+
import { getHttpHeader } from './getHttpHeader';
|
|
3
3
|
import { inferContentTypeFromPostData } from './inferContentTypeFromPostData';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -12,7 +12,7 @@ export function applyReactNativeRequestHeadersLogic(
|
|
|
12
12
|
headers: HttpHeaders,
|
|
13
13
|
postData?: RequestPostData
|
|
14
14
|
): HttpHeaders {
|
|
15
|
-
const existingContentType =
|
|
15
|
+
const existingContentType = getHttpHeader(headers, 'content-type');
|
|
16
16
|
|
|
17
17
|
if (existingContentType) {
|
|
18
18
|
return headers;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { HttpHeaders, XHRHeaders } from '../shared/client';
|
|
2
|
+
import { splitSetCookieHeaderByComma } from './cookieParser';
|
|
3
|
+
import { getHttpHeader } from './getHttpHeader';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Applies React Native specific logic to response headers.
|
|
7
|
+
* React Native concatenates multiple header values into single strings,
|
|
8
|
+
* this function parses them back into arrays where appropriate.
|
|
9
|
+
*
|
|
10
|
+
* See:
|
|
11
|
+
* https://github.com/facebook/react-native/blob/588f0c5ce6c283f116228456da2170d2adc3cbf4/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java#L637
|
|
12
|
+
*/
|
|
13
|
+
export const applyReactNativeResponseHeadersLogic = (
|
|
14
|
+
headers: XHRHeaders
|
|
15
|
+
): HttpHeaders => {
|
|
16
|
+
const parsedHeaders: HttpHeaders = { ...headers };
|
|
17
|
+
|
|
18
|
+
const setCookieHeader = getHttpHeader(headers, 'set-cookie');
|
|
19
|
+
|
|
20
|
+
if (setCookieHeader) {
|
|
21
|
+
const { value, originalKey } = setCookieHeader;
|
|
22
|
+
|
|
23
|
+
const cookies = splitSetCookieHeaderByComma(value);
|
|
24
|
+
|
|
25
|
+
parsedHeaders[originalKey] = cookies.length > 0 ? cookies : value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return parsedHeaders;
|
|
29
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Cookie, HttpHeaders } from '../shared/client';
|
|
2
|
+
import { getHttpHeader } from './getHttpHeader';
|
|
3
|
+
|
|
4
|
+
export const parseSetCookieHeader = (setCookieStr: string): Cookie => {
|
|
5
|
+
const parts = setCookieStr.split(';').map((p) => p.trim());
|
|
6
|
+
const [nameValue, ...attributes] = parts;
|
|
7
|
+
const [name, ...valueParts] = nameValue.split('=');
|
|
8
|
+
|
|
9
|
+
const value = valueParts.join('=');
|
|
10
|
+
|
|
11
|
+
const cookieObj: Cookie = {
|
|
12
|
+
name: name?.trim() || '',
|
|
13
|
+
value: value?.trim() || '',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
attributes.forEach((attr) => {
|
|
17
|
+
const [attrName, ...attrValueParts] = attr.split('=');
|
|
18
|
+
const lowerAttrName = attrName.trim().toLowerCase();
|
|
19
|
+
const attrValue = attrValueParts.join('=');
|
|
20
|
+
|
|
21
|
+
switch (lowerAttrName) {
|
|
22
|
+
case 'domain':
|
|
23
|
+
cookieObj.domain = attrValue;
|
|
24
|
+
|
|
25
|
+
break;
|
|
26
|
+
|
|
27
|
+
case 'path':
|
|
28
|
+
cookieObj.path = attrValue;
|
|
29
|
+
|
|
30
|
+
break;
|
|
31
|
+
|
|
32
|
+
case 'expires':
|
|
33
|
+
cookieObj.expires = attrValue;
|
|
34
|
+
|
|
35
|
+
break;
|
|
36
|
+
|
|
37
|
+
case 'max-age':
|
|
38
|
+
cookieObj.maxAge = attrValue;
|
|
39
|
+
|
|
40
|
+
break;
|
|
41
|
+
|
|
42
|
+
case 'secure':
|
|
43
|
+
cookieObj.secure = true;
|
|
44
|
+
|
|
45
|
+
break;
|
|
46
|
+
|
|
47
|
+
case 'httponly':
|
|
48
|
+
cookieObj.httpOnly = true;
|
|
49
|
+
|
|
50
|
+
break;
|
|
51
|
+
|
|
52
|
+
case 'samesite':
|
|
53
|
+
cookieObj.sameSite = attrValue;
|
|
54
|
+
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return cookieObj;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const splitSetCookieHeaderByComma = (header: string): string[] => {
|
|
63
|
+
const regex = /(?:^|,\s)([^=;,]+=[^;]+(?:;[^,]*)*)/g;
|
|
64
|
+
const matches: string[] = [];
|
|
65
|
+
|
|
66
|
+
let match;
|
|
67
|
+
|
|
68
|
+
while ((match = regex.exec(header)) !== null) {
|
|
69
|
+
matches.push(match[1].trim());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return matches;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const parseCookieHeader = (cookieString: string): Cookie[] => {
|
|
76
|
+
if (!cookieString) return [];
|
|
77
|
+
|
|
78
|
+
return cookieString
|
|
79
|
+
.split(';')
|
|
80
|
+
.map((cookieStr) => {
|
|
81
|
+
const [name, ...valueParts] = cookieStr.trim().split('=');
|
|
82
|
+
const value = valueParts.join('=');
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
name: name?.trim() || '',
|
|
86
|
+
value: value?.trim() || '',
|
|
87
|
+
};
|
|
88
|
+
})
|
|
89
|
+
.filter((cookieObj) => cookieObj.name);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const parseRequestCookiesFromHeaders = (
|
|
93
|
+
headers: HttpHeaders
|
|
94
|
+
): Cookie[] => {
|
|
95
|
+
const cookieHeader = getHttpHeader(headers, 'cookie');
|
|
96
|
+
|
|
97
|
+
if (!cookieHeader) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { value } = cookieHeader;
|
|
102
|
+
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
return value.flatMap(parseCookieHeader);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return parseCookieHeader(value);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const parseResponseCookiesFromHeaders = (
|
|
111
|
+
headers: HttpHeaders
|
|
112
|
+
): Cookie[] => {
|
|
113
|
+
const setCookieHeader = getHttpHeader(headers, 'set-cookie');
|
|
114
|
+
|
|
115
|
+
if (!setCookieHeader) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { value } = setCookieHeader;
|
|
120
|
+
|
|
121
|
+
if (Array.isArray(value)) {
|
|
122
|
+
return value.flatMap(parseSetCookieHeader);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [parseSetCookieHeader(value)];
|
|
126
|
+
};
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { HttpHeaders } from '../shared/client';
|
|
2
|
-
import {
|
|
2
|
+
import { getHttpHeader } from './getHttpHeader';
|
|
3
3
|
|
|
4
4
|
export function getContentTypeMime(headers: HttpHeaders) {
|
|
5
|
-
const contentType =
|
|
5
|
+
const contentType = getHttpHeader(headers, 'content-type');
|
|
6
6
|
|
|
7
|
-
if (contentType) {
|
|
8
|
-
return
|
|
7
|
+
if (!contentType) {
|
|
8
|
+
return undefined;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const { value } = contentType;
|
|
12
|
+
|
|
13
|
+
// Content-Type can't be an array, but if it does we simply get the first element.
|
|
14
|
+
const actualValue = Array.isArray(value) ? value[0] : value;
|
|
15
|
+
|
|
16
|
+
return actualValue.split(';')[0].trim();
|
|
12
17
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { HttpHeaders, XHRHeaders } from '../shared/client';
|
|
2
|
+
|
|
3
|
+
// Utility to get header value and actual key case-insensitively
|
|
4
|
+
export function getHttpHeader<T extends HttpHeaders | XHRHeaders>(
|
|
5
|
+
headers: T,
|
|
6
|
+
name: string
|
|
7
|
+
) {
|
|
8
|
+
const lowerName = name.toLowerCase();
|
|
9
|
+
|
|
10
|
+
for (const key in headers) {
|
|
11
|
+
if (key.toLowerCase() === lowerName) {
|
|
12
|
+
return { value: headers[key], originalKey: key };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { HttpHeaders } from '../shared/client';
|
|
2
|
-
|
|
3
|
-
// Utility to get header value case-insensitively
|
|
4
|
-
export function getHttpHeaderValue(headers: HttpHeaders, name: string) {
|
|
5
|
-
const lowerName = name.toLowerCase();
|
|
6
|
-
|
|
7
|
-
for (const key in headers) {
|
|
8
|
-
if (key.toLowerCase() === lowerName) {
|
|
9
|
-
return headers[key];
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
return undefined;
|
|
14
|
-
}
|