add cdp jsonl logging and update log docs

This commit is contained in:
Tommy D. Rossi
2026-01-26 13:28:18 +01:00
parent 4d2606253b
commit a64fc2d8a0
9 changed files with 156 additions and 7 deletions
+10 -1
View File
@@ -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
+6
View File
@@ -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
```
+7 -1
View File
@@ -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
+65
View File
@@ -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<object>()
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<void> = 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,
}
}
+52 -1
View File
@@ -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<K extends keyof RelayServerEvents>(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<RelayServer> {
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<RelayServer> {
const emitter = new EventEmitter()
const connectedTargets = new Map<string, ConnectedTarget>()
const resolvedCdpLogger = cdpLogger || createCdpLogger()
const logCdpJson = (entry: CdpLogEntry) => {
resolvedCdpLogger.log(entry)
}
const playwrightClients = new Map<string, PlaywrightClient>()
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,
+4 -2
View File
@@ -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=<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
+7 -1
View File
@@ -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.
+2
View File
@@ -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...')
+3 -1
View File
@@ -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