@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.
Files changed (113) hide show
  1. package/README.md +2 -0
  2. package/dist/App.html +2 -2
  3. package/dist/assets/{App-DoHQsY5s.css → App-BrSkOkws.css} +223 -2
  4. package/dist/assets/{App-CA1Fbh0I.js → App-Kyi7zHUX.js} +8188 -2671
  5. package/dist/react-native.cjs +4 -1
  6. package/dist/react-native.js +4 -1
  7. package/dist/rozenite.json +1 -1
  8. package/dist/src/react-native/config.d.ts +20 -0
  9. package/dist/src/react-native/http/overrides-registry.d.ts +6 -0
  10. package/dist/src/react-native/http/xhr-interceptor.d.ts +7 -1
  11. package/dist/src/react-native/sse/sse-interceptor.d.ts +2 -2
  12. package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -1
  13. package/dist/src/react-native/utils/getBlobName.d.ts +35 -0
  14. package/dist/src/react-native/utils/getFormDataEntries.d.ts +18 -0
  15. package/dist/src/shared/client.d.ts +55 -4
  16. package/dist/src/shared/sse-events.d.ts +4 -1
  17. package/dist/src/ui/components/Button.d.ts +2 -2
  18. package/dist/src/ui/components/CodeBlock.d.ts +3 -0
  19. package/dist/src/ui/components/CodeEditor.d.ts +5 -0
  20. package/dist/src/ui/components/CookieCard.d.ts +7 -0
  21. package/dist/src/ui/components/CopyRequestDropdown.d.ts +7 -0
  22. package/dist/src/ui/components/DropdownMenu.d.ts +27 -0
  23. package/dist/src/ui/components/FilterBar.d.ts +10 -0
  24. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +1 -1
  25. package/dist/src/ui/components/KeyValueGrid.d.ts +13 -0
  26. package/dist/src/ui/components/OverrideResponse.d.ts +8 -0
  27. package/dist/src/ui/components/RequestBody.d.ts +6 -0
  28. package/dist/src/ui/components/RequestList.d.ts +9 -4
  29. package/dist/src/ui/components/ScrollArea.d.ts +3 -2
  30. package/dist/src/ui/components/Section.d.ts +8 -0
  31. package/dist/src/ui/components/Separator.d.ts +2 -1
  32. package/dist/src/ui/components/Tabs.d.ts +7 -0
  33. package/dist/src/ui/state/hooks.d.ts +4 -0
  34. package/dist/src/ui/state/model.d.ts +22 -7
  35. package/dist/src/ui/state/store.d.ts +27 -3
  36. package/dist/src/ui/utils/checkRequestBodyBinary.d.ts +2 -0
  37. package/dist/src/ui/utils/escapeShellArg.d.ts +1 -0
  38. package/dist/src/ui/utils/generateCurlCommand.d.ts +2 -0
  39. package/dist/src/ui/utils/generateFetchCall.d.ts +2 -0
  40. package/dist/src/ui/utils/generateMultipartBody.d.ts +4 -0
  41. package/dist/src/utils/applyReactNativeRequestHeadersLogic.d.ts +7 -0
  42. package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +9 -0
  43. package/dist/src/utils/cookieParser.d.ts +6 -0
  44. package/dist/src/utils/getContentTypeMimeType.d.ts +2 -0
  45. package/dist/src/utils/getHttpHeader.d.ts +5 -0
  46. package/dist/src/utils/getHttpHeaderValueAsString.d.ts +11 -0
  47. package/dist/src/utils/getStringSizeInBytes.d.ts +1 -0
  48. package/dist/src/utils/inferContentTypeFromPostData.d.ts +2 -0
  49. package/dist/src/utils/safeStringify.d.ts +1 -0
  50. package/dist/src/utils/typeChecks.d.ts +9 -0
  51. package/dist/useNetworkActivityDevTools.cjs +337 -24
  52. package/dist/useNetworkActivityDevTools.js +338 -25
  53. package/package.json +7 -4
  54. package/react-native.ts +6 -1
  55. package/src/react-native/config.ts +43 -0
  56. package/src/react-native/http/network-inspector.ts +190 -8
  57. package/src/react-native/http/overrides-registry.ts +32 -0
  58. package/src/react-native/http/xhr-interceptor.ts +19 -2
  59. package/src/react-native/sse/sse-inspector.ts +27 -5
  60. package/src/react-native/sse/sse-interceptor.ts +26 -8
  61. package/src/react-native/useNetworkActivityDevTools.ts +86 -8
  62. package/src/react-native/utils/getBlobName.ts +45 -0
  63. package/src/react-native/utils/getFormDataEntries.ts +32 -0
  64. package/src/react-native/utils.ts +3 -3
  65. package/src/shared/client.ts +81 -4
  66. package/src/shared/sse-events.ts +4 -1
  67. package/src/ui/components/Button.tsx +1 -0
  68. package/src/ui/components/CodeBlock.tsx +19 -0
  69. package/src/ui/components/CodeEditor.tsx +26 -0
  70. package/src/ui/components/CookieCard.tsx +64 -0
  71. package/src/ui/components/CopyRequestDropdown.tsx +95 -0
  72. package/src/ui/components/DropdownMenu.tsx +206 -0
  73. package/src/ui/components/FilterBar.tsx +117 -0
  74. package/src/ui/components/Input.tsx +1 -1
  75. package/src/ui/components/JsonTree.tsx +10 -3
  76. package/src/ui/components/JsonTreeCopyableItem.tsx +14 -10
  77. package/src/ui/components/KeyValueGrid.tsx +51 -0
  78. package/src/ui/components/OverrideResponse.tsx +132 -0
  79. package/src/ui/components/RequestBody.tsx +86 -0
  80. package/src/ui/components/RequestList.tsx +74 -14
  81. package/src/ui/components/ScrollArea.tsx +1 -0
  82. package/src/ui/components/Section.tsx +46 -0
  83. package/src/ui/components/SidePanel.tsx +15 -5
  84. package/src/ui/components/Toolbar.tsx +3 -2
  85. package/src/ui/globals.css +4 -0
  86. package/src/ui/hooks/useCopyToClipboard.ts +2 -2
  87. package/src/ui/state/derived.ts +2 -0
  88. package/src/ui/state/hooks.ts +8 -0
  89. package/src/ui/state/model.ts +28 -7
  90. package/src/ui/state/store.ts +640 -500
  91. package/src/ui/tabs/CookiesTab.tsx +60 -263
  92. package/src/ui/tabs/HeadersTab.tsx +78 -89
  93. package/src/ui/tabs/RequestTab.tsx +58 -46
  94. package/src/ui/tabs/ResponseTab.tsx +98 -67
  95. package/src/ui/tabs/SSEMessagesTab.tsx +50 -39
  96. package/src/ui/utils/checkRequestBodyBinary.ts +7 -0
  97. package/src/ui/utils/escapeShellArg.ts +12 -0
  98. package/src/ui/utils/generateCurlCommand.ts +83 -0
  99. package/src/ui/utils/generateFetchCall.ts +64 -0
  100. package/src/ui/utils/generateMultipartBody.ts +19 -0
  101. package/src/ui/views/InspectorView.tsx +15 -3
  102. package/src/utils/applyReactNativeRequestHeadersLogic.ts +30 -0
  103. package/src/utils/applyReactNativeResponseHeadersLogic.ts +28 -0
  104. package/src/utils/cookieParser.ts +126 -0
  105. package/src/utils/getContentTypeMimeType.ts +17 -0
  106. package/src/utils/getHttpHeader.ts +17 -0
  107. package/src/utils/getHttpHeaderValueAsString.ts +13 -0
  108. package/src/utils/getStringSizeInBytes.ts +3 -0
  109. package/src/utils/inferContentTypeFromPostData.ts +9 -0
  110. package/src/utils/safeStringify.ts +7 -0
  111. package/src/utils/typeChecks.ts +27 -0
  112. package/dist/src/ui/utils/getHttpHeaderValue.d.ts +0 -2
  113. package/src/ui/utils/getHttpHeaderValue.ts +0 -14
@@ -1,18 +1,49 @@
1
- import { useEffect, useRef } from 'react';
1
+ import { useEffect, useRef, useState } 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';
8
+ import { useOverrides } from '../state/hooks';
9
+ import { RequestOverride } from '../../shared/client';
10
+ import { OverrideResponse } from '../components/OverrideResponse';
11
+ import { Button } from '../components/Button';
12
+ import { Pencil } from 'lucide-react';
5
13
 
6
14
  export type ResponseTabProps = {
7
15
  selectedRequest: HttpNetworkEntry;
8
16
  onRequestResponseBody: (requestId: string) => void;
9
17
  };
10
18
 
19
+ type ResponseBodySectionProps = {
20
+ action?: React.ReactNode;
21
+ children: React.ReactNode;
22
+ };
23
+
24
+ const RenderResponseBodySection = ({
25
+ children,
26
+ action,
27
+ }: ResponseBodySectionProps) => {
28
+ return (
29
+ <Section title="Response Body" collapsible={false} action={action}>
30
+ <div className="space-y-4">{children}</div>
31
+ </Section>
32
+ );
33
+ };
34
+
11
35
  export const ResponseTab = ({
12
36
  selectedRequest,
13
37
  onRequestResponseBody,
14
38
  }: ResponseTabProps) => {
15
39
  const onRequestResponseBodyRef = useRef(onRequestResponseBody);
40
+ const overrides = useOverrides();
41
+ const [initialOverride, setInitialOverride] = useState<
42
+ RequestOverride | undefined
43
+ >(() => {
44
+ const override = overrides.get(selectedRequest.request.url);
45
+ return override;
46
+ });
16
47
 
17
48
  useEffect(() => {
18
49
  onRequestResponseBodyRef.current = onRequestResponseBody;
@@ -24,10 +55,10 @@ export const ResponseTab = ({
24
55
  }
25
56
  }, [selectedRequest.id]);
26
57
 
27
- const renderResponseBody = () => {
28
- const responseBody = selectedRequest.response?.body;
58
+ const responseBody = selectedRequest.response?.body;
29
59
 
30
- if (!responseBody) {
60
+ const renderResponseBody = () => {
61
+ if (!responseBody || responseBody.data === null) {
31
62
  return (
32
63
  <div className="text-sm text-gray-400">
33
64
  No response body available for this request
@@ -36,103 +67,103 @@ export const ResponseTab = ({
36
67
  }
37
68
 
38
69
  const { type, data } = responseBody;
70
+ const statusCode = selectedRequest.response?.status;
71
+
72
+ const contentTypeGrid = (
73
+ <KeyValueGrid
74
+ items={[
75
+ {
76
+ key: 'Content-Type',
77
+ value: type,
78
+ valueClassName: 'text-blue-400',
79
+ },
80
+ ]}
81
+ />
82
+ );
83
+
84
+ const overrideAction = (
85
+ <Button
86
+ variant="ghost"
87
+ size="xs"
88
+ className="text-violet-300 hover:text-violet-300"
89
+ onClick={() =>
90
+ setInitialOverride({
91
+ body: data,
92
+ status: statusCode,
93
+ })
94
+ }
95
+ >
96
+ <Pencil className="h-2 w-2" />
97
+ Override
98
+ </Button>
99
+ );
39
100
 
40
- // Handle null data
41
- if (data === null) {
101
+ if (initialOverride !== undefined) {
42
102
  return (
43
- <div className="text-sm text-gray-400">
44
- No response body available for this request
45
- </div>
103
+ <OverrideResponse
104
+ selectedRequest={selectedRequest}
105
+ initialOverride={initialOverride}
106
+ onClear={() => setInitialOverride(undefined)}
107
+ />
46
108
  );
47
109
  }
48
110
 
49
- // Handle JSON content
50
- if (type === 'application/json') {
111
+ if (type.startsWith('application/json')) {
112
+ let bodyContent;
113
+
51
114
  try {
52
115
  const jsonData = JSON.parse(data);
53
- return (
54
- <div className="space-y-4">
55
- <div className="text-sm mb-2">
56
- <span className="text-gray-400">Content-Type: </span>
57
- <span className="text-blue-400">{type}</span>
58
- </div>
59
- <div className="bg-gray-800 p-3 rounded border border-gray-700">
60
- <JsonTree data={jsonData} />
61
- </div>
62
- </div>
116
+
117
+ bodyContent = (
118
+ <CodeBlock>
119
+ <JsonTree data={jsonData} />
120
+ </CodeBlock>
63
121
  );
64
122
  } catch {
65
- // Fallback to pre tag if JSON parsing fails
66
- return (
67
- <div className="space-y-4">
68
- <div className="text-sm mb-2">
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">
123
+ bodyContent = (
124
+ <>
125
+ <CodeBlock>{data}</CodeBlock>
126
+ <div className="text-xs text-gray-500 mt-1">
76
127
  ⚠️ Failed to parse as JSON, showing as raw text
77
128
  </div>
78
- </div>
129
+ </>
79
130
  );
80
131
  }
81
- }
82
132
 
83
- // Handle HTML content
84
- if (type === 'text/html') {
85
133
  return (
86
- <div className="space-y-4">
87
- <div className="text-sm mb-2">
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>
134
+ <RenderResponseBodySection action={overrideAction}>
135
+ {contentTypeGrid}
136
+ {bodyContent}
137
+ </RenderResponseBodySection>
95
138
  );
96
139
  }
97
140
 
98
- // Handle other text content types
99
141
  if (
100
142
  type.startsWith('text/') ||
101
- type === 'application/xml' ||
102
- type === 'application/javascript'
143
+ type.startsWith('application/xml') ||
144
+ type.startsWith('application/javascript')
103
145
  ) {
104
146
  return (
105
- <div className="space-y-4">
106
- <div className="text-sm mb-2">
107
- <span className="text-gray-400">Content-Type: </span>
108
- <span className="text-blue-400">{type}</span>
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>
147
+ <RenderResponseBodySection action={overrideAction}>
148
+ {contentTypeGrid}
149
+ <CodeBlock>{data}</CodeBlock>
150
+ </RenderResponseBodySection>
114
151
  );
115
152
  }
116
153
 
117
- // Handle other content types
118
154
  return (
119
- <div className="space-y-4">
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>
155
+ <RenderResponseBodySection>
156
+ {contentTypeGrid}
124
157
  <div className="text-sm text-gray-400">
125
158
  Binary content not shown - {data.length} bytes
126
159
  </div>
127
- </div>
160
+ </RenderResponseBodySection>
128
161
  );
129
162
  };
130
163
 
131
164
  return (
132
165
  <ScrollArea className="h-full w-full">
133
- <div className="p-4">
134
- {renderResponseBody()}
135
- </div>
166
+ <div className="p-4">{renderResponseBody()}</div>
136
167
  </ScrollArea>
137
168
  );
138
169
  };
@@ -15,30 +15,64 @@ export type SSEMessagesTabProps = {
15
15
 
16
16
  interface SSEMessageRow {
17
17
  id: string;
18
+ type: string;
18
19
  data: string;
19
20
  timestamp: number;
20
21
  }
21
22
 
22
23
  const columnHelper = createColumnHelper<SSEMessageRow>();
23
24
 
25
+ const formatPreviewData = (data: string) => {
26
+ return (
27
+ <span className="max-w-xs truncate text-gray-400">
28
+ {data.substring(0, 100) + (data.length > 100 ? '...' : '')}
29
+ </span>
30
+ );
31
+ };
32
+
33
+ const formatTimestamp = (timestamp: number) => {
34
+ const date = new Date(timestamp);
35
+ const timeString = date.toLocaleTimeString('en-US', {
36
+ hour12: false,
37
+ hour: '2-digit',
38
+ minute: '2-digit',
39
+ second: '2-digit',
40
+ });
41
+ const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
42
+ return `${timeString}.${milliseconds}`;
43
+ };
44
+
45
+ const columns = [
46
+ columnHelper.accessor('timestamp', {
47
+ header: 'Timestamp',
48
+ cell: ({ getValue }) => (
49
+ <div className="text-gray-400">{formatTimestamp(getValue())}</div>
50
+ ),
51
+ size: 120,
52
+ }),
53
+ columnHelper.accessor('type', {
54
+ header: 'Type',
55
+ cell: ({ getValue }) => (
56
+ <div className="text-purple-400 font-medium">{getValue()}</div>
57
+ ),
58
+ size: 100,
59
+ }),
60
+ columnHelper.accessor('data', {
61
+ header: 'Data',
62
+ cell: ({ getValue }) => {
63
+ const data = getValue();
64
+ return formatPreviewData(data);
65
+ },
66
+ size: 300,
67
+ }),
68
+ ];
69
+
24
70
  export const SSEMessagesTab = ({ selectedRequest }: SSEMessagesTabProps) => {
25
71
  // Capture the selected message, so when it gets removed (message limit), it's still displayed
26
72
  const [selectedMessage, setSelectedMessage] = useState<SSEMessageRow | null>(
27
73
  null
28
74
  );
29
75
 
30
- const formatTimestamp = (timestamp: number) => {
31
- const date = new Date(timestamp);
32
- const timeString = date.toLocaleTimeString('en-US', {
33
- hour12: false,
34
- hour: '2-digit',
35
- minute: '2-digit',
36
- second: '2-digit',
37
- });
38
- const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
39
- return `${timeString}.${milliseconds}`;
40
- };
41
-
42
76
  const formatData = (data: string) => {
43
77
  if (typeof data === 'string') {
44
78
  try {
@@ -65,38 +99,13 @@ export const SSEMessagesTab = ({ selectedRequest }: SSEMessagesTabProps) => {
65
99
  return selectedRequest.messages.map(
66
100
  (message): SSEMessageRow => ({
67
101
  id: message.id,
102
+ type: message.type,
68
103
  data: message.data,
69
104
  timestamp: message.timestamp,
70
105
  })
71
106
  );
72
107
  }, [selectedRequest.messages]);
73
108
 
74
- const formatPreviewData = (data: string) => {
75
- return (
76
- <span className="max-w-xs truncate text-gray-400">
77
- {data.substring(0, 100) + (data.length > 100 ? '...' : '')}
78
- </span>
79
- );
80
- };
81
-
82
- const columns = [
83
- columnHelper.accessor('data', {
84
- header: 'Data',
85
- cell: ({ getValue }) => {
86
- const data = getValue();
87
- return formatPreviewData(data);
88
- },
89
- size: 300,
90
- }),
91
- columnHelper.accessor('timestamp', {
92
- header: 'Timestamp',
93
- cell: ({ getValue }) => (
94
- <div className="text-gray-400">{formatTimestamp(getValue())}</div>
95
- ),
96
- size: 120,
97
- }),
98
- ];
99
-
100
109
  const table = useReactTable({
101
110
  data: tableData,
102
111
  columns,
@@ -189,7 +198,9 @@ export const SSEMessagesTab = ({ selectedRequest }: SSEMessagesTabProps) => {
189
198
  <div className="grid grid-cols-2 gap-4 text-sm">
190
199
  <div>
191
200
  <span className="text-gray-400">Type: </span>
192
- <span className="text-purple-400">SSE</span>
201
+ <span className="text-purple-400">
202
+ {selectedMessage.type}
203
+ </span>
193
204
  </div>
194
205
  <div>
195
206
  <span className="text-gray-400">Timestamp: </span>
@@ -0,0 +1,7 @@
1
+ import { NetworkEntry } from '../state/model';
2
+
3
+ export const checkRequestBodyBinary = (request: NetworkEntry) => {
4
+ return (
5
+ request.type === 'http' && request.request.body?.data.type === 'binary'
6
+ );
7
+ };
@@ -0,0 +1,12 @@
1
+ // Escapes special characters in shell arguments
2
+ export function escapeShellArg(arg: string): string {
3
+ if (!arg) return "''";
4
+
5
+ // If the argument contains no special characters, return as is
6
+ if (/^[a-zA-Z0-9_./:-]+$/.test(arg)) {
7
+ return arg;
8
+ }
9
+
10
+ // Replace single quotes with '\'' and wrap in single quotes
11
+ return `'${arg.replace(/'/g, "'\"'\"'")}'`;
12
+ }
@@ -0,0 +1,83 @@
1
+ import { HttpHeaders, RequestPostData } from '../../shared/client';
2
+ import { HttpNetworkEntry, SSENetworkEntry } from '../state/model';
3
+ import { escapeShellArg } from './escapeShellArg';
4
+
5
+ const BASE_TAB_INDENT = 2; // Number of spaces for indentation
6
+
7
+ function stringifyData(postData: unknown): string {
8
+ try {
9
+ const jsonString = JSON.stringify(
10
+ typeof postData === 'string' ? JSON.parse(postData) : postData,
11
+ null,
12
+ BASE_TAB_INDENT * 4
13
+ );
14
+
15
+ return jsonString.replace(/([}\]])$/, '$1'.padStart(BASE_TAB_INDENT * 2));
16
+ } catch {
17
+ return String(postData);
18
+ }
19
+ }
20
+
21
+ // Adds a curl parameter with proper indentation
22
+ function addCurlParam(curlParts: string[], flag: string, value: string): void {
23
+ curlParts.push(`${flag.padStart(BASE_TAB_INDENT + flag.length)} ${value}`);
24
+ }
25
+
26
+ function addHttpMethodToCurl(curlParts: string[], method: string): void {
27
+ if (method && hasRequestBody(method)) {
28
+ addCurlParam(curlParts, '-X', method.toUpperCase());
29
+ }
30
+ }
31
+
32
+ function addHeadersToCurl(curlParts: string[], headers: HttpHeaders): void {
33
+ Object.entries(headers).forEach(([key, value]) => {
34
+ addCurlParam(curlParts, '-H', escapeShellArg(`${key}: ${value}`));
35
+ });
36
+ }
37
+
38
+ function hasRequestBody(method: string): boolean {
39
+ const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];
40
+
41
+ return methodsWithBody.includes(method.toUpperCase());
42
+ }
43
+
44
+ function addBodyToCurl(curlParts: string[], postData: RequestPostData): void {
45
+ if (!postData) {
46
+ return;
47
+ }
48
+
49
+ const { type, value } = postData;
50
+
51
+ if (type === 'form-data') {
52
+ const formParts = Object.entries(value).map(
53
+ ([key, value]) => `${key}=${stringifyData(value)}`
54
+ );
55
+
56
+ formParts.forEach((part) =>
57
+ addCurlParam(curlParts, '--form', escapeShellArg(part))
58
+ );
59
+
60
+ return;
61
+ }
62
+
63
+ addCurlParam(curlParts, '--data-raw', escapeShellArg(stringifyData(value)));
64
+ }
65
+
66
+ export function generateCurlCommand(
67
+ request: HttpNetworkEntry | SSENetworkEntry
68
+ ) {
69
+ const { method, url, headers = {}, body } = request.request;
70
+
71
+ const postData = body?.data;
72
+
73
+ const curlParts: string[] = [`curl ${escapeShellArg(url)}`];
74
+
75
+ addHttpMethodToCurl(curlParts, method);
76
+ addHeadersToCurl(curlParts, headers);
77
+
78
+ if (postData && hasRequestBody(method)) {
79
+ addBodyToCurl(curlParts, postData);
80
+ }
81
+
82
+ return curlParts.join(' \\\n');
83
+ }
@@ -0,0 +1,64 @@
1
+ import { generateMultipartBody } from './generateMultipartBody';
2
+ import {
3
+ HttpNetworkEntry,
4
+ HttpRequestData,
5
+ SSENetworkEntry,
6
+ } from '../state/model';
7
+ import { getHttpHeaderValueAsString } from '../../utils/getHttpHeaderValueAsString';
8
+ import { HttpHeaders, XHRHeaders } from '../../shared/client';
9
+
10
+ const processHeaders = (requestHeaders: HttpHeaders | undefined) => {
11
+ const headers: XHRHeaders = {};
12
+
13
+ if (!requestHeaders) {
14
+ return headers;
15
+ }
16
+
17
+ Object.entries(requestHeaders).forEach(([name, value]) => {
18
+ // Filter out HTTP/2 pseudo-headers
19
+ if (!name.startsWith(':')) {
20
+ headers[name] = getHttpHeaderValueAsString(value);
21
+ }
22
+ });
23
+
24
+ return headers;
25
+ };
26
+
27
+ const processRequestBody = (body: HttpRequestData, headers: XHRHeaders) => {
28
+ const { type, value } = body.data;
29
+
30
+ switch (type) {
31
+ case 'text':
32
+ return value;
33
+
34
+ case 'form-data': {
35
+ const { body, contentType } = generateMultipartBody(value);
36
+
37
+ headers['Content-Type'] = contentType;
38
+
39
+ return body;
40
+ }
41
+
42
+ default:
43
+ return undefined;
44
+ }
45
+ };
46
+
47
+ export const generateFetchCall = (
48
+ request: HttpNetworkEntry | SSENetworkEntry
49
+ ) => {
50
+ const { url, headers: requestHeaders, method, body } = request.request;
51
+
52
+ const headers = processHeaders(requestHeaders);
53
+ const requestBody = body ? processRequestBody(body, headers) : undefined;
54
+
55
+ const fetchOptions: RequestInit = {
56
+ headers: Object.keys(headers).length ? headers : undefined,
57
+ body: requestBody,
58
+ method,
59
+ };
60
+
61
+ const options = JSON.stringify(fetchOptions, null, 2);
62
+
63
+ return `fetch(${JSON.stringify(url)}, ${options});`;
64
+ };
@@ -0,0 +1,19 @@
1
+ export const generateMultipartBody = (formData: Record<string, unknown>) => {
2
+ const boundary = 'FormBoundary' + Math.random().toString(36).substr(2, 16);
3
+
4
+ const parts: string[] = [];
5
+
6
+ Object.entries(formData).forEach(([key, value]) => {
7
+ parts.push(`--${boundary}`);
8
+ parts.push(`Content-Disposition: form-data; name="${key}"`);
9
+ parts.push('');
10
+ parts.push(String(value));
11
+ });
12
+
13
+ parts.push(`--${boundary}--`);
14
+
15
+ return {
16
+ body: parts.join('\r\n'),
17
+ contentType: `multipart/form-data; boundary=${boundary}`,
18
+ };
19
+ };
@@ -1,12 +1,14 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useState } from 'react';
2
2
  import { Toolbar } from '../components/Toolbar';
3
3
  import { RequestList } from '../components/RequestList';
4
4
  import { SidePanel } from '../components/SidePanel';
5
+ import { FilterBar, FilterState } from '../components/FilterBar';
5
6
  import { NetworkActivityDevToolsClient } from '../../shared/client';
6
7
  import {
7
8
  useNetworkActivityClientManagement,
8
9
  useHasSelectedRequest,
9
10
  useNetworkActivityActions,
11
+ useOverrides,
10
12
  } from '../state/hooks';
11
13
 
12
14
  export type InspectorViewProps = {
@@ -17,6 +19,11 @@ export const InspectorView = ({ client }: InspectorViewProps) => {
17
19
  const actions = useNetworkActivityActions();
18
20
  const clientManagement = useNetworkActivityClientManagement();
19
21
  const hasSelectedRequest = useHasSelectedRequest();
22
+ const overrides = useOverrides();
23
+ const [filter, setFilter] = useState<FilterState>({
24
+ text: '',
25
+ types: new Set(['http', 'websocket', 'sse']),
26
+ });
20
27
 
21
28
  useEffect(() => {
22
29
  if (!client) {
@@ -26,15 +33,20 @@ export const InspectorView = ({ client }: InspectorViewProps) => {
26
33
  clientManagement.setupClient(client);
27
34
  actions.setRecording(true);
28
35
 
36
+ client.send('set-overrides', {
37
+ overrides: Array.from(overrides.entries()),
38
+ });
39
+
29
40
  return () => {
30
41
  actions.setRecording(false);
31
42
  clientManagement.cleanupClient();
32
43
  };
33
- }, [client, clientManagement, actions]);
44
+ }, [client, clientManagement, actions, overrides]);
34
45
 
35
46
  return (
36
47
  <div className="h-screen bg-gray-900 text-gray-100 flex flex-col">
37
48
  <Toolbar />
49
+ <FilterBar filter={filter} onFilterChange={setFilter} />
38
50
 
39
51
  <div className="flex flex-1 overflow-hidden">
40
52
  {/* Request List */}
@@ -43,7 +55,7 @@ export const InspectorView = ({ client }: InspectorViewProps) => {
43
55
  hasSelectedRequest ? 'w-1/2' : 'w-full'
44
56
  } border-r border-gray-700 overflow-hidden`}
45
57
  >
46
- <RequestList />
58
+ <RequestList filter={filter} />
47
59
  </div>
48
60
 
49
61
  {hasSelectedRequest && <SidePanel />}
@@ -0,0 +1,30 @@
1
+ import { HttpHeaders, RequestPostData } from '../shared/client';
2
+ import { getHttpHeader } from './getHttpHeader';
3
+ import { inferContentTypeFromPostData } from './inferContentTypeFromPostData';
4
+
5
+ /**
6
+ * Partially emulates React Native's behavior for setting HTTP headers.
7
+ *
8
+ * @see https://github.com/facebook/react-native/blob/de5093c88771977b58f7bec3f3ffa64a9595334e/packages/react-native/Libraries/Network/RCTNetworking.mm#L345-L349
9
+ */
10
+ export function applyReactNativeRequestHeadersLogic(
11
+ headers: HttpHeaders,
12
+ postData?: RequestPostData
13
+ ): HttpHeaders {
14
+ const existingContentType = getHttpHeader(headers, 'content-type');
15
+
16
+ if (existingContentType) {
17
+ return headers;
18
+ }
19
+
20
+ const inferredContentType = inferContentTypeFromPostData(postData);
21
+
22
+ if (!inferredContentType) {
23
+ return headers;
24
+ }
25
+
26
+ return {
27
+ ...headers,
28
+ 'Content-Type': inferredContentType,
29
+ };
30
+ }
@@ -0,0 +1,28 @@
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 https://github.com/facebook/react-native/blob/588f0c5ce6c283f116228456da2170d2adc3cbf4/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java#L637
11
+ */
12
+ export const applyReactNativeResponseHeadersLogic = (
13
+ headers: XHRHeaders
14
+ ): HttpHeaders => {
15
+ const parsedHeaders: HttpHeaders = { ...headers };
16
+
17
+ const setCookieHeader = getHttpHeader(headers, 'set-cookie');
18
+
19
+ if (setCookieHeader) {
20
+ const { value, originalKey } = setCookieHeader;
21
+
22
+ const cookies = splitSetCookieHeaderByComma(value);
23
+
24
+ parsedHeaders[originalKey] = cookies.length > 0 ? cookies : value;
25
+ }
26
+
27
+ return parsedHeaders;
28
+ };