feat(network-activity-plugin): binary hex viewer + metadata card#274
Merged
V3RON merged 4 commits intoMay 19, 2026
Merged
Conversation
The XHR getResponseBody now widens its binary path past image content-types. After the text / JSON / SVG carve-outs, any blob is captured as base64 — application/pdf, application/zip, audio/*, video/*, font/*, application/octet-stream, anything else a server might serve. arraybuffer responseType gains its own branch that reads request.response as an ArrayBuffer and base64-encodes the bytes through a chunked helper so we don't hit the String.fromCharCode.apply argument-limit on multi-megabyte buffers. The 5 MB cap applies uniformly to both paths. null payloads and zero-length arraybuffers short-circuit to null — the existing semantics for "request did not produce a body." Post-commit invariant: body === null at the UI means only "the plugin genuinely could not read the body" — every other shape arrives via the wire union. The nitro-fetch capture path is unchanged at runtime — its native module still surfaces only text bodies, and its type widened in PR1. A native-side change in react-native-nitro-fetch is tracked separately. The test file's "PR2 widens this" placeholder for application/pdf is flipped to assert the binary union output. New tests cover the arraybuffer branch (basic capture, chunked 100 KB round-trip, cap short-circuit, null payload, empty buffer).
Non-image binary responses (PDF, audio, video, fonts, application/
octet-stream, anything else) get a real first-class rendering: a
metadata card on top, a virtualized hex view underneath. The Raw
view of image responses is upgraded with the same pair, so the
"show me the bytes" affordance is consistent across every binary
shape.
New surfaces:
- HexView (src/ui/components/HexView.tsx): react-virtuoso list of
classic offset / hex / ASCII rows, native text selection over
the rendered rows, no toolbar — when users need every byte they
reach for the Download button on the metadata card.
- MetadataCard (src/ui/components/MetadataCard.tsx): decoded size +
Content-Length header (when present) + derived filename via a
three-tier rule (Content-Disposition → URL last path segment →
response.<ext> with a small content-type → extension map).
Download button decodes the base64 to a Blob and triggers an
anchor click. Disabled for the binary-too-large variant with a
tooltip explaining the 5 MB cap.
- utils/hex.ts: toHexPair, toAsciiChar, formatHexRow (preserves
the post-byte-8 double-space gap so the column reads like xxd
output), rowCountForByteLength.
- utils/download.ts: base64ToBytes / base64ToBlob (uses an
explicit ArrayBuffer allocation so the BlobPart type is happy),
readHeader (case-insensitive lookup), deriveFilename with RFC
5987 filename* preference, downloadBlob.
- binaryRenderer (src/ui/response-renderers/binary.tsx): matches
body.kind === 'binary' && !contentType.startsWith('image/');
views: ['raw']; renders MetadataCard + HexView. Registered
between imageRenderer and jsonRenderer in the dispatcher.
Modified surfaces:
- imageRenderer Raw view: was a metadata-only card; now the same
MetadataCard + HexView pair as binaryRenderer. The image's
Preview is unchanged.
- RenderCtx gains optional headers + size fields. ResponseTab
populates them from selectedRequest.response.
Tooling:
- react-virtuoso ^4.6.0 added to network-activity-plugin devDeps
(was already a workspace dep via perfmon-plugin).
- vitest.setup.ts replaces react-virtuoso's Virtuoso with a non-
virtualized passthrough so RTL tests can assert on rendered row
content (jsdom can't drive the ResizeObserver virtuoso needs).
Tests (100 passing across the plugin, 41 new in this commit):
- utils/__tests__/hex.test.ts (11 cases) — byte/char helpers,
full / short / offset / half-row formatting.
- utils/__tests__/download.test.ts (13 cases) — base64 decode +
Blob construction, case-insensitive header lookup, three-tier
filename derivation with RFC 5987 preference.
- components/__tests__/HexView.test.tsx (4 cases) — empty-buffer
placeholder, single-byte row, fully-formatted row asserted with
whitespace-preserving matcher.
- components/__tests__/MetadataCard.test.tsx (8 cases) — size /
Content-Length / filename rendering, Download enable/disable
semantics, jsdom-stubbed createObjectURL on click.
- response-renderers/__tests__/binary.test.tsx (7 cases) — match
predicate (claims non-image binary, declines image binary and
strings), composed rendering of metadata + hex.
- response-renderers/__tests__/dispatch.test.ts (+2 cases) —
binary routing matrix + new priority assertion (image before
binary, since both claim body.kind === 'binary').
- response-renderers/__tests__/image.test.tsx — Raw assertion
updated to reflect the new MetadataCard + HexView composition.
A new "Binary" main-tab on the Network Test screen fires GETs at PDF / octet-stream / WOFF2 / MP3 / ZIP endpoints so the panel's hex viewer and metadata card can be exercised end-to-end. Same component pattern as the existing Images tab — title + hint, button grid, last-result label, error / loading states. URL set was pre-verified during PR planning: stable hosts (W3C, httpbin, jsdelivr, Kozco, GitHub codeload), small enough to stay under the 5 MB capture cap, content-types that cover the binary-renderer's discriminated families (application/pdf, application/octet-stream, font/*, audio/*, application/zip).
Minor bump for @rozenite/network-activity-plugin. The "Response Viewing" section in network-activity.mdx is extended to cover the new binary case: image Raw view upgraded to metadata + hex, a dedicated bullet for non-image binary (PDF / font / audio / video / zip / octet-stream), an explanatory note on the size cap also disabling Download, and a new "Downloading bytes" subsection that documents the three-tier filename derivation rule.
943ef3f to
a2ac73c
Compare
10 tasks
V3RON
approved these changes
May 19, 2026
V3RON
pushed a commit
that referenced
this pull request
May 19, 2026
Part of #244 — "Improve response viewing in Network Activity." Makes the response panel's CodeBlock threshold-aware so multi-megabyte text bodies don't pin hundreds of thousands of DOM nodes. ## Summary - **`CodeBlock` becomes threshold-aware.** When `children` is a string longer than 50,000 characters, the body renders inside a `react-virtuoso` `<Virtuoso>` and only the rows currently visible inside a 500 px window stay in the DOM. Below the threshold (and for any non-string children — `<JsonTree>`, `<XmlTree>` wrapped in CodeBlock for the monospace-on-dark frame), rendering is unchanged: a flat `<pre>`. The dispatch is a runtime `typeof children === 'string'` branch — zero changes required at any of the 11+ existing CodeBlock call sites (json / xml / svg / text-fallback renderers, malformed-fallback paths, RequestBody). - **Virtuoso configuration.** Fixed `style={{ height: 500 }}` matches the visual scale of HexView and the iframe elsewhere in the response panel. `<div>` rows (Virtuoso positions rows absolutely and needs block-level items). Per-row `whitespace-pre-wrap wrap-anywhere` preserves today's wrapping UX so long URLs / base64 / minified-on-one-line content wrap visually instead of forcing horizontal scroll. Outer styling reuses the existing `codeBlockClassNames` constant so the dark bg / monospace / border / padding are identical between the two branches. - **Known shape limitation.** A 50 KB body with no newlines virtualizes to a single row containing the entire payload. Browser-level wrapping keeps the layout sane but there's no real virtualization benefit for that shape — the perf win shows up once the body has many lines. - **UX trade-off worth knowing.** Today's `<pre>` expands to content (height grows with body length). After this change, content above 50 KB scrolls inside a 500 px window. Deliberate, for snappy rendering on multi-megabyte responses. - **Adds `react-virtuoso ^4.6.0`** to `@rozenite/network-activity-plugin`'s `dependencies`. - **Playground.** A new "Large text" main-tab on `NetworkTestScreen` with two pre-verified URLs straddling the threshold so reviewers can see both branches: Alice in Wonderland from Gutenberg (~174 KB, virtualizes) and the GPL-3 license text (~35 KB, stays flat). ## Test plan - [ ] `pnpm --filter @rozenite/network-activity-plugin test` — all tests pass (8 new in `CodeBlock.test.tsx`). - [ ] `pnpm --filter @rozenite/network-activity-plugin typecheck` — clean. - [ ] `pnpm --filter @rozenite/network-activity-plugin lint` — clean. - [ ] `npx prettier --check` on changed files — clean. - [ ] Playground (`pnpm --filter @rozenite/playground ios` or `:android`) → Network Test → Large text tab: - [ ] **Alice in Wonderland (~174 KB)** — Response body scrolls inside a 500 px window. NOT an unbounded `<pre>`. Scrolling is smooth at the end of the ~3300-line file. Dark bg / monospace / border look identical to a small `<pre>`. - [ ] **GPL-3 license (~35 KB)** — Response body renders as a flat `<pre>`, height grows with content, no internal scrolling. (Control: confirms the threshold gating works.) - [ ] **JSON tree responses unchanged** — open any JSON response (HTTP Test → User-list). Renders as JsonTree wrapped in CodeBlock as before. No virtualization (children is a React element, not a string). - [ ] **SVG Raw view unchanged** — Images tab → SVG → toggle to Raw. Renders as flat `<pre>` with SVG source (SVG bodies are well under threshold). - [ ] **Toggle behavior unchanged** — sticky Preview/Raw preference, override button presence, content-type label all work as before. ## New test surface `components/__tests__/CodeBlock.test.tsx` (8 tests): - Small string content renders as `<pre>` with no virtuoso-mock present. - Exactly 50,000 chars stays on the flat path (inclusive-boundary semantics — the branch condition is `>`, not `>=`). - 50,001 chars switches to the virtualized rendering. - Content survives the threshold split (recognizable head and tail tokens both visible in the DOM after virtualization). - Newlines in virtualized content produce one row per line. - React-element children stay on the `<pre>` path even when the wrapped element contains a 50,001-char string internally (the `typeof children === 'string'` check takes precedence). - `className` forwarding on both the flat and virtualized branches. `vitest.setup.ts` adds a `vi.mock('react-virtuoso')` passthrough so jsdom-based tests can assert content correctness. jsdom can't drive Virtuoso's resize observation otherwise — without the stub any test importing a virtualizing component fails to render rows. Production code is unaffected. ## Followups (deliberately not in this PR) - Search inside virtualized content. - Truncation modes. - Virtualizing trees (`JsonTree`, `XmlTree`) — would require forking `react-json-tree` or rewriting from scratch. - Per-row line numbers in the virtualized branch. - Syntax highlighting (cross-cutting feature; would also benefit XML / JSON / HTML Raw views). - Programmatic line-wrapping for single-line content longer than the threshold (would require choosing a column width). ## Coordination notes - **`react-virtuoso` dep + `vi.mock('react-virtuoso')` test stub** are also added by #274 (binary hex viewer, ready for review). The lines are identical on both branches; whichever lands second will see a clean auto-merge (or a one-line trivial conflict on the dependencies block). - **`network-activity.mdx`** Response Viewing section is also edited by #275 (HTML) and #277 (XML + JSON Raw). This PR appends a separate paragraph at the end of the section rather than touching their bullets — additive, no overlap.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #244 — "Improve response viewing in Network Activity." Adds a real hex viewer + Download affordance for binary responses.
Summary
getResponseBodynow treats every non-text-friendly blob as binary, not justimage/*. The same branch also handlesresponseType === 'arraybuffer'via a chunkedString.fromCharCodeencoder so multi-MB payloads don't blow the engine's argument limit. The 5 MB cap continues to apply uniformly. Invariant after this PR:body === nullon the wire means only "we genuinely could not read the body."<HexView>component renders responses as the classic offset / hex / ASCII grid, virtualized viareact-virtuosofrom the first row. Native text selection covers what's currently on screen; for whole-payload extraction users reach for the Download button.imageRenderer(Raw view, upgraded) and the newbinaryRenderer. Shows decoded size,Content-Lengthheader when present, derived filename, and a Download button that decodes the base64 to aBloband triggers an anchor click. The filename uses a three-tier rule:Content-Disposition(RFC 5987filename*preferred) → URL last path segment →response.<ext>from a small content-type → extension map (fallback.bin).binaryRenderermatchesbody.kind === 'binary' && !contentType.startsWith('image/')— PDF / font / audio / video / zip / octet-stream / anything-else. Declaresviews: ['raw'](the Preview / Raw toggle is hidden adaptively). Registered in the dispatcher betweenimageRendererand the JSON / text-fallback renderers.<MetadataCard /><HexView />pair, matching binary entries. Image's Preview is unchanged.RenderCtxgrows two optional fields —headersandsize— whichResponseTabpulls fromselectedRequest.response.react-native-nitro-fetchis tracked separately.Test plan
pnpm --filter @rozenite/network-activity-plugin test— 100 tests pass (41 new in this PR).pnpm --filter @rozenite/network-activity-plugin typecheck— clean.pnpm --filter @rozenite/network-activity-plugin lint— clean.pnpm start:playground:iosor:android) → Network Test → Binary tab:%PDF-1.xheader in the hex ASCII column. Download saves a working PDF.wOF2magic visible at byte 0.ID3magic visible if the source has an ID3v2 header.PK\x03\x04magic visible. Download saves a working zip.binaryRendererdeclares only Raw, adaptive rule hides the toggle).react-virtuosowarnings, no ResizeObserver errors.New test surface
utils/__tests__/hex.test.ts(11) — toHexPair / toAsciiChar / formatHexRow (full row + short row + offset + half-row gap) / rowCountForByteLength.utils/__tests__/download.test.ts(13) — base64ToBytes, base64ToBlob (Blob.type fallback), readHeader (case-insensitive lookup, array-valued headers), deriveFilename (Content-Disposition incl. RFC 5987 filename*, URL last segment, content-type → extension fallback, parameter stripping).components/__tests__/HexView.test.tsx(4) — empty-buffer placeholder, single-byte row, full row asserted with whitespace-preserving matcher.components/__tests__/MetadataCard.test.tsx(8) — size / Content-Length / filename rendering, Download enable/disable semantics, jsdom-stubbedcreateObjectURLon click, binary-too-large variant.response-renderers/__tests__/binary.test.tsx(7) — match predicate (claims non-image binary, declines image binary and strings), composed rendering of metadata + hex.response-renderers/__tests__/dispatch.test.ts(+2) — binary routing matrix + priority assertion (image before binary, since both claimbody.kind === 'binary').react-native/http/__tests__/http-utils.test.ts(+5) — non-image-blob → binary union, arraybuffer capture, chunked 100 KB base64 round-trip, arraybuffer cap short-circuit, null payload + empty buffer guards.Followups (deliberately not in this PR)
react-native-nitro-fetchso nitro-captured image / binary responses also reach the panel as bytes. Tracked separately.Tooling
react-virtuoso ^4.6.0added to@rozenite/network-activity-plugindevDeps (was already a workspace dep via perfmon-plugin).vitest.setup.tsreplacesreact-virtuoso'sVirtuosowith a non-virtualized passthrough so RTL tests can assert on rendered row content (jsdom can't drive the ResizeObserver virtuoso needs).