diff options
| author | Dani Bednarski <49581958+DaniBedz@users.noreply.github.com> | 2026-05-15 22:08:50 +1000 |
|---|---|---|
| committer | Tianren Dong <tianrendong.dtr@gmail.com> | 2026-05-16 13:16:44 -0400 |
| commit | d9f13a368e0acc9ac49e61a948d2b15ee0651d23 (patch) | |
| tree | 1415610926a494309a159182e8b3bdd2894614ee | |
| parent | 0168a4e8b3e6144cdc6e0d13a94b383cb7f205ce (diff) | |
fix: capture early console/network messages at document_start
Adds an early-capture script injected at document_start on every
navigation via webNavigation.onCommitted + injectImmediately:true.
Wraps console.*, fetch, and XMLHttpRequest before page JS runs, so
console messages, errors, unhandled rejections, and network requests
during page load are captured in window.__PI_CHROME_STATE__.
Sets __piChromeWrapped flags so the existing post-hoc
installPiChromeInstrumentation() is idempotent (checks instrumentationInstalled).
Fixes chrome_list_console_messages returning empty for errors
that fire during React render / initial API calls.
| -rw-r--r-- | extensions/chrome-profile-bridge/browser-extension/service_worker.js | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js index 88391a7..afa962b 100644 --- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js +++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js @@ -966,6 +966,24 @@ if (chrome.webNavigation && chrome.webNavigation.onCommitted) { }); } +// Always inject early console/network capture at document_start on every navigation. +// Catches console messages, errors, and network requests that fire during page load, +// before chrome_snapshot or chrome_evaluate install the instrumentation normally. +// The function installEarlyCapture sets __piChromeWrapped flags so the post-hoc +// installPiChromeInstrumentation() call is idempotent. +if (chrome.webNavigation && chrome.webNavigation.onCommitted) { + chrome.webNavigation.onCommitted.addListener((details) => { + if (details.frameId !== 0) return; + chrome.scripting.executeScript({ + target: { tabId: details.tabId, frameIds: [0] }, + world: "MAIN", + injectImmediately: true, + func: installEarlyCapture, + args: [], + }).catch(() => undefined); + }); +} + async function bringToFront(tab) { await chrome.windows.update(tab.windowId, { focused: true }); await chrome.tabs.update(tab.id, { active: true }); @@ -1310,6 +1328,141 @@ function installPiChromeInstrumentation() { } } +// Early-capture version of installPiChromeInstrumentation, designed to be injected +// at document_start via webNavigation.onCommitted. Wraps console, fetch, and XHR +// before the page's own JavaScript runs, so page-load errors are captured. +// Sets __piChromeWrapped flags so the post-hoc installPiChromeInstrumentation() +// sees them and skips (idempotent). +// NOTE: This function is self-contained — it does NOT close over any outer scope +// because it gets serialized by chrome.scripting.executeScript({func: ...}). +function installEarlyCapture() { + if (window.__piChromeEarlyCaptureInstalled) return; + window.__piChromeEarlyCaptureInstalled = true; + var state = window.__PI_CHROME_STATE__; + if (!state) { + state = { + nextElementUid: 1, + elements: {}, + console: [], + network: [], + nextRequestId: 1, + instrumentationInstalled: false, + }; + window.__PI_CHROME_STATE__ = state; + } + function pushConsole(level, args) { + state.console.push({ + id: state.console.length + 1, + level: level, + timestamp: Date.now(), + url: location.href, + args: Array.from(args).map(function(arg) { + try { + if (typeof arg === "string") return arg; + if (arg instanceof Error) return { name: arg.name, message: arg.message, stack: arg.stack }; + return JSON.parse(JSON.stringify(arg)); + } catch (e) { + return String(arg); + } + }), + }); + if (state.console.length > 500) state.console.splice(0, state.console.length - 500); + } + for (var i = 0; i < 5; i++) { + var levels = ["debug", "log", "info", "warn", "error"]; + var level = levels[i]; + var original = console[level]; + if (typeof original !== "function" || original.__piChromeWrapped) continue; + var wrapped = function(lvl, orig) { + return function() { + pushConsole(lvl, arguments); + return orig.apply(this, arguments); + }; + }(level, original); + wrapped.__piChromeWrapped = true; + console[level] = wrapped; + } + window.addEventListener("error", function(event) { + pushConsole("pageerror", [event.message, event.filename + ":" + event.lineno + ":" + event.colno]); + }); + window.addEventListener("unhandledrejection", function(event) { + pushConsole("unhandledrejection", [event.reason]); + }); + var trimBody = function(text) { + return typeof text === "string" && text.length > 200000 ? text.slice(0, 200000) + "\n[truncated " + (text.length - 200000) + " chars]" : text; + }; + var record = function(entry) { + state.network.push(entry); + if (state.network.length > 1000) state.network.splice(0, state.network.length - 1000); + return entry; + }; + if (window.fetch && !window.fetch.__piChromeWrapped) { + var originalFetch = window.fetch.bind(window); + var wrappedFetch = async function() { + var args = []; + for (var k = 0; k < arguments.length; k++) args.push(arguments[k]); + var id = "req-" + state.nextRequestId++; + var startedAt = Date.now(); + var input = args[0]; + var init = args[1] || {}; + var url = typeof input === "string" ? input : (input ? input.url : ""); + var method = (init.method || (input ? input.method : null) || "GET").toUpperCase(); + var entry = record({ id: id, type: "fetch", method: method, url: String(url || ""), startedAt: startedAt, pageUrl: location.href, status: "pending" }); + try { + var response = await originalFetch.apply(window, args); + entry.status = response.status; + entry.statusText = response.statusText; + entry.ok = response.ok; + entry.responseUrl = response.url; + entry.durationMs = Date.now() - startedAt; + entry.responseHeaders = Array.from(response.headers.entries()); + response.clone().text().then(function(text) { + entry.responseBody = trimBody(text); + entry.responseBodyTruncated = typeof text === "string" && text.length > 200000; + }).catch(function(error) { entry.responseBodyError = error ? error.message : String(error); }); + return response; + } catch (error) { + entry.error = error ? error.message : String(error); + entry.durationMs = Date.now() - startedAt; + throw error; + } + }; + wrappedFetch.__piChromeWrapped = true; + window.fetch = wrappedFetch; + } + if (window.XMLHttpRequest && !XMLHttpRequest.prototype.open.__piChromeWrapped) { + var originalOpen = XMLHttpRequest.prototype.open; + var originalSend = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.open = function(method, url) { + this.__piChromeRequest = { method: String(method || "GET").toUpperCase(), url: String(url || "") }; + return originalOpen.apply(this, arguments); + }; + XMLHttpRequest.prototype.open.__piChromeWrapped = true; + XMLHttpRequest.prototype.send = function(body) { + var id = "req-" + state.nextRequestId++; + var startedAt = Date.now(); + var info = this.__piChromeRequest || {}; + var entry = record({ id: id, type: "xhr", method: info.method || "GET", url: info.url || "", startedAt: startedAt, pageUrl: location.href, status: "pending" }); + this.addEventListener("loadend", function() { + entry.status = this.status; + entry.statusText = this.statusText; + entry.responseUrl = this.responseURL; + entry.durationMs = Date.now() - startedAt; + try { entry.responseHeadersText = this.getAllResponseHeaders(); } catch (e) {} + try { + if (typeof this.responseText === "string") { + entry.responseBody = trimBody(this.responseText); + entry.responseBodyTruncated = this.responseText.length > 200000; + } + } catch (error) { entry.responseBodyError = error ? error.message : String(error); } + }); + this.addEventListener("error", function() { entry.error = "XMLHttpRequest error"; entry.durationMs = Date.now() - startedAt; }); + return originalSend.apply(this, arguments); + }; + } + state.instrumentationInstalled = true; +} + function snapshotPage(maxElements, containingText, roleFilter, nearUid) { installPiChromeInstrumentation(); const unique = (selector) => { |
