Claude Skill

browser-trace

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-progre

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download mxyhi-ok-skills-browser-trace-7933c15.zip · 31 KB
mxyhi/ok-skills 490 46 forks Apache-2.0 Updated 5d ago
Part of mxyhi/ok-skills — 37 skills

Install

skills CLI npx skills add https://github.com/mxyhi/ok-skills/tree/main/browser-trace
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install mxyhi-ok-skills@llmmart
Git git clone https://github.com/mxyhi/ok-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole mxyhi/ok-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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 (ok-skills)
  • scripts
    • bb-capture.mjs 3.7 KB · in bundle
    • bb-finalize.mjs 3.2 KB · in bundle
    • bisect-cdp.mjs 7.6 KB · in bundle
    • lib.mjs 4.2 KB · in bundle
    • query.mjs 8.6 KB · in bundle
    • snapshot-loop.mjs 2.6 KB · in bundle
    • start-capture.mjs 2.9 KB · in bundle
    • stop-capture.mjs 1.5 KB · in bundle
  • EXAMPLES.md 8.4 KB
    # Browser Trace — Examples
    
    Five end-to-end debug scenarios. Each one shows: setup, running the capture, and the queries you'd run on the resulting tree.
    
    The recipes below use raw `jq` on the bisected files so you can see exactly what's there. Most everyday drill-down can also be done through `scripts/query.mjs <run-id> <command>` — see SKILL.md.
    
    ## Example 1: A form submit failed — find the request and see the page state
    
    **User says**: "The signup form submit isn't working. I clicked Submit and nothing happened."
    
    ```bash
    # Launch debuggable Chrome and start the tracer.
    "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
      --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-o11y about:blank &
    node scripts/start-capture.mjs 9222 form-bug
    
    # Reproduce the bug.
    browse open https://example.com/signup --cdp 9222
    browse fill 'input[name=email]' 'user@example.com'
    browse fill 'input[name=password]' 'hunter2'
    browse snapshot
    browse click @0-7   # Submit button ref from `browse snapshot`
    
    node scripts/stop-capture.mjs form-bug
    node scripts/bisect-cdp.mjs form-bug
    ```
    
    Then the agent inspects:
    
    ```bash
    cd .o11y/form-bug
    
    # Did the POST go out?
    jq -c 'select(.params.request.method == "POST")
           | {url: .params.request.url, body: .params.request.postData}' \
      cdp/network/requests.jsonl
    
    # Did it 4xx/5xx?
    jq -c 'select(.params.response.status >= 400)
           | {status: .params.response.status, url: .params.response.url}' \
      cdp/network/responses.jsonl
    
    # Any console error around that time?
    jq -c 'select(.params.type == "error")' cdp/console/logs.jsonl
    
    # Was a JS exception thrown when you clicked?
    jq -c '.params.exceptionDetails
           | {text, url, line: .lineNumber}' cdp/console/exceptions.jsonl
    
    # Open the DOM dump captured right after the click
    ls dom/ | tail -3
    ```
    
    If the POST is missing entirely, the click handler is broken — open `dom/<latest>.html` and look at the button. If the POST returned 4xx, look at the body in `network/responses.jsonl`. If an exception fired, the stack frame in `console/exceptions.jsonl` points at the file and line.
    
    ## Example 2: Audit every 4xx/5xx and every third-party request in a run
    
    **User says**: "Are we leaking any data to third parties on this page? Show me every cross-origin request."
    
    ```bash
    node scripts/start-capture.mjs 9222 audit
    browse open https://your-site.example --cdp 9222
    # ...interact with the page...
    node scripts/stop-capture.mjs audit
    node scripts/bisect-cdp.mjs audit
    ```
    
    Queries:
    
    ```bash
    cd .o11y/audit
    
    # Top hosts and counts
    jq -r '.params.request.url' cdp/network/requests.jsonl \
      | awk -F/ '{print $3}' | sort | uniq -c | sort -rn
    
    # Everything not on your-site.example
    jq -r 'select(.params.request.url | test("your-site\\.example") | not)
           | .params.request.url' cdp/network/requests.jsonl | sort -u
    
    # All non-2xx responses with their initiator
    jq -c 'select(.params.response.status >= 400 and .params.response.status < 600)
           | {status: .params.response.status,
              url: .params.response.url,
              mime: .params.response.mimeType}' cdp/network/responses.jsonl
    ```
    
    ## Example 3: Find where the page got stuck
    
    **User says**: "The page hangs after I click Continue. It just sits there."
    
    ```bash
    node scripts/start-capture.mjs 9222 hang
    browse open https://example.com/checkout --cdp 9222
    browse click @0-12   # Continue button
    sleep 30             # let the hang play out
    node scripts/stop-capture.mjs hang
    node scripts/bisect-cdp.mjs hang
    ```
    
    Queries:
    
    ```bash
    cd .o11y/hang
    
    # Last navigation that completed
    jq -r '.params.frame.url' cdp/page/navigations.jsonl | tail
    
    # Pending requests: requestWillBeSent without a corresponding loadingFinished/Failed
    jq -s '
      ([.[0][].params.requestId] - [.[1][].params.requestId] - [.[2][].params.requestId]) as $pending |
      .[0] | map(select(.params.requestId | IN($pending[]))) | map(.params.request.url)
    ' cdp/network/requests.jsonl cdp/network/finished.jsonl cdp/network/failed.jsonl
    
    # Any JS dialog blocking?
    cat cdp/page/dialogs.jsonl
    
    # Look at the last screenshot to see what the user is staring at
    ls screenshots/ | tail -1
    ```
    
    The pending-requests query is the smoking gun: if a fetch never finishes, the page is waiting on it.
    
    ## Example 4: Reproduce a JS exception from production and locate the source
    
    **User says**: "Production logs say `TypeError: Cannot read properties of undefined (reading 'foo')` on `/dashboard`. I can't reproduce locally."
    
    ```bash
    # Use Browserbase remote so the run uses the same Browserbase Identity / Verified browser setup as prod.
    export BROWSERBASE_API_KEY=...
    SESSION=$(browse cloud sessions create --keep-alive --timeout 600)
    SID=$(echo "$SESSION" | jq -r .id)
    URL=$(echo "$SESSION" | jq -r .connectUrl)
    
    BROWSE_NAME=prod-repro-browser
    browse open https://app.example.com/dashboard --cdp "$URL" --session "$BROWSE_NAME"
    node scripts/start-capture.mjs "$URL" prod-repro
    
    # Drive whatever flow is suspected. The daemon caches the remote target,
    # so subsequent commands only need --session to pick the right daemon.
    browse click @0-5 --session "$BROWSE_NAME"
    browse type 'search query' --session "$BROWSE_NAME"
    browse press Enter --session "$BROWSE_NAME"
    sleep 5
    
    node scripts/stop-capture.mjs prod-repro
    node scripts/bisect-cdp.mjs prod-repro
    browse cloud sessions update "$SID" --status REQUEST_RELEASE
    ```
    
    Queries:
    
    ```bash
    cd .o11y/prod-repro
    
    # Any matching exceptions?
    jq -c '.params.exceptionDetails | select(.text | test("Cannot read properties of undefined"))
           | {text, url, line: .lineNumber, col: .columnNumber, stack: .stackTrace.callFrames[0:5]}' \
      cdp/console/exceptions.jsonl
    
    # Get the Runtime.exceptionThrown timestamp and find the screenshot/dom right before it
    EVT_MS=$(jq -r 'select(.params.exceptionDetails.text | test("Cannot read"))
                    | .params.timestamp' cdp/console/exceptions.jsonl | head -1)
    EVT_ISO=$(date -u -r $((${EVT_MS%.*}/1000)) +%Y%m%dT%H%M%SZ)
    ls screenshots/ | sort | awk -v t="$EVT_ISO" '$0 < t { keep=$0 } END { print keep }'
    
    # What network requests were in flight when it threw?
    jq -c --argjson t "$EVT_MS" '
      select(.params.timestamp <= $t/1000 and .params.timestamp > ($t/1000 - 5))
      | {ts: .params.timestamp, url: .params.request.url}
    ' cdp/network/requests.jsonl
    ```
    
    The stack frame points at the prod JS file + line; the screenshot shows what the user was looking at; the network query shows what XHRs were in flight in the 5 seconds before the throw.
    
    ## Example 5: Attach a trace to a Browserbase session that is already running
    
    **User says**: "Our staging worker is running a Browserbase session right now and the customer says it's stuck. Can you attach without killing it?"
    
    ```bash
    export BROWSERBASE_API_KEY=...
    
    # Find running sessions (no --status flag, so filter client-side).
    browse cloud sessions list | jq -r '.[] | select(.status == "RUNNING") | "\(.id)\t\(.region)\t\(.startedAt)"'
    
    # Attach the tracer to the session you care about.
    SID=<session-id-from-above>
    node scripts/bb-capture.mjs "$SID" stuck-debug 2
    
    # Open the live debugger URL in your browser to watch interactively.
    open "$(jq -r '.browserbase.debugger_url' .o11y/stuck-debug/manifest.json)"
    
    # Let it record for a minute or two while the worker does whatever it does.
    sleep 120
    
    # Stop the tracer and pull artifacts. NO --release: the worker still owns this session.
    node scripts/stop-capture.mjs stuck-debug
    node scripts/bisect-cdp.mjs stuck-debug
    node scripts/bb-finalize.mjs stuck-debug
    ```
    
    Then look for the smoking gun:
    
    ```bash
    cd .o11y/stuck-debug
    
    # Pending requests that never finished — the most common cause of "stuck"
    jq -s '
      ([.[0][].params.requestId] - [.[1][].params.requestId] - [.[2][].params.requestId]) as $pending |
      .[0] | map(select(.params.requestId | IN($pending[])))
           | map({age_s: (now - .params.timestamp), url: .params.request.url})
    ' cdp/network/requests.jsonl cdp/network/finished.jsonl cdp/network/failed.jsonl
    
    # Last DOMContentLoaded / load on the top frame — when did the page actually settle?
    jq -c 'select(.params.frameId == .params.loaderId or .params.frameId != null)
           | select(.params.name == "DOMContentLoaded" or .params.name == "load")
           | {name: .params.name, ts: .params.timestamp}' cdp/page/lifecycle.jsonl | tail
    
    # How much has Browserbase billed in proxy bytes so far?
    jq '.proxyBytes' browserbase/session.json
    ```
    
    **Key idea**: `bb-capture.mjs <session-id>` (no `--new`) only adds an tracer; it never sends action commands. The production worker keeps running. `bb-finalize.mjs` *without* `--release` leaves the session alive when you're done.
    
  • LICENSE.txt 1 KB
    MIT License
    
    Copyright (c) 2026 Browserbase, Inc.
    
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    
    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.
    
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
    
  • package.json 91 B
    {
      "name": "browser-trace",
      "version": "0.1.0",
      "private": true,
      "type": "module"
    }
    
  • REFERENCE.md 20.1 KB
    # 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](#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:
    
    | Subcommand                                | Output                                                         |
    | ----------------------------------------- | -------------------------------------------------------------- |
    | `list`                                    | one-line page table (`pid`, `events`, `duration`, `url`)       |
    | `summary`                                 | full `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]` |
    | `timeline`                                | ordered 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
    
    | File                                    | CDP method                       | What's in it                                                 |
    | --------------------------------------- | -------------------------------- | ------------------------------------------------------------ |
    | `cdp/network/requests.jsonl`            | `Network.requestWillBeSent`      | every outgoing request: url, method, headers, postData, requestId |
    | `cdp/network/responses.jsonl`           | `Network.responseReceived`       | response status, headers, mimeType, remoteIPAddress, fromDiskCache |
    | `cdp/network/finished.jsonl`            | `Network.loadingFinished`        | byte count + timestamp on success                            |
    | `cdp/network/failed.jsonl`              | `Network.loadingFailed`          | errorText (e.g. `net::ERR_ABORTED`), `canceled`              |
    | `cdp/network/websocket.jsonl`           | `Network.webSocket*`             | every WebSocket lifecycle event                              |
    | `cdp/console/logs.jsonl`                | `Runtime.consoleAPICalled`       | `console.log/info/warn/error` with `args[]`                  |
    | `cdp/console/exceptions.jsonl`          | `Runtime.exceptionThrown`        | unhandled JS errors with stack                               |
    | `cdp/runtime/all.jsonl`                 | `Runtime.*`                      | execution-context create/destroy, binding calls, etc.        |
    | `cdp/log/entries.jsonl`                 | `Log.entryAdded`                 | browser-level warnings (CSP, deprecation, mixed content)     |
    | `cdp/page/navigations.jsonl`            | `Page.frameNavigated`            | each top-level + iframe navigation                           |
    | `cdp/page/lifecycle.jsonl`              | `Page.lifecycleEvent`            | per-navigation milestones: `init`, `commit`, `DOMContentLoaded`, `load`, `firstPaint`, `firstContentfulPaint`, `firstMeaningfulPaint`, `networkAlmostIdle`, `networkIdle` |
    | `cdp/page/frames.jsonl`                 | `Page.frame*`                    | frame attached/detached/started/stoppedLoading                |
    | `cdp/page/dialogs.jsonl`                | `Page.javascriptDialog*`         | alert / confirm / prompt / beforeunload                      |
    | `cdp/page/all.jsonl`                    | `Page.*`                         | catch-all for everything Page emits                          |
    | `cdp/dom/all.jsonl`                     | `DOM.*`                          | tree mutations *(only populated if `O11Y_DOMAINS` adds `DOM`)* |
    | `cdp/target/attached.jsonl`             | `Target.attachedToTarget`        | each new page/iframe target attached to the tracer         |
    | `cdp/target/detached.jsonl`             | `Target.detachedFromTarget`      | each 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
    
    ```bash
    # 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
    
    ```bash
    # 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
    
    ```bash
    # 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](https://github.com/browserbase/stagehand/pull/2056); on older builds `lifecycle.jsonl` will be empty.
    
    ```bash
    # 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:
    
    ```bash
    # 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:
    
    ```bash
    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.
    
    ```bash
    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
    
    ```bash
    # 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
    
    | Var                | Default                                | Effect                                                       |
    | ------------------ | -------------------------------------- | ------------------------------------------------------------ |
    | `O11Y_ROOT`        | `.o11y`                                | base directory under which `<run-id>/` is created             |
    | `O11Y_DOMAINS`     | `Network Console Runtime Log Page`     | space-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
    
    | Symptom                                        | Likely cause                                                  | Fix                                                          |
    | ---------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------ |
    | `browse cdp exited immediately`                | unreachable target / completed Browserbase session             | verify 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 command                          | `npm install -g browse@latest` (or the alpha tag if needed)   |
    | Browserbase session ends as soon as tracer connects | tracer was the only client; no automation attached          | create with `--keep-alive`, attach automation with `browse open --cdp <connectUrl> --session <name>` first   |
    | `index.jsonl` shows `"url": ""`                 | sampler `browse get url` failed transiently                   | benign; happens during navigation transitions                 |
    | Screenshots empty / huge / inconsistent sizes  | viewport not set                                              | `browse viewport 1920 1080 --cdp <target>` once before capture |
    | `raw.ndjson` grows but bisect buckets empty    | wrong domains; e.g. you wanted DOM but didn't enable it       | `O11Y_DOMAINS="Network Console Runtime Log Page DOM" bash start-capture.mjs ...` |
    | Loop process leaks after crash                  | `stop-capture.mjs` not run                                     | `pkill -f snapshot-loop.mjs`; PID files in `<run-dir>` are stale  |
    
  • SKILL.md 14.7 KB
    ---
    name: browser-trace
    description: 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.
    compatibility: "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."
    license: MIT
    allowed-tools: Bash, Read, Grep
    ---
    
    # 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
    
    ```bash
    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:
    
    ```bash
    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
    
    ```bash
    # 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.
    
    ```bash
    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`:
    
    ```bash
    # 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).
    
    ```json
    {
      "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:
    
    ```bash
    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
    
    ```bash
    # 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](REFERENCE.md).
    For example debug runs, see [EXAMPLES.md](EXAMPLES.md).
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related