Ryan Malloy afaa8a7014 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.
2025-08-31 16:28:43 -06:00

193 lines
5.8 KiB
JavaScript

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