Skip to content

fix(recommendations): plumb provider on_demand_cost through to frontend (closes #274)#277

Merged
cristim merged 1 commit intofeat/multicloud-web-frontendfrom
fix/recs-effective-pct-274
May 5, 2026
Merged

fix(recommendations): plumb provider on_demand_cost through to frontend (closes #274)#277
cristim merged 1 commit intofeat/multicloud-web-frontendfrom
fix/recs-effective-pct-274

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 5, 2026

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-upfront
recs monthly_cost = $0, so the reconstruction collapses to
savings + amortized — much smaller than the real on-demand cost.
Result: the Standard_D11_v2 1y all-upfront row from the screenshot
read 85.9%.

After: when the provider populates on_demand_cost (Azure
CostWithNoReservedInstances, AWS EstimatedMonthlyOnDemandCost),
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

provider SDK
  → pkg/common/types.go::Recommendation.OnDemandCost  (already populated)
  → internal/config/types.go::RecommendationRecord.OnDemandCost  ← NEW
  → JSONB `payload` column in recommendations table  (no DDL migration —
                                                      record persisted as
                                                      JSON via existing
                                                      payload column)
  → internal/api/types.go (uses config.RecommendationRecord directly — no API change)
  → frontend/src/api/types.ts::Recommendation.on_demand_cost  ← NEW
  → frontend/src/types.ts::LocalRecommendation.on_demand_cost  ← NEW
  → effectiveSavingsPct prefers it; falls back to reconstruction if null/0

Compatibility

  • Older cached recs without on_demand_cost → fallback to the
    previous reconstruction formula (visually unchanged).
  • Provider returns 0nonZeroPtr writes nil so the frontend
    falls back. Defensive: a literal $0 baseline is impossible (the
    resource would be free).
  • monthly_cost === null + populated on_demand_cost → previously
    returned null (em-dash); now returns a real value. New rescue path.

Test plan

  • TestScheduler_ConvertRecommendations_OnDemandCost — pins
    populated values round-trip and 0 → nil.
  • 5 new frontend tests in 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:
    • live D11_v2 repro (86% reconstructed → 22% with baseline)
    • override behaviour (baseline wins over reconstruction)
    • null fallback (back-compat with older cached recs)
    • 0 fallback (defensive — provider returned 0)
    • rescue of monthly_cost === null when baseline is populated
  • npm test -- --testPathPattern=recommendations — 145 / 145 pass.
  • npm run typecheck — clean.
  • go test ./internal/scheduler/... — pass.
  • gocyclo -over 10 clean (helper extraction keeps
    convertRecommendations under the gate).

🤖 Generated with claude-flow

Summary by CodeRabbit

  • New Features

    • Recommendations now utilize provider-supplied on-demand cost baselines for more accurate calculations.
  • Bug Fixes

    • Fixed inflated effective savings percentage calculations for all-upfront pricing scenarios.
    • Improved fallback logic when on-demand cost baseline data is unavailable.

…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.
@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/m Days type/bug Defect triaged Item has been triaged labels May 5, 2026
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 5, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

The changes introduce an on_demand_cost field across frontend and backend types to represent the provider-supplied on-demand baseline, update the effective savings percentage formula to prefer this field when present, and modify the scheduler to persist it correctly, treating zero values as unpopulated (nil) to fix implausibly high percentages on Azure all-upfront recommendations.

Changes

On-Demand Baseline Integration

Layer / File(s) Summary
Data Shape
frontend/src/api/types.ts, frontend/src/types.ts, internal/config/types.go
Added on_demand_cost?: number | null (frontend) and OnDemandCost \*float64 (backend) fields to Recommendation, LocalRecommendation, and RecommendationRecord types with documentation clarifying it is the canonical provider-supplied baseline.
Core Calculation
frontend/src/recommendations.ts
Updated effectiveSavingsPct to prefer on_demand_cost as the denominator when present and > 0, return null early when both monthly_cost and on_demand_cost are missing, and reconstruct denominator only as fallback using updated onDemand = monthly_cost + savings + amortized formula.
Persistence & Nullability
internal/scheduler/scheduler.go
Added nonZeroPtr helper to convert provider OnDemandCost to nullable pointer, storing nil when value is 0 to prevent zero from being treated as a populated baseline.
Tests
frontend/src/__tests__/recommendations.test.ts, internal/scheduler/scheduler_test.go
Added test suite for #274 covering on_demand_cost preference, fallback behavior, and zero-value handling; added TestScheduler_ConvertRecommendations_OnDemandCost to verify round-trip persistence of non-zero and zero baselines.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

Possibly related PRs

  • LeanerCloud/CUDly#256: Both treat zero vs. null semantics specially to prevent zero values from being interpreted as populated baselines.
  • LeanerCloud/CUDly#254: Both update the effectiveSavingsPct denominator logic to improve accuracy of the savings percentage calculation.
  • LeanerCloud/CUDly#242: Introduces related changes to the same effectiveSavingsPct function and its test suite.

Poem

🐰 A cost baseline hops into view,
No more reconstructions (that math was askew),
Percentages plausible, zeros now null,
The effective savings is finally truthful! 📊

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: plumbing the provider's on_demand_cost field through to the frontend to fix implausible Effective % values (issue #274).
Linked Issues check ✅ Passed The PR addresses all key coding objectives from #274: plumbing on_demand_cost through the stack, updating type definitions, implementing frontend fallback logic, and adding comprehensive tests for the new behavior.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the on_demand_cost plumbing and associated fallback logic specified in #274; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/recs-effective-pct-274

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

✅ 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Add the term-based sanity cap before returning effectiveSavingsPct.

Using on_demand_cost fixes the denominator, but this still returns arbitrarily large Effective % values when provider data is stale or scaled incorrectly. Issue #274 explicitly 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

📥 Commits

Reviewing files that changed from the base of the PR and between e010987 and a696a29.

📒 Files selected for processing (7)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/api/types.ts
  • frontend/src/recommendations.ts
  • frontend/src/types.ts
  • internal/config/types.go
  • internal/scheduler/scheduler.go
  • internal/scheduler/scheduler_test.go

Comment thread frontend/src/__tests__/recommendations.test.ts
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

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.

@cristim cristim merged commit 66a993f into feat/multicloud-web-frontend May 5, 2026
5 checks passed
cristim added a commit that referenced this pull request May 5, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant