/**
* MAIN-world tap present on every http(s) page from document_start, before
* any page script runs: wraps console.error/warn and global error handlers
* so the recorder can attach a console dump to the report.
*
* It must live in the MAIN world — the page calls *its own* console object,
* which the isolated content-script world never sees. And since MAIN-world
* scripts have no extension messaging, everything goes out through
* window.postMessage to the relay content script.
*/
interface BugtrailWindow {
__bugtrailTap?: boolean;
}
const w = window as unknown as BugtrailWindow;
// the page could re-inject us (SPA frameworks re-running scripts) — wrap once
if (!w.__bugtrailTap) {
w.__bugtrailTap = true;
const MAX_TEXT = 2000;
/** Formats a console argument; errors keep their message, objects go JSON. */
function format(arg: unknown, depth = 0): string {
if (arg == null) return String(arg);
if (typeof arg === "string") return arg;
if (arg instanceof Error) return arg.stack ?? `${arg.name}: ${arg.message}`;
if (depth === 0 && (typeof arg === "object" || typeof arg === "function")) {
try {
return JSON.stringify(arg, (_key, value) => (typeof value === "bigint" ? String(value) : value)) ?? String(arg);
} catch {
// circular or throwing toJSON — fall back to the string form
return String(arg);
}
}
return String(arg);
}
function post(level: "error" | "warning", text: string) {
try {
window.postMessage({ source: "bugtrail-tap", type: "console", level, text: text.slice(0, MAX_TEXT), at: Date.now() }, window.location.origin);
} catch {
// structured clone can refuse exotic values — never break the page
}
}
const wrap = (method: "error" | "warn", level: "error" | "warning") => {
const original = console[method].bind(console);
console[method] = (...args: unknown[]) => {
try {
post(level, args.map((arg) => format(arg)).join(" "));
} catch {
// never break the page's own logging
}
original(...args);
};
};
wrap("error", "error");
wrap("warn", "warning");
window.addEventListener("error", (event) => {
// resources (img/script) failing also fire "error" — only real JS errors carry a message
if (event instanceof ErrorEvent && event.message) {
post("error", `Uncaught ${event.message} (${event.filename}:${event.lineno})`);
}
});
window.addEventListener("unhandledrejection", (event) => {
post("error", `Unhandled rejection: ${format((event as PromiseRejectionEvent).reason)}`);
});
}