Skip to content

fix(api/dashboard): populate ServiceSavings.CurrentSavings from active commitments - #926

Merged
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/current-savings-per-service
Jun 4, 2026
Merged

fix(api/dashboard): populate ServiceSavings.CurrentSavings from active commitments#926
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/current-savings-per-service

Conversation

@cristim

@cristim cristim commented Jun 2, 2026

Copy link
Copy Markdown
Member

Problem

ServiceSavings.CurrentSavings was declared in internal/api/types.go but never populated. The frontend at frontend/src/dashboard.ts reads byService[s]?.current_savings for the green bar dataset, so every service rendered as $0. summarizeRecommendationsWithCoverage only filled PotentialSavings; no code path ever aggregated active purchase history into the per-service map.

Changes

  • aggregateActiveCommitmentsPerService (new): pure function that sums EstimatedSavings of active (non-expired) rows from purchase history, grouped by Service. This is the shared primitive for both the KPI total and the per-service chart so both paths apply the same isActiveCommitment gate.
  • calculateCommitmentMetrics: now returns a fourth value -- savingsByService map[string]float64 -- and derives committedMonthly from the per-service map (previously summed inline). No change to the returned KPI values.
  • getDashboardSummary: after the recommendations reducer runs, merges the per-service commitment map into byService, setting entry.CurrentSavings for each service.

Tests added

Three new tests, 9 new sub-cases total:

  • TestAggregateActiveCommitmentsPerService: two active services accumulate independently; one expired + one active row for the same service; all-expired returns empty map.
  • TestHandler_getDashboardSummary_CurrentSavingsPopulated: EC2 + RDS active commitments with an expired EC2 row that must not be counted; asserts both PotentialSavings and CurrentSavings are correct in the response struct.
  • TestHandler_getDashboardSummary_CurrentSavingsJSON: asserts the current_savings field carries the active purchase's EstimatedSavings through the response.

All existing TestHandler_calculateCommitmentMetrics cases updated for the new 4-return-value signature.

Coordination with sibling PRs

If a fix/committed-monthly PR adds status filtering (e.g. only counting "succeeded" rows) or unit normalisation to calculateCommitmentMetrics, aggregateActiveCommitmentsPerService is the single place to update. Both the KPI total and the per-service breakdown flow through it. This PR should merge after any such sibling so the gating is consistent from day one -- or the sibling can build on top of this primitive.

Test run

go test ./internal/api/...: 1391 passed
frontend jest --testPathPattern=dashboard: 83 passed, 1 skipped (pre-existing)

@cristim cristim added bug Something isn't working triaged Item has been triaged priority/p2 Backlog-worthy urgency/this-sprint Within the current sprint impact/all-users Affects every user labels Jun 2, 2026
@cristim

cristim commented Jun 2, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@cristim, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 13 minutes and 28 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b5d0f4be-0e2f-47d0-8588-e2dbb2cf9d19

📥 Commits

Reviewing files that changed from the base of the PR and between 623758c and b0820dd.

📒 Files selected for processing (2)
  • internal/api/handler_dashboard.go
  • internal/api/handler_dashboard_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/current-savings-per-service

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…ve commitments

ServiceSavings.CurrentSavings was declared in types.go but never written,
causing the Home page chart's green "Current Savings" bars to always render
as zero.

Root cause: summarizeRecommendationsWithCoverage only filled PotentialSavings;
no code path ever aggregated active purchase history into the per-service map.

Fix: extract aggregateActiveCommitmentsPerService (the shared primitive that
applies the same isActiveCommitment gate as calculateCommitmentMetrics) and
have calculateCommitmentMetrics derive its committedMonthly total from the
map it returns. getDashboardSummary merges the per-service map into byService
after the recommendations reducer runs.

Tests added:
- TestAggregateActiveCommitmentsPerService: two active services, one
  expired+one active, all expired
- TestHandler_getDashboardSummary_CurrentSavingsPopulated: EC2+RDS active
  commitments with an expired EC2 row that must be excluded
- TestHandler_getDashboardSummary_CurrentSavingsJSON: wire-shape assertion on
  the current_savings field

Note: if a fix/committed-monthly PR adds status filtering or unit
normalisation to calculateCommitmentMetrics, the aggregateActiveCommitmentsPerService
primitive is the single place to update for consistent gating on both the KPI
total and the per-service breakdown.
@cristim
cristim force-pushed the fix/current-savings-per-service branch from c067469 to b0820dd Compare June 3, 2026 22:06
@cristim

cristim commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

Rebased on feat/multicloud-web-frontend (now includes #927's committed_monthly tightening). Resolved conflicts in internal/api/handler_dashboard.go and internal/api/handler_dashboard_test.go.

The two PRs touched the same calculateCommitmentMetrics code path, so this was a semantic (not purely additive) merge. Reconciliation:

Verified: gofmt clean, go vet clean, go build ./... ok, go test ./internal/api/... -count=1 -> 1412 passed.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

@cristim: Thanks for the detailed rebase summary. The reconciliation approach looks solid — deriving committedMonthly from the savingsByService map rather than maintaining a separate inline accumulator is a clean way to ensure the KPI total and per-service breakdown stay in sync while transparently inheriting #927's isActiveCommitment gate. Triggering a fresh review of the rebased commits now.


🧠 Learnings used
Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-05T15:44:54.463Z
Learning: In the LeanerCloud/CUDly repository, when a CodeRabbit review flags something as "outside-diff" that was already implemented in an earlier commit of the same PR, it is likely a stale-snapshot artifact where the review diff was generated against a pre-fix view. The inline comment convention `(CR pass on PR #<number> Finding N)` is used by the maintainer (cristim) to mark lines that were changed in direct response to a prior CodeRabbit review pass.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 277
File: frontend/src/__tests__/recommendations.test.ts:2472-2476
Timestamp: 2026-05-05T07:46:01.902Z
Learning: In the CUDly frontend (`frontend/src/recommendations.ts`), `effectiveSavingsPct` intentionally has NO plausibility guard (no null return or warning for reconstructed percentages above per-term ceilings). The maintainer explicitly rejected that approach in PR `#277`. The fix for inflated percentages (issue `#274`) is purely a data-path fix: plumbing the provider's canonical `on_demand_cost` through to the frontend so the denominator is not reconstructed from potentially misleading `monthly_cost + savings + amortized`. Tests that pin the reconstructed >80% behavior are intentional documentation of the pre-fix path, not bugs.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 0
File: :0-0
Timestamp: 2026-05-25T21:45:57.471Z
Learning: In this repository (CUDly), GCP CUD commitments are inherently monthly-billed. The GCP CUD purchase API (`providers/gcp/services/computeengine/client.go:350-373`, `buildCommitmentRequests`) takes only a `Plan` field (TWELVE_MONTH / THIRTY_SIX_MONTH) and never reads a payment-option field. Therefore, the only valid payment option for GCP in `ValidPaymentOptionsByProvider` is `"monthly"`. The `NormalizePaymentOption` GCP branch should collapse any non-monthly token (including legacy `"upfront"`) to `"monthly"`, with a WARN log at the `scheduler.convertRecommendations` boundary.

Learnt from: cristim
Repo: LeanerCloud/CUDly PR: 339
File: providers/aws/recommendations/coverage.go:177-205
Timestamp: 2026-05-13T21:30:43.093Z
Learning: In the CUDly project (LeanerCloud/CUDly), the CLI flag for target-based coverage sizing is `--target-coverage` (not `--target-utilization`). The PR renamed it during design iteration: "utilization" (how full each RI is) stays ~100% under the new sizing, while "coverage" (% of demand covered) is the user-facing knob. `ce:GetReservationCoverage` is required only when `--target-coverage` is set, gated by `cfg.TargetCoverage <= 0` in `cmd/multi_service.go`. The legacy `--coverage` path is unaffected and pays no CE cost or IAM change.
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim merged commit 59da2d2 into feat/multicloud-web-frontend Jun 4, 2026
4 checks passed
cristim added a commit that referenced this pull request Jun 8, 2026
closes #1031)

summarizeRecommendationsWithCoverage incorrectly wrote CurrentSavings to
the coverage-scaled potential amount (same as PotentialSavings). Because
getDashboardSummary only overwrites CurrentSavings for services present in
the purchase-history result, services with recommendations but no active
purchases shipped a non-zero current_savings equal to potential_savings.
This was wrong: the frontend renders current_savings as committed/realized
savings from active purchase history, so an uncommitted service must ship 0.

The partial fix in #926 (aggregateActiveCommitmentsPerService + purchase-
history overwrite) was correct in concept but left the incorrect assignment
in summarizeRecommendationsWithCoverage active, masking the bug for services
that had no purchases.

Fix: remove svc.CurrentSavings += scaled from summarizeRecommendationsWithCoverage.
CurrentSavings is getDashboardSummary's responsibility (via the overwrite loop
that calls aggregateActiveCommitmentsPerService). The function comment and the
three affected test assertions/functions are updated accordingly.

Regression test added: TestHandler_getDashboardSummary_CurrentSavingsZeroWhenNoCommitments
asserts current_savings: 0 for services with recommendations but no purchases.
cristim added a commit that referenced this pull request Jul 10, 2026
closes #1031)

summarizeRecommendationsWithCoverage incorrectly wrote CurrentSavings to
the coverage-scaled potential amount (same as PotentialSavings). Because
getDashboardSummary only overwrites CurrentSavings for services present in
the purchase-history result, services with recommendations but no active
purchases shipped a non-zero current_savings equal to potential_savings.
This was wrong: the frontend renders current_savings as committed/realized
savings from active purchase history, so an uncommitted service must ship 0.

The partial fix in #926 (aggregateActiveCommitmentsPerService + purchase-
history overwrite) was correct in concept but left the incorrect assignment
in summarizeRecommendationsWithCoverage active, masking the bug for services
that had no purchases.

Fix: remove svc.CurrentSavings += scaled from summarizeRecommendationsWithCoverage.
CurrentSavings is getDashboardSummary's responsibility (via the overwrite loop
that calls aggregateActiveCommitmentsPerService). The function comment and the
three affected test assertions/functions are updated accordingly.

Regression test added: TestHandler_getDashboardSummary_CurrentSavingsZeroWhenNoCommitments
asserts current_savings: 0 for services with recommendations but no purchases.
cristim added a commit that referenced this pull request Jul 17, 2026
closes #1031)

summarizeRecommendationsWithCoverage incorrectly wrote CurrentSavings to
the coverage-scaled potential amount (same as PotentialSavings). Because
getDashboardSummary only overwrites CurrentSavings for services present in
the purchase-history result, services with recommendations but no active
purchases shipped a non-zero current_savings equal to potential_savings.
This was wrong: the frontend renders current_savings as committed/realized
savings from active purchase history, so an uncommitted service must ship 0.

The partial fix in #926 (aggregateActiveCommitmentsPerService + purchase-
history overwrite) was correct in concept but left the incorrect assignment
in summarizeRecommendationsWithCoverage active, masking the bug for services
that had no purchases.

Fix: remove svc.CurrentSavings += scaled from summarizeRecommendationsWithCoverage.
CurrentSavings is getDashboardSummary's responsibility (via the overwrite loop
that calls aggregateActiveCommitmentsPerService). The function comment and the
three affected test assertions/functions are updated accordingly.

Regression test added: TestHandler_getDashboardSummary_CurrentSavingsZeroWhenNoCommitments
asserts current_savings: 0 for services with recommendations but no purchases.
cristim added a commit that referenced this pull request Jul 17, 2026
closes #1031) (#1041)

* fix(api): current_savings zero for services with no active commitments (closes #1031)

summarizeRecommendationsWithCoverage incorrectly wrote CurrentSavings to
the coverage-scaled potential amount (same as PotentialSavings). Because
getDashboardSummary only overwrites CurrentSavings for services present in
the purchase-history result, services with recommendations but no active
purchases shipped a non-zero current_savings equal to potential_savings.
This was wrong: the frontend renders current_savings as committed/realized
savings from active purchase history, so an uncommitted service must ship 0.

The partial fix in #926 (aggregateActiveCommitmentsPerService + purchase-
history overwrite) was correct in concept but left the incorrect assignment
in summarizeRecommendationsWithCoverage active, masking the bug for services
that had no purchases.

Fix: remove svc.CurrentSavings += scaled from summarizeRecommendationsWithCoverage.
CurrentSavings is getDashboardSummary's responsibility (via the overwrite loop
that calls aggregateActiveCommitmentsPerService). The function comment and the
three affected test assertions/functions are updated accordingly.

Regression test added: TestHandler_getDashboardSummary_CurrentSavingsZeroWhenNoCommitments
asserts current_savings: 0 for services with recommendations but no purchases.

* fix(api/dashboard): YTD month accuracy, store error log, fresh slice, first-service dedup

* fix(test): update CurrentSavingsZeroWhenNoCommitments to mock GetActivePurchaseHistory

getDashboardSummary now calls GetActivePurchaseHistory (active-only,
uncapped) instead of GetAllPurchaseHistory to aggregate commitment
KPIs (see fetchCommitmentPurchases). The regression test was written
before this change and still mocked the old method, causing a testify
panic on unexpected method call.

Replace the GetAllPurchaseHistory mock with GetActivePurchaseHistory
using three mock.Anything matchers (asOf, accountUUIDs,
accountExternalIDsByProvider), matching the pattern already used in
the sibling CurrentSavingsPopulated and CurrentSavingsJSON tests.

* lint(api): fix rangeValCopy and godot findings in handler_dashboard.go

golangci-lint (gocritic, godot) flagged three issues introduced by the
dashboard commits:

- Line 179: range-value copy of config.RecommendationRecord (312 bytes)
  per iteration; switch to index-based _rvc pattern consistent with the
  rest of handler_dashboard.go.
- Line 229: godot: comment block before summarizeRecommendationsWithCoverage
  ended in ")" not "."; reword to end in period.
- Line 703: godot: calculateCurrentCoverage doc comment missing trailing
  period.

No behavioural change.
@cristim
cristim deleted the fix/current-savings-per-service branch July 27, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working impact/all-users Affects every user priority/p2 Backlog-worthy triaged Item has been triaged urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant