feat(dashboard): rework usage charts with cache-aware diverging bars - #574
Conversation
Restructure the usage page's by-model, by-user-path, and by-label charts into horizontal diverging bar charts: one row per entity, input-side series growing left from zero and output-side series growing right, with an alternative stacked view (everything piling right) and the existing table view behind a three-way toggle. Charts flow two per row; a chart alone in its row stretches to full width. The charts now also surface cache traffic: - Provider prompt-cache reads join the input side as their own series (tokens and, as a read-time estimate priced from current catalog pricing, costs); the Input series becomes paid input (uncached + cache writes). - Locally cached tokens (GoModel response-cache hits) join each side they belong to: cached input on the left, cached output on the right. They are free, so costs mode shows no local series. Backend: the by-model/user-path/label aggregates gain a shared GroupCacheFields block (prompt-cache split, per-side local-cache tokens, estimated cached input cost), folded per row from raw_data in a second streaming pass shared by the SQLite, PostgreSQL, and MongoDB readers — the same pattern the daily series uses. The admin handlers price each group's cached tokens with the existing PricingResolver. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughUsage reporting now aggregates cache-partitioned tokens and cached-input costs across storage backends, exposes them through admin APIs and OpenAPI schemas, and renders them in dashboard charts and tables with chart, stacked, and table views. ChangesCache analytics
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Dashboard
participant UsageHandler
participant UsageReader
participant UsageStore
Dashboard->>UsageHandler: Request model, user-path, or label usage
UsageHandler->>UsageReader: Read grouped usage
UsageReader->>UsageStore: Run cache-stat pass with CacheModeAll
UsageStore-->>UsageReader: Stream cache rows
UsageReader-->>UsageHandler: Merge cache token fields
UsageHandler-->>Dashboard: Add cached-input cost
Dashboard->>Dashboard: Render charts and tables
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 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 |
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
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 `@internal/usage/group_cache_stats_test.go`:
- Around line 159-189: Refactor TestEstimateCachedInputCost into table-driven
subtests using a []struct with a descriptive name, breakdown, resolver, and
expected cost; run each case with t.Run. Preserve all four existing scenarios
and assertions, including nil expectations and the $1.00 result.
🪄 Autofix (Beta)
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: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 88c8ea0b-4627-4a41-882e-4fbb0d9b161b
📒 Files selected for processing (14)
cmd/gomodel/docs/docs.godocs/openapi.jsoninternal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/modules/charts.jsinternal/admin/dashboard/static/js/modules/charts.test.cjsinternal/admin/dashboard/static/js/modules/dashboard-layout.test.cjsinternal/admin/dashboard/templates/page-usage.htmlinternal/admin/handler_usage.gointernal/usage/group_cache_stats.gointernal/usage/group_cache_stats_test.gointernal/usage/reader.gointernal/usage/reader_mongodb.gointernal/usage/reader_postgresql.gointernal/usage/reader_sqlite.go
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Confidence Score: 4/5Cache-only groups, explicit cache modes, and current-price cost splits can produce incorrect chart data.
internal/usage/group_cache_stats.go, the three storage readers, and internal/admin/dashboard/static/js/modules/charts.js
What T-Rex did
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
Q[Usage query] --> A[Aggregate pass]
Q --> C[Cache pass with all modes]
A --> G[Grouped usage rows]
C --> S[Prompt and local cache stats]
G --> M[Attach stats to existing groups]
S --> M
M --> P[Estimate cached input cost]
P --> D[Dashboard charts and tables]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart LR
Q[Usage query] --> A[Aggregate pass]
Q --> C[Cache pass with all modes]
A --> G[Grouped usage rows]
C --> S[Prompt and local cache stats]
G --> M[Attach stats to existing groups]
S --> M
M --> P[Estimate cached input cost]
P --> D[Dashboard charts and tables]
Reviews (1): Last reviewed commit: "feat(dashboard): rework usage charts wit..." | Re-trigger Greptile |
| func applyModelCacheStats(rows []ModelUsage, stats map[string]*GroupCacheStats) { | ||
| for i := range rows { | ||
| rows[i].assign(stats[usageModelGroupKey(rows[i].Model, rows[i].Provider, rows[i].ProviderName)]) | ||
| } |
There was a problem hiding this comment.
The cache fold can only attach data to rows created by the uncached aggregate pass. If a model has only exact or semantic cache hits in the selected period, no aggregate row exists, so its local input and output tokens are discarded and the model is absent from the chart. The user-path and label apply functions have the same gap.
Context Used: CLAUDE.md (source)
There was a problem hiding this comment.
Confirmed and fixed in 4d01b5c: the fold now captures each group's identity from the first folded row, and the apply step materializes synthetic rows (zero provider tokens, nil costs) for groups with only local-cache traffic — covered by TestSQLiteLocalOnlyGroupsMaterializeRows for all three groupings.
| stats, err := r.usageCacheStats(ctx, params, "user_path", nil, modelGroupKeys) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| applyModelCacheStats(result, stats) | ||
|
|
There was a problem hiding this comment.
All Mode Double-Counts Cache Tokens
When an API caller requests cache_mode=all or cache_mode=cached, the aggregate pass includes local-cache tokens in InputTokens and OutputTokens, while this second pass exposes the same tokens again in the additive local-cache fields. Consumers that combine the new series, including the chart accounting this response shape was designed for, report local traffic twice. PostgreSQL and MongoDB use the same two-pass behavior.
Context Used: CLAUDE.md (source)
There was a problem hiding this comment.
Confirmed and fixed in 4d01b5c: the cache-stat pass now runs only for uncached-mode requests (the dashboard's mode); for cache_mode=cached/all the fields stay zero since the local tokens are already inside the aggregate sums (and the provider split would not partition them). Covered by TestSQLiteCacheStatsSkippedOutsideUncachedMode.
| if (costs) return Math.max(0, num(row.input_cost) - num(row.cached_input_cost)); | ||
| const split = num(row.uncached_input_tokens) + num(row.cached_input_tokens) + num(row.cache_write_input_tokens); | ||
| return split > 0 ? num(row.uncached_input_tokens) + num(row.cache_write_input_tokens) : num(row.input_tokens); | ||
| }; | ||
| const outputOf = (row) => num(costs ? row.output_cost : row.output_tokens); | ||
| const promptOf = (row) => num(costs ? row.cached_input_cost : row.cached_input_tokens); |
There was a problem hiding this comment.
Estimated Split Exceeds Recorded Cost
cached_input_cost uses current catalog pricing, but input_cost is the recorded historical charge. After a cached-input price increase, the estimate can exceed the recorded input cost; this clamps paid input to zero but still displays the full estimate as prompt-cached cost, so the chart total exceeds both the recorded input and total cost.
Context Used: CLAUDE.md (source)
Artifacts
Repro: targeted Node test exercising the real dashboard chart data path
- Contains supporting evidence from the run (text/javascript; charset=utf-8).
Repro: verbose runtime output showing the inconsistent chart series and totals
- Keeps the command output available without making the summary code-heavy.
There was a problem hiding this comment.
Confirmed and fixed in 4d01b5c: the costs-mode cached segment is now capped at the recorded input cost, so the two input-side segments always sum to it and the bar never exceeds recorded totals. Covered by a new chart test with an over-estimate fixture.
Three confirmed review findings on #574: - Local-only groups were dropped: a model/user path/label served entirely from the local response cache produced no uncached aggregate row, so its fold stats were discarded. The apply step now materializes synthetic rows (zero provider tokens, nil costs) from the fold's captured group identity; synthetic label rows count their local-cache requests. - cache_mode=all/cached double-counted local tokens: the aggregate sums already include local-cache rows in those modes while the additive local_cached_* fields repeated them. The cache-stat pass now runs only for uncached-mode requests (the dashboard's mode) and the fields stay zero otherwise, documented on the reader helpers. - The estimated cached-input cost (current prices) could exceed the recorded input cost, making chart totals exceed recorded totals. The costs-mode cached segment is now capped at the recorded input cost, so the two input-side segments always sum to it. Also refactors TestEstimateCachedInputCost into table-driven subtests per coding guidelines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the Tokens/Costs switch from the usage page header into the sticky bar it shares with the date picker, so the mode stays reachable while scrolling the charts and request log — it changes the meaning of everything on the page, like the date range next to it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
internal/usage/reader_postgresql.go (1)
120-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd PostgreSQL-specific tests for the cache-stats second pass.
Codecov flagged this file as missing coverage on the changed cache-stats lines. SQLite already has dedicated coverage for the equivalent behavior (
TestSQLiteLocalOnlyGroupsMaterializeRows,TestSQLiteCacheStatsSkippedOutsideUncachedModeingroup_cache_stats_test.go); porting analogous cases here (local-only synthetic rows, mode-skip, and ideally aUserPath-filtered case) would close the gap and guard this new code path.🤖 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 `@internal/usage/reader_postgresql.go` around lines 120 - 146, Add PostgreSQL-specific tests covering the usageCacheStats second pass in PostgreSQLReader: verify local-only groups materialize cache-stat rows, verify the pass is skipped for cached and all cache modes, and add a UserPath-filtered case if supported by the existing test helpers. Follow the analogous SQLite tests and use the PostgreSQL setup and assertions already present in the test suite.internal/usage/reader_mongodb.go (1)
290-351: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winMongoDB cache-stats fold doesn't canonicalize the
UserPathfilter, unlike its ownGetUsageByUserPathmain query.
GetUsageByUserPath(lines 354-393 in this file) deliberately avoids filtering the rawuser_pathfield: it clearsmatchParams.UserPathbefore the$match, adds a canonical_gomodel_user_pathfield via$addFields, and only then matches the subtree regex against that canonical field — with a comment explaining this is needed to match "the same grouped (root-normalized) expression the rows are grouped by."
usageCacheStats(called at line 436 with the original, unmodifiedparams) skips this entirely and callsmongoUsageMatchFilters(params)directly, which matches the rawuser_pathfield. When a caller filtersGetUsageByUserPathbyUserPath, the local-cache fold pass can therefore select a different row set than the main aggregate, producing incorrectLocalCachedInputTokens/LocalCachedOutputTokensand potentially wrong/missing local-only synthetic rows for that dimension.Note
reader_sqlite.goandreader_postgresql.goavoid this exact pitfall by threading auserPathExprparameter through theirusageCacheStats, passing the canonical grouped expression specifically for theGetUsageByUserPathcall site.🐛 Proposed fix: canonicalize the UserPath filter in the fold pass
func (r *MongoDBReader) usageCacheStats(ctx context.Context, params UsageQueryParams, keysFor groupKeysFunc) (map[string]*GroupCacheStats, error) { if normalizeCacheMode(params.CacheMode) != CacheModeUncached { return nil, nil } params.CacheMode = CacheModeAll - matchFilters, err := mongoUsageMatchFilters(params) + matchParams := params + matchParams.UserPath = "" + matchFilters, err := mongoUsageMatchFilters(matchParams) if err != nil { return nil, err } pipeline := bson.A{} if len(matchFilters) > 0 { pipeline = append(pipeline, bson.D{{Key: "$match", Value: matchFilters}}) } + const canonicalUserPathField = "_gomodel_user_path" + pipeline = append(pipeline, bson.D{{Key: "$addFields", Value: bson.D{ + {Key: canonicalUserPathField, Value: mongoUsageGroupedUserPathExpr()}, + }}}) + userPath, err := normalizeUsageUserPathFilter(params.UserPath) + if err != nil { + return nil, err + } + if userPath != "" { + pipeline = append(pipeline, bson.D{{Key: "$match", Value: bson.D{{Key: canonicalUserPathField, Value: bson.D{ + {Key: "$regex", Value: usageUserPathSubtreeRegex(userPath)}, + }}}}}) + } pipeline = append(pipeline, bson.D{{Key: "$project", Value: bson.D{ {Key: "model", Value: 1}, {Key: "provider", Value: 1}, {Key: "provider_name", Value: 1}, {Key: "user_path", Value: 1}, {Key: "labels", Value: 1}, {Key: "cache_type", Value: 1}, {Key: "input_tokens", Value: 1}, {Key: "output_tokens", Value: 1}, {Key: "raw_data", Value: 1}, }}})Want me to open an issue to track this and add a regression test (e.g. a
UserPath-filtered variant of the local-only-groups test) alongside the fix?Also applies to: 436-442
🤖 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 `@internal/usage/reader_mongodb.go` around lines 290 - 351, Update usageCacheStats and its GetUsageByUserPath call path to apply UserPath filtering against the same canonical, root-normalized expression used by the main grouped query. Clear UserPath before mongoUsageMatchFilters, add the canonical user-path field before matching, and preserve raw-field filtering for other callers; ensure cache-fold rows match the aggregate rows and retain the existing local-only grouping behavior.
🤖 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.
Outside diff comments:
In `@internal/usage/reader_mongodb.go`:
- Around line 290-351: Update usageCacheStats and its GetUsageByUserPath call
path to apply UserPath filtering against the same canonical, root-normalized
expression used by the main grouped query. Clear UserPath before
mongoUsageMatchFilters, add the canonical user-path field before matching, and
preserve raw-field filtering for other callers; ensure cache-fold rows match the
aggregate rows and retain the existing local-only grouping behavior.
In `@internal/usage/reader_postgresql.go`:
- Around line 120-146: Add PostgreSQL-specific tests covering the
usageCacheStats second pass in PostgreSQLReader: verify local-only groups
materialize cache-stat rows, verify the pass is skipped for cached and all cache
modes, and add a UserPath-filtered case if supported by the existing test
helpers. Follow the analogous SQLite tests and use the PostgreSQL setup and
assertions already present in the test suite.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: f175b0ae-8759-4a89-ac36-10c1d7b89144
📒 Files selected for processing (7)
internal/admin/dashboard/static/js/modules/charts.jsinternal/admin/dashboard/static/js/modules/charts.test.cjsinternal/usage/group_cache_stats.gointernal/usage/group_cache_stats_test.gointernal/usage/reader_mongodb.gointernal/usage/reader_postgresql.gointernal/usage/reader_sqlite.go
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 `@internal/admin/dashboard/static/js/modules/dashboard-layout.test.cjs`:
- Around line 128-131: Update the sticky-bar assertion in the dashboard layout
test to scope matching within the .sticky-date-range.usage-sticky-controls
container, preventing matches from crossing sibling elements. Replace the greedy
regular-expression approach with a bounded parser or scope-aware assertion while
still verifying the usage-mode-toggle and date-picker content inside that target
container.
🪄 Autofix (Beta)
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: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e012b049-2700-4bc5-a099-0c80dd287d05
📒 Files selected for processing (3)
internal/admin/dashboard/static/css/dashboard.cssinternal/admin/dashboard/static/js/modules/dashboard-layout.test.cjsinternal/admin/dashboard/templates/page-usage.html
| assert.match( | ||
| usage, | ||
| /<div class="sticky-date-range usage-sticky-controls">[\s\S]*class="usage-mode-toggle"[\s\S]*{{template "date-picker" \.}}[\s\S]*<\/div>/, | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Scope the sticky-bar assertion to the target container.
The greedy [\s\S]* portions can cross sibling elements, so the test may pass even if the date picker is moved outside .sticky-date-range.usage-sticky-controls. Use a bounded parser or a scope-aware assertion.
🤖 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 `@internal/admin/dashboard/static/js/modules/dashboard-layout.test.cjs` around
lines 128 - 131, Update the sticky-bar assertion in the dashboard layout test to
scope matching within the .sticky-date-range.usage-sticky-controls container,
preventing matches from crossing sibling elements. Replace the greedy
regular-expression approach with a bounded parser or scope-aware assertion while
still verifying the usage-mode-toggle and date-picker content inside that target
container.
The MongoDB cache-stat fold filtered the raw user_path field while GetUsageByUserPath filters the canonical (trimmed, root-normalized) expression it groups by, so a UserPath-filtered request could fold a different row set than the aggregate it decorates — wrong or missing local-cache figures for padded or blank paths. The fold now mirrors the main query's canonical $addFields + regex match, but only for the user-path call site: the model/label aggregates filter the raw field, and their folds must keep doing the same. Also covers the SQL-backend scan path through the shared pgx-style row fixture (nullable columns, label expansion, local-vs-provider split), closing the coverage gap flagged on the PostgreSQL reader's second pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed the two outside-diff-range findings in 7fd31da: MongoDB fold vs. canonical user-path filtering (Major — confirmed). The fold matched the raw PostgreSQL cache-stats coverage (Trivial). The PG reader's unit tests use fake pgx rows rather than a live database, so the meaningful gap was the shared scan/fold path behind its second pass. Added |
There was a problem hiding this comment.
Pull request overview
This PR reworks the admin dashboard “Usage Analytics” breakdown charts (by model, user path, and label) to use horizontal diverging/stacked bar views and to surface provider prompt-cache and GoModel local response-cache traffic, backed by new per-group cache aggregation fields and cost estimation.
Changes:
- Added shared per-group cache breakdown fields (
GroupCacheFields) to usage aggregate rows and populated them via a second streaming pass across SQLite/PostgreSQL/MongoDB backends. - Estimated prompt-cached input cost at read time in the admin handler using current catalog pricing.
- Reworked dashboard usage charts UI/JS: diverging vs stacked toggle, cache series, “Other” folding, responsive two-per-row layout, and updated JS/CSS/tests/docs.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/usage/reader.go | Adds GroupCacheFields (and pricing key) to model/user-path/label aggregate DTOs. |
| internal/usage/reader_sqlite.go | Runs a second pass in uncached mode to fold per-group cache stats and merges them into grouped results. |
| internal/usage/reader_postgresql.go | Same cache-stats second pass + merge for PostgreSQL. |
| internal/usage/reader_mongodb.go | Adds MongoDB cache-stats fold pass (aggregate + per-row decode) and merges results into grouped outputs. |
| internal/usage/group_cache_stats.go | Implements shared folding/merging logic for per-group cache stats and cached-input cost estimation helper. |
| internal/usage/group_cache_stats_test.go | Adds tests covering folding behavior, filter-following, local-only group materialization, and cached cost estimation. |
| internal/admin/handler_usage.go | Computes cached_input_cost in admin responses using PricingResolver. |
| internal/admin/dashboard/templates/page-usage.html | Updates usage page layout, chart toggles, and table columns to display cache series and new views. |
| internal/admin/dashboard/static/js/modules/dashboard-layout.test.cjs | Updates layout tests for the usage page sticky controls and new view toggle. |
| internal/admin/dashboard/static/js/modules/charts.test.cjs | Extends chart tests for diverging/stacked behavior, cache series, cost split, and “Other” folding. |
| internal/admin/dashboard/static/js/modules/charts.js | Reworks chart data shaping/config to horizontal diverging/stacked bar charts and cache series handling. |
| internal/admin/dashboard/static/css/dashboard.css | Adds sticky usage controls styling and two-per-row usage chart grid layout. |
| docs/openapi.json | Updates OpenAPI schema for new cache-related aggregate fields. |
| cmd/gomodel/docs/docs.go | Updates embedded swagger doc template for new cache-related aggregate fields. |
Files not reviewed (1)
- cmd/gomodel/docs/docs.go: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func applyLabelCacheStats(rows []LabelUsage, stats map[string]*GroupCacheStats) []LabelUsage { | ||
| seen := map[string]bool{} | ||
| for i := range rows { | ||
| seen[rows[i].Label] = true | ||
| rows[i].assign(stats[rows[i].Label]) | ||
| } | ||
| for key, s := range stats { | ||
| if seen[key] || !s.hasLocalTokens() { | ||
| continue | ||
| } | ||
| row := LabelUsage{Label: key, Requests: s.LocalRequests} | ||
| row.assign(s) | ||
| rows = append(rows, row) | ||
| } | ||
| return rows | ||
| } |
| // usageModelGroupKey mirrors the SQL grouping (model, provider, | ||
| // COALESCE(NULLIF(TRIM(provider_name), ”), provider)). |
| <div class="usage-mode-toggle" role="group" aria-label="Usage mode"> | ||
| <button type="button" class="usage-mode-btn" :class="{active: usageMode==='tokens'}" @click="toggleUsageMode('tokens')">Tokens</button> | ||
| <button type="button" class="usage-mode-btn" :class="{active: usageMode==='costs'}" @click="toggleUsageMode('costs')">Costs</button> | ||
| </div> |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
internal/usage/reader_mongodb.go (2)
294-335: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCanonical user-path fold fix looks correct; consider extracting the duplicated filter-stage builder.
The
canonicalUserPathbranch correctly mirrorsGetUsageByUserPath's own match/addFields/match sequence (394-404), so the fold now selects the same row set as the aggregate it decorates — this resolves the described mismatch. However, the addFields+regex-match pipeline construction (325-334) is now duplicated almost verbatim fromGetUsageByUserPath(391-404). Since this exact duplication is what allowed the two pipelines to drift out of sync originally, extracting a shared helper (e.g.,mongoCanonicalUserPathFilterStages(userPath string) []bson.D) would keep both call sites structurally coupled and prevent recurrence.As per coding guidelines,
**/*.go: "Keep the implementation explicit and maintainable rather than relying on clever abstractions" and "isolate provider-specific logic" — a small shared helper here reduces duplication without adding indirection complexity.♻️ Proposed extraction
+func mongoCanonicalUserPathFilterStages(userPath string) []bson.D { + const canonicalUserPathField = "_gomodel_user_path" + if userPath == "" { + return nil + } + return []bson.D{ + {{Key: "$addFields", Value: bson.D{ + {Key: canonicalUserPathField, Value: mongoUsageGroupedUserPathExpr()}, + }}}, + {{Key: "$match", Value: bson.D{{Key: canonicalUserPathField, Value: bson.D{ + {Key: "$regex", Value: usageUserPathSubtreeRegex(userPath)}, + }}}}}, + } +}Then both
GetUsageByUserPathandusageCacheStatscan appendmongoCanonicalUserPathFilterStages(userPath)...instead of repeating the stage construction inline.🤖 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 `@internal/usage/reader_mongodb.go` around lines 294 - 335, Extract the duplicated canonical user-path $addFields and regex $match stage construction into a shared helper such as mongoCanonicalUserPathFilterStages(userPath string) []bson.D. Update both usageCacheStats and GetUsageByUserPath to append the helper’s stages, preserving their existing filtering behavior and stage order.Source: Coding guidelines
294-335: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winAdd MongoDB regression coverage for the canonical user-path fold path.
internal/usage/reader_mongodb_test.gocovers match filters, data filters, search regex, and rewrite savings, but notusageCacheStatswithcanonicalUserPath=trueand a subtree filter. That path still needs a test to keep the fold and aggregate aligned.🤖 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 `@internal/usage/reader_mongodb.go` around lines 294 - 335, Add regression coverage in reader_mongodb_test.go for usageCacheStats with canonicalUserPath=true and a subtree user-path filter. Verify the generated fold pipeline applies the canonical grouped-path expression and subtree match consistently with the aggregate path, preserving alignment between both row sets.
🤖 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.
Outside diff comments:
In `@internal/usage/reader_mongodb.go`:
- Around line 294-335: Extract the duplicated canonical user-path $addFields and
regex $match stage construction into a shared helper such as
mongoCanonicalUserPathFilterStages(userPath string) []bson.D. Update both
usageCacheStats and GetUsageByUserPath to append the helper’s stages, preserving
their existing filtering behavior and stage order.
- Around line 294-335: Add regression coverage in reader_mongodb_test.go for
usageCacheStats with canonicalUserPath=true and a subtree user-path filter.
Verify the generated fold pipeline applies the canonical grouped-path expression
and subtree match consistently with the aggregate path, preserving alignment
between both row sets.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: b830fcc6-fe99-44a3-b8db-ceb071b7e8b0
📒 Files selected for processing (2)
internal/usage/group_cache_stats_test.gointernal/usage/reader_mongodb.go
…fold pipeline Extract the canonical user-path $addFields/$match stage construction into shared helpers used by both GetUsageByUserPath and the cache-stat fold, and pull the fold's pipeline building into a pure mongoUsageCacheStatsPipeline so the row-set alignment is unit-testable. New tests pin both behaviors: with canonicalUserPath the raw user_path filter is cleared and the subtree match runs against the materialized canonical field via the aggregate's own stages; without it the raw-field filter is preserved and no canonical stages are inserted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Note Docstrings generation - SUCCESS |
Docstrings generation was requested by @SantiagoDePolonia. The following files were modified: * `internal/admin/dashboard/static/js/modules/charts.js` * `internal/usage/group_cache_stats.go` * `internal/usage/reader_mongodb.go` These files were ignored: * `internal/admin/dashboard/static/js/modules/charts.test.cjs` * `internal/admin/dashboard/static/js/modules/dashboard-layout.test.cjs` * `internal/usage/group_cache_stats_test.go` * `internal/usage/reader_mongodb_test.go` These file types are not supported: * `docs/openapi.json` * `internal/admin/dashboard/static/css/dashboard.css` * `internal/admin/dashboard/templates/page-usage.html`
Summary
Reworks the usage page's three breakdown charts (by model, by user path, by label) and surfaces cache traffic in them.
Chart rework
Cache series (new)
PricingResolver(same trade-off as pricing recalculation).Backend
ModelUsage/UserPathUsage/LabelUsagegain a sharedGroupCacheFieldsblock: prompt-cache input split, per-side local-cache tokens, and estimatedcached_input_cost.raw_data), folded through one shared accumulator reusingEntryInputSegments— the same pattern as the daily series. Implemented for SQLite, PostgreSQL, and MongoDB; label expansion happens in Go, matching each backend'sjson_each/$unwindsemantics.User-visible notes
cached_input_costis a read-time estimate at today's prices; models without a cached-input rate are omitted from it.Testing
EstimateCachedInputCostunit tests; fullinternal/...suite green.🤖 Generated with Claude Code
Summary by CodeRabbit