Generated banner for Browser Trace
Generated logo for Browser Trace

Browser Trace

BrowserbaseOfficialPublished by Browserbase

  • Skill
  • Review and audit
  • Software development
  • MIT
  • Low risk
  • Has scripts

From the author

Capture a full DevTools-protocol trace of any browser automation — CDP firehose, screenshots, and DOM dumps — then bisect the stream into per-page searchable buckets. Use when the user wants to debug a failed run, audit network/console/DOM activity, attach a trace to an in-progress session, or feed structured per-page summaries back into an agent loop so its next iteration learns from the last one.
— the description by Browserbase

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

Browser Trace is a diagnostic tool that attaches a read-only CDP client to an active browser automation session. It records the full DevTools firehose, including network activity, console logs, and DOM snapshots, into a structured directory tree. By bisecting these events into per-page buckets, it allows developers to audit failed runs, inspect network traffic, and correlate visual screenshots with internal browser events without interrupting the main automation process.

What it does

  • Streams full CDP events to NDJSON files for comprehensive audit trails.
  • Polls for periodic screenshots and DOM dumps to provide visual context.
  • Bisects raw event streams into searchable, per-page buckets.
  • Provides a query interface to inspect errors, network requests, and page navigations.
  • Supports local Chrome debugging and remote Browserbase session monitoring.
  • Generates summary reports detailing session duration, event counts, and network activity.

When to use it

  • Debugging failed browser automation runs or unexpected JS exceptions.
  • Auditing network, console, or DOM activity during a live session.
  • Attaching a trace to an in-progress automation without restarting it.
  • Creating structured summaries of browser activity to inform future agent iterations.

Advantages

  • Non-intrusive observation that does not interfere with the main automation.
  • Provides a unified view of visual state and internal browser events.
  • Enables deep inspection of network and console logs via standard command-line tools.
  • Supports both local and remote browser environments with consistent tooling.

Usage examples

“I need to debug why my automation is failing on a specific page. How do I start a trace?”
The agent guides you to run the start-capture script with your browser port, perform your automation, and then run the stop and bisect scripts to generate the searchable trace files.
“How can I see all failed network requests from my last run?”
The agent provides a jq command to filter the network/failed.jsonl file within your run directory, showing the status and URL of each failed request.
“Can I attach this tracer to a Browserbase session that is already running?”
Yes, the agent explains how to use the bb-capture script with your existing session ID to begin capturing the firehose without disrupting the active automation.
“How do I find the screenshot that corresponds to a specific console error?”
The agent shows how to use the query tool or manual file inspection to match the timestamp of the console error with the nearest file in the screenshots directory.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/browserbase/browser-trace/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip browser-trace.zip -d .agents/skills

Documentation by Browserbase

Browser Trace

Attach a second, read-only CDP client to a browser session that is already being driven by your main automation. The trace records the full DevTools firehose to NDJSON, polls for screenshots and DOM dumps in parallel, and slices everything into a directory tree that bash tools can search.

This skill does not drive pages — it only listens. Pair it with the browser skill, browse, Stagehand, Playwright, or anything else that speaks CDP.

When to use

  • The user wants to debug a browser-automation run (failing form, missing element, hung navigation, JS exception).

  • The user has a running automation and wants to attach a trace mid-flight without restarting it.

  • The user wants to split a CDP firehose into network / console / DOM / page buckets.

  • The user wants screenshots + DOM snapshots over time, joined to CDP events by timestamp.

If the user just wants to drive the browser, use the browser skill instead.

Setup check

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:

  1. Firehose: browse cdp <target> streams every CDP event as one JSON object per line to cdp/raw.ndjson.

  2. Sampler: a polling loop calls browse screenshot --cdp <target> --path <file> and browse get html body --cdp <target> on an interval (default 2s). The helper passes --cdp when 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.

  3. Bisector: after the run, bisect-cdp.mjs walks raw.ndjson once, slices it into per-bucket JSONL files keyed by CDP method, and additionally bisects per page using top-level Page.frameNavigated events 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 — final browse cloud sessions get snapshot (proxyBytes, status, ended_at, viewport, …)

  • <run>/browserbase/logs.json — browse cloud sessions logs output. Often empty. The CDP firehose in cdp/raw.ndjson is 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

  1. Use bb-capture.mjs on Browserbase: it enforces --keep-alive, fetches the connectUrl, captures the debugger URL, and stamps the manifest. Doing it manually invites mistakes.

  2. Don't --release a session you don't own: bb-finalize.mjs --release is for sessions you created with --new. When attaching to a production session via bb-capture.mjs <session-id>, run bb-finalize.mjs without --release so the original automation keeps running.

  3. 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.

  4. Don't poll faster than ~1s: each sample runs browser CLI read commands and screenshots Chrome. 2s is a good default.

  5. Pick domains deliberately: defaults (Network Console Runtime Log Page) cover most debugging. Add DOM for DOM-tree mutations (very noisy) via O11Y_DOMAINS="$O11Y_DOMAINS DOM".

  6. Reuse one Browserbase session for the automation client on remote by attaching to that session's connectUrl with browse open ... --cdp "$CONNECT_URL" --session <name>. The --session flag names the local browse daemon; it is not a Browserbase session attach flag.

  7. Always run stop-capture.mjs, even after a crash, so background processes don't linger and the manifest gets stopped_at.

  8. Bisect once per run: bisect-cdp.mjs is idempotent — it overwrites the per-bucket files from raw.ndjson each time.

Troubleshooting

  • browse cdp exited immediately: usually means the target is unreachable (wrong port) or the Browserbase session has already ended. For remote, verify with browse cloud sessions get <id> — if status is COMPLETED, recreate with --keep-alive and attach automation first.

  • Empty raw.ndjson even 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 — if url doesn'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 with browse cloud sessions list | jq '.[] | select(.status == "RUNNING")' and try again.

  • browserbase/logs.json is empty []: expected — browse cloud sessions logs is sparse in practice. The CDP firehose in cdp/raw.ndjson is 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 in dom/.

For full reference, see REFERENCE.md. For example debug runs, see EXAMPLES.md.

Files

REFERENCE.md

Browser Trace — Reference

Technical reference for the capture pipeline, the bisect mapping, and the jq recipe library.

Architecture

                       ┌──────────────────────────────────────┐
   main automation ──▶ │  Chrome / Browserbase CDP target     │ ◀── tracer (this skill)
   (any framework)     └──────────────────────────────────────┘
        │                                    │
        ▼                                    ▼
    drives page                browse cdp <target>     (firehose → raw.ndjson)
                               browse screenshot --cdp <target> --path <file>  (sampler → screenshots/)
                               browse get html body --cdp <target>             (sampler → dom/)

CDP allows multiple concurrent clients on the same target. The tracer enables only read-only domains and never sends action commands like Input.dispatch* or Runtime.evaluate, so it cannot perturb the run.

Sampler commands pass --cdp <target> because they run from the trace helper process and need to attach to the traced target directly. Normal follow-up commands in a browse daemon session do not need to repeat --cdp after the first browse open ... --cdp <target>. If the default daemon may already be active in another mode, use a named --session for sampler or automation commands.

Scripts

All scripts read O11Y_ROOT (default .o11y) so runs land under $O11Y_ROOT/<run-id>/. They are Node ESM modules (node 18+) and depend only on browse plus the Node standard library — no npm install step. jq is referenced throughout the docs for ad-hoc querying but the scripts themselves don't need it.

start-capture.mjs <target> [run-id] [interval-sec]

Starts both background processes and writes manifest.json.

  • target — port number (e.g. 9222) or full WebSocket URL.

  • run-id — optional; defaults to YYYYMMDDTHHMMSSZ.

  • interval-sec — sampler period in seconds; default 2.

Honours O11Y_DOMAINS (space-separated) to control which CDP domains the firehose enables. Default: Network Console Runtime Log Page. Add DOM for DOM tree mutations, Performance for navigation timing, Security for mixed-content/cert events.

PIDs are stored in <run-dir>/.cdp.pid and <run-dir>/.loop.pid so stop-capture.mjs can find them.

stop-capture.mjs <run-id>

SIGTERM → 3s grace → SIGKILL on both background processes, then stamps manifest.json with stopped_at.

bisect-cdp.mjs <run-id>

Slices cdp/raw.ndjson two ways, then writes cdp/summary.json:

  1. Session-wide buckets at cdp/<domain>/... (legacy layout, always written; see bisect map below).

  2. Per-page buckets at cdp/pages/<pid>/..., indexed by top-level Page.frameNavigated boundaries. Pages are zero-padded so they lex-sort numerically (000, 001, …). Within each page only non-empty bucket files are written so empty directories don't pollute search results.

cdp/summary.json carries the run-level rollup: sessionId, duration (wall-clock ms anchored to manifest.started_at), totalEvents, and a pages[] array. Each entry has {pageId, url, startMs, endMs, durationMs, eventCount, domains, network} — same shape as the per-page summary.json.

Idempotent: rerun safely. The cdp/pages/ tree is wiped and rebuilt each call.

query.mjs <run-id> <subcommand> [args...]

Reads the bisected output and prints either tabular text or NDJSON. Subcommands:

SubcommandOutput
listone-line page table (pid, events, duration, url)
summaryfull cdp/summary.json
page <pid>per-page summary.json
page <pid> <bucket>cat pages/<pid>/<bucket>.jsonl (e.g. network/failed, console/logs, raw)
errors [pid|all]unified error stream across pages: network failed, runtime exceptions, console errors, log-level errors. Each line tagged with pid and kind
hosts [pid|all]top hosts by request count
host <hostname> [pid|all]every request/response for that hostname, prefixed with [pid]
timelineordered nav + lifecycle markers

Bypassable with raw jq/rg against cdp/summary.json and cdp/pages/<pid>/ once you know the layout.

snapshot-loop.mjs (internal)

Invoked by start-capture.mjs; not meant to be called directly. Loops at the configured interval, writing PNG + HTML + an entry to index.jsonl per tick. DOM dumps go through a .partial temp file so a SIGTERM mid-write never leaves a 0-byte HTML behind; stop-capture.mjs sweeps any survivors.

bb-capture.mjs --new|<session-id> [run-id] [interval-sec]

Browserbase wrapper around start-capture.mjs. With --new, runs browse cloud sessions create --keep-alive and starts the tracer. With an existing session id, fetches its connectUrl via browse cloud sessions get and asserts the session is RUNNING before attaching.

Stamps the run's manifest.json with a browserbase object containing session_id, project_id, region, started_at, expires_at, keep_alive, and the debugger_url from browse cloud sessions debug.

Reads BROWSERBASE_API_KEY. BB_SESSION_TIMEOUT (default 600) controls the timeout passed to --new sessions.

bb-finalize.mjs <run-id> [--release]

Pulls platform-side artifacts after the tracer has stopped:

  • browserbase/session.json — browse cloud sessions get snapshot. Always written; contains the post-run proxyBytes, status, endedAt.

  • browserbase/logs.json — browse cloud sessions logs output. Often []. The CDP firehose is authoritative; this is a side channel for cases where Browserbase happened to record server-side log entries.

  • browserbase/downloads.zip — only kept when there's real content (size > 22 bytes — an empty Browserbase downloads zip is exactly the EOCD record).

--release calls browse cloud sessions update --status REQUEST_RELEASE to end the session. Skip it when attaching to a session you don't own (e.g. one a production worker is using).

Bisect map

FileCDP methodWhat's in it
cdp/network/requests.jsonlNetwork.requestWillBeSentevery outgoing request: url, method, headers, postData, requestId
cdp/network/responses.jsonlNetwork.responseReceivedresponse status, headers, mimeType, remoteIPAddress, fromDiskCache
cdp/network/finished.jsonlNetwork.loadingFinishedbyte count + timestamp on success
cdp/network/failed.jsonlNetwork.loadingFailederrorText (e.g. net::ERR_ABORTED), canceled
cdp/network/websocket.jsonlNetwork.webSocket*every WebSocket lifecycle event
cdp/console/logs.jsonlRuntime.consoleAPICalledconsole.log/info/warn/error with args[]
cdp/console/exceptions.jsonlRuntime.exceptionThrownunhandled JS errors with stack
cdp/runtime/all.jsonlRuntime.*execution-context create/destroy, binding calls, etc.
cdp/log/entries.jsonlLog.entryAddedbrowser-level warnings (CSP, deprecation, mixed content)
cdp/page/navigations.jsonlPage.frameNavigatedeach top-level + iframe navigation
cdp/page/lifecycle.jsonlPage.lifecycleEventper-navigation milestones: init, commit, DOMContentLoaded, load, firstPaint, firstContentfulPaint, firstMeaningfulPaint, networkAlmostIdle, networkIdle
cdp/page/frames.jsonlPage.frame*frame attached/detached/started/stoppedLoading
cdp/page/dialogs.jsonlPage.javascriptDialog*alert / confirm / prompt / beforeunload
cdp/page/all.jsonlPage.*catch-all for everything Page emits
cdp/dom/all.jsonlDOM.*tree mutations (only populated if O11Y_DOMAINS adds DOM)
cdp/target/attached.jsonlTarget.attachedToTargeteach new page/iframe target attached to the tracer
cdp/target/detached.jsonlTarget.detachedFromTargeteach detach
Note on response bodies

browse cdp does not embed response bodies in the firehose — that requires a synchronous Network.getResponseBody round-trip per request. If you need bodies, use browse network on (in the browser skill) which writes per-request directories with request.json + response.json including body. The two skills compose: run browse network on for bodies + browse cdp for the timeline.

jq recipe library

All recipes assume cd .o11y/<run-id>/cdp for brevity.

Network
# Top hosts by request count
jq -r '.params.request.url' network/requests.jsonl \
  | awk -F/ '{print $3}' | sort | uniq -c | sort -rn | head

# All XHR/fetch (exclude subresources)
jq -c 'select(.params.type == "XHR" or .params.type == "Fetch")' \
  network/requests.jsonl

# Slow responses (>1000ms) — join finished against requests by requestId
jq -s '
  (.[0] | map({(.params.requestId): .params.timestamp}) | add) as $start |
  .[1] | map(select(.params.encodedDataLength != null))
       | map({
           rid: .params.requestId,
           dur_ms: ((.params.timestamp - $start[.params.requestId]) * 1000 | floor),
           bytes: .params.encodedDataLength
         })
       | map(select(.dur_ms > 1000))
       | sort_by(-.dur_ms)
' network/requests.jsonl network/finished.jsonl

# All POST bodies that aren't form-encoded
jq -c 'select(.params.request.method == "POST")
       | {url: .params.request.url, body: .params.request.postData}' \
  network/requests.jsonl
Console & exceptions
# Console errors with the originating url+line
jq -r 'select(.params.type == "error")
       | "\(.params.stackTrace.callFrames[0].url):\(.params.stackTrace.callFrames[0].lineNumber)\t\(.params.args[0].value // .params.args[0].description // "")"' \
  console/logs.jsonl

# Pretty-print every exception
jq -c '.params.exceptionDetails
       | {text, line: .lineNumber, url, stack: .stackTrace.callFrames[0:3]}' \
  console/exceptions.jsonl
Page navigation
# Linear visit log
jq -r '.params.frame.url' page/navigations.jsonl

# Navigations only on the top frame (skip iframes)
jq -r 'select(.params.frame.parentId == null) | .params.frame.url' \
  page/navigations.jsonl
Page lifecycle (timing milestones)

Page.lifecycleEvent fires per-navigation for init, commit, DOMContentLoaded, load, firstPaint, firstContentfulPaint, firstMeaningfulPaint, networkAlmostIdle, networkIdle. Requires browse cdp ≥ the build that includes stagehand#2056; on older builds lifecycle.jsonl will be empty.

# Time-to-DOMContentLoaded and time-to-load per navigation (seconds since loader start)
jq -s '
  group_by(.params.loaderId) | map({
    loader: .[0].params.loaderId,
    init:               (map(select(.params.name == "init"))               | first | .params.timestamp // null),
    DOMContentLoaded:   (map(select(.params.name == "DOMContentLoaded"))   | first | .params.timestamp // null),
    load:               (map(select(.params.name == "load"))               | first | .params.timestamp // null),
    firstContentfulPaint: (map(select(.params.name == "firstContentfulPaint")) | first | .params.timestamp // null),
    networkIdle:        (map(select(.params.name == "networkIdle"))        | first | .params.timestamp // null)
  } | . + {
    ttDCL_s:  (if .DOMContentLoaded   and .init then (.DOMContentLoaded   - .init) else null end),
    ttLoad_s: (if .load               and .init then (.load               - .init) else null end),
    ttFCP_s:  (if .firstContentfulPaint and .init then (.firstContentfulPaint - .init) else null end),
    ttIdle_s: (if .networkIdle        and .init then (.networkIdle        - .init) else null end)
  })
' page/lifecycle.jsonl
Joining events to screenshots

index.jsonl (sibling of cdp/) holds the sampler index. To find the screenshot closest to a CDP event timestamp:

# Pick an exception timestamp (Runtime.exceptionThrown uses .params.timestamp in ms)
EVT_MS=$(jq -r '.params.timestamp' console/exceptions.jsonl | head -1)
EVT_ISO=$(date -u -r $((EVT_MS/1000)) +%Y%m%dT%H%M%SZ)

# Find the first screenshot >= that ISO timestamp
ls ../screenshots | sort | awk -v t="$EVT_ISO" '$0 >= t { print; exit }'

For a quick visual diff, open ../dom/<ts>.html at the same timestamp.

Pairing with Browserbase platform data

When a run was captured through bb-capture.mjs, its manifest.json carries a browserbase block and bb-finalize.mjs adds a browserbase/ subdir. A few useful joins:

RUN=.o11y/<run-id>

# Pull session metadata into context
jq '.browserbase' "$RUN/manifest.json"

# How many bytes did Browserbase's proxy bill us?
jq '.proxyBytes' "$RUN/browserbase/session.json"

# Sum the encoded bytes the tracer saw across responses; compare to proxyBytes.
jq -s 'map(.params.encodedDataLength // 0) | add' \
  "$RUN/cdp/network/finished.jsonl"

# Open the live debugger view for an in-flight run
open "$(jq -r '.browserbase.debugger_url' "$RUN/manifest.json")"

# Find every run that touched a particular Browserbase project
grep -lr '"project_id": "5a9c3bfb' .o11y/*/manifest.json

# List of session ids by run
for m in .o11y/*/manifest.json; do
  jq -r '"\(.run_id)\t\(.browserbase.session_id // "local")"' "$m"
done
When to use browse cloud sessions debug vs the tracer

They're complementary:

  • tracer (this skill) captures the firehose to disk — durable, searchable, scriptable. Use for postmortem and automated checks.

  • browse cloud sessions debug URL is an interactive Chrome DevTools view served by Browserbase, scoped to one running session. Use when you want to watch a live run, single-step through requests, or inspect the live DOM by hand.

You can do both simultaneously: bb-capture.mjs --new prints the debugger URL when it starts, and stamps it in the manifest for later.

Notes on Browserbase data sources
  • browse cloud sessions logs is best-effort; in practice it's frequently empty even with --log-session on. Don't build queries on top of it; treat anything that lands there as a bonus.

  • Session replay artifact fetching is deprecated — neither helper fetches it. Use the screenshots + DOM dumps in screenshots/ and dom/.

  • browse cloud sessions list doesn't accept a --status filter; pipe through jq (select(.status == "RUNNING")).

  • The Browserbase proxy charges per byte. browse cloud sessions get returns running proxyBytes; the tracer's network buckets give you per-host detail to attribute it.

Per-page drill-down

The same recipes work scoped to a single page. Replace cdp/<bucket>.jsonl with cdp/pages/<pid>/<bucket>.jsonl, or use query.mjs for the common patterns.

RUN=.o11y/<run-id>

# Browse the page index quickly
jq '.pages | map({pageId, url, durationMs, eventCount})' $RUN/cdp/summary.json

# Pages with the most network errors
jq '.pages | map(select(.domains.Network.errors > 0))
            | map({pageId, url, errors: .domains.Network.errors})' \
  $RUN/cdp/summary.json

# Pages by event volume (hot pages)
jq '.pages | sort_by(-.eventCount) | .[:5] | map({pageId, url, eventCount})' \
  $RUN/cdp/summary.json

# All requests on page 2 grouped by type
jq -r '.params.type' $RUN/cdp/pages/002/network/requests.jsonl \
  | sort | uniq -c | sort -rn

# Did page 1 fire firstContentfulPaint?
jq -c 'select(.params.name == "firstContentfulPaint") | .params.timestamp' \
  $RUN/cdp/pages/001/page/lifecycle.jsonl

# All POST bodies submitted on page 3
jq -c 'select(.params.request.method == "POST")
       | {url: .params.request.url, body: .params.request.postData}' \
  $RUN/cdp/pages/003/network/requests.jsonl

Bash traversal cheatsheet

# Total artifact size
du -sh .o11y/<run-id>

# Every URL ever requested, deduped
jq -r '.params.request.url' .o11y/*/cdp/network/requests.jsonl | sort -u

# Find runs that hit a specific host
grep -lr 'api\.example\.com' .o11y/*/cdp/network/requests.jsonl

# Search DOM dumps for an element class that came and went
rg -l 'class="error-banner"' .o11y/<run-id>/dom/

# Tail the firehose live (re-run start-capture is fine — it appends to raw.ndjson? no, it overwrites)
tail -f .o11y/<run-id>/cdp/raw.ndjson | jq -c '{m:.method, u:.params.request.url // .params.frame.url // ""}'

Configuration

VarDefaultEffect
O11Y_ROOT.o11ybase directory under which <run-id>/ is created
O11Y_DOMAINSNetwork Console Runtime Log Pagespace-separated CDP domains for the firehose
BROWSERBASE_API_KEY—required for browse cloud sessions create / browse cloud sessions get

The interval-second arg to start-capture.mjs controls only the sampler. The firehose is always streamed in real time.

Troubleshooting

SymptomLikely causeFix
browse cdp exited immediatelyunreachable target / completed Browserbase sessionverify port is listening (curl http://localhost:9222/json/version) or session is RUNNING (browse cloud sessions get)
error: unknown command 'cdp'older browse build lacks the commandnpm install -g browse@latest (or the alpha tag if needed)
Browserbase session ends as soon as tracer connectstracer was the only client; no automation attachedcreate with --keep-alive, attach automation with browse open --cdp <connectUrl> --session <name> first
index.jsonl shows "url": ""sampler browse get url failed transientlybenign; happens during navigation transitions
Screenshots empty / huge / inconsistent sizesviewport not setbrowse viewport 1920 1080 --cdp <target> once before capture
raw.ndjson grows but bisect buckets emptywrong domains; e.g. you wanted DOM but didn't enable itO11Y_DOMAINS="Network Console Runtime Log Page DOM" bash start-capture.mjs ...
Loop process leaks after crashstop-capture.mjs not runpkill -f snapshot-loop.mjs; PID files in <run-dir> are stale

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Declares allowed tools, Needs an API key, Needs Node.js

The author says: Requires Node 18+, the browse CLI (`npm install -g browse`) with `browse cdp`, and optionally `jq` for ad-hoc querying of the bisected JSONL files. For remote Browserbase sessions, also requires `BROWSERBASE_API_KEY`. The skill scripts themselves use only the Node standard library — no `npm install` step.

Security

What our scanner noticed· Low risk

  • Instructions that use the network
  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Browserbase
Publisher
Browserbase (Official)
Source
browserbase/skills · skills/browser-trace · commit 6811ca3
Licence
MIT License (redistribution allowed)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Browserbase or Browserbase.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download browser-trace.zip