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);
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue