feat(agent-sessions): Overview page - #841
Conversation
Two internal reads behind the Agent Sessions overview page, both off `ai_trace_index` alone: `/overview/summary` returns the window's measures, the equal-length window before it, and both as a bucketed series; `/overview/breakdown` returns the busiest keys of one dimension, each measured over both windows. Every number reconciles with the sessions list over the same window, because it is derived the same way: the session key is resolved per trace with `sessionKey`, the filters are the list's per-trace existence tests, and usage runs through `usageReportersExpr` / `sessionUsageSum`, so a wrapper's roll-up, a gateway's mirror of a call and a provider retry each count once. A session is filed under the bucket its first span started in, so the series sums to the totals; the totals are their own un-bucketed read because quantiles do not merge. The breakdown keys a row by the value the span itself carries and nets inside that key, so a session that used two models is a session under each while its tokens are charged to the call that reported them. `model` reads model calls, `tool` reads tool calls, and a span that names no value keys under `''`. `sessionPricedLlmCalls` joins the shared span columns: coverage for a cost that only exists where the instrumentation reported one.
A model call's failures and its volume are different populations, and the LLM error rate was dividing one by the other. `erroredLlmCalls` is a raw `sumIf` over the model-call SPANS — a failure cannot be netted, the index carries no error flag into the reporters — while `llmCalls` is the netted volume, so a gateway's mirror of a failed call is two failures of one call and the rate passes 100%. The measures carry `llmCallSpans` now, the same spans counted, and the rate is `erroredLlmCalls / llmCallSpans`. The e2e seeds the mirrored call and the call it mirrors as failed and pins all three numbers: three failures over five spans, which net to four calls, and two over two under the model they belong to. The comparison window is `[start - length, start)`. Its upper bound is exclusive now, so the second where the two windows meet is measured in the caller's window alone rather than in both. `AiOverviewBreakdownRow` documents which measures a row can mean under which dimension, since `model` and `tool` restrict the population to their own spans and the rest of the columns are structurally zero. The breakdown's `limit` keeps the one cap the request contract enforces instead of clamping again behind it.
`/overview/model-mix` returns the window's model-call SPANS split by model, bucket by bucket — the band chart beside the tiles. It takes the summary's request shape (the window, a bucket width, the same six filters and `hasErrors`, now one shared bag) and answers the current window alone: the chart has no comparison band. The population is the raw one, not the netted one. `llmCallSpans` is what the tiles already report and what the LLM error rate divides by, so a gateway's mirror of a call is a band under the model it names, twice; netting would charge a mirrored call to one model and leave the bands disagreeing with the rate above them. A call whose instrumentation named no model has no share of a model mix and is left out, which is the only difference between this read's total and the tile's. A span is filed under the bucket its own timestamp falls in — the rows are spans, so there is no session to keep whole — while sessions are selected the way every other overview read selects them, so the mix describes the sessions the tiles measure. `LIMIT AI_OVERVIEW_MODEL_MIX_MAX_ROWS` is a blow-up guard and not a top-N: the client folds the minor models into an "other" band and needs every model of every bucket to do it.
…adder The priced share divided the netted priced calls by the raw span count, so a fully priced window read as the netting factor rather than 100%. The window label came from the page's default preset, which the resolver ignores once both endpoints are in the URL — an absolute range was labelled "prev 7d" whatever its length. The grid asked for ~100 buckets, which nine ~104px plots cannot separate, at widths like 105 minutes an axis had to call "2h". It now snaps to a ladder a reader recognises: a day at 1h, a week at 6h, a month at 1d. Also: the bucketed input mirrors the domain's `BucketSeconds`, the active tab merges its search instead of replacing it, and the two per-call duration quantiles leave the view model, which never rendered them.
The nine small multiples reserved a fixed 32px gutter, so any y-axis label wider than four characters was drawn off the canvas's left edge and lost its leading character: $0.50 read as 0.50, 150.0K as 50.0K. The gutter is now measured from the widest top-tick label in the grid and shared by all nine, so the plots still line up column to column. Durations on the board went through formatLatency (42.00s, 2.5min) while the Sessions list reads them in clock units. The page's own wrapper now delegates to formatSessionDuration above a minute and keeps a tenth of a second below one, and the STARTED column matches the list's relative-time rendering.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (45)
Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughThis pull request adds an AI sessions overview feature. It introduces warehouse queries and internal API endpoints, web-side decoding and analytics, a new ChangesAI sessions overview
Priority: ➖ Normal Estimated code review effort: 5 (Critical) | ~120 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant Browser
participant OverviewRoute
participant useAgentOverview
participant InternalAPI
participant ClickHouse
Browser->>OverviewRoute: Open /agent-sessions/overview
OverviewRoute->>useAgentOverview: Resolve window and search
useAgentOverview->>InternalAPI: Read summary, breakdowns, model mix, top sessions
InternalAPI->>ClickHouse: Run overview queries
ClickHouse-->>InternalAPI: Return aggregates and series
InternalAPI-->>useAgentOverview: Return section results
useAgentOverview-->>OverviewRoute: Return overview data inputs
OverviewRoute-->>Browser: Render AgentOverviewView
Merge Risk: ⚪ Minimal · up to This change adds a new, feature-flagged Agent Sessions Overview page plus three read-only internal endpoints over the existing session index. No persisted data, permissions, or existing pages change behavior, and the new numbers are covered by tests that reconcile them with the Sessions list. No merge-blocking risk remains. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 78.72% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 94 functions across 44 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- `withinWindow` takes a `CH.Expr<string>`: the index's `Timestamp` is the string flavour every Maple warehouse timestamp is, which is what `param.dateTimeString` compares against. SQL is unchanged. - the facet select normalises the `null` Base UI reports for a cleared selection onto the `undefined` the toolbar spells "no filter" with. - a link into the Sessions list carries `AgentSessionsLinkSearch`, whose arrays are the mutable flavour the route's search schema declares. - the tab strip's `TabLink` takes a `TimeRangeSearch` — an interface has no index signature to satisfy a `Record<string, unknown>`. - drop the unused token band label.
`TinybirdDateTime` is a pattern, so `2026-13-45 99:99:99` passed the contract and reached the route, where the comparison window it derives parsed to NaN and `toISOString` threw — a 500 for a malformed request. The three overview requests now carry the same NaN check the details request already applies, and the route reads the shared `parseWarehouseDateTime`/`formatWarehouseDateTime` rather than local copies of them.
… breakdown scans The overview's `traceKeys` carried a byte-for-byte copy of the list's six counted filters; both now read one exported `sessionFilterConditions`, and the list's SQL is unchanged. Two reads the breakdown was making twice are gone. The failed-session set is built once per window, at the trace level, instead of at every level that filters — and it is summed off the trace rollup rather than a second pass over the index. The key count reads the groups the ranking already forms instead of re-deriving the same aggregation. A service breakdown under `hasErrors` goes from 20 index reads to 15; a `UNION ALL` takes no `WITH`, so the ranking subquery is still spelled out once per measuring branch.
…he model-mix tail Nothing rendered the per-call latency quantiles, and collecting them was an array of every model call of every session carried up two levels — gone from the builder, the row shape, the contract and the web adapter. The model mix ordered by bucket and cut at a row cap, so the cap dropped the NEWEST buckets — the end of the chart — in exactly the org that routes across enough models to reach it. The window's models are ranked once in SQL now and everything past the busiest five is counted under `other`, which bounds a bucket at six rows; the cap stays as a guard the page cannot reach. The client folds the same key, so it keeps working — it just stops plotting `other` twice when the band arrives already folded.
…-sessions parity The ghost series is shifted by a whole number of buckets rather than by the window's raw length: both windows are cut into epoch-aligned bucket starts and the ghost is matched bucket for bucket, so the default calendar-aligned 7d window — a midnight to a now floored to the quarter hour — put every previous point minutes off the axis and drew nothing. The trends axis is built from the current buckets alone, and a plot names a previous-period mark only where one actually landed on it. A window whose comparison ran no sessions has no baseline, so every tile, chart and mover delta is empty there — including the point-valued ones, where a raw subtraction reads 0% to 26% as a 26-point rise. The movers rail is empty with the comparison off and says so instead of reading as "nothing moved"; its coverage block stays. Top sessions prints what the Sessions list prints: every service the session touched, the model through `ModelLabel` over the page's own detection, and one duration formatter for the whole board, so `45.4s` and `45s` can no longer be the same session. Only the open tab's list read runs — the tab moved to the route, which is what issues it — and the view model is built once per resolved read rather than once per render. Cleanups alongside: the breakdown delta is an `overviewDelta` rendered through `deltaToneClass` rather than a hand-rolled threshold; the breakdown lanes read their widths from the column list the header reads; the bucket ladder moved out of the infra chart helpers into `overview-buckets.ts`; and the unread token band shares, bucket seconds and unreachable spec branch are gone.
…names The web adapter's local schemas said they mirrored the overview request's bounds, but omitted the per-value filter cap and the window rule, so a >200-character value or an impossible datetime threw a defect out of the request constructor instead of failing one read. Both bounds now come from the domain, which also refuses an inverted or retention-wide window rather than running it. Every read the board makes now says what happened to it: a failed summary keeps the header, tab strip and toolbar and offers a retry that re-runs the page, a failed breakdown, model mix or top-sessions read draws an error in its own section instead of an empty one, and a failed facets read says so in a line and leaves a set filter clearable.
Summary
/agent-sessions/overview: the high-level trend view for every agent session — volume, cost, tokens, reliability and latency — for a quick health check and as the starting point of a debugging session./internal/ai-sessions/overview/*(summary, breakdown, model-mix) overai_trace_index, reconciled with the Sessions list: the same session key and usage netting, sessions bucketed by their first span so the buckets sum to the totals, and a half-open previous window./lab/agent-overviewwith a healthy and a regression fixture.Notes
erroredLlmCalls / llmCallSpans);llmCallsstays the netted volume the list shows.Verification
ai-overview.clickhouse.e2e.test.ts) and the SQL catalog sweep against a local ClickHouse; the lab page checked at 1440 and 1280.Review round
comparestays a client-side display flag (the previous window is always read); the "Open in Sessions" link lands on the list's own rolling window; nettingIsErrorinto the usage reporter tuple was rejected because it would rewrite every list query.Summary by CodeRabbit
New Features
Bug Fixes