BugMojoBugMojoBugMojo
FeaturesPricingBlogGuidesAbout
Log inGet started
BugMojoBugMojo

Bug reports that actually help fix bugs — capture, replay, share.

A product of Softech Infra.

Product

  • Features
  • Pricing
  • Get started
  • Log in

Resources

  • Blog
  • Guides
  • Compare
  • Glossary

Company

  • About
  • Contact
  • Privacy
  • Sitemap
  • Engineering
  • Playbooks
© 2026 BugMojo. All rights reserved.
AllGuidesEngineeringPlaybooksCompareGlossaryAlternativesBy roleBug tracking by framework
  1. Home
  2. Blog
  3. Engineering
  4. Capturing Console and Network Without an SDK: How an Extension Sees Everything
Engineering

Capturing Console and Network Without an SDK: How an Extension Sees Everything

A Manifest V3 Chrome extension can capture console output and network traffic with zero SDK in the app — by injecting into the page's MAIN world and wrapping fetch, XHR, and console at runtime. Here is the exact capture surface, grounded in Chrome's own docs.

Hrishikesh BaidyaHrishikesh Baidya·Jun 5, 2026·8 min read
Engineering
Isometric wireframe of a browser emitting console, network, and DOM streams through a redaction gate, lime on dark.
TL;DR

A Manifest V3 extension captures console and network with no app SDK by injecting a hook into the page's MAIN world at document_start. That hook wraps the five console methods and monkey-patches window.fetch and XMLHttpRequest; rrweb records the DOM in the content script. The DevTools HAR and declarativeNetRequest paths cannot give you full request and response bodies, so MAIN-world patching is the practical route. The trade-off: an extension is on-demand and per-machine, not always-on production monitoring.

You want to file a bug report that includes the failing network request, the console error, and a replay of what the user did — without asking the app team to install anything. No SDK, no npm install, no redeploy. A browser extension can do this because it instruments a build it does not own, at runtime, from outside the application bundle.

The catch is that "capture the console" and "capture fetch" are deceptively hard under Manifest V3. The naive approach — a content script that wraps console.log — captures nothing the page emits. This article walks the exact surface that works, and grounds each constraint in the precise Chrome documentation sentence that explains it.

How does an extension capture console and network with no SDK?

The extension injects a script into the page's MAIN world at document start. That script wraps the five native console methods and monkey-patches window.fetch and XMLHttpRequest to record requests, responses, and logs. rrweb captures the DOM separately in the content script. Nothing is compiled into the app; the instrumentation is added at runtime from the extension.

Why a content script can't just wrap console.log

Content scripts feel like they run "in the page," but they do not share the page's JavaScript objects. Chrome's documentation defines a content script's isolated world as "a private execution environment that isn't accessible to the page," and states plainly that "JavaScript variables in an extension's content scripts are not visible to the host page." The corollary runs both ways: the console and fetch your content script sees are its own private copies, not the objects the application calls.

So if you write console.log = wrap(console.log) in a content script, you have wrapped a console nobody else uses. The page keeps calling its own. You capture zero application output. This single fact is why the entire capture architecture pivots on world boundaries.

The mistake that captures nothing

Patching console or fetch from the content script (the default ISOLATED world). It runs without error and logs your own calls, so it looks like it works in testing — but it never sees a single line the page emits. Verify by triggering a known console.error in the app, not by triggering one yourself.

The fix: inject into the MAIN world at document_start

To reach the page's real console and fetch, you inject into the MAIN world — the same execution environment as the host page's own JavaScript. The chrome.scripting reference documents world: "MAIN" as "the main world of the DOM, which is the execution environment shared with the host page's JavaScript." executeScript defaults to ISOLATED, so you set MAIN explicitly. Timing matters as much as world: register at document_start so your wrappers replace the natives before any page or third-party script calls them. Patch late and you miss everything that fired during load.

manifest.json (excerpt)json
{
  "manifest_version": 3,
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["hook.js"],
      "run_at": "document_start",
      "world": "MAIN"
    },
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_start",
      "world": "ISOLATED"
    }
  ]
}
hook.js — runs in MAIN worldjavascript
// 1) Wrap the five console levels the page actually calls.
for (const level of ['log', 'info', 'warn', 'error', 'debug']) {
  const native = console[level].bind(console);
  console[level] = (...args) => {
    postToContentScript({ kind: 'console', level, args: serialize(args), ts: Date.now() });
    native(...args); // preserve normal behavior
  };
}

// 2) Patch fetch — clone the response so the page still reads the body.
const nativeFetch = window.fetch;
window.fetch = async (input, init) => {
  const started = Date.now();
  const res = await nativeFetch(input, init);
  const body = await res.clone().text().catch(() => '');      // res.clone() is mandatory
  postToContentScript({
    kind: 'network', method: (init?.method) || 'GET',
    url: String(input), status: res.status,
    reqHeaders: redact(init?.headers), respBody: cap(body, 64 * 1024), // 64KB cap
    durationMs: Date.now() - started,
  });
  return res; // hand the untouched response back to the app
};

// 3) XMLHttpRequest still ships in plenty of apps — patch open/send too.
const open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
  this.__capture = { method, url: String(url) };
  return open.call(this, method, url, ...rest);
};

Three details make this production-safe rather than a demo. res.clone() is mandatory — a Response body is a one-shot stream, so reading it without cloning would starve the application of its own data. The 64KB body cap keeps a single large payload from blowing up the event buffer. And the MAIN-world hook posts to the ISOLATED content script (via window.postMessage or a shared CustomEvent), because only the content script can talk to the extension's background service worker — the MAIN world cannot reach chrome.* APIs.

Why not chrome.webRequest or the DevTools HAR?

Two Chrome APIs look like they should hand you network data for free. Both fall short for body capture, and the docs say so.

Under Manifest V3 the blocking webRequestBlocking permission is no longer available to most extensions; Chrome steers interception toward declarativeNetRequest, which matches rules rather than offering runtime inspection — and it cannot read request or response bodies at all. So webRequest can tell you a request happened and give you headers in observational mode, but not the JSON payload you need to reproduce a bug.

The chrome.devtools.network surface returns HAR, but its reference states plainly that "request content is not provided as part of HAR for efficiency reasons" — response bodies require a separate getContent() call per entry, and that only works while DevTools is attached to the tab. You cannot ship a capture product that demands every user keep DevTools open. That is why dependable request and response body capture lands back on monkey-patching fetch and XHR in the MAIN world.

FeatureMAIN-world patch (BugMojo)declarativeNetRequest / webRequestDevTools HAR (getContent)
Captures full request + response bodiesYes (res.clone(), capped)No — bodies never exposedPer-entry getContent(), DevTools must be open
Captures console output (5 levels)Yes — wraps native consoleNoNo (network only)
Works without DevTools attachedYesYesNo
Requires app to ship an SDKNoNoNo
Always-on, fleet-wide production monitoringNo — on-demand, per-machineNoNo
Emits MCP / AI-agent-readable bug contextYes — rrweb + logs + network over MCPNoNo

Read the matrix honestly. BugMojo loses the always-on production monitoring row outright — an extension only runs where a human installed it and pressed capture. It cannot watch every real user, cannot see server-side errors, and cannot record a mobile app or a backend job. Where it wins is the combination no always-on tool gives a developer for free: full bodies, console, DOM replay, zero app changes, and a structured surface an AI agent can read.

rrweb fills the third stream: the DOM

Console and network tell you what failed; the DOM replay shows what the user saw. rrweb's one-line mandate is "record and replay the web." It serializes the DOM and records incremental mutations by timestamp rather than capturing video, which is what lets the extension reconstruct a session from a few hundred KB of events instead of a multi-megabyte screen recording. rrweb runs in the content script (it needs the live DOM, not the page's JS objects), so it sits naturally on the ISOLATED side of the world boundary while the console/network hook lives in MAIN.

Redact PII before the buffer leaves the browser

Breadth is the extension's superpower and its liability. Because the MAIN-world hook sits in front of every fetch and every console call, it sees things the app's own logger would never record — third-party script traffic, Authorization and Cookie headers, bearer tokens in query strings, and personal data in response bodies. The rule is simple and non-negotiable: redact client-side, in the content script, before the buffer is uploaded. Server-side redaction is theater — by the time the secret reaches your server, it has already crossed the wire.

Pre-upload redaction checklist
  • Strip auth headers — drop or mask Authorization, Cookie, Set-Cookie, X-Api-Key.
  • Scrub query strings — remove token, access_token, sig, password params from captured URLs.
  • Mask body fields — replace values for keys matching /pass|token|secret|ssn|card/i with ***.
  • Regex sweep response bodies — emails, card-number patterns, JWT-shaped strings.
  • Apply rrweb masking — mask input values and block sensitive DOM regions via rrweb's masking options.
  • Cap and truncate — 64KB body limit doubles as a blast-radius limit on accidental leakage.

From capture surface to agent-readable context

The three streams — rrweb DOM, console log buffer, network requests — plus screenshots are only as useful as what reads them. BugMojo exposes them through an MCP server so an AI coding agent like Claude Code or Cursor connects over the Model Context Protocol and reads the bug as structured context: the failing request with status and body, the console error with its stack, the repro steps. The capture surface and the agent-readable surface are the same data. That is the gap between a screenshot a human pastes into a ticket and a bug an agent can triage and act on directly.

Key takeaway

An SDK-free extension hinges on one boundary: inject into the MAIN world at document_start, wrap the five console levels, and patch fetch/XHR with res.clone() and body caps. The DevTools HAR and declarativeNetRequest paths can't deliver full bodies. Redact client-side before upload. And stay honest: an extension is on-demand capture, not always-on production monitoring.

Capture the failing request, console error, and replay — and hand them to your AI agent
Try BugMojo

Frequently asked questions

Frequently asked questions

Sources

  1. Chrome for Developers — Content scripts (isolated world) — Google (2025)
  2. Chrome for Developers — chrome.scripting API (world: MAIN/ISOLATED) — Google (2025)
  3. Chrome for Developers — chrome.webRequest API reference — Google (2025)
  4. Chrome for Developers — chrome.devtools.network API reference — Google (2025)
  5. rrweb — record and replay the web (GitHub) — rrweb-io (2026)
  6. MDN Web Docs — console API — Mozilla (2025)
Share:
Hrishikesh Baidya
Hrishikesh Baidya· Chief Technology Officer

Hrishikesh Baidya is the CTO at Softech Infra. He is drawn to architecture that is invisible — systems that simply work — and leads the engineering behind BugMojo.

On this page

  • How does an extension capture console and network with no SDK?
  • Why a content script can't just wrap console.log
  • The fix: inject into the MAIN world at document_start
  • Why not chrome.webRequest or the DevTools HAR?
  • rrweb fills the third stream: the DOM
  • Redact PII before the buffer leaves the browser
  • From capture surface to agent-readable context

Get bug-tracking insights, weekly.

Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.