How to: Capture console log info in chrome extension (manifest v3)
👾

How to: Capture console log info in chrome extension (manifest v3)

Date
Sep 27, 2022
Tags
Front-end
React
Chrome Extension
TypeScript

Introduction

One of the first tasks I was assigned to at my new workplace ( ✨Deep BlueDot ✨) was to find a way to fetch console logs (info, errors, warnings, asserts etc) and HAR network logs of a webpage and access it through chrome extension.
These logs give the context of what was going on in the background which can be useful information for a developer to understand when a bug occurs.
Visit Devign here:
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.scriptingAPI, you need to specify a "manifest_version"of 3or higher and include the "scripting" permission in your manifest file.
notion image

Usage

chrome.scriptingAPI 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.scriptingAPI, extensions can make decisions at runtime.

Injection target

The target parameter is used to specify a target to inject JavaScript or CSS into.
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.
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.
notion image
 

References:

 
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 })); }); };