SKILL.md
$27
node --version # require Node 18+
which browse || npm install -g browse
which jq || true # optional — used only for ad-hoc querying
Verify browse cdp exists:
browse --help | grep -q "^\s*cdp " || echo "browse cdp not available — update browse"
How it works
Every Chrome DevTools target accepts multiple concurrent CDP clients. Your main automation is one client; this skill adds a second one that only enables observation domains (Network, Console, Runtime, Log, Page) and never sends action commands.
The tracer has three pieces:
- Firehose:
browse cdp <target>streams every CDP event as one JSON object per line tocdp/raw.ndjson.
- Sampler: a polling loop calls
browse screenshot --cdp <target> --path <file>andbrowse get html body --cdp <target>on an interval (default 2s). The helper passes--cdpwhen it samples so it can attach to the traced target from its own process; once a browse daemon session is attached to a CDP target, follow-up commands in that session do not need to repeat--cdp.
- Bisector: after the run,
bisect-cdp.mjswalksraw.ndjsononce, slices it into per-bucket JSONL files keyed by CDP method, and additionally bisects per page using top-levelPage.frameNavigatedevents as boundaries.
Quickstart
Local Chrome
# 1. Launch Chrome with a debugger port (any user-data-dir keeps it isolated).
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-o11y \
about:blank &
# 2. Start the tracer.
node scripts/start-capture.mjs 9222 my-run
# 3. Run your main automation against port 9222.
browse open https://example.com --cdp 9222
# ...whatever the run does...
# 4. Stop and bisect.
node scripts/stop-capture.mjs my-run
node scripts/bisect-cdp.mjs my-run
Browserbase remote
Two helpers wrap the platform-side bookkeeping: bb-capture.mjs creates or attaches to a session and starts the tracer; bb-finalize.mjs pulls platform artifacts (final session metadata, server logs, downloads) into the run dir at the end.
Browserbase ends a session as soon as its last CDP client disconnects. **Create with --keep-alive, then attach automation to the session's connectUrl before or together with the tracer.** bb-capture.mjs --new handles the keep-alive session and tracer setup; your automation still needs to attach.
export BROWSERBASE_API_KEY=...
# 1. Create a keep-alive session AND start the tracer in one step.
# Prints the session id, connectUrl prefix, and a live debugger URL you
# can open in a browser to watch the run interactively.
node scripts/bb-capture.mjs --new my-run
# 2. Drive automation. bb-capture stamped the session id into the manifest.
SID=$(jq -r .browserbase.session_id .o11y/my-run/manifest.json)
CONNECT_URL="$(browse cloud sessions get "$SID" | jq -r .connectUrl)"
BROWSE_NAME=my-run-browser
browse open https://example.com --cdp "$CONNECT_URL" --session "$BROWSE_NAME"
browse open https://news.ycombinator.com --session "$BROWSE_NAME"
# 3. Stop the tracer, bisect, then pull platform artifacts and release.
node scripts/stop-capture.mjs my-run
node scripts/bisect-cdp.mjs my-run
node scripts/bb-finalize.mjs my-run --release
Attaching to a session that's already running (e.g. one your production worker created) — bb-capture.mjs accepts a session id instead of --new:
# Pick a running session (filter client-side; browse cloud sessions list has no --status flag)
browse cloud sessions list | jq -r '.[] | select(.status == "RUNNING") | .id'
node scripts/bb-capture.mjs <session-id> mid-flight-debug
# ...tracer runs alongside the existing automation client; no disruption...
node scripts/stop-capture.mjs mid-flight-debug
node scripts/bisect-cdp.mjs mid-flight-debug
node scripts/bb-finalize.mjs mid-flight-debug # without --release: leave the session running
#### What you get from the Browserbase platform
bb-capture.mjs adds a browserbase block to manifest.json (session id, project, region, started_at, expires_at, debugger URL). bb-finalize.mjs writes:
<run>/browserbase/session.json— finalbrowse cloud sessions getsnapshot (proxyBytes, status, ended_at, viewport, …)
<run>/browserbase/logs.json—browse cloud sessions logsoutput. Often empty. The CDP firehose incdp/raw.ndjsonis the source of truth; this is a side channel.
<run>/browserbase/downloads.zip— files the session downloaded, if any (the script discards the empty 22-byte zip you get when there are none)
Session replay artifact fetching is deprecated and isn't fetched. Use the screenshots + DOM dumps in screenshots/ and dom/ for visual ground truth.
The live debugger_url in the manifest opens an interactive Chrome DevTools view served by Browserbase — handy for watching a long-running automation while the tracer captures the firehose to disk.
Filesystem layout
.o11y/<run-id>/
manifest.json run metadata: target, domains, started_at, stopped_at
index.jsonl one line per sample: {ts, screenshot, dom, url}
cdp/
raw.ndjson full CDP firehose (one JSON object per line)
summary.json {sessionId, duration, totalEvents, pages[]} — see shape below
network/{requests,responses,finished,failed,websocket}.jsonl session-wide buckets (always written)
console/{logs,exceptions}.jsonl
runtime/all.jsonl
log/entries.jsonl
page/{navigations,lifecycle,frames,dialogs,all}.jsonl
dom/all.jsonl (only if O11Y_DOMAINS includes DOM)
target/{attached,detached}.jsonl
pages/ per-page slices, indexed by top-level frameNavigated boundaries
000/ first concrete page
url.txt the URL for this page
summary.json this page's domains/network/timing block (same shape as a pages[] entry)
raw.jsonl firehose scoped to this page
network/, console/, page/, runtime/, log/, target/, dom/ same buckets, only non-empty files
screenshots/<iso-ts>.png one PNG per sample interval
dom/<iso-ts>.html one HTML dump per sample interval
browserbase/ added by bb-finalize.mjs (Browserbase runs only)
session.json final `browse cloud sessions get` snapshot (proxyBytes, status, ended_at, …)
logs.json `browse cloud sessions logs` output (often [])
downloads.zip `browse cloud sessions downloads get` output (only if the session downloaded files)
When a run was started via bb-capture.mjs, manifest.json also carries a top-level browserbase block: session_id, project_id, region, started_at, expires_at, keep_alive, debugger_url.
Summary shape
cdp/summary.json is the entry point for any analysis: it has session-level totals and a pages[] array indexed by top-level Page.frameNavigated. Per-page entries are emitted in navigation order (page 0 = first concrete URL).
{
"sessionId": "45f28023-…",
"duration": { "startMs": 1777312533000, "endMs": 1777312609000, "totalMs": 76000 },
"totalEvents": 420,
"pages": [
{
"pageId": 0,
"url": "https://example.com/",
"startMs": 1777312533000, "endMs": 1777312538886, "durationMs": 5886,
"eventCount": 60,
"domains": {
"Network": { "count": 18, "errors": 1 },
"Console": { "count": 2 },
"Page": { "count": 24 },
"Runtime": { "count": 13 }
},
"network": { "requests": 4, "failed": 1, "byType": { "Document": 2, "Script": 1, "Other": 1 } }
}
]
}
startMs / endMs / durationMs are wall-clock ms, derived from manifest.started_at plus the offset of each event's CDP monotonic timestamp. domains[*] only includes errors/warnings keys when non-zero.
Drilling in with query.mjs
For interactive exploration, use scripts/query.mjs <run-id> <command> instead of remembering paths:
node scripts/query.mjs my-run list # one-line table of pages
node scripts/query.mjs my-run page 1 # full summary for page 1
node scripts/query.mjs my-run page 1 network/failed # cat failed.jsonl for page 1
node scripts/query.mjs my-run errors # all errors across pages, attributed by pid
node scripts/query.mjs my-run errors 2 # errors from page 2 only
node scripts/query.mjs my-run hosts # top hosts by request count
node scripts/query.mjs my-run host api.example.com # all requests/responses for a host
node scripts/query.mjs my-run summary # full summary.json
Behind the scenes it just reads cdp/summary.json and the cdp/pages/<pid>/ tree — feel free to bypass it with raw jq/rg once you know the shape.
Top traversal recipes
# All failed network requests (use jq -c to keep it line-delimited)
jq -c '.params' .o11y/<run>/cdp/network/failed.jsonl
# Find requests to a specific host
jq -c 'select(.params.request.url | test("api\\.example\\.com"))' \
.o11y/<run>/cdp/network/requests.jsonl
# 4xx/5xx responses
jq -c 'select(.params.response.status >= 400)
| {status: .params.response.status, url: .params.response.url}' \
.o11y/<run>/cdp/network/responses.jsonl
# Console errors only
jq -c 'select(.params.type == "error")' .o11y/<run>/cdp/console/logs.jsonl
# Sequence of URLs visited
jq -r '.params.frame.url' .o11y/<run>/cdp/page/navigations.jsonl
# Find the screenshot taken closest to a timestamp (e.g., when an exception fired)
ls .o11y/<run>/screenshots/ | sort | awk -v t=20260427T1714123NZ '
$0 >= t { print; exit }'
See REFERENCE.md for the full jq recipe library and a method-by-method bisect map. See EXAMPLES.md for end-to-end debug scenarios.
Best practices
- **Use
bb-capture.mjson Browserbase**: it enforces--keep-alive, fetches the connectUrl, captures the debugger URL, and stamps the manifest. Doing it manually invites mistakes.
- **Don't
--releasea session you don't own**:bb-finalize.mjs --releaseis for sessions you created with--new. When attaching to a production session viabb-capture.mjs <session-id>, runbb-finalize.mjswithout--releaseso the original automation keeps running.
- Order matters for remote: on Browserbase, attach the main automation client before (or together with) the tracer, and create the session with
--keep-alive. Otherwise the session ends as soon as the tracer's WS closes.
- Don't poll faster than ~1s: each sample runs browser CLI read commands and screenshots Chrome. 2s is a good default.
- Pick domains deliberately: defaults (
Network Console Runtime Log Page) cover most debugging. AddDOMfor DOM-tree mutations (very noisy) viaO11Y_DOMAINS="$O11Y_DOMAINS DOM".
- Reuse one Browserbase session for the automation client on remote by attaching to that session's
connectUrlwithbrowse open ... --cdp "$CONNECT_URL" --session <name>. The--sessionflag names the local browse daemon; it is not a Browserbase session attach flag.
- **Always run
stop-capture.mjs**, even after a crash, so background processes don't linger and the manifest getsstopped_at.
- Bisect once per run:
bisect-cdp.mjsis idempotent — it overwrites the per-bucket files fromraw.ndjsoneach time.
Troubleshooting
- **
browse cdp exited immediately**: usually means the target is unreachable (wrong port) or the Browserbase session has already ended. For remote, verify withbrowse cloud sessions get <id>— ifstatusisCOMPLETED, recreate with--keep-aliveand attach automation first.
- **Empty
raw.ndjsoneven though processes are running**: confirm a CDP client is actually driving the page. The tracer only emits events that the browser generates, so an idle browser produces ~5 lines of attach/discover messages and nothing else.
- Screenshots all look identical: check
index.jsonl— ifurldoesn't change, the page hasn't navigated yet. The polling loop runs independently of the main automation's pace.
- Browserbase session ends mid-run: it likely hit
--timeout. Recreate with a higher timeout (BB_SESSION_TIMEOUT=1800 node scripts/bb-capture.mjs --new ...) or remove the timeout flag.
- **
bb-capture.mjs <id>says "not RUNNING"**: the session you tried to attach to ended. List candidates withbrowse cloud sessions list | jq '.[] | select(.status == "RUNNING")'and try again.
- **
browserbase/logs.jsonis empty[]**: expected —browse cloud sessions logsis sparse in practice. The CDP firehose incdp/raw.ndjsonis the source of truth.
- Where's the session recording (rrweb)?: session replay artifact fetching is deprecated; this skill doesn't fetch it. Use the screenshot stream in
screenshots/and DOM dumps indom/.
For full reference, see REFERENCE.md.
For example debug runs, see EXAMPLES.md.