add cdp jsonl logging and update log docs
This commit is contained in:
@@ -189,7 +189,13 @@ ignore ./claude-extension. this is the source code of the Claude Chrome extensio
|
|||||||
|
|
||||||
## reading playwriter logs
|
## 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
|
# core guidelines
|
||||||
|
|
||||||
@@ -226,6 +232,7 @@ you can open files when i ask me "open in zed the line where ..." using the comm
|
|||||||
# typescript
|
# 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).
|
- 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.
|
- 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 `import fs from 'fs'; fs.writeFileSync(...)`
|
||||||
DO NOT `import { writeFileSync } from 'fs';`
|
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
|
# 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
|
this project uses pnpm workspaces to manage dependencies. important scripts are in the root package.json or various packages' package.json
|
||||||
|
|||||||
@@ -188,3 +188,9 @@ ignore ./claude-extension. this is the source code of the Claude Chrome extensio
|
|||||||
## reading playwriter logs
|
## reading playwriter logs
|
||||||
|
|
||||||
you can find the logfile for playwriter executing `playwriter logfile`. read that then to understand issues happening and debug them
|
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
|
||||||
|
```
|
||||||
|
|||||||
@@ -247,7 +247,13 @@ playwriter logfile # prints the log file path
|
|||||||
# typically: /tmp/playwriter/relay-server.log (Linux/macOS)
|
# 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
|
## Known Issues
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import type { ExtensionMessage, ExtensionEventMessage } from './protocol.js'
|
|||||||
import pc from 'picocolors'
|
import pc from 'picocolors'
|
||||||
import { EventEmitter } from 'node:events'
|
import { EventEmitter } from 'node:events'
|
||||||
import { VERSION } from './utils.js'
|
import { VERSION } from './utils.js'
|
||||||
|
import { createCdpLogger, type CdpLogEntry, type CdpLogger } from './cdp-log.js'
|
||||||
|
|
||||||
type ConnectedTarget = {
|
type ConnectedTarget = {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
@@ -69,10 +70,27 @@ export type RelayServer = {
|
|||||||
off<K extends keyof RelayServerEvents>(event: K, listener: RelayServerEvents[K]): void
|
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 emitter = new EventEmitter()
|
||||||
const connectedTargets = new Map<string, ConnectedTarget>()
|
const connectedTargets = new Map<string, ConnectedTarget>()
|
||||||
|
|
||||||
|
const resolvedCdpLogger = cdpLogger || createCdpLogger()
|
||||||
|
const logCdpJson = (entry: CdpLogEntry) => {
|
||||||
|
resolvedCdpLogger.log(entry)
|
||||||
|
}
|
||||||
|
|
||||||
const playwrightClients = new Map<string, PlaywrightClient>()
|
const playwrightClients = new Map<string, PlaywrightClient>()
|
||||||
let extensionWs: WSContext | null = null
|
let extensionWs: WSContext | null = null
|
||||||
|
|
||||||
@@ -179,6 +197,14 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|||||||
? { ...message, __serverGenerated: true }
|
? { ...message, __serverGenerated: true }
|
||||||
: message
|
: message
|
||||||
|
|
||||||
|
logCdpJson({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
direction: 'to-playwright',
|
||||||
|
clientId,
|
||||||
|
source,
|
||||||
|
message: messageToSend,
|
||||||
|
})
|
||||||
|
|
||||||
if ('method' in message) {
|
if ('method' in message) {
|
||||||
logCdpMessage({
|
logCdpMessage({
|
||||||
direction: 'to-playwright',
|
direction: 'to-playwright',
|
||||||
@@ -212,6 +238,18 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|||||||
const id = ++extensionMessageId
|
const id = ++extensionMessageId
|
||||||
const message = { id, method, params }
|
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))
|
extensionWs.send(JSON.stringify(message))
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -584,6 +622,13 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|||||||
|
|
||||||
const { id, sessionId, method, params, source } = message
|
const { id, sessionId, method, params, source } = message
|
||||||
|
|
||||||
|
logCdpJson({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
direction: 'from-playwright',
|
||||||
|
clientId,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
|
||||||
logCdpMessage({
|
logCdpMessage({
|
||||||
direction: 'from-playwright',
|
direction: 'from-playwright',
|
||||||
clientId,
|
clientId,
|
||||||
@@ -820,6 +865,12 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|||||||
|
|
||||||
const { method, params, sessionId } = extensionEvent.params
|
const { method, params, sessionId } = extensionEvent.params
|
||||||
|
|
||||||
|
logCdpJson({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
direction: 'from-extension',
|
||||||
|
message: { method, params, sessionId },
|
||||||
|
})
|
||||||
|
|
||||||
logCdpMessage({
|
logCdpMessage({
|
||||||
direction: 'from-extension',
|
direction: 'from-extension',
|
||||||
method,
|
method,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import fs from 'node:fs'
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { cac } from '@xmorse/cac'
|
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'
|
import { ensureRelayServer, RELAY_PORT, waitForExtension } from './relay-client.js'
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
@@ -299,6 +299,7 @@ cli
|
|||||||
console.log(` Port: ${RELAY_PORT}`)
|
console.log(` Port: ${RELAY_PORT}`)
|
||||||
console.log(` Token: ${token ? '(configured)' : '(none)'}`)
|
console.log(` Token: ${token ? '(configured)' : '(none)'}`)
|
||||||
console.log(` Logs: ${logger.logFilePath}`)
|
console.log(` Logs: ${logger.logFilePath}`)
|
||||||
|
console.log(` CDP Logs: ${LOG_CDP_FILE_PATH}`)
|
||||||
console.log('')
|
console.log('')
|
||||||
console.log(`CDP endpoint: http://${options.host}:${RELAY_PORT}${token ? '?token=<token>' : ''}`)
|
console.log(`CDP endpoint: http://${options.host}:${RELAY_PORT}${token ? '?token=<token>' : ''}`)
|
||||||
console.log('')
|
console.log('')
|
||||||
@@ -320,7 +321,8 @@ cli
|
|||||||
cli
|
cli
|
||||||
.command('logfile', 'Print the path to the relay server log file')
|
.command('logfile', 'Print the path to the relay server log file')
|
||||||
.action(() => {
|
.action(() => {
|
||||||
console.log(LOG_FILE_PATH)
|
console.log(`relay: ${LOG_FILE_PATH}`)
|
||||||
|
console.log(`cdp: ${LOG_CDP_FILE_PATH}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
cli
|
cli
|
||||||
|
|||||||
@@ -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)
|
# 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { startPlayWriterCDPRelayServer } from './cdp-relay.js'
|
import { startPlayWriterCDPRelayServer } from './cdp-relay.js'
|
||||||
import { createFileLogger } from './create-logger.js'
|
import { createFileLogger } from './create-logger.js'
|
||||||
|
import { LOG_CDP_FILE_PATH } from './utils.js'
|
||||||
|
|
||||||
process.title = 'playwriter-ws-server'
|
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('CDP Relay Server running. Press Ctrl+C to stop.')
|
||||||
console.log('Logs are being written to:', logger.logFilePath)
|
console.log('Logs are being written to:', logger.logFilePath)
|
||||||
|
console.log('CDP logs are being written to:', LOG_CDP_FILE_PATH)
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
console.log('\nShutting down...')
|
console.log('\nShutting down...')
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ export function getCdpUrl({ port = 19988, host = '127.0.0.1', token }: { port?:
|
|||||||
return `ws://${host}:${port}/cdp/${id}${queryString}`
|
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')
|
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
|
export const VERSION = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')).version as string
|
||||||
|
|||||||
Reference in New Issue
Block a user