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.
…irectly (closes #330) (#331) * feat(frontend/recs): On-Demand Monthly column uses provider on_demand_cost directly (closes #330) The On-Demand Monthly column now returns the provider-supplied `r.on_demand_cost` directly instead of reconstructing the value from `monthly_cost + savings + (upfront_cost / months_in_term)`. When `on_demand_cost` is missing, undefined, or `0`, the cell renders the existing em-dash sentinel. PR #322 originally scoped the column to a *reconstruction-only* helper ("the column's purpose is to display the reconstructed denominator so users can verify the formula against the raw fields visible in the same row"). In practice, post-#277/#312/#324 every supported provider (AWS, Azure, GCP) plumbs `on_demand_cost` through to the frontend, and `effectiveSavingsPct` already prefers the provider value when present (its `hasOnDemand` branch). The two columns therefore disagreed for any row where the formula reconstruction drifted from the provider's billed price (Azure all-upfront RIs, AWS Capacity Reservation discounts, partial-day proration, list-price rounding). The product decision: surface the **authoritative** number from the provider, not a teaching-tool reconstruction. Users who want to verify the formula can still compute `monthly_cost + savings + upfront_cost/(term*12)` from the row's other columns. Aligning this column with `effectiveSavingsPct`'s denominator means the two never disagree. Changes: * `frontend/src/recommendations.ts::onDemandMonthly` simplified to: `return r.on_demand_cost > 0 ? r.on_demand_cost : null` — drops the `monthly_cost`/`term`/`upfront_cost` arithmetic entirely. * Updated the function's doc comment to reflect the new contract (the prior comment explicitly said "does NOT use on_demand_cost" and is now exactly inverted). * Updated the `SORTABLE_NUMERIC_COLUMNS.on_demand_monthly` preamble — the null-rendering condition is now "missing on_demand_cost" rather than "null monthly_cost / term=0". Tests: * Replaced the seven reconstruction-arithmetic tests in the `describe('onDemandMonthly')` block with five tests covering the new contract: provider value used directly, null/undefined/0 return null, supporting fields ignored when on_demand_cost is positive. * Updated three column-rendering tests in `describe('Monthly Cost + Effective % column rendering')` to set `on_demand_cost` on the fixture so the rendered value matches the new logic — the em-dash and filter regression tests still cover the same semantics, just with clearer fixture intent. `effectiveSavingsPct` keeps its existing reconstruction fallback unchanged — it's the canonical Effective Savings % calculation and must stay backward-compatible with legacy cached rows that pre-date #312/#324. Verification: - `npx tsc --noEmit` clean - `npm test` clean (1578 passed / 0 failed across 42 suites) * fix(frontend/recs): cover explicit-undefined + freshen filter comment (CR pass on PR #331) Addresses both items from CodeRabbit review 4244506117 on PR #331: 1. **Actionable** — `frontend/src/__tests__/recommendations.test.ts`: the single "undefined / missing" test only exercised the delete-the-property branch. TypeScript callers can also pass `on_demand_cost: undefined` explicitly (strict-null safety pattern), and the type system distinguishes "field absent" from "field present with value undefined" — so a regression that uses `'on_demand_cost' in r` instead of `r.on_demand_cost > 0` would be caught by one but not the other. Split into two tests: one for explicit undefined, one for the deleted-property case, with comments explaining why both matter. 2. **Nitpick** — `frontend/src/recommendations.ts:1085-1088`: the parenthetical "(null monthly_cost or term=0)" described the pre-#330 reconstruction's null conditions. Refresh to match the new contract: "missing or zero on_demand_cost — see onDemandMonthly() for the contract". Adds a back-reference so a future reader can find the source of truth. Verification: - `npx tsc --noEmit` clean - `npm test` clean (1579 passed / 0 failed across 42 suites — +1 test)
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