feat: add console output file option for debugging and monitoring

Add comprehensive console logging to file functionality:
- CLI option --console-output-file to specify output file path
- Environment variable PLAYWRIGHT_MCP_CONSOLE_OUTPUT_FILE support
- Session configuration via browser_configure_snapshots tool
- Real-time structured logging with timestamp, session ID, and URL
- Automatic directory creation and graceful error handling
- Captures all console message types (log, error, warn, page errors)

Useful for debugging browser interactions and monitoring console activity
during automated sessions.
This commit is contained in:
Ryan Malloy 2025-08-24 14:12:00 -06:00
parent ec8b0c24b5
commit 7de63b5bab
6 changed files with 64 additions and 2 deletions

7
config.d.ts vendored
View File

@ -143,4 +143,11 @@ export type Config = {
* Default is false. * Default is false.
*/ */
differentialSnapshots?: boolean; differentialSnapshots?: boolean;
/**
* File path to write browser console output to. When specified, all console
* messages from browser pages will be written to this file in real-time.
* Useful for debugging and monitoring browser activity.
*/
consoleOutputFile?: string;
}; };

View File

@ -31,6 +31,7 @@ export type CLIOptions = {
caps?: string[]; caps?: string[];
cdpEndpoint?: string; cdpEndpoint?: string;
config?: string; config?: string;
consoleOutputFile?: string;
device?: string; device?: string;
executablePath?: string; executablePath?: string;
headless?: boolean; headless?: boolean;
@ -93,6 +94,7 @@ export type FullConfig = Config & {
includeSnapshots: boolean; includeSnapshots: boolean;
maxSnapshotTokens: number; maxSnapshotTokens: number;
differentialSnapshots: boolean; differentialSnapshots: boolean;
consoleOutputFile?: string;
}; };
export async function resolveConfig(config: Config): Promise<FullConfig> { export async function resolveConfig(config: Config): Promise<FullConfig> {
@ -212,6 +214,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
includeSnapshots: cliOptions.includeSnapshots, includeSnapshots: cliOptions.includeSnapshots,
maxSnapshotTokens: cliOptions.maxSnapshotTokens, maxSnapshotTokens: cliOptions.maxSnapshotTokens,
differentialSnapshots: cliOptions.differentialSnapshots, differentialSnapshots: cliOptions.differentialSnapshots,
consoleOutputFile: cliOptions.consoleOutputFile,
}; };
return result; return result;
@ -238,6 +241,7 @@ function configFromEnv(): Config {
options.includeSnapshots = envToBoolean(process.env.PLAYWRIGHT_MCP_INCLUDE_SNAPSHOTS); options.includeSnapshots = envToBoolean(process.env.PLAYWRIGHT_MCP_INCLUDE_SNAPSHOTS);
options.maxSnapshotTokens = envToNumber(process.env.PLAYWRIGHT_MCP_MAX_SNAPSHOT_TOKENS); options.maxSnapshotTokens = envToNumber(process.env.PLAYWRIGHT_MCP_MAX_SNAPSHOT_TOKENS);
options.differentialSnapshots = envToBoolean(process.env.PLAYWRIGHT_MCP_DIFFERENTIAL_SNAPSHOTS); options.differentialSnapshots = envToBoolean(process.env.PLAYWRIGHT_MCP_DIFFERENTIAL_SNAPSHOTS);
options.consoleOutputFile = envToString(process.env.PLAYWRIGHT_MCP_CONSOLE_OUTPUT_FILE);
options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX); options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR); options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR);
options.port = envToNumber(process.env.PLAYWRIGHT_MCP_PORT); options.port = envToNumber(process.env.PLAYWRIGHT_MCP_PORT);

View File

@ -641,6 +641,7 @@ export class Context {
includeSnapshots?: boolean; includeSnapshots?: boolean;
maxSnapshotTokens?: number; maxSnapshotTokens?: number;
differentialSnapshots?: boolean; differentialSnapshots?: boolean;
consoleOutputFile?: string;
}): void { }): void {
// Update configuration at runtime // Update configuration at runtime
if (updates.includeSnapshots !== undefined) if (updates.includeSnapshots !== undefined)
@ -659,5 +660,9 @@ export class Context {
this.resetDifferentialSnapshot(); this.resetDifferentialSnapshot();
} }
if (updates.consoleOutputFile !== undefined)
(this.config as any).consoleOutputFile = updates.consoleOutputFile === '' ? undefined : updates.consoleOutputFile;
} }
} }

View File

@ -38,6 +38,7 @@ program
.option('--caps <caps>', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList) .option('--caps <caps>', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList)
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.') .option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
.option('--config <path>', 'path to the configuration file.') .option('--config <path>', 'path to the configuration file.')
.option('--console-output-file <path>', 'file path to write browser console output to for debugging and monitoring.')
.option('--device <device>', 'device to emulate, for example: "iPhone 15"') .option('--device <device>', 'device to emulate, for example: "iPhone 15"')
.option('--executable-path <path>', 'path to the browser executable.') .option('--executable-path <path>', 'path to the browser executable.')
.option('--headless', 'run browser in headless mode, headed by default') .option('--headless', 'run browser in headless mode, headed by default')

View File

@ -15,6 +15,8 @@
*/ */
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import fs from 'fs';
import path from 'path';
import * as playwright from 'playwright'; import * as playwright from 'playwright';
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js'; import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
import { logUnhandledError } from './log.js'; import { logUnhandledError } from './log.js';
@ -123,6 +125,39 @@ export class Tab extends EventEmitter<TabEventsInterface> {
private _handleConsoleMessage(message: ConsoleMessage) { private _handleConsoleMessage(message: ConsoleMessage) {
this._consoleMessages.push(message); this._consoleMessages.push(message);
this._recentConsoleMessages.push(message); this._recentConsoleMessages.push(message);
// Write to console output file if configured
if (this.context.config.consoleOutputFile)
this._writeConsoleToFile(message);
}
private _writeConsoleToFile(message: ConsoleMessage) {
try {
const consoleFile = this.context.config.consoleOutputFile!;
const timestamp = new Date().toISOString();
const url = this.page.url();
const sessionId = this.context.sessionId;
const logEntry = `[${timestamp}] [${sessionId}] [${url}] ${message.toString()}\n`;
// Ensure directory exists
const dir = path.dirname(consoleFile);
if (!fs.existsSync(dir))
fs.mkdirSync(dir, { recursive: true });
// Append to file (async to avoid blocking)
fs.appendFile(consoleFile, logEntry, err => {
if (err) {
// Log error but don't fail the operation
logUnhandledError(err);
}
});
} catch (error) {
// Silently handle errors to avoid breaking browser functionality
logUnhandledError(error);
}
} }
private _onClose() { private _onClose() {

View File

@ -77,7 +77,8 @@ const installPopularExtensionSchema = z.object({
const configureSnapshotsSchema = z.object({ const configureSnapshotsSchema = z.object({
includeSnapshots: z.boolean().optional().describe('Enable/disable automatic snapshots after interactive operations. When false, use browser_snapshot for explicit snapshots.'), includeSnapshots: z.boolean().optional().describe('Enable/disable automatic snapshots after interactive operations. When false, use browser_snapshot for explicit snapshots.'),
maxSnapshotTokens: z.number().min(0).optional().describe('Maximum tokens allowed in snapshots before truncation. Use 0 to disable truncation.'), maxSnapshotTokens: z.number().min(0).optional().describe('Maximum tokens allowed in snapshots before truncation. Use 0 to disable truncation.'),
differentialSnapshots: z.boolean().optional().describe('Enable differential snapshots that show only changes since last snapshot instead of full page snapshots.') differentialSnapshots: z.boolean().optional().describe('Enable differential snapshots that show only changes since last snapshot instead of full page snapshots.'),
consoleOutputFile: z.string().optional().describe('File path to write browser console output to. Set to empty string to disable console file output.')
}); });
export default [ export default [
@ -564,6 +565,14 @@ export default [
} }
if (params.consoleOutputFile !== undefined) {
if (params.consoleOutputFile === '')
changes.push(`📝 Console output file: disabled`);
else
changes.push(`📝 Console output file: ${params.consoleOutputFile}`);
}
// Apply the updated configuration using the context method // Apply the updated configuration using the context method
context.updateSnapshotConfig(params); context.updateSnapshotConfig(params);
@ -572,7 +581,8 @@ export default [
response.addResult('No snapshot configuration changes specified.\n\n**Current settings:**\n' + response.addResult('No snapshot configuration changes specified.\n\n**Current settings:**\n' +
`📸 Auto-snapshots: ${context.config.includeSnapshots ? 'enabled' : 'disabled'}\n` + `📸 Auto-snapshots: ${context.config.includeSnapshots ? 'enabled' : 'disabled'}\n` +
`📏 Max snapshot tokens: ${context.config.maxSnapshotTokens === 0 ? 'unlimited' : context.config.maxSnapshotTokens.toLocaleString()}\n` + `📏 Max snapshot tokens: ${context.config.maxSnapshotTokens === 0 ? 'unlimited' : context.config.maxSnapshotTokens.toLocaleString()}\n` +
`🔄 Differential snapshots: ${context.config.differentialSnapshots ? 'enabled' : 'disabled'}`); `🔄 Differential snapshots: ${context.config.differentialSnapshots ? 'enabled' : 'disabled'}\n` +
`📝 Console output file: ${context.config.consoleOutputFile || 'disabled'}`);
return; return;
} }