feat: comprehensive console capture and offline mode support
Major enhancements to browser automation and debugging capabilities: **Console Capture Features:** - Add console output file option (CLI, env var, session config) - Enhanced CDP console capture for service worker messages - Browser-level security warnings and mixed content errors - Network failure and loading error capture - All console contexts written to structured log files - Chrome extension for comprehensive console message interception **Offline Mode Support:** - Add browser_set_offline tool for DevTools-equivalent offline mode - Integrate offline mode into browser_configure tool - Support for testing network failure scenarios and service worker behavior **Extension Management:** - Improved extension installation messaging about session persistence - Console capture extension with debugger API access - Clear communication about extension lifecycle to MCP clients **Technical Implementation:** - CDP session management across multiple domains (Runtime, Network, Security, Log) - Service worker context console message interception - Browser context factory integration for offline mode - Pure Chromium configuration for optimal extension support All features provide MCP clients with powerful debugging capabilities equivalent to Chrome DevTools console and offline functionality.
This commit is contained in:
parent
7de63b5bab
commit
afaa8a7014
13 changed files with 603 additions and 20 deletions
193
console-capture-extension/background.js
Normal file
193
console-capture-extension/background.js
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// Background script for comprehensive console capture
|
||||
console.log('Console Capture Extension: Background script loaded');
|
||||
|
||||
// Track active debug sessions
|
||||
const debugSessions = new Map();
|
||||
|
||||
// Message storage for each tab
|
||||
const tabConsoleMessages = new Map();
|
||||
|
||||
chrome.tabs.onCreated.addListener((tab) => {
|
||||
if (tab.id) {
|
||||
attachDebugger(tab.id);
|
||||
}
|
||||
});
|
||||
|
||||
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||
if (changeInfo.status === 'loading' && tab.url && !tab.url.startsWith('chrome://')) {
|
||||
attachDebugger(tabId);
|
||||
}
|
||||
});
|
||||
|
||||
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||
if (debugSessions.has(tabId)) {
|
||||
try {
|
||||
chrome.debugger.detach({ tabId });
|
||||
} catch (e) {
|
||||
// Ignore errors when detaching
|
||||
}
|
||||
debugSessions.delete(tabId);
|
||||
tabConsoleMessages.delete(tabId);
|
||||
}
|
||||
});
|
||||
|
||||
async function attachDebugger(tabId) {
|
||||
try {
|
||||
// Don't attach to chrome:// pages or if already attached
|
||||
if (debugSessions.has(tabId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach debugger
|
||||
await chrome.debugger.attach({ tabId }, '1.3');
|
||||
debugSessions.set(tabId, true);
|
||||
|
||||
console.log(`Console Capture Extension: Attached to tab ${tabId}`);
|
||||
|
||||
// Enable domains for comprehensive console capture
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable');
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Log.enable');
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Network.enable');
|
||||
await chrome.debugger.sendCommand({ tabId }, 'Security.enable');
|
||||
|
||||
// Initialize console messages array for this tab
|
||||
if (!tabConsoleMessages.has(tabId)) {
|
||||
tabConsoleMessages.set(tabId, []);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(`Console Capture Extension: Failed to attach to tab ${tabId}:`, error);
|
||||
debugSessions.delete(tabId);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for debugger events
|
||||
chrome.debugger.onEvent.addListener((source, method, params) => {
|
||||
const tabId = source.tabId;
|
||||
if (!tabId || !debugSessions.has(tabId)) return;
|
||||
|
||||
let consoleMessage = null;
|
||||
|
||||
try {
|
||||
switch (method) {
|
||||
case 'Runtime.consoleAPICalled':
|
||||
consoleMessage = {
|
||||
type: params.type || 'log',
|
||||
text: params.args?.map(arg =>
|
||||
arg.value !== undefined ? String(arg.value) :
|
||||
arg.unserializableValue || '[object]'
|
||||
).join(' ') || '',
|
||||
location: `runtime:${params.stackTrace?.callFrames?.[0]?.lineNumber || 0}`,
|
||||
source: 'js-console',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
break;
|
||||
|
||||
case 'Runtime.exceptionThrown':
|
||||
const exception = params.exceptionDetails;
|
||||
consoleMessage = {
|
||||
type: 'error',
|
||||
text: exception?.text || exception?.exception?.description || 'Runtime Exception',
|
||||
location: `runtime:${exception?.lineNumber || 0}`,
|
||||
source: 'js-exception',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
break;
|
||||
|
||||
case 'Log.entryAdded':
|
||||
const entry = params.entry;
|
||||
if (entry && entry.text) {
|
||||
consoleMessage = {
|
||||
type: entry.level || 'info',
|
||||
text: entry.text,
|
||||
location: `browser-log:${entry.lineNumber || 0}`,
|
||||
source: 'browser-log',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Network.loadingFailed':
|
||||
if (params.errorText) {
|
||||
consoleMessage = {
|
||||
type: 'error',
|
||||
text: `Network Error: ${params.errorText} - ${params.blockedReason || 'Unknown reason'}`,
|
||||
location: 'network-layer',
|
||||
source: 'network-error',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Security.securityStateChanged':
|
||||
if (params.securityState === 'insecure' && params.explanations) {
|
||||
for (const explanation of params.explanations) {
|
||||
if (explanation.description && explanation.description.toLowerCase().includes('mixed content')) {
|
||||
consoleMessage = {
|
||||
type: 'error',
|
||||
text: `Security Warning: ${explanation.description}`,
|
||||
location: 'security-layer',
|
||||
source: 'security-warning',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (consoleMessage) {
|
||||
// Store the message
|
||||
const messages = tabConsoleMessages.get(tabId) || [];
|
||||
messages.push(consoleMessage);
|
||||
tabConsoleMessages.set(tabId, messages);
|
||||
|
||||
console.log(`Console Capture Extension: Captured message from tab ${tabId}:`, consoleMessage);
|
||||
|
||||
// Send to content script for potential file writing
|
||||
chrome.tabs.sendMessage(tabId, {
|
||||
type: 'CONSOLE_MESSAGE',
|
||||
message: consoleMessage
|
||||
}).catch(() => {
|
||||
// Ignore errors if content script not ready
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log('Console Capture Extension: Error processing event:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle detach events
|
||||
chrome.debugger.onDetach.addListener((source, reason) => {
|
||||
const tabId = source.tabId;
|
||||
if (tabId && debugSessions.has(tabId)) {
|
||||
console.log(`Console Capture Extension: Detached from tab ${tabId}, reason: ${reason}`);
|
||||
debugSessions.delete(tabId);
|
||||
tabConsoleMessages.delete(tabId);
|
||||
}
|
||||
});
|
||||
|
||||
// API to get console messages for a tab
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.type === 'GET_CONSOLE_MESSAGES') {
|
||||
const tabId = request.tabId || sender.tab?.id;
|
||||
if (tabId) {
|
||||
const messages = tabConsoleMessages.get(tabId) || [];
|
||||
sendResponse({ messages });
|
||||
} else {
|
||||
sendResponse({ messages: [] });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize for existing tabs
|
||||
chrome.tabs.query({}, (tabs) => {
|
||||
for (const tab of tabs) {
|
||||
if (tab.id && tab.url && !tab.url.startsWith('chrome://')) {
|
||||
attachDebugger(tab.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
50
console-capture-extension/content.js
Normal file
50
console-capture-extension/content.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Content script for console capture extension
|
||||
console.log('Console Capture Extension: Content script loaded');
|
||||
|
||||
// Listen for console messages from background script
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.type === 'CONSOLE_MESSAGE') {
|
||||
const message = request.message;
|
||||
|
||||
// Forward to window for Playwright to access
|
||||
window.postMessage({
|
||||
type: 'PLAYWRIGHT_CONSOLE_CAPTURE',
|
||||
consoleMessage: message
|
||||
}, '*');
|
||||
|
||||
console.log('Console Capture Extension: Forwarded message:', message);
|
||||
}
|
||||
});
|
||||
|
||||
// Also capture any window-level console messages that might be missed
|
||||
const originalConsole = {
|
||||
log: window.console.log,
|
||||
warn: window.console.warn,
|
||||
error: window.console.error,
|
||||
info: window.console.info
|
||||
};
|
||||
|
||||
function wrapConsoleMethod(method, level) {
|
||||
return function(...args) {
|
||||
// Call original method
|
||||
originalConsole[method].apply(window.console, args);
|
||||
|
||||
// Forward to Playwright
|
||||
window.postMessage({
|
||||
type: 'PLAYWRIGHT_CONSOLE_CAPTURE',
|
||||
consoleMessage: {
|
||||
type: level,
|
||||
text: args.map(arg => String(arg)).join(' '),
|
||||
location: `content-script:${new Error().stack?.split('\n')[2]?.match(/:(\d+):/)?.[1] || 0}`,
|
||||
source: 'content-wrapper',
|
||||
timestamp: Date.now()
|
||||
}
|
||||
}, '*');
|
||||
};
|
||||
}
|
||||
|
||||
// Wrap console methods
|
||||
window.console.log = wrapConsoleMethod('log', 'log');
|
||||
window.console.warn = wrapConsoleMethod('warn', 'warning');
|
||||
window.console.error = wrapConsoleMethod('error', 'error');
|
||||
window.console.info = wrapConsoleMethod('info', 'info');
|
||||
BIN
console-capture-extension/icon-128.png
Normal file
BIN
console-capture-extension/icon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
BIN
console-capture-extension/icon-16.png
Normal file
BIN
console-capture-extension/icon-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 571 B |
BIN
console-capture-extension/icon-32.png
Normal file
BIN
console-capture-extension/icon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
console-capture-extension/icon-48.png
Normal file
BIN
console-capture-extension/icon-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2 KiB |
37
console-capture-extension/manifest.json
Normal file
37
console-capture-extension/manifest.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Console Capture Extension",
|
||||
"version": "1.0.0",
|
||||
"description": "Captures comprehensive console messages including browser-level warnings and errors",
|
||||
|
||||
"permissions": [
|
||||
"debugger",
|
||||
"tabs",
|
||||
"activeTab",
|
||||
"storage"
|
||||
],
|
||||
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
|
||||
"icons": {
|
||||
"16": "icon-16.png",
|
||||
"32": "icon-32.png",
|
||||
"48": "icon-48.png",
|
||||
"128": "icon-128.png"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue