fix(recommendations): plumb provider on_demand_cost through to frontend (closes #274)#277
Conversation
…nd (closes #274) The "Effective %" column displayed implausibly high values (85.9% on a 1-year Azure all-upfront RI; 54% on a savings=$0 row) because the frontend was reconstructing the on-demand denominator from `monthly_cost + savings + amortized_upfront`. For Azure all-upfront recs `monthly_cost = $0`, which collapses the reconstruction to `savings + amortized` — much smaller than the real on-demand monthly — and inflates the percentage. The cloud providers actually return the canonical baseline (`Azure CostWithNoReservedInstances`, `AWS EstimatedMonthlyOnDemandCost`) and `pkg/common/types.go::Recommendation.OnDemandCost` already carries it through the provider layer. But it was being dropped in `scheduler.convertRecommendations` and never made it to the persisted record / API response / frontend. Plumbing through, end-to-end: - `internal/config/types.go::RecommendationRecord` adds `OnDemandCost *float64`. No DDL migration: the recommendations table stores the full record as JSONB in the `payload` column (`store_postgres_recommendations.go:178`), so adding a struct field round-trips through `json.Marshal` / `json.Unmarshal` automatically. - `internal/scheduler/scheduler.go::convertRecommendations` populates the new field via a small `nonZeroPtr(v) *float64` helper that returns nil for v == 0 (real on-demand baselines are non-zero; the alternative would poison the frontend's "is this populated?" branch). Helper extraction also keeps the function under the project's gocyclo gate. - `internal/api/types.go` uses `config.RecommendationRecord` directly, so the API response already exposes the new field via the existing type — no API-layer change needed. - `frontend/src/api/types.ts::Recommendation` and `frontend/src/types.ts::LocalRecommendation` add `on_demand_cost?: number | null`. - `frontend/src/recommendations.ts::effectiveSavingsPct` prefers the populated `on_demand_cost` over reconstruction. Falls back to the prior `monthly_cost + savings + amortized` formula when the field is null/undefined (older cached recs) or 0 (defensive — a literal 0 baseline is impossible). When `on_demand_cost` is populated, it now also rescues the previously-null-returning case where `monthly_cost === null`. Verification with the live row from the screenshot: Standard_D11_v2, term=1, count=2, savings=$29, upfront=$26, monthly=$0, on_demand=$122.64 (real CostWithNoReservedInstances) - Reconstructed: pct ≈ 86.1% - With baseline: pct ≈ 21.9% ← realistic 1-year RI savings Tests added: - TestScheduler_ConvertRecommendations_OnDemandCost pins that populated values round-trip and 0 → nil. - 5 new frontend tests in effectiveSavingsPct describing the on_demand_cost preference: live D11_v2 repro, override behaviour, null-fallback (back-compat), 0-fallback (defensive), and the missing-monthly_cost rescue path.
|
@coderabbitai review |
📝 WalkthroughWalkthroughThe changes introduce an ChangesOn-Demand Baseline Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/recommendations.ts (1)
414-431:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd the term-based sanity cap before returning
effectiveSavingsPct.Using
on_demand_costfixes the denominator, but this still returns arbitrarily largeEffective %values when provider data is stale or scaled incorrectly. Issue#274explicitly called for rendering implausible values as unknown, so without the guard the UI can still surface nonsense percentages.Suggested guard
const onDemand = hasOnDemand ? (r.on_demand_cost as number) : (r.monthly_cost as number) + r.savings + amortized; if (onDemand === 0) return null; - return (effectiveSavings / onDemand) * 100; + const pct = (effectiveSavings / onDemand) * 100; + const maxPct = r.term === 1 ? 60 : r.term === 3 ? 75 : null; + if (maxPct !== null && pct > maxPct) return null; + return pct;🤖 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 `@frontend/src/recommendations.ts` around lines 414 - 431, The effectiveSavingsPct function can still return implausibly large percentages; add a term-based sanity cap before returning: define a constant MAX_EFFECTIVE_SAVINGS_PCT (e.g. 1000) and compute a threshold scaled by term (e.g. threshold = MAX_EFFECTIVE_SAVINGS_PCT / r.term), then after you compute pct = (effectiveSavings / onDemand) * 100 check if Math.abs(pct) > threshold and return null if so, otherwise return pct; update the effectiveSavingsPct function and use symbols monthsInTerm / r.term, effectiveSavings, onDemand, and the new MAX_EFFECTIVE_SAVINGS_PCT to locate and apply the guard.
🤖 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 `@frontend/src/__tests__/recommendations.test.ts`:
- Around line 2472-2476: The test currently expects an implausible high
reconstructed percentage from effectiveSavingsPct(mk(...)) but should validate
the "unknown + warning" guardrail; change the assertion for pctReconstructed
produced by effectiveSavingsPct (called with mk({ savings: 29, upfront_cost: 26,
monthly_cost: 0, term: 1 })) to assert it returns the unknown sentinel (e.g.,
undefined or null) rather than >80, and add an expectation that a warning was
emitted (use the existing warning/log spy in the test harness or spy on
console.warn) to confirm the guardrail path was taken.
---
Outside diff comments:
In `@frontend/src/recommendations.ts`:
- Around line 414-431: The effectiveSavingsPct function can still return
implausibly large percentages; add a term-based sanity cap before returning:
define a constant MAX_EFFECTIVE_SAVINGS_PCT (e.g. 1000) and compute a threshold
scaled by term (e.g. threshold = MAX_EFFECTIVE_SAVINGS_PCT / r.term), then after
you compute pct = (effectiveSavings / onDemand) * 100 check if Math.abs(pct) >
threshold and return null if so, otherwise return pct; update the
effectiveSavingsPct function and use symbols monthsInTerm / r.term,
effectiveSavings, onDemand, and the new MAX_EFFECTIVE_SAVINGS_PCT to locate and
apply the guard.
🪄 Autofix (Beta)
❌ Autofix failed (check again to retry)
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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2ec56332-14be-474d-8d36-054d1f549c1e
📒 Files selected for processing (7)
frontend/src/__tests__/recommendations.test.tsfrontend/src/api/types.tsfrontend/src/recommendations.tsfrontend/src/types.tsinternal/config/types.gointernal/scheduler/scheduler.gointernal/scheduler/scheduler_test.go
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Autofix skipped. No unresolved CodeRabbit review comments with fix instructions found. |
closes #303) (#312) The AWS Savings Plans parser did not populate common.Recommendation.OnDemandCost, so effectiveSavingsPct() fell back to reconstructing the on-demand denominator from monthly_cost + savings + amortized. For SP rows monthly_cost reflects only the recurring commitment charge, not the full on-demand baseline, so the reconstruction is inaccurate. Fix: derive OnDemandCost from CurrentAverageHourlyOnDemandSpend × 730 in parseSavingsPlanDetail — the same field AWS Cost Explorer uses internally when computing EstimatedSavingsPercentage. The scheduler's existing nonZeroPtr helper (added in #277) converts 0 → nil so the frontend falls back to reconstruction when the field is absent, preserving backward compatibility. The AWS RI parser already populated OnDemandCost (from EstimatedMonthlyOnDemandCost via parseAWSCostDetails) and the scheduler already plumbs it through to RecommendationRecord — both landed in #277. This commit closes the SP gap and adds the AWS-specific frontend and provider regression tests the AC list requires.
Summary
Closes #274. Fixes the implausibly-high "Effective %" values by
plumbing the cloud providers' canonical on-demand baseline through to
the frontend.
Before: the frontend reconstructed the on-demand denominator from
monthly_cost + savings + amortized_upfront. For Azure all-upfrontrecs
monthly_cost = $0, so the reconstruction collapses tosavings + amortized— much smaller than the real on-demand cost.Result: the
Standard_D11_v2 1y all-upfrontrow from the screenshotread 85.9%.
After: when the provider populates
on_demand_cost(AzureCostWithNoReservedInstances, AWSEstimatedMonthlyOnDemandCost),the frontend uses it directly. With the real on-demand of $122.64 for
the same row, the percentage drops to 21.9% — within realistic
1-year RI ceilings.
Plumbing path
Compatibility
on_demand_cost→ fallback to theprevious reconstruction formula (visually unchanged).
nonZeroPtrwrites nil so the frontendfalls back. Defensive: a literal $0 baseline is impossible (the
resource would be free).
monthly_cost === null+ populatedon_demand_cost→ previouslyreturned
null(em-dash); now returns a real value. New rescue path.Test plan
TestScheduler_ConvertRecommendations_OnDemandCost— pinspopulated values round-trip and 0 → nil.
effectiveSavingsPct's"on_demand_cost preference (bug(frontend/recs): Effective % shows implausible values (85.9% on Azure 1y RI, 54% on savings=$0 row) #274)" sub-describe:
D11_v2repro (86% reconstructed → 22% with baseline)monthly_cost === nullwhen baseline is populatednpm test -- --testPathPattern=recommendations— 145 / 145 pass.npm run typecheck— clean.go test ./internal/scheduler/...— pass.gocyclo -over 10clean (helper extraction keepsconvertRecommendationsunder the gate).🤖 Generated with claude-flow
Summary by CodeRabbit
New Features
Bug Fixes