feat(dashboard): filter usage views on several models, users, or keys - #521
Conversation
… or keys
The Usage page's User, Model, and API key pickers committed one value each, so
a spend question that is really a comparison ("these two models", "this team's
three keys") could only be asked one entity at a time, and the chart could never
show them side by side.
The three entity filters are now repeatable on the analytics endpoints
(/v1/usage/summary, /v1/usage/series, /v1/usage/summary.csv): several values
match any of them, capped at 50 per call so a caller cannot post an unbounded IN
list. A single value stays an equality test, so every existing caller and the
existing wire form are unaffected.
The pickers accumulate values, each pick becomes its own removable chip, and the
request log's own filters stay single-value: its bulk delete / set-price
selection is expressed one value per dimension, and widening it there would let
a bulk op reach past the rows the operator was shown. A drill-down therefore
carries an entity filter only while it holds a single value.
Fixes #489
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 40 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (24)
WalkthroughChangesUsage filter multiselect
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
⚔️ Resolve merge conflicts 💡
🧪 Generate unit tests (beta)
✨ Simplify code
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 |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
web/src/api/types.ts (1)
560-568: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winKeep raw activity filters scalar in the type system.
UsageFiltersnow permits arrays for all consumers.useUsageLogsanduseUsageCountaccept this type, but/v1/usageand/v1/usage/countremain single-value endpoints. A future caller can therefore send repeated entity parameters to a scalar endpoint.Define an analytics-specific multi-value filter type. Keep the raw list and count hook inputs scalar. This preserves the bulk-operation scope contract at compile time.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/api/types.ts` around lines 560 - 568, Split the shared UsageFilters definition into scalar raw activity filters and an analytics-specific type that permits string arrays for model, user_id, and api_key_id. Update useUsageLogs and useUsageCount to accept the scalar type, while analytics consumers use the multi-value type; preserve endpoint and provider as scalar fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/gateway/static/dashboard/assets/index-Dit1BUBh.js`:
- Line 2: Cap multi-select usage filters for user_id, model, and api_key_id at
the backend limit of 50, matching _MAX_FILTER_VALUES. Update the relevant
usage-filter combobox selection handler to prevent selections beyond the cap or
display clear client-side feedback, and ensure query construction never includes
more than 50 values.
In `@tests/integration/test_usage_summary.py`:
- Line 20: Update the usage summary tests to use a test-local limit value of 50
instead of importing the private _MAX_FILTER_VALUES constant. Extend the
coverage around the existing usage summary limit tests to assert that the CSV
endpoint enforces the same 50-value limit contract.
In `@web/src/components/ui.test.tsx`:
- Around line 233-244: Update the test case around the Harness combobox to also
assert that the matching option “claude-sonnet-5” remains visible after typing
“claude”, while preserving the existing assertion that “gpt-5.6” is hidden and
onChange is not called.
In `@web/src/components/ui.tsx`:
- Around line 554-615: Update FilterMultiComboBox to accept a maxSelections prop
defaulting to 50, prevent onSelectionChange from adding values once
values.length reaches that limit, and display a concise limit message when the
limit is reached. Keep maxVisible limited to suggestion rendering only and
preserve existing selection behavior below the limit.
---
Nitpick comments:
In `@web/src/api/types.ts`:
- Around line 560-568: Split the shared UsageFilters definition into scalar raw
activity filters and an analytics-specific type that permits string arrays for
model, user_id, and api_key_id. Update useUsageLogs and useUsageCount to accept
the scalar type, while analytics consumers use the multi-value type; preserve
endpoint and provider as scalar fields.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 98c0e4fc-4766-4638-9fbd-4223ce9248d9
⛔ Files ignored due to path filters (1)
docs/public/openapi.jsonis excluded by!docs/public/openapi.json
📒 Files selected for processing (33)
docs/dashboard.mddocs/public/otari.postman_collection.jsonsrc/gateway/api/routes/usage.pysrc/gateway/static/dashboard/assets/ActivityPage-BbEENQgu.jssrc/gateway/static/dashboard/assets/BudgetsPage-DGkl3NSe.jssrc/gateway/static/dashboard/assets/ConfirmDialog-Dt_8xaSM.jssrc/gateway/static/dashboard/assets/DocsPage-AglHrVWY.jssrc/gateway/static/dashboard/assets/FilterChips-C0emi5Kg.jssrc/gateway/static/dashboard/assets/FilterChips-CTE3I1G3.jssrc/gateway/static/dashboard/assets/KeysPage-CEc7g4XL.jssrc/gateway/static/dashboard/assets/ModelScopeControl-BhMRwgM-.jssrc/gateway/static/dashboard/assets/ModelsPage-299cCHBM.jssrc/gateway/static/dashboard/assets/OverviewPage-CHysnnsw.jssrc/gateway/static/dashboard/assets/ProvidersPage-BPyKQR5x.jssrc/gateway/static/dashboard/assets/RoutingPage-2qgzgln4.jssrc/gateway/static/dashboard/assets/SettingsPage-CLw9HtK0.jssrc/gateway/static/dashboard/assets/TablePagination-BynkRKqB.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-CSbQtPkh.jssrc/gateway/static/dashboard/assets/UsagePage-BTnJt3lF.jssrc/gateway/static/dashboard/assets/UsagePage-tyubYvXE.jssrc/gateway/static/dashboard/assets/UsersPage-C_yR1ElB.jssrc/gateway/static/dashboard/assets/index-D-R1nuKP.jssrc/gateway/static/dashboard/assets/index-Dit1BUBh.jssrc/gateway/static/dashboard/index.htmltests/integration/test_usage_summary.pyweb/src/api/hooks.tsweb/src/api/types.tsweb/src/components/FilterChips.tsxweb/src/components/ui.test.tsxweb/src/components/ui.tsxweb/src/pages/ActivityPage.tsxweb/src/pages/UsagePage.test.tsxweb/src/pages/UsagePage.tsx
💤 Files with no reviewable changes (3)
- src/gateway/static/dashboard/assets/FilterChips-CTE3I1G3.js
- src/gateway/static/dashboard/assets/index-D-R1nuKP.js
- src/gateway/static/dashboard/assets/UsagePage-tyubYvXE.js
…s accept Review follow-ups on the analytics multiselect. The picker had no ceiling while the endpoints cap a repeatable filter at 50 values: picked options drop out of the list and it refills, so a 51st pick was reachable and failed every query on the page with a 422 the operator cannot act on. At the ceiling the remaining options are now offered but inert and the input reads "50 selected (max)", rather than an empty popover that explains nothing. Also drops a no-op: the Usage page prepended its picked models to the option list, which the picker then filters right back out, and the comment claimed behavior the component contradicts. The repeated one-value-or-many logic in the request-group filter now goes through the same helper as the entity filters, and the API reference names the three repeatable filters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…keys Extends the multiselect to the Activity page, so both usage views ask the same kind of question and a drill-down from the analytics charts arrives intact. What gated this was the bulk path, not the UI. "Select all N matching" counts rows over the filters on screen and re-derives the set server-side from the mutation body, so a body that could not express a multi-value filter would delete or reprice rows the operator was never shown. UsageSelection now takes the same repeatable form as the read filters, and the one-value-or-many helper moved to core so the route and the service build the condition from one place rather than two that could drift. The request log's own read endpoints (/v1/usage, /v1/usage/count) take the three entity filters repeatably too, the URL carries a filter as repeated params, and each value is its own chip. The Model box keeps accepting a name outside its suggestions (Enter commits it), since the suggestion list only covers models with traffic in the window. With the log able to express a set, the analytics drill-down no longer drops a multi-value filter: it carries the whole selection. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-up on the multi-value filters. Widening the bulk delete / set-price selection to accept several values per dimension left it with no ceiling while every read endpoint caps at 50. A count of 51 values is refused (422), yet the delete over those same 51 ran, on an unbounded IN list. That contradicts the agreement the selection body exists to hold: the "N matching" an operator confirms is taken over the read filters, so a set the count cannot express must not be deletable either. Unreachable from the dashboard, whose picker stops at 50, and reachable by any master-key caller. The ceiling is annotated on the list arm alone, not on the str | list[str] field: on the union it would also bound a single value's character length and reject a long provider-qualified model name, which a test now pins. The constant moved beside match_any in core, since a service may not import the API layer and both sides need the same number. Also drops FilterComboBox, whose last caller went away when the request log picked up the multi-value picker; trims URL filter values so a hand-edited whitespace param reads as cleared rather than as a filter on a space; and applies a key's default only when the param is absent, matching how the single-value reader treats present-but-blank. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/api-reference.md`:
- Around line 277-280: Add a `/v1/usage/series` row to the API reference table,
documenting its series response and repeatable `user_id`, `model`, and
`api_key_id` filters with a maximum of 50 values each and any-match behavior.
Keep the authorization requirement consistent with the related usage endpoints.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 5ac4a5e8-f04d-4aae-bafc-44cac5220c6f
⛔ Files ignored due to path filters (1)
docs/public/openapi.jsonis excluded by!docs/public/openapi.json
📒 Files selected for processing (34)
docs/api-reference.mddocs/dashboard.mddocs/public/otari.postman_collection.jsonsrc/gateway/api/routes/usage.pysrc/gateway/core/sql.pysrc/gateway/services/usage_admin_service.pysrc/gateway/static/dashboard/assets/ActivityPage-BPU2LGjK.jssrc/gateway/static/dashboard/assets/BudgetsPage-DJlQbSqO.jssrc/gateway/static/dashboard/assets/ConfirmDialog-SjNq9mb0.jssrc/gateway/static/dashboard/assets/DocsPage-y0JhnHoz.jssrc/gateway/static/dashboard/assets/KeysPage-Bpm4Jwdc.jssrc/gateway/static/dashboard/assets/ModelScopeControl-C95TdfTG.jssrc/gateway/static/dashboard/assets/ModelsPage-SIJ8rtyI.jssrc/gateway/static/dashboard/assets/OverviewPage-DuVfvos1.jssrc/gateway/static/dashboard/assets/ProvidersPage-CooXsqcL.jssrc/gateway/static/dashboard/assets/RoutingPage-C1MiygDT.jssrc/gateway/static/dashboard/assets/SettingsPage-C-d0gEqT.jssrc/gateway/static/dashboard/assets/TablePagination-C0s9RgHF.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-BOqIdkNT.jssrc/gateway/static/dashboard/assets/UsagePage-DKSiL958.jssrc/gateway/static/dashboard/assets/UsersPage-ClMRPPza.jssrc/gateway/static/dashboard/assets/index-hq5nyILv.jssrc/gateway/static/dashboard/index.htmltests/integration/test_usage_admin.pytests/integration/test_usage_summary.pyweb/src/api/types.tsweb/src/components/ui.test.tsxweb/src/components/ui.tsxweb/src/lib/urlState.test.tsxweb/src/lib/urlState.tsweb/src/pages/ActivityPage.test.tsxweb/src/pages/ActivityPage.tsxweb/src/pages/UsagePage.test.tsxweb/src/pages/UsagePage.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- src/gateway/static/dashboard/index.html
- docs/dashboard.md
- web/src/components/ui.test.tsx
- tests/integration/test_usage_summary.py
/v1/usage/series was missing from the endpoint table while its siblings were all listed, so the two public references disagreed: the Postman collection carries it (generated from the spec) and the reference did not. This PR changes its filters, which is what surfaced the gap. Documents the required group_by, the repeatable entity filters it shares with /summary, the eight-group fold that keeps the stack reconciling with the summary totals, and the 422 an hourly bucket over a too-wide window returns. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…vironment Three notes this PR earned the hard way, plus one stale pointer found beside them. src/gateway/AGENTS.md gains the rule that a bulk usage mutation re-derives its target set from the filter fields in its body, so the read filters and UsageSelection have to agree on which filters exist, what value space each accepts, and what bounds it. Each of those three was violated at least once while building this change, the last one only on the destructive path, so the section says what goes wrong rather than only what the rule is. The root Test Notes now say PostgreSQL is required rather than preferred, and why SQLite is not a fallback despite the URL helper accepting one (the fixtures tear down with DROP TABLE ... CASCADE). It also names the two tests that assert error sanitization through a real outbound provider call, which report a status mismatch in a sandbox with no egress and are not a regression signal. The frontend components table lists the filter-bar primitives, and its "Tabular data" row pointed at components/Table.tsx, which does not exist; the component is DataTable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cs-multiselect # Conflicts: # src/gateway/static/dashboard/assets/ActivityPage-CHnjo06U.js # src/gateway/static/dashboard/assets/BudgetsPage-B9iEC7ec.js # src/gateway/static/dashboard/assets/BudgetsPage-DJlQbSqO.js # src/gateway/static/dashboard/assets/BudgetsPage-H5EC9TjF.js # src/gateway/static/dashboard/assets/ConfirmDialog-DzIVWoi5.js # src/gateway/static/dashboard/assets/ConfirmDialog-SjNq9mb0.js # src/gateway/static/dashboard/assets/ConfirmDialog-mbnZRETP.js # src/gateway/static/dashboard/assets/DocsPage-D53o1bCm.js # src/gateway/static/dashboard/assets/DocsPage-DT-vQq9F.js # src/gateway/static/dashboard/assets/DocsPage-y0JhnHoz.js # src/gateway/static/dashboard/assets/FilterChips-o0JYRTLi.js # src/gateway/static/dashboard/assets/KeysPage-Bpm4Jwdc.js # src/gateway/static/dashboard/assets/KeysPage-CtY-9BqY.js # src/gateway/static/dashboard/assets/KeysPage-fg3Rz_lV.js # src/gateway/static/dashboard/assets/ModelScopeControl-BBYX_HiM.js # src/gateway/static/dashboard/assets/ModelScopeControl-C95TdfTG.js # src/gateway/static/dashboard/assets/ModelScopeControl-dT2wvSYp.js # src/gateway/static/dashboard/assets/ModelsPage-SIJ8rtyI.js # src/gateway/static/dashboard/assets/OverviewPage-0PkW5qfi.js # src/gateway/static/dashboard/assets/OverviewPage-B5D-L-_p.js # src/gateway/static/dashboard/assets/OverviewPage-DuVfvos1.js # src/gateway/static/dashboard/assets/ProvidersPage-CooXsqcL.js # src/gateway/static/dashboard/assets/RoutingPage-C1MiygDT.js # src/gateway/static/dashboard/assets/RoutingPage-CD9-r0aL.js # src/gateway/static/dashboard/assets/RoutingPage-D1os8M2m.js # src/gateway/static/dashboard/assets/SettingsPage-BtoAJ9Y8.js # src/gateway/static/dashboard/assets/SettingsPage-C-d0gEqT.js # src/gateway/static/dashboard/assets/SettingsPage-C2Hp1OPt.js # src/gateway/static/dashboard/assets/TablePagination-BEmYAlSB.js # src/gateway/static/dashboard/assets/TablePagination-Bnlr8FbD.js # src/gateway/static/dashboard/assets/TablePagination-C0s9RgHF.js # src/gateway/static/dashboard/assets/ToolsGuardrailsPage-BOqIdkNT.js # src/gateway/static/dashboard/assets/ToolsGuardrailsPage-C-E4XKsV.js # src/gateway/static/dashboard/assets/ToolsGuardrailsPage-DNC3Wa-x.js # src/gateway/static/dashboard/assets/UsagePage-pCJIBBUc.js # src/gateway/static/dashboard/assets/UsersPage-Be1Tcz9b.js # src/gateway/static/dashboard/assets/UsersPage-CRNgQ9x1.js # src/gateway/static/dashboard/assets/UsersPage-ClMRPPza.js # src/gateway/static/dashboard/assets/index-BE7E0N2z.js # src/gateway/static/dashboard/index.html
Description
The User, Model, and API key pickers committed one value each, so a spend question that is really a comparison ("these two models", "this team's three keys") could only be asked one entity at a time. Both usage views now take sets.
API.
model,user_id, andapi_key_idare repeatable on every usage endpoint (/v1/usage,/v1/usage/count,/summary,/series,/summary.csv): several values match any of them, capped at 50 per call so a caller cannot post an unbounded IN list. A single value stays an equality test, so the existing wire form and every existing caller are unaffected.Bulk mutations. This is what gated extending the multiselect to the request log. "Select all N matching" counts rows over the filters on screen and re-derives the target set server-side from the mutation body, so a body that could not express a multi-value filter would delete or reprice rows the operator was never shown.
UsageSelectionnow takes the same repeatable form, and the one-value-or-many helper lives incore/sql.pyso the route and the service build the condition from one place instead of two that could drift.Dashboard. A new
FilterMultiComboBoxaccumulates picks, keeps the list open on what remains, and stops at 50 with the remaining options inert (a 51st pick would 422 every query on the page). Each value is its own chip whose ✕ clears just it. Activity carries a filter as repeated URL params, and its Model box still accepts a name outside its suggestions (Enter commits it), since those suggestions only cover models with traffic in the window. The analytics drill-down now carries a whole selection into the log rather than dropping it.Generated artifacts refreshed: dashboard bundle, OpenAPI spec, Postman collection.
PR Type
Relevant issues
Fixes #489
Checklist
tests/unit,tests/integration).make lint,make typecheck,make test).uv run python scripts/generate_openapi.py).Test notes:
npm --prefix web test469 passed;uv run pytest2284 passed, 10 skipped. The load-bearing new tests are the destructive ones: a two-model bulk delete removes exactly those two models' imported rows and leaves the third, and the count that sized "all matching" was taken over the same two values. Two pre-existing failures (test_provider_error_does_not_leak_details,test_streaming_creation_error_returns_http_error) expect a 502 from a real provider call and return 404 in a sandbox with no network egress; they fail the same way without this change.Also smoke-tested against a live gateway seeded with 1,140 imported rows over 50 days across four models, four users, and three keys: two-model and two-key unions reconcile exactly with their singles, the grouped series and CSV export honor a multi-value filter, and 50 values pass while 51 return 422.
AI Usage
AI Model/Tool used: Claude Code (Opus 5)
Any additional AI details you'd like to share:
Implemented and tested by Claude Code through back and forth with @njbrake; the scoping decisions are his, including the call to cover the request log in the same PR once the bulk-mutation hazard had a clean fix.
NOTE:
When responding to reviewer questions, please respond yourself rather than copy/pasting reviewer comments into an AI and pasting back its answer. We want to discuss with you, not your AI :)
Summary
Technical notes
model,user_id, andapi_key_idparameters.match_anyfiltering and URL-state support for repeated values.