fix(extension): capture initial-page-load XHR/fetch bodies (injection-timing race)#67
Merged
Merged
Conversation
Add four config options to startNetworkRecording (Chrome/Edge only): - disableCache: wipe HTTP cache for the origin at record start - wipeServiceWorkers: unregister SWs + clear Cache API at start - recordAjax (default true): when false, xhr/fetch not recorded at all - requestScope (default all): "top-level" records only frameId 0 Cache/SW wipes coalesce into one fire-and-forget chrome.browsingData.remove inside the tabs.create callback (preserves the sidePanel.open gesture); browsingData permission added to chrome+edge manifests only, feature-guarded so Firefox/Safari no-op. Harness updated with the four toggles. Refs RQ-3330 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- disableCache: document that it clears the ENTIRE browser HTTP cache (all sites), not just the recorded origin — Chrome ignores the origins filter for the `cache` data type. - injectBodyRecorder: fix stale comment — when recordAjax=false, xhr/fetch are dropped by the webRequest path too (isAjaxRequest), so ajax is not recorded at all (not "falls to the webRequest skeleton path"). Comment-only; no functional change. Addresses the two confirmed (doc-only) findings from the multi-agent PR review. Refs RQ-3330 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-timing race) v2 body capture missed an SPA's bootstrap XHR/fetch responses because the web-sdk interceptor was injected via executeScript from webNavigation.onCommitted and only armed on the START round-trip — landing after the page's first requests fired. Those responses carry the IDs/slugs LTS auto-correlation needs, so correlation chained 0 rows. Fix — register before the recorded navigation, buffer from t=0: - Create the recorded tab at about:blank, open the side panel synchronously (gesture intact), THEN register the web-sdk UMD + body recorder as one ordered document_start MAIN-world content script (registerContentScripts), THEN navigate to the URL. The interceptor is guaranteed armed before request #1. The URL loads only once, so nothing is double-captured. - Page script arms the interceptor at injection and BUFFERS captures until START, then flushes — so even an early request is retained. - Pull-based READY->START handshake with retry: the page re-posts READY until START arrives (the one-shot version raced the content-script relay on fast loads and silently dropped, leaving the buffer un-flushed / 0 entries). - Guard clientHandler + tabService onCommitted/DOMContentLoaded handlers against non-http(s) URLs so the about:blank step doesn't log access errors. - Unregister the body-recorder scripts when the last recording stops; clean up orphaned registrations on SW init. Also raise DEFAULT_MAX_PAYLOAD_SIZE 200KB -> 10MB (safe maximum: covers realistic API/JSON bodies, stays well under Chrome's ~32MB sendMessage ceiling so oversized bodies truncate gracefully instead of throwing). Validated on realworld.show and practicesoftwaretesting.com: bootstrap XHRs now captured with request+response bodies; panel counts them; no double-send. Stacked on the advanced-settings branch (PR #66). Refs RQ-2894 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rohanmathur91
approved these changes
Jun 10, 2026
Base automatically changed from
feat/network-recording-advanced-settings
to
master
June 10, 2026 10:33
…pture-injection-race # Conflicts: # browser-extension/mv3/src/service-worker/services/networkRecording/index.ts
Buffer leak: the body recorder is registered into every http(s) tab while a
recording is active, but only the recorded tab gets START. On every other tab
the interceptor sat in "buffering" forever, accumulating raw request/response
bodies for the page's lifetime. Now:
- the READY handshake, on giving up (no START after ~6s), transitions to
"stopped" and frees the buffer — so non-recorded tabs stop capturing;
- the pre-START buffer is hard-capped (MAX_BUFFERED_ENTRIES, drop-oldest) so it
can't grow unbounded even before give-up / on a slow-to-START recorded tab.
Late START (>6s, effectively never for a recorded tab) is ignored — the
recorded-tab live path is unaffected (START arrives in ms).
Focus: navigate the recorded tab with { active: true } so Chrome moves focus to
the page instead of leaving it in the omnibox (the about:blank tab held no
focus). Best-effort nudge.
Addresses the buffer-leak finding from the PR review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
What
Fixes a v2 body-capture bug: an SPA's initial page-load XHR/fetch responses were missed, so LTS auto-correlation chained 0 rows (those bootstrap responses carry the IDs/slugs correlation needs).
Refs RQ-2894.
Root cause
The web-sdk fetch/XHR interceptor was injected via
executeScriptfromwebNavigation.onCommittedand only armed on theSTARTround-trip — so it landed after the page's first requests fired. Those request bodies were never captured (no webRequest fallback for xhr/fetch — it's hard-suppressed). On fast/cached SPAs this silently lost the correlation-critical bootstrap responses.Fix — register before navigation, buffer from t=0
about:blank, open the side panel synchronously (preserves thesidePanel.open()user gesture — about:blank doesn't consume it), thenawaitregistering the web-sdk UMD + body recorder as one ordereddocument_startMAIN-world content script (registerContentScripts), thenchrome.tabs.updateto the real URL. The interceptor is guaranteed armed before request bug: 307 in mock server leads to failure when used user with net:http module #1, and the URL loads exactly once (no double-send).START, then flushes — so even an early request is retained.READYuntilSTARTarrives. (A one-shotREADYraced the content-script relay on fast loads and silently dropped, leaving the buffer un-flushed → 0 entries. This was thepracticesoftwaretesting.comfailure.)clientHandler+tabServiceonCommitted/onDOMContentLoadedhandlers now skip non-http(s) URLs, so the about:blank step doesn't log "Cannot access contents of url about:blank" / "Receiving end does not exist".Also
DEFAULT_MAX_PAYLOAD_SIZE200KB → 10MB — a "safe maximum": covers realistic API/JSON bodies, stays well under Chrome's ~32MBchrome.runtime.sendMessageceiling (bodies stream one-per-message) so an oversized body truncates gracefully (RESPONSE_TOO_LARGEflag) instead of throwing and losing the entry. (An earlier LTS run sentmaxPayloadSize: 200— 200 bytes — and stripped all bodies; unit is bytes, documented.)Known limitation (documented, not coded around)
START/flush delivery rides the client content-script relay, which attaches only on HTML top documents. A recorded tab whose top document is non-HTML (raw JSON/XML/no-doctype) won't capture xhr/fetch bodies. Acceptable — LTS records web apps, not raw endpoints. A comment documents how to add an SW-side watchdog if that assumption ever changes (deliberately not added — avoids machinery for a case real targets don't hit).
Approach validation
The about:blank approach was chosen after a head-to-head evaluation (vs. CDP/debugger, static-shim, accept-the-race). It's the only one that structurally guarantees request #1 is captured (await-before-navigate → document_start ordering), with zero new permissions. CDP was rejected (persistent debugging banner, mutual-exclusion with the user's DevTools, debugger permission, not race-free for body retrieval).
Testing
Manually validated on realworld.show and practicesoftwaretesting.com (the latter intermittently failed before the READY-retry fix): bootstrap XHRs captured with request+response bodies, panel counts them, no double-send, side panel opens, no console errors.
Build clean (only pre-existing TS7030/circular-dep warnings).
🤖 Generated with Claude Code