From a64fc2d8a092ff624f4aedcfd48fecadf6c9156e Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Mon, 26 Jan 2026 13:28:18 +0100 Subject: [PATCH] add cdp jsonl logging and update log docs --- AGENTS.md | 11 ++++- PLAYWRITER_AGENTS.md | 6 +++ README.md | 8 +++- playwriter/src/cdp-log.ts | 65 ++++++++++++++++++++++++++++ playwriter/src/cdp-relay.ts | 53 ++++++++++++++++++++++- playwriter/src/cli.ts | 6 ++- playwriter/src/skill.md | 8 +++- playwriter/src/start-relay-server.ts | 2 + playwriter/src/utils.ts | 4 +- 9 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 playwriter/src/cdp-log.ts diff --git a/AGENTS.md b/AGENTS.md index 9014bb0..90befab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -189,7 +189,13 @@ ignore ./claude-extension. this is the source code of the Claude Chrome extensio ## reading playwriter logs -you can find the logfile for playwriter with `playwriter logfile` +you can find the logfile for playwriter executing `playwriter logfile`. read that then to understand issues happening and debug them + +the cdp log is a jsonl file (one json object per line). you can use jq to compress it. for example, list direction + method: + +```bash +jq -r '.direction + "\t" + (.message.method // "response")' /tmp/playwriter/cdp.jsonl | uniq -c +``` # core guidelines @@ -226,6 +232,7 @@ you can open files when i ask me "open in zed the line where ..." using the comm # typescript - ALWAYS use normal imports instead of dynamic imports, unless there is an issue with es module only packages and you are in a commonjs package (this is rare). +- when throwing errors always use clause instead of error inside message: `new Error("wrapping error", { cause: e })` instead of `new Error(\`wrapping error ${e}\`)` - use a single object argument instead of multiple positional args: use object arguments for new typescript functions if the function would accept more than one argument, so it is more readable, ({a,b,c}) instead of (a,b,c). this way you can use the object as a sort of named argument feature, where order of arguments does not matter and it's easier to discover parameters. @@ -324,6 +331,8 @@ remember to always add the explicit type to avoid unexpected type inference. DO `import fs from 'fs'; fs.writeFileSync(...)` DO NOT `import { writeFileSync } from 'fs';` +- NEVER pass a string to abortController.abort(). instead if you want to pass a reason always pass an Error instance. like `controller.abort(new Error('reason'))`. This way catch blocks receive an Error instance and not something else. + # package manager: pnpm with workspace this project uses pnpm workspaces to manage dependencies. important scripts are in the root package.json or various packages' package.json diff --git a/PLAYWRITER_AGENTS.md b/PLAYWRITER_AGENTS.md index e7a6fba..1f97b14 100644 --- a/PLAYWRITER_AGENTS.md +++ b/PLAYWRITER_AGENTS.md @@ -188,3 +188,9 @@ ignore ./claude-extension. this is the source code of the Claude Chrome extensio ## reading playwriter logs you can find the logfile for playwriter executing `playwriter logfile`. read that then to understand issues happening and debug them + +the cdp log is a jsonl file (one json object per line). you can use jq to compress it. for example, list direction + method: + +```bash +jq -r '.direction + "\t" + (.message.method // "response")' /tmp/playwriter/cdp.jsonl | uniq -c +``` diff --git a/README.md b/README.md index acfcb87..57b22d6 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,13 @@ playwriter logfile # prints the log file path # typically: /tmp/playwriter/relay-server.log (Linux/macOS) ``` -The log file contains extension, MCP and WebSocket server logs with all CDP events. It's recreated on each server start. +The relay log contains extension, MCP and WebSocket server logs. A separate CDP JSONL log is also created alongside it (see `playwriter logfile`). Both are recreated on each server start. + +Example: summarize CDP traffic counts by direction + method: + +```bash +jq -r '.direction + "\t" + (.message.method // "response")' /tmp/playwriter/cdp.jsonl | uniq -c +``` ## Known Issues diff --git a/playwriter/src/cdp-log.ts b/playwriter/src/cdp-log.ts new file mode 100644 index 0000000..f7eb9c6 --- /dev/null +++ b/playwriter/src/cdp-log.ts @@ -0,0 +1,65 @@ +import fs from 'node:fs' +import path from 'node:path' +import { LOG_CDP_FILE_PATH } from './utils.js' + +export type CdpLogEntry = { + timestamp: string + direction: 'from-playwright' | 'to-playwright' | 'from-extension' | 'to-extension' + clientId?: string + source?: 'extension' | 'server' + message: unknown +} + +export type CdpLogger = { + log(entry: CdpLogEntry): void + logFilePath: string +} + +const DEFAULT_MAX_STRING_LENGTH = Number(process.env.PLAYWRITER_CDP_LOG_MAX_STRING_LENGTH || 2000) + +function truncateString(value: string, maxLength: number): string { + if (value.length <= maxLength) { + return value + } + const truncatedCount = value.length - maxLength + return `${value.slice(0, maxLength)}…[truncated ${truncatedCount} chars]` +} + +function createTruncatingReplacer({ maxStringLength }: { maxStringLength: number }) { + const seen = new WeakSet() + return (_key: string, value: unknown) => { + if (typeof value === 'string') { + return truncateString(value, maxStringLength) + } + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[Circular]' + } + seen.add(value) + } + return value + } +} + +export function createCdpLogger({ logFilePath, maxStringLength }: { logFilePath?: string; maxStringLength?: number } = {}): CdpLogger { + const resolvedLogFilePath = logFilePath || LOG_CDP_FILE_PATH + const logDir = path.dirname(resolvedLogFilePath) + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }) + } + fs.writeFileSync(resolvedLogFilePath, '') + + let queue: Promise = Promise.resolve() + const maxLength = maxStringLength ?? DEFAULT_MAX_STRING_LENGTH + + const log = (entry: CdpLogEntry): void => { + const replacer = createTruncatingReplacer({ maxStringLength: maxLength }) + const line = JSON.stringify(entry, replacer) + queue = queue.then(() => fs.promises.appendFile(resolvedLogFilePath, `${line}\n`)) + } + + return { + log, + logFilePath: resolvedLogFilePath, + } +} diff --git a/playwriter/src/cdp-relay.ts b/playwriter/src/cdp-relay.ts index f78aefa..f38d383 100644 --- a/playwriter/src/cdp-relay.ts +++ b/playwriter/src/cdp-relay.ts @@ -10,6 +10,7 @@ import type { ExtensionMessage, ExtensionEventMessage } from './protocol.js' import pc from 'picocolors' import { EventEmitter } from 'node:events' import { VERSION } from './utils.js' +import { createCdpLogger, type CdpLogEntry, type CdpLogger } from './cdp-log.js' type ConnectedTarget = { sessionId: string @@ -69,10 +70,27 @@ export type RelayServer = { off(event: K, listener: RelayServerEvents[K]): void } -export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.0.0.1', token, logger }: { port?: number; host?: string; token?: string; logger?: { log(...args: any[]): void; error(...args: any[]): void } } = {}): Promise { +export async function startPlayWriterCDPRelayServer({ + port = 19988, + host = '127.0.0.1', + token, + logger, + cdpLogger, +}: { + port?: number + host?: string + token?: string + logger?: { log(...args: any[]): void; error(...args: any[]): void } + cdpLogger?: CdpLogger +} = {}): Promise { const emitter = new EventEmitter() const connectedTargets = new Map() + const resolvedCdpLogger = cdpLogger || createCdpLogger() + const logCdpJson = (entry: CdpLogEntry) => { + resolvedCdpLogger.log(entry) + } + const playwrightClients = new Map() let extensionWs: WSContext | null = null @@ -179,6 +197,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127. ? { ...message, __serverGenerated: true } : message + logCdpJson({ + timestamp: new Date().toISOString(), + direction: 'to-playwright', + clientId, + source, + message: messageToSend, + }) + if ('method' in message) { logCdpMessage({ direction: 'to-playwright', @@ -212,6 +238,18 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127. const id = ++extensionMessageId const message = { id, method, params } + if (method === 'forwardCDPCommand' && params?.method) { + logCdpJson({ + timestamp: new Date().toISOString(), + direction: 'to-extension', + message: { + method: params.method, + sessionId: params.sessionId, + params: params.params, + }, + }) + } + extensionWs.send(JSON.stringify(message)) return new Promise((resolve, reject) => { @@ -584,6 +622,13 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127. const { id, sessionId, method, params, source } = message + logCdpJson({ + timestamp: new Date().toISOString(), + direction: 'from-playwright', + clientId, + message, + }) + logCdpMessage({ direction: 'from-playwright', clientId, @@ -820,6 +865,12 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127. const { method, params, sessionId } = extensionEvent.params + logCdpJson({ + timestamp: new Date().toISOString(), + direction: 'from-extension', + message: { method, params, sessionId }, + }) + logCdpMessage({ direction: 'from-extension', method, diff --git a/playwriter/src/cli.ts b/playwriter/src/cli.ts index 97234ed..c86bbf3 100644 --- a/playwriter/src/cli.ts +++ b/playwriter/src/cli.ts @@ -4,7 +4,7 @@ import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import { cac } from '@xmorse/cac' -import { VERSION, LOG_FILE_PATH } from './utils.js' +import { VERSION, LOG_FILE_PATH, LOG_CDP_FILE_PATH } from './utils.js' import { ensureRelayServer, RELAY_PORT, waitForExtension } from './relay-client.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -299,6 +299,7 @@ cli console.log(` Port: ${RELAY_PORT}`) console.log(` Token: ${token ? '(configured)' : '(none)'}`) console.log(` Logs: ${logger.logFilePath}`) + console.log(` CDP Logs: ${LOG_CDP_FILE_PATH}`) console.log('') console.log(`CDP endpoint: http://${options.host}:${RELAY_PORT}${token ? '?token=' : ''}`) console.log('') @@ -320,7 +321,8 @@ cli cli .command('logfile', 'Print the path to the relay server log file') .action(() => { - console.log(LOG_FILE_PATH) + console.log(`relay: ${LOG_FILE_PATH}`) + console.log(`cdp: ${LOG_CDP_FILE_PATH}`) }) cli diff --git a/playwriter/src/skill.md b/playwriter/src/skill.md index 2bf23b1..5d5f2d9 100644 --- a/playwriter/src/skill.md +++ b/playwriter/src/skill.md @@ -99,7 +99,13 @@ playwriter logfile # prints the log file path # typically: /tmp/playwriter/relay-server.log (Linux/macOS) or %TEMP%\playwriter\relay-server.log (Windows) ``` -The log file contains logs from the extension, MCP and WS server together with all CDP events. The file is recreated every time the server starts. For debugging internal playwriter errors, read this file with grep/rg to find relevant lines. +The relay log contains logs from the extension, MCP and WS server. A separate CDP JSONL log is created alongside it (see `playwriter logfile`) with all CDP commands/responses and events, with long strings truncated. Both files are recreated every time the server starts. For debugging internal playwriter errors, read these files with grep/rg to find relevant lines. + +Example: summarize CDP traffic counts by direction + method: + +```bash +jq -r '.direction + "\t" + (.message.method // "response")' /tmp/playwriter/cdp.jsonl | uniq -c +``` If you find a bug, you can create a gh issue using `gh issue create -R remorses/playwriter --title title --body body`. Ask for user confirmation before doing this. diff --git a/playwriter/src/start-relay-server.ts b/playwriter/src/start-relay-server.ts index 0365c50..b43edf2 100644 --- a/playwriter/src/start-relay-server.ts +++ b/playwriter/src/start-relay-server.ts @@ -1,5 +1,6 @@ import { startPlayWriterCDPRelayServer } from './cdp-relay.js' import { createFileLogger } from './create-logger.js' +import { LOG_CDP_FILE_PATH } from './utils.js' process.title = 'playwriter-ws-server' @@ -25,6 +26,7 @@ export async function startServer({ port = 19988, host = '127.0.0.1', token }: { console.log('CDP Relay Server running. Press Ctrl+C to stop.') console.log('Logs are being written to:', logger.logFilePath) + console.log('CDP logs are being written to:', LOG_CDP_FILE_PATH) process.on('SIGINT', () => { console.log('\nShutting down...') diff --git a/playwriter/src/utils.ts b/playwriter/src/utils.ts index 6869ea2..b3b5783 100644 --- a/playwriter/src/utils.ts +++ b/playwriter/src/utils.ts @@ -9,7 +9,9 @@ export function getCdpUrl({ port = 19988, host = '127.0.0.1', token }: { port?: return `ws://${host}:${port}/cdp/${id}${queryString}` } -export const LOG_FILE_PATH = process.env.PLAYWRITER_LOG_FILE_PATH || path.join(os.tmpdir(), 'playwriter', 'relay-server.log') +const LOG_BASE_DIR = os.platform() === 'win32' ? os.tmpdir() : '/tmp' +export const LOG_FILE_PATH = process.env.PLAYWRITER_LOG_FILE_PATH || path.join(LOG_BASE_DIR, 'playwriter', 'relay-server.log') +export const LOG_CDP_FILE_PATH = process.env.PLAYWRITER_CDP_LOG_FILE_PATH || path.join(path.dirname(LOG_FILE_PATH), 'cdp.jsonl') const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json') export const VERSION = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')).version as string