@rozenite/network-activity-plugin 1.8.1 → 1.10.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/CHANGELOG.md +56 -0
- package/dist/devtools/App.html +2 -2
- package/dist/devtools/assets/{App-m6xge0az.css → App-CUXU0mup.css} +152 -2
- package/dist/devtools/assets/{App-B3xlUjs6.js → App-DsimzJvx.js} +6833 -966
- package/dist/react-native/chunks/boot-recording.cjs +156 -28
- package/dist/react-native/chunks/boot-recording.js +156 -28
- package/dist/react-native/chunks/get-nitro-module.cjs +12 -0
- package/dist/react-native/chunks/get-nitro-module.js +13 -0
- package/dist/react-native/chunks/useNetworkActivityDevTools.require.cjs +20 -1
- package/dist/react-native/chunks/useNetworkActivityDevTools.require.js +20 -1
- package/dist/react-native/index.d.ts +39 -3
- package/dist/rozenite.json +1 -1
- package/dist/sdk/index.d.ts +37 -1
- package/package.json +12 -7
- package/src/react-native/agent/use-network-activity-agent-tools.ts +22 -4
- package/src/react-native/http/__tests__/http-utils.test.ts +228 -0
- package/src/react-native/http/http-utils.ts +209 -25
- package/src/react-native/network-inspector.ts +2 -2
- package/src/react-native/nitro-fetch/get-nitro-module.ts +13 -0
- package/src/react-native/nitro-fetch/nitro-network-inspector.ts +10 -11
- package/src/shared/http-events.ts +40 -1
- package/src/ui/components/CodeBlock.tsx +45 -1
- package/src/ui/components/FilterBar.tsx +366 -58
- package/src/ui/components/HexView.tsx +54 -0
- package/src/ui/components/MetadataCard.tsx +95 -0
- package/src/ui/components/RequestList.tsx +192 -34
- package/src/ui/components/SidePanel.tsx +42 -1
- package/src/ui/components/ViewToggle.tsx +44 -0
- package/src/ui/components/XmlTree.tsx +160 -0
- package/src/ui/components/__tests__/CodeBlock.test.tsx +89 -0
- package/src/ui/components/__tests__/HexView.test.tsx +41 -0
- package/src/ui/components/__tests__/MetadataCard.test.tsx +107 -0
- package/src/ui/components/__tests__/ViewToggle.test.tsx +80 -0
- package/src/ui/components/__tests__/XmlTree.test.tsx +149 -0
- package/src/ui/response-renderers/__tests__/binary-too-large.test.tsx +56 -0
- package/src/ui/response-renderers/__tests__/binary.test.tsx +96 -0
- package/src/ui/response-renderers/__tests__/dispatch.test.ts +124 -0
- package/src/ui/response-renderers/__tests__/html.test.tsx +101 -0
- package/src/ui/response-renderers/__tests__/image.test.tsx +73 -0
- package/src/ui/response-renderers/__tests__/json.test.tsx +95 -0
- package/src/ui/response-renderers/__tests__/svg.test.tsx +46 -0
- package/src/ui/response-renderers/__tests__/xml.test.tsx +100 -0
- package/src/ui/response-renderers/binary-too-large.tsx +36 -0
- package/src/ui/response-renderers/binary.tsx +31 -0
- package/src/ui/response-renderers/empty.tsx +14 -0
- package/src/ui/response-renderers/html.tsx +36 -0
- package/src/ui/response-renderers/image.tsx +37 -0
- package/src/ui/response-renderers/index.ts +55 -0
- package/src/ui/response-renderers/json.tsx +40 -0
- package/src/ui/response-renderers/svg.tsx +27 -0
- package/src/ui/response-renderers/text-fallback.tsx +14 -0
- package/src/ui/response-renderers/types.ts +38 -0
- package/src/ui/response-renderers/unknown.tsx +18 -0
- package/src/ui/response-renderers/xml.tsx +46 -0
- package/src/ui/state/derived.ts +12 -0
- package/src/ui/state/model.ts +6 -1
- package/src/ui/state/store.ts +39 -2
- package/src/ui/tabs/InitiatorTab.tsx +230 -0
- package/src/ui/tabs/ResponseTab.tsx +80 -96
- package/src/ui/tabs/__tests__/ResponseTab.test.tsx +102 -0
- package/src/ui/utils/__tests__/download.test.ts +115 -0
- package/src/ui/utils/__tests__/hex.test.ts +84 -0
- package/src/ui/utils/__tests__/symbolication.test.ts +207 -0
- package/src/ui/utils/download.ts +154 -0
- package/src/ui/utils/hex.ts +59 -0
- package/src/ui/utils/initiator.ts +136 -0
- package/src/ui/utils/symbolication.ts +248 -0
- package/src/ui/views/InspectorView.tsx +8 -5
- package/src/utils/__tests__/getContentTypeMimeType.test.ts +65 -0
- package/src/utils/getContentTypeMimeType.ts +28 -0
- package/vite.config.ts +9 -1
- package/vitest.setup.ts +31 -0
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {
|
|
2
2
|
XHRPostData,
|
|
3
3
|
RequestPostData,
|
|
4
4
|
RequestTextPostData,
|
|
5
5
|
RequestBinaryPostData,
|
|
6
6
|
RequestFormDataPostData,
|
|
7
|
+
ResponseBody,
|
|
8
|
+
Initiator,
|
|
9
|
+
InitiatorStackFrame,
|
|
7
10
|
} from '../../shared/client';
|
|
8
11
|
import { safeStringify } from '../../utils/safeStringify';
|
|
9
12
|
import { getStringSizeInBytes } from '../../utils/getStringSizeInBytes';
|
|
13
|
+
import {
|
|
14
|
+
isJsonContentType,
|
|
15
|
+
isXmlContentType,
|
|
16
|
+
} from '../../utils/getContentTypeMimeType';
|
|
10
17
|
import {
|
|
11
18
|
isBlob,
|
|
12
19
|
isArrayBuffer,
|
|
@@ -113,9 +120,48 @@ export const getResponseSize = (request: XMLHttpRequest): number | null => {
|
|
|
113
120
|
}
|
|
114
121
|
};
|
|
115
122
|
|
|
123
|
+
// Cap on binary capture. Above this, we ship a 'binary-too-large' variant
|
|
124
|
+
// with just the size — no bytes cross the bridge. 5MB comfortably covers
|
|
125
|
+
// debug-relevant images while keeping the bridge from choking on outliers.
|
|
126
|
+
export const BINARY_CAPTURE_SIZE_CAP = 5 * 1024 * 1024;
|
|
127
|
+
|
|
128
|
+
const readBlobAsText = (blob: Blob): Promise<string> =>
|
|
129
|
+
new Promise((resolve) => {
|
|
130
|
+
const reader = new FileReader();
|
|
131
|
+
reader.onload = () => resolve(reader.result as string);
|
|
132
|
+
reader.readAsText(blob);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const readBlobAsBase64 = (blob: Blob): Promise<string> =>
|
|
136
|
+
new Promise((resolve) => {
|
|
137
|
+
const reader = new FileReader();
|
|
138
|
+
reader.onload = () => {
|
|
139
|
+
// FileReader.readAsDataURL returns "data:<mime>;base64,<payload>".
|
|
140
|
+
// We only want the base64 payload.
|
|
141
|
+
const dataUrl = reader.result as string;
|
|
142
|
+
resolve(dataUrl.substring(dataUrl.indexOf(',') + 1));
|
|
143
|
+
};
|
|
144
|
+
reader.readAsDataURL(blob);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// String.fromCharCode.apply(null, hugeArray) blows up around 64K elements
|
|
148
|
+
// in some engines; chunk through 32K windows so we work uniformly across
|
|
149
|
+
// large arraybuffer responses.
|
|
150
|
+
const ARRAY_BUFFER_BASE64_CHUNK = 0x8000;
|
|
151
|
+
|
|
152
|
+
const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
|
|
153
|
+
const bytes = new Uint8Array(buffer);
|
|
154
|
+
let binary = '';
|
|
155
|
+
for (let i = 0; i < bytes.length; i += ARRAY_BUFFER_BASE64_CHUNK) {
|
|
156
|
+
const chunk = bytes.subarray(i, i + ARRAY_BUFFER_BASE64_CHUNK);
|
|
157
|
+
binary += String.fromCharCode.apply(null, chunk as unknown as number[]);
|
|
158
|
+
}
|
|
159
|
+
return btoa(binary);
|
|
160
|
+
};
|
|
161
|
+
|
|
116
162
|
export const getResponseBody = async (
|
|
117
163
|
request: XMLHttpRequest,
|
|
118
|
-
): Promise<
|
|
164
|
+
): Promise<ResponseBody> => {
|
|
119
165
|
const responseType = request.responseType;
|
|
120
166
|
|
|
121
167
|
// Response type is empty in certain cases, like when using axios.
|
|
@@ -124,22 +170,39 @@ export const getResponseBody = async (
|
|
|
124
170
|
}
|
|
125
171
|
|
|
126
172
|
if (responseType === 'blob') {
|
|
127
|
-
// This may be a text blob.
|
|
128
173
|
const contentType = request.getResponseHeader('Content-Type') || '';
|
|
129
174
|
|
|
175
|
+
// Text-shaped content stays on the text path so the panel can
|
|
176
|
+
// show source in the Raw view without base64 round-tripping. XML
|
|
177
|
+
// composite types (application/xml, atom+xml, soap+xml, ...) and
|
|
178
|
+
// image/svg+xml are all covered by isXmlContentType per RFC 7303.
|
|
130
179
|
if (
|
|
131
180
|
contentType.startsWith('text/') ||
|
|
132
|
-
contentType
|
|
181
|
+
isJsonContentType(contentType) ||
|
|
182
|
+
isXmlContentType(contentType)
|
|
133
183
|
) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
184
|
+
return readBlobAsText(request.response);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Everything else is binary — image/*, application/pdf, audio/*,
|
|
188
|
+
// video/*, font/*, application/octet-stream, anything novel a
|
|
189
|
+
// server might serve.
|
|
190
|
+
const blob = request.response as Blob;
|
|
191
|
+
if (blob.size > BINARY_CAPTURE_SIZE_CAP) {
|
|
192
|
+
return { kind: 'binary-too-large', size: blob.size };
|
|
193
|
+
}
|
|
194
|
+
return { kind: 'binary', base64: await readBlobAsBase64(blob) };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (responseType === 'arraybuffer') {
|
|
198
|
+
const buffer = request.response as ArrayBuffer | null;
|
|
199
|
+
if (!buffer || buffer.byteLength === 0) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
if (buffer.byteLength > BINARY_CAPTURE_SIZE_CAP) {
|
|
203
|
+
return { kind: 'binary-too-large', size: buffer.byteLength };
|
|
142
204
|
}
|
|
205
|
+
return { kind: 'binary', base64: arrayBufferToBase64(buffer) };
|
|
143
206
|
}
|
|
144
207
|
|
|
145
208
|
if (responseType === 'json') {
|
|
@@ -149,26 +212,147 @@ export const getResponseBody = async (
|
|
|
149
212
|
return null;
|
|
150
213
|
};
|
|
151
214
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
215
|
+
const STACK_PREVIEW_FRAME_LIMIT = 8;
|
|
216
|
+
const INITIATOR_STACK_FRAME_OFFSET = 3;
|
|
217
|
+
|
|
218
|
+
const parseStackLocation = (
|
|
219
|
+
location: string,
|
|
220
|
+
): Pick<InitiatorStackFrame, 'url' | 'lineNumber' | 'columnNumber'> | null => {
|
|
221
|
+
const match = location.match(/^(.*):(\d+):(\d+)$/);
|
|
222
|
+
|
|
223
|
+
if (!match) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
url: match[1],
|
|
229
|
+
lineNumber: Number.parseInt(match[2], 10),
|
|
230
|
+
columnNumber: Number.parseInt(match[3], 10),
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const normalizeFunctionName = (functionName?: string) => {
|
|
235
|
+
const trimmedFunctionName = functionName?.trim();
|
|
236
|
+
|
|
237
|
+
return trimmedFunctionName &&
|
|
238
|
+
trimmedFunctionName !== '<anonymous>' &&
|
|
239
|
+
trimmedFunctionName !== 'anonymous' &&
|
|
240
|
+
trimmedFunctionName !== '<unknown>'
|
|
241
|
+
? trimmedFunctionName
|
|
242
|
+
: undefined;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const parseStackFrame = (line: string): InitiatorStackFrame | null => {
|
|
246
|
+
const trimmedLine = line.trim();
|
|
247
|
+
|
|
248
|
+
if (!trimmedLine) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let functionName: string | undefined;
|
|
253
|
+
let location: string | undefined;
|
|
254
|
+
|
|
255
|
+
const v8FunctionFrame = trimmedLine.match(/^at\s+(.*?)\s+\((.*)\)$/);
|
|
256
|
+
if (v8FunctionFrame) {
|
|
257
|
+
functionName = v8FunctionFrame[1];
|
|
258
|
+
location = v8FunctionFrame[2];
|
|
259
|
+
} else {
|
|
260
|
+
const v8LocationFrame = trimmedLine.match(/^at\s+(.*)$/);
|
|
261
|
+
const jscFrame = trimmedLine.match(/^(.*?)@(.*)$/);
|
|
262
|
+
|
|
263
|
+
if (v8LocationFrame) {
|
|
264
|
+
location = v8LocationFrame[1];
|
|
265
|
+
} else if (jscFrame) {
|
|
266
|
+
functionName = jscFrame[1];
|
|
267
|
+
location = jscFrame[2];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!location) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const parsedLocation = parseStackLocation(location);
|
|
276
|
+
if (!parsedLocation) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
functionName: normalizeFunctionName(functionName),
|
|
282
|
+
...parsedLocation,
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const toGeneratedStackFrame = (
|
|
287
|
+
frame: InitiatorStackFrame,
|
|
288
|
+
): InitiatorStackFrame => ({
|
|
289
|
+
functionName: frame.functionName,
|
|
290
|
+
generatedUrl: frame.url,
|
|
291
|
+
generatedLineNumber: frame.lineNumber,
|
|
292
|
+
generatedColumnNumber: frame.columnNumber,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const getGeneratedFrameLocation = (frame: InitiatorStackFrame) => ({
|
|
296
|
+
url: frame.generatedUrl ?? frame.url,
|
|
297
|
+
lineNumber: frame.generatedLineNumber ?? frame.lineNumber,
|
|
298
|
+
columnNumber: frame.generatedColumnNumber ?? frame.columnNumber,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const canSymbolicateStack = (stack?: InitiatorStackFrame[]) =>
|
|
302
|
+
stack?.some((frame) =>
|
|
303
|
+
getGeneratedFrameLocation(frame).url?.startsWith('http'),
|
|
304
|
+
) ?? false;
|
|
305
|
+
|
|
306
|
+
const getStackPreview = (frames: InitiatorStackFrame[]) => {
|
|
307
|
+
// The first frames are this helper, the HTTP inspector callback and the XHR
|
|
308
|
+
// wrapper. The caller starts after that fixed interception boundary.
|
|
309
|
+
const callerFrames = frames.slice(INITIATOR_STACK_FRAME_OFFSET);
|
|
310
|
+
|
|
311
|
+
return (callerFrames.length > 0 ? callerFrames : frames).slice(
|
|
312
|
+
0,
|
|
313
|
+
STACK_PREVIEW_FRAME_LIMIT,
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export const getInitiatorFromStack = (): Initiator => {
|
|
158
318
|
try {
|
|
159
319
|
const stack = new Error().stack;
|
|
160
320
|
if (!stack) {
|
|
161
321
|
return { type: 'other' };
|
|
162
322
|
}
|
|
163
323
|
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
324
|
+
const parsedFrames = stack
|
|
325
|
+
.split('\n')
|
|
326
|
+
.map(parseStackFrame)
|
|
327
|
+
.filter((frame): frame is InitiatorStackFrame => frame !== null);
|
|
328
|
+
|
|
329
|
+
const stackPreview = getStackPreview(parsedFrames);
|
|
330
|
+
const initiatorFrame = stackPreview[0];
|
|
331
|
+
const generatedStackPreview = stackPreview.map(toGeneratedStackFrame);
|
|
332
|
+
|
|
333
|
+
if (initiatorFrame?.url) {
|
|
167
334
|
return {
|
|
168
335
|
type: 'script',
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
336
|
+
functionName: initiatorFrame.functionName,
|
|
337
|
+
generatedUrl: initiatorFrame.url,
|
|
338
|
+
generatedLineNumber: initiatorFrame.lineNumber,
|
|
339
|
+
generatedColumnNumber: initiatorFrame.columnNumber,
|
|
340
|
+
stack: generatedStackPreview,
|
|
341
|
+
symbolicationStatus: canSymbolicateStack(generatedStackPreview)
|
|
342
|
+
? 'pending'
|
|
343
|
+
: 'unavailable',
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (parsedFrames.length > 0) {
|
|
348
|
+
const fallbackStack = stackPreview.map(toGeneratedStackFrame);
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
type: 'other',
|
|
352
|
+
stack: fallbackStack,
|
|
353
|
+
symbolicationStatus: canSymbolicateStack(fallbackStack)
|
|
354
|
+
? 'pending'
|
|
355
|
+
: 'unavailable',
|
|
172
356
|
};
|
|
173
357
|
}
|
|
174
358
|
} catch {
|
|
@@ -195,7 +379,7 @@ export const setupRequestOverride = (
|
|
|
195
379
|
Object.defineProperty(request, 'responseText', { writable: true });
|
|
196
380
|
|
|
197
381
|
const contentType = getContentType(request);
|
|
198
|
-
if (contentType
|
|
382
|
+
if (isJsonContentType(contentType)) {
|
|
199
383
|
request.responseType = 'json';
|
|
200
384
|
} else if (contentType === 'text/plain') {
|
|
201
385
|
request.responseType = 'text';
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
NITRO_NETWORK_EVENTS,
|
|
16
16
|
} from './nitro-fetch/nitro-network-inspector';
|
|
17
17
|
import { EventsListener } from './events-listener';
|
|
18
|
-
import { NetworkActivityEventMap } from '../shared/client';
|
|
18
|
+
import { NetworkActivityEventMap, ResponseBody } from '../shared/client';
|
|
19
19
|
import type { InspectorsConfig } from './config';
|
|
20
20
|
import { getResponseBody as getHTTPResponseBody } from './http/http-utils';
|
|
21
21
|
|
|
@@ -28,7 +28,7 @@ export type NetworkInspector = {
|
|
|
28
28
|
enable: (config?: InspectorsConfig) => void;
|
|
29
29
|
disable: () => void;
|
|
30
30
|
dispose: () => void;
|
|
31
|
-
getResponseBody: (requestId: string) => Promise<
|
|
31
|
+
getResponseBody: (requestId: string) => Promise<ResponseBody>;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
const createNetworkInspectorInstance = (): NetworkInspector => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NitroModule } from './nitro-network-inspector';
|
|
2
|
+
|
|
3
|
+
const nitroModule = (() => {
|
|
4
|
+
try {
|
|
5
|
+
return require('react-native-nitro-fetch') as NitroModule;
|
|
6
|
+
} catch {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
})();
|
|
10
|
+
|
|
11
|
+
export const getNitroModule = (): NitroModule | null => {
|
|
12
|
+
return nitroModule;
|
|
13
|
+
};
|
|
@@ -4,9 +4,11 @@ import type {
|
|
|
4
4
|
HttpHeaders,
|
|
5
5
|
HttpMethod,
|
|
6
6
|
RequestPostData,
|
|
7
|
+
ResponseBody,
|
|
7
8
|
} from '../../shared/client';
|
|
8
9
|
import type { WebSocketEventMap } from '../../shared/websocket-events';
|
|
9
10
|
import type { Inspector } from '../inspector';
|
|
11
|
+
import { getNitroModule as loadNitroModule } from './get-nitro-module';
|
|
10
12
|
|
|
11
13
|
type NitroHttpHeader = {
|
|
12
14
|
key: string;
|
|
@@ -62,7 +64,7 @@ type NitroWebSocketEntry = {
|
|
|
62
64
|
|
|
63
65
|
type NitroInspectorEntry = NitroHttpEntry | NitroWebSocketEntry;
|
|
64
66
|
|
|
65
|
-
type NitroModule = {
|
|
67
|
+
export type NitroModule = {
|
|
66
68
|
NetworkInspector: {
|
|
67
69
|
enable: () => void;
|
|
68
70
|
disable: () => void;
|
|
@@ -91,7 +93,12 @@ type NanoEventsMap = {
|
|
|
91
93
|
};
|
|
92
94
|
|
|
93
95
|
export type NitroNetworkInspector = Inspector<NitroNetworkEventMap> & {
|
|
94
|
-
|
|
96
|
+
// Returns ResponseBody so the wire shape is consistent across capture
|
|
97
|
+
// paths. The nitro native module today only surfaces text response
|
|
98
|
+
// bodies, so at runtime this resolves to string | null — but typing it
|
|
99
|
+
// as ResponseBody lets future native support for binary slot in without
|
|
100
|
+
// a wire-format change.
|
|
101
|
+
getResponseBody: (requestId: string) => ResponseBody;
|
|
95
102
|
};
|
|
96
103
|
|
|
97
104
|
export const NITRO_NETWORK_EVENTS: (keyof NitroNetworkEventMap)[] = [
|
|
@@ -107,14 +114,6 @@ export const NITRO_NETWORK_EVENTS: (keyof NitroNetworkEventMap)[] = [
|
|
|
107
114
|
'websocket-error',
|
|
108
115
|
];
|
|
109
116
|
|
|
110
|
-
const loadNitroModule = (): NitroModule | null => {
|
|
111
|
-
try {
|
|
112
|
-
return require('react-native-nitro-fetch') as NitroModule;
|
|
113
|
-
} catch {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
|
|
118
117
|
const timestampOrigin =
|
|
119
118
|
typeof performance !== 'undefined' &&
|
|
120
119
|
typeof performance.timeOrigin === 'number'
|
|
@@ -170,7 +169,7 @@ export const createNitroNetworkInspector = (
|
|
|
170
169
|
): NitroNetworkInspector => {
|
|
171
170
|
const eventEmitter = createNanoEvents<NanoEventsMap>();
|
|
172
171
|
const previousEntries = new Map<string, NitroInspectorEntry>();
|
|
173
|
-
const responseBodies = new Map<string,
|
|
172
|
+
const responseBodies = new Map<string, ResponseBody>();
|
|
174
173
|
let nitroModule: NitroModule | null = null;
|
|
175
174
|
let unsubscribe: (() => void) | null = null;
|
|
176
175
|
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
export type HttpHeaders = Record<string, string | string[]>;
|
|
2
2
|
export type XHRHeaders = NonNullable<XMLHttpRequest['responseHeaders']>;
|
|
3
3
|
|
|
4
|
+
// Discriminated union for response bodies on the bridge.
|
|
5
|
+
// string → text body (today's path)
|
|
6
|
+
// { kind: 'binary' } → base64-encoded binary payload (e.g. images)
|
|
7
|
+
// { kind: 'binary-too-large' } → response exceeded the in-capture size cap; bytes not shipped
|
|
8
|
+
// null → body could not be read at all
|
|
9
|
+
export type ResponseBody =
|
|
10
|
+
| string
|
|
11
|
+
| { kind: 'binary'; base64: string }
|
|
12
|
+
| { kind: 'binary-too-large'; size: number }
|
|
13
|
+
| null;
|
|
14
|
+
|
|
4
15
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';
|
|
5
16
|
|
|
6
17
|
export type RequestId = string;
|
|
@@ -72,11 +83,39 @@ export type Response = {
|
|
|
72
83
|
responseTime: Timestamp;
|
|
73
84
|
};
|
|
74
85
|
|
|
86
|
+
export type InitiatorStackFrame = {
|
|
87
|
+
functionName?: string;
|
|
88
|
+
url?: string;
|
|
89
|
+
lineNumber?: number;
|
|
90
|
+
columnNumber?: number;
|
|
91
|
+
generatedUrl?: string;
|
|
92
|
+
generatedLineNumber?: number;
|
|
93
|
+
generatedColumnNumber?: number;
|
|
94
|
+
isCollapsed?: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type InitiatorCodeFrame = {
|
|
98
|
+
content: string;
|
|
99
|
+
fileName: string;
|
|
100
|
+
location?: {
|
|
101
|
+
row: number;
|
|
102
|
+
column: number;
|
|
103
|
+
} | null;
|
|
104
|
+
};
|
|
105
|
+
|
|
75
106
|
export type Initiator = {
|
|
76
107
|
type: string;
|
|
108
|
+
symbolicationStatus?: 'pending' | 'complete' | 'failed' | 'unavailable';
|
|
109
|
+
symbolicationError?: string;
|
|
110
|
+
functionName?: string;
|
|
77
111
|
url?: string;
|
|
78
112
|
lineNumber?: number;
|
|
79
113
|
columnNumber?: number;
|
|
114
|
+
generatedUrl?: string;
|
|
115
|
+
generatedLineNumber?: number;
|
|
116
|
+
generatedColumnNumber?: number;
|
|
117
|
+
codeFrame?: InitiatorCodeFrame | null;
|
|
118
|
+
stack?: InitiatorStackFrame[];
|
|
80
119
|
};
|
|
81
120
|
|
|
82
121
|
export type ResourceType = 'XHR' | 'Fetch' | 'Other';
|
|
@@ -137,7 +176,7 @@ export type HttpEventMap = {
|
|
|
137
176
|
|
|
138
177
|
'response-body': {
|
|
139
178
|
requestId: RequestId;
|
|
140
|
-
body:
|
|
179
|
+
body: ResponseBody;
|
|
141
180
|
};
|
|
142
181
|
|
|
143
182
|
'set-overrides': {
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { HTMLProps } from 'react';
|
|
1
|
+
import { HTMLProps, useMemo } from 'react';
|
|
2
|
+
import { Virtuoso } from 'react-virtuoso';
|
|
2
3
|
import { cn } from '../utils/cn';
|
|
3
4
|
|
|
4
5
|
export type CodeBlockProps = HTMLProps<HTMLPreElement>;
|
|
5
6
|
|
|
7
|
+
// Above this character count, string content renders through Virtuoso
|
|
8
|
+
// instead of a flat <pre>. Tuned so typical responses (<20KB) stay on
|
|
9
|
+
// the simple path, while pathological payloads (large pretty-printed
|
|
10
|
+
// JSON / minified bundles served as text / huge logs) virtualize.
|
|
11
|
+
const VIRTUALIZATION_THRESHOLD = 50_000;
|
|
12
|
+
|
|
6
13
|
const codeBlockClassNames =
|
|
7
14
|
'text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded-md border border-gray-700 overflow-x-auto wrap-anywhere';
|
|
8
15
|
|
|
@@ -11,9 +18,46 @@ export const CodeBlock = ({
|
|
|
11
18
|
className,
|
|
12
19
|
...props
|
|
13
20
|
}: CodeBlockProps) => {
|
|
21
|
+
// Only string children are eligible for virtualization. Component
|
|
22
|
+
// children (JsonTree / XmlTree / etc.) manage their own rendering;
|
|
23
|
+
// CodeBlock here just provides the monospace-on-dark frame.
|
|
24
|
+
if (
|
|
25
|
+
typeof children === 'string' &&
|
|
26
|
+
children.length > VIRTUALIZATION_THRESHOLD
|
|
27
|
+
) {
|
|
28
|
+
return <VirtualizedCodeBlock text={children} className={className} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
return (
|
|
15
32
|
<pre className={cn(codeBlockClassNames, className)} {...props}>
|
|
16
33
|
{children}
|
|
17
34
|
</pre>
|
|
18
35
|
);
|
|
19
36
|
};
|
|
37
|
+
|
|
38
|
+
type VirtualizedCodeBlockProps = {
|
|
39
|
+
text: string;
|
|
40
|
+
className?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const VirtualizedCodeBlock = ({
|
|
44
|
+
text,
|
|
45
|
+
className,
|
|
46
|
+
}: VirtualizedCodeBlockProps) => {
|
|
47
|
+
const lines = useMemo(() => text.split('\n'), [text]);
|
|
48
|
+
|
|
49
|
+
// Content with no newlines collapses to a single row containing the
|
|
50
|
+
// entire payload. Browser wrapping (whitespace-pre-wrap, wrap-anywhere)
|
|
51
|
+
// still keeps layout sane, but there's no real virtualization benefit
|
|
52
|
+
// for that shape — the size win shows up once the body has many lines.
|
|
53
|
+
return (
|
|
54
|
+
<Virtuoso
|
|
55
|
+
style={{ height: 500 }}
|
|
56
|
+
totalCount={lines.length}
|
|
57
|
+
itemContent={(idx) => (
|
|
58
|
+
<div className="whitespace-pre-wrap wrap-anywhere">{lines[idx]}</div>
|
|
59
|
+
)}
|
|
60
|
+
className={cn(codeBlockClassNames, className)}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
};
|