aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDani Bednarski <49581958+DaniBedz@users.noreply.github.com>2026-05-15 22:08:50 +1000
committerTianren Dong <tianrendong.dtr@gmail.com>2026-05-16 13:16:44 -0400
commitd9f13a368e0acc9ac49e61a948d2b15ee0651d23 (patch)
tree1415610926a494309a159182e8b3bdd2894614ee
parent0168a4e8b3e6144cdc6e0d13a94b383cb7f205ce (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.js153
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) => {