Skip to content

feat(traces): add output bounding flags to traces get#64

Open
dark-sorceror wants to merge 8 commits into
mainfrom
feat/traces-get-bounds
Open

feat(traces): add output bounding flags to traces get#64
dark-sorceror wants to merge 8 commits into
mainfrom
feat/traces-get-bounds

Conversation

@dark-sorceror

@dark-sorceror dark-sorceror commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #57

traces get was unbounded: --json serialized the entire trace — every span — as one physical line, and the human tree recursively printed every span with no depth or count cap. On a several-thousand-span agent trace that's a multi-megabyte single line, which is a context bomb for the CLI's core consumer: a coding agent piping traces get --json into its own context.

This adds four opt-in flags to bound the output — --max-spans, --depth, --errors-only, --output jsonl — all default-off, so default output stays byte-identical to today. Field selection (the backend's fields projection) is deliberately deferred to #51's --fields.

How it works

  • --max-spans <n> — caps emitted spans. JSON gains spans_truncated: { shown, total }; the human tree appends a … N more spans line, where N is the true remainder.
  • --depth <n> — caps tree depth (roots = 1). Spans deeper than the cap are dropped from both the human tree and the JSON/JSONL body; the human tree marks each elided subtree with … N deeper spans hidden.
  • --errors-only — keeps only error-status spans plus their full ancestor chains, dropping unrelated branches. When nothing matches, that's surfaced explicitly rather than as a silent empty list: errors_only_no_matches: true in JSON, (no error spans in this trace) in the human tree.
  • --output jsonl — streams a header line (the trace minus its spans array, plus finding and any truncation markers) followed by one JSON line per span, written incrementally so a downstream head/grep can stop early. Implies machine output whether or not --json is also set, and the global EPIPE handler keeps it clean under a closed pipe.

The flags compose in a fixed order — --errors-only first, then --depth, then --max-spans — applied once in boundSpans() so every emit path (human tree, JSON, JSONL) reads from the same result and can't diverge. The --max-spans truncation total always reflects the post-filter set (e.g. --errors-only --max-spans 2 on 5 error-path spans reports total: 5, not the trace's full span count).

Truncation selects spans in tree traversal order — the same depth-first, sibling-by-start-time order the human tree prints in (treeOrder()), built on the same shared buildTree() the renderer and depth/errors-only filters use — so the human tree and the JSON/JSONL body always keep the same spans, even when array order and tree order disagree (e.g. a later-starting sibling listed earlier in the backend array) or the span graph is malformed (orphans, self-parents, parent cycles). The kept spans are then emitted in the backend's original array order, so the JSON body stays array-stable under truncation.

# Just the failures and how they were reached, as JSON.
traceroot traces get <trace-id> --errors-only --json | jq '.spans[].name'

# Stream spans and grep for errors without buffering the whole trace.
traceroot traces get <trace-id> --output jsonl | grep '"status":"error"'

What's included

src/render/tree.ts

  • buildTree() — shared roots/children/sibling-order construction, used by renderTree, treeOrder, and depth computation so they can't drift apart.
  • isErrorStatus() — hoisted out as the single source of truth for error classification (renderer + --errors-only filter).
  • spansWithinDepth() — flat-array depth filter matching the renderer's depth cap, including cycle/orphan recovery.
  • filterErrorsWithAncestors() — keeps error spans plus their ancestor chain.
  • treeOrder() — spans in the renderer's exact traversal order; the shared selection order for --max-spans.
  • renderTree() — new maxDepth/maxSpans options, with … N deeper spans hidden and … N more spans elision markers.

src/commands/traces/get.ts

  • boundSpans() — applies --errors-only--depth--max-spans once, feeding every output path.
  • --max-spans <n>, --depth <n>, --errors-only, --output jsonl flag registration, plus a positiveIntOnce() validator shared in spirit with onceOption.
  • JSONL header/body streaming path in runGet.

Tests

  • tests/render/tree.test.ts — depth cap, max-spans cap, spansWithinDepth, filterErrorsWithAncestors, isErrorStatus, treeOrder, and adversarial cases (cycles, orphans, self-parents) pinning human/JSON agreement.
  • tests/commands/traces-get.test.ts — unit coverage for each flag, flag composition, and tree-order selection consistency (sibling reordering, child-before-parent array order).
  • tests/helpers/traceServer.mjs — throwaway local stub server (own process, since contract tests block the event loop on spawnSync) serving a configurable-size trace.
  • tests/output.contract.test.ts — contract tests against the spawned binary + stub server: jsonl validity over a 2000-span trace, truncation marker totals, and a real | head -1 EPIPE check.

README.md

  • Documents all four flags, their composition order, and the tree-order truncation guarantee, with example commands.

Test plan

  • 626/626 vitest tests pass, including contract tests against the spawned local stub server (tests/helpers/traceServer.mjs) over a 2000-span trace, and a real traces get ... --output jsonl | head -1 EPIPE check
  • Adversarial renderer tests (parent cycles, orphans, self-parent spans) confirming the human tree and the JSON/JSONL span selection always agree
  • Typecheck, lint, and format all clean
  • Live e2e against app.traceroot.ai on a 123-span trace: default output unchanged — 123 spans, no marker keys
  • Live e2e: --max-spans 10 → 10 spans + spans_truncated: {shown: 10, total: 123} + … 113 more spans in the human tree
  • Live e2e: --depth 1 → root only + … 122 deeper spans hidden
  • Live e2e: --errors-only on an error trace → 2 error spans + 1 ancestor; on a clean trace → explicit errors_only_no_matches / (no error spans in this trace)
  • Live e2e: --output jsonl → 124 lines, all independently JSON-valid
  • Live e2e: size reduction from 78,320 bytes (full trace) to 14,912 bytes (--max-spans 10) / 9,395 bytes (--errors-only)
  • Live e2e: --max-spans 0 and --output json both rejected as usage errors

Summary by cubic

Adds opt-in bounds to traces get to cap span count, depth, or show errors-only, plus a --output jsonl streaming mode; default output stays unchanged. Also ensures depth-elision markers remain visible when a span cap is applied.

  • New Features

    • --max-spans <n>: cap span count; JSON adds spans_truncated { shown, total }; human tree shows … N more spans.
    • --depth <n>: cap tree depth (roots = 1); deeper spans are dropped; human tree shows … N deeper spans hidden.
    • --errors-only: keep error spans and their ancestors; explicit no-match indicators in human and JSON (errors_only_no_matches: true).
    • --output jsonl: stream a header (trace without spans, plus finding and markers) then one span per line; safe for head/grep; implies machine output.
    • Flags compose in order: errors-onlydepthmax-spans. Truncation selects by tree traversal order so human and JSON/JSONL keep the same spans; JSON/JSONL emit kept spans in original backend array order.
  • Refactors

    • Shared tree index/traversal (buildTree, treeOrder), depth filter (spansWithinDepth), and error check (isErrorStatus) keep human and JSON/JSONL in sync, including cycles/orphans.
    • Keeps depth elision markers visible at the span cap boundary when combining --depth and --max-spans.
    • Added JSONL streaming path and input validation; documented and covered by unit + contract tests (including EPIPE/head checks).

Written for commit c0b42bb. Summary will update on new commits.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/render/tree.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

traces get output is unbounded — no span cap, depth limit, error filter, or field selection

1 participant