Skip to content

feat(extension): Network Interceptor v2 — request/response bodies + headers (XHR/Fetch via web-sdk)#56

Merged
nafees87n merged 9 commits into
feat/network-recording-harfrom
feat/network-recording-bodies-v2
Jun 3, 2026
Merged

feat(extension): Network Interceptor v2 — request/response bodies + headers (XHR/Fetch via web-sdk)#56
nafees87n merged 9 commits into
feat/network-recording-harfrom
feat/network-recording-bodies-v2

Conversation

@nafees87n
Copy link
Copy Markdown
Contributor

What

v2 of the LT Network Interceptor: captures full request + response bodies and headers for XHR/Fetch, filling the HAR fields v1 left empty (request.postData, response.content.text). Builds on v1 (PR #53).

Proposal: https://browserstack.atlassian.net/wiki/spaces/ENG/pages/6238732295 (v2 section)

Base branch: this PR targets feat/network-recording-har (PR #53), not master, so the diff is just the v2 commit. Re-target to master once v1 merges.

How it works

Source-of-truth split — one source per request class, no correlation:

Request class Source Contents
XHR / Fetch web-sdk Network interceptor only full HAR entry: headers + request body + response body
Everything else (doc, img, css, js, font, media, ws) chrome.webRequest (v1, unchanged) headers, status, timings — no body
  • Reuses the web-sdk Network module (the same one session recording uses in production) — consumed directly (not via SessionRecorder, which strips headers), so headers survive. A new MAIN-world page script (networkBodyRecorder) calls the global Requestly.Network.intercept(..., overrideResponse=false) — observe only, never alters the real request/response.
  • webRequest hard-suppressed for xmlhttprequest (the resource type for both XHR and fetch) → exactly one source, no dedupe/supersede/correlation. The v1 correlationMap is untouched and stays load-bearing for non-xhr/fetch types.
  • No header injection into real requests — the SDK reads bodies in-page; nothing added to the wire (avoids CORS-preflight distortion in load tests).
  • Injection lifecycle: web-sdk UMD + page script injected per recorded tab, re-injected on webNavigation.onCommitted (single-tab scoped). Stop gates the page callback off (never Network.clearInterceptors() — that would nuke other SDK consumers).

Config & caps

  • startNetworkRecording config gains maxPayloadSize (bytes, default 100 KB, LTS-overridable).
  • Caps re-implemented in the page script (Network.intercept has no size options — they live only on SessionRecorder): media-response skip + per-body cap. Over-cap/dropped bodies are flagged via a _truncated HAR extension field (101 = request too large, 102 = response too large), so LTS can tell "dropped (too large)" from genuinely empty.

SDK-sourced entries flow through the same stream/buffer path as v1 — LTS and the side panel consume them identically. No new stream message type. Side panel stays metadata-only this increment (bodies stream to LTS; in-panel body display deferred).

Known limitation

An XHR/Fetch the SDK can't capture — fired before the page script injected, a no-cors/opaque fetch, or a page where MAIN-world injection is blocked by strict CSP — is dropped entirely (not backfilled from webRequest). Accepted trade for the no-correlation simplicity. (CSP is mitigable via the existing handleCSPError DNR strip, but that alters page behavior / load-test fidelity — left as a separate, LTS-confirmed decision.)

Testing

  • Both packages build clean (no new TS errors/warnings).
  • Message wiring verified statically end-to-end (page → content-script relay → SW handler; start/stop control signals; requestly:client/requestly:extension sources).
  • Not yet exercised in a live browser — load mv3/dist unpacked and confirm: XHR/fetch entries carry headers + bodies, no duplicate xhr/fetch entries, request.postData/response.content.text populate, over-cap → _truncated, media response skipped, navigation re-injects.

🤖 Generated with Claude Code

nafees87n and others added 9 commits June 2, 2026 19:03
…ders for XHR/Fetch via web-sdk

Captures full request + response bodies and headers for XHR/Fetch by reusing
the web-sdk Network interceptor (the module session recording uses), filling
the HAR fields v1 left empty (request.postData, response.content.text).

- SDK is the SOLE source for XHR/Fetch: webRequest is hard-suppressed for
  "xmlhttprequest" in onBeforeSendHeaders/onRequestCompleted/onRequestError, so
  there's exactly one source and no correlation. The v1 correlationMap is left
  intact for the non-xhr/fetch resource types that still come from webRequest.
- New MAIN-world page script (networkBodyRecorder) calls the global
  Requestly.Network.intercept; the web-sdk UMD is injected per recorded tab
  (re-injected on webNavigation.onCommitted, single-tab scoped). overrideResponse
  is false (observe only — never alters the real request/response).
- Size caps re-implemented in the page script (Network.intercept has no options;
  maxPayloadSize/ignoreMediaResponse live only on SessionRecorder): media-skip +
  per-body maxPayloadSize, surfaced as a _truncated extension field on the entry.
- maxPayloadSize is configurable via startNetworkRecording config (default 100KB).
- Stop gates the page callback off (never Network.clearInterceptors() — that would
  nuke other SDK consumers like session recording).
- SDK-sourced entries flow through the same deliverEntry/stream path as v1, so LTS
  and the side panel consume them identically. No new stream message type.

Known limitation: an XHR/Fetch the SDK can't capture (pre-injection race,
no-cors/opaque, CSP-blocked injection) is dropped entirely (not backfilled from
webRequest) — accepted trade for the no-correlation simplicity.

Builds clean; message wiring verified static end-to-end. Live-browser recording
not yet exercised.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… window.Requestly

The web-sdk UMD declares a top-level `var Requestly` (no explicit window
attach). Referencing it bare — as the shipping sessionRecorderHelper.js does
with `Requestly.SessionRecorder` — resolves the global binding regardless of
how the injected file's scope reflects onto window; `window.Requestly` was a
fragile stronger assumption.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… feat/network-recording-bodies-v2

# Conflicts:
#	browser-extension/mv3/src/service-worker/services/networkRecording/index.ts
… feat/network-recording-bodies-v2

# Conflicts:
#	browser-extension/mv3/src/service-worker/services/networkRecording/index.ts
…ed start guard (v2)

The decoupled CLIENT_PAGE_LOADED panel-open path didn't open the side panel (a
page-load event is not a user gesture), and the isExtensionEnabled await before
tabs.create pushed sidePanel.open() past the gesture window. Restore the
synchronous start path with eager openPanel(tab.id), and drop the start-guard.

(Converges with base's 53d9d75; reconciled in the following merge.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… feat/network-recording-bodies-v2

# Conflicts:
#	browser-extension/mv3/src/service-worker/services/networkRecording/index.ts
… feat/network-recording-bodies-v2

# Conflicts:
#	browser-extension/mv3/src/service-worker/services/networkRecording/index.ts
… feat/network-recording-bodies-v2

# Conflicts:
#	browser-extension/mv3/src/service-worker/services/networkRecording/index.ts
@nafees87n nafees87n merged commit 8a95ecc into feat/network-recording-har Jun 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant