How to capture Console Logs (Chrome Extension Manifest v3)
Introduction
The following article explains the logic behind how the Devign Chrome Extension captures console log information by dispatching custom events to content script.
Manifest
In order to use the chrome.scripting API, you need to specify a "manifest_version"of 3 or higher and include the "scripting" permission in your manifest file.

Usage
chrome.scripting API is used to inject JavaScript and CSS into websites. This is similar to what you can do with content scripts, but by using the chrome.scripting API, extensions can make decisions at runtime.
Injection target
The target parameter is used to specify a target to inject JavaScript or CSS into.
Code
export const setLogScript = (tabId: number) => {
chrome.scripting.executeScript({
target: { tabId },
func: setLogFunc,
world: 'MAIN',
});
};

Injected code
Extensions can specify the code to be injected either via an external file or a runtime variable.
The following code is the code that we wish to inject at runtime.
Code
const setLogFunc = () => {
let console: any = window.console;
if (console.everything === undefined) {
console.defaultLog = console.log.bind(console);
console.log = function () {
const data = {
type: 'log',
datetime: Date().toLocaleString(),
value: Array.from(arguments),
};
document.dispatchEvent(new CustomEvent('log', { detail: data }));
console.defaultLog.apply(console, arguments);
};
console.defaultError = console.error.bind(console);
console.error = function () {
const data = {
type: 'error',
datetime: Date().toLocaleString(),
value: Array.from(arguments),
};
document.dispatchEvent(new CustomEvent('log', { detail: data }));
console.defaultError.apply(console, arguments);
};
console.defaultWarn = console.warn.bind(console);
console.warn = function () {
const data = {
type: 'warn',
datetime: Date().toLocaleString(),
value: Array.from(arguments),
};
document.dispatchEvent(new CustomEvent('log', { detail: data }));
console.defaultWarn.apply(console, arguments);
};
console.defaultDebug = console.debug.bind(console);
console.debug = function () {
const data = {
type: 'debug',
datetime: Date().toLocaleString(),
value: Array.from(arguments),
};
document.dispatchEvent(new CustomEvent('log', { detail: data }));
console.defaultDebug.apply(console, arguments);
};
console.defaultInfo = console.info.bind(console);
console.info = function () {
const data = {
type: 'info',
datetime: Date().toLocaleString(),
value: Array.from(arguments),
};
document.dispatchEvent(new CustomEvent('log', { detail: data }));
console.defaultInfo.apply(console, arguments);
};
console.defaultGroupCollapsed = console.groupCollapsed.bind(console);
console.groupCollapsed = function () {
const data = {
type: 'groupCollapsed',
datetime: Date().toLocaleString(),
value: Array.from(arguments),
};
document.dispatchEvent(new CustomEvent('log', { detail: data }));
console.defaultGroupCollapsed.apply(console, arguments);
};
console.defaultTrace = console.trace.bind(console);
console.trace = function () {
const data = {
type: 'trace',
datetime: Date().toLocaleString(),
value: Array.from(arguments),
};
document.dispatchEvent(new CustomEvent('log', { detail: data }));
console.defaultTrace.apply(console, arguments);
};
}
window.addEventListener('error', function (e) {
const windowError = {
type: 'windowError',
datetime: Date().toLocaleString(),
value: [e.error.message, e.error.stack],
};
document.dispatchEvent(new CustomEvent('log', { detail: windowError }));
});
window.addEventListener('unhandledrejection', function (e) {
const rejection = {
type: 'unhandledrejection',
datetime: Date().toLocaleString(),
value: [e.reason.message, e.reason.stack],
};
document.dispatchEvent(new CustomEvent('log', { detail: rejection }));
});
};

Dispatching Custom Events
What the code above is doing - programmatically creating and dispatching events using dispatchEvent() method.
To generate an event programmatically, you follow these steps:
First, create a new CustomEvent object.
Then, trigger the event using element.dispatchEvent() method.
We are creating a custom event called ‘log’ to capture console logs and passing the log data as a parameter.
src/content/index.tsx (in content scripts)
Send message to the service worker requesting the log data
document.addEventListener('log', (e: any) => {
chrome.runtime.sendMessage({ setConsoleLog: true, log: e.detail });
});
src/background/index.ts (in service workers)
import { setLogScript } from './log';
async function messageHandler(
message: any,
sender: chrome.runtime.MessageSender,
sendResponse: (response?: any) => void,
) {
if (message.setLogScript) {
if (!sender?.tab?.id) return;
setLogScript(sender.tab.id);
}
}
Here’s what it looks like in the Devign Dashboard
Once the console log data has been stored in the backend server, it can be parsed and displayed as shown below.

References:
1. https://developer.chrome.com/docs/extensions/reference/scripting/
2. https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent