Skip to content

fix(hubs): Fix Data Explorer dashboard savings formulas and exclude commitment purchases - #2248

Open
Michael Flanakin (flanakin) wants to merge 36 commits into
devfrom
flanakin/2093-dashboard-savings-formulas
Open

Michael Flanakin (flanakin) wants to merge 36 commits into
devfrom
flanakin/2093-dashboard-savings-formulas

Conversation

@flanakin

@flanakin Michael Flanakin (flanakin) commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

🤖 [AI]

Summary

Fixes the Data Explorer dashboard savings math from #2093 Phase 1.3 so every savings tile uses one clamped basis, reconciles on screen, and never counts commitment purchases as savings.

Changes (dashboard.json)

  • Clamped savings basis – Savings come from the row-level clamped columns (x_NegotiatedDiscountSavings, x_CommitmentDiscountSavings) instead of sum(ListCost) - sum(EffectiveCost).
  • Derived operands – Tiles that show an equation build List and Contracted cost from the same basis, so the numbers on screen add up:
    • Contracted = Effective + Commitment savings
    • List = Contracted + Negotiated savings
    • Total savings = Negotiated + Commitment
  • Purchases excluded (combined with #2352) – All 9 Summary queries use sumif(x_*Savings, x_AmortizationClass != 'Principal'). Effective cost stays unfiltered because Principal rows have EffectiveCost = 0.
  • One purchase filter – Every Rate optimization tile now uses x_AmortizationClass != 'Principal'. The local re-derivations and the ChargeCategory == 'Usage' or isempty(CommitmentDiscountId) variant are gone.
  • NaN guard – ESR uses iff(ListCost == 0, real(null), Savings / ListCost) on both ESR tiles and Savings breakdown by month.
  • Left unchanged – The Data ingestion alignment diagnostic keeps its raw deltas on purpose.

Behavior change: ESR on the affected tiles is now (Negotiated + Commitment savings) / derived List cost. Release note is under Unreleased → FinOps hubs v16.

Compatibility with #2352

#2352 (v15) is still open and edits the same 9 queries. This PR already has the combined form (derived basis plus the Principal exclusion). When both merge, resolve conflicts on those queries by taking this PR's side.

Known gap

Non-Azure commitment purchases with an empty x_AmortizationClass aren't excluded. That should be fixed in ingestion (set the column for all providers), not per tile.

Verification

  • dashboard.json is valid JSON. Only the edited query strings changed.
  • Pester Lint + Unit: 6299 passed, 0 failed.
  • Counterexample check: rows (100,120,80) and (100,80,60) give Negotiated 20, Commitment 60, Contracted' 200, List' 220, ESR 36.4%. Adding a 300 Principal row leaves Summary and Rate optimization both at 80.

🤖 Generated with Claude Code

Replaces inline sum(ListCost) - sum(EffectiveCost) style savings
calculations in dashboard.json with sum() of the canonical, row-level
clamped columns (x_TotalSavings, x_CommitmentDiscountSavings,
x_NegotiatedDiscountSavings) across 21 tiles. Raw subtraction of
aggregated sums can diverge from the canonical clamped values.

Also removes the silent 1:1 exchange-rate fallback
(coalesce(x_BillingExchangeRate, real(1))) used when deriving
CommitmentDiscountQuantity for spend-based commitments in
HubSetup_v1_2.kql and IngestionSetup_v1_2.kql. When the rate is
missing, the quantity (and any utilization built on it) is now left
unset rather than silently distorted by the true FX factor.

Addresses the remaining Phase 1.3 and savings-plan FX distortion
items from #2093; BillingCurrency visibility in detail tables and the
Phase 2 USD-normalization toggle remain out of scope.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the refactor and validated the assumptions by running the before/after formulas against three production hubs I have read access to (read-only queries, no identifying details below). The mechanical KQL refactor is clean, but the behavior change is materially larger than the description suggests, and it breaks the tiles that render savings as a visible equation.

Summary

The core issue is that x_TotalSavings, x_CommitmentDiscountSavings, and x_NegotiatedDiscountSavings are clamped per row. Summing them is therefore not equivalent to subtracting the aggregates — it is a different metric, one that structurally cannot net out credits, refunds, and corrections. On one hub the last-3-months total savings comes out ~40% higher after this change; on another the divergence is ~0.04%. So the blast radius is entirely tenant-dependent, which makes shipping it silently riskier, not safer.

That may well be the metric we want. But it is a product decision that belongs in #2093 with a release note, not a side effect of a "use the canonical column" refactor.

Details in the inline comments. Ordered by severity:

  1. Savings-breakdown tiles now display arithmetic that does not add up — operands stayed raw, results became clamped.
  2. Parts no longer sum to the whole — negotiated + commitment overstates total by ~61% on one hub.
  3. Effective savings rate silently inflated — clamped numerator over raw denominator.
  4. The FX guard likely regresses non-Microsoft FOCUS ingestion — and is a no-op on every Azure hub I checked.
  5. The auditability claim in the description only holds for one of the two code paths.

What is right

  • The refactor itself is correct KQL. The union/pivot tiles properly re-sum the already-renamed columns in the second summarize.
  • Dropping the redundant local extend re-derivations in 3b3f0a58 and 0341b3e4 is a genuine simplification — the latter's were entirely dead code.
  • Removing now-unused ListCost/ContractedCost from summarize lists (e.g. 2d7b6447) is a real efficiency win on wide scans.
  • Leaving the Summary of list, contracted, and effective cost alignment diagnostic on raw unclamped deltas is the right call, and the reasoning in the description is sound.

Two things not introduced here, but now load-bearing

The two clamp formulas disagree. HubSetup_v1_2.kql:169-171 uses iff(ContractedCost < EffectiveCost, real(0), ...); IngestionSetup_v1_2.kql:706-708 uses iff(isempty(ListCost) or ListCost == 0 or ListCost - EffectiveCost < 0.0001, real(0), ...). Pre-existing, but this PR makes 21 tiles depend on those columns, so a hub with mixed v1.0/v1.2 data will now get two different clamping semantics inside a single displayed number. Worth aligning while we are here.

Dashboard/hub version coupling. I verified via getschema that a hub running a current released version does not yet expose the x_*Savings columns on Costs_v1_2() — they come from this branch. Fine for a normal template deploy, but importing the new dashboard.json against a not-yet-upgraded hub now produces hard query failures on all 21 tiles rather than degraded output. Probably worth a release-note line.

Recommendation

Blocking on 1-3: either keep raw subtraction in the tiles that render savings as a visible equation, or derive the displayed operands from the clamped relationship so the math closes. 4 should be narrowed to a currency-equality guard before merge. 5 is a follow-up.

Happy to take the tile fixes if that is useful.

Comment thread src/templates/finops-hub/dashboard.json Outdated
Comment thread src/templates/finops-hub/dashboard.json Outdated
Comment thread src/templates/finops-hub/dashboard.json Outdated
Comment thread src/templates/finops-hub/dashboard.json Outdated
Comment thread src/templates/finops-hub/dashboard.json Outdated
Comment thread src/templates/finops-hub/dashboard.json Outdated
@microsoft-github-policy-service microsoft-github-policy-service Bot added Needs: Attention 👋 Issue or PR needs to be reviewed by the author or it will be closed due to no activity and removed Needs: Review 👀 PR that is ready to be reviewed labels Aug 13, 2026
The edited branch is only reached by FOCUS-sourced rows where a null
x_BillingExchangeRate means single-currency billing (PricingCurrency ==
BillingCurrency, so Cost Management doesn't populate a rate) -- not the
EA/legacy null-source scenario the change was justified against. Those
EA/legacy paths also null CommitmentDiscountQuantity upstream, so they
short-circuit at the first case() branch and never reach this one.

For single-currency accounts, a rate of 1.0 is correct, not a guess.
Requiring isnotempty(x_BillingExchangeRate) instead silently drops these
rows (and their CommitmentDiscountUnit) from utilization aggregates --
trading a bounded, visible number for a silent hole in sum()-based
denominators. Restoring coalesce(x_BillingExchangeRate, real(1)).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@microsoft-github-policy-service microsoft-github-policy-service Bot added Needs: Review 👀 PR that is ready to be reviewed and removed Needs: Attention 👋 Issue or PR needs to be reviewed by the author or it will be closed due to no activity labels Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed at 0e05f98. The KQL revert resolves the two threads it touches — replied there, no objection. The PR is now a pure dashboard.json change, +21/-21.

The six dashboard threads are untouched by this commit and all still apply, so keeping changes requested. Consolidating them here, since they're one root cause rather than six independent problems.

Root cause: the change swaps raw subtractions for sum(x_*Savings) on the result side of each tile, while leaving sum(ListCost) / sum(ContractedCost) / sum(EffectiveCost) raw on the operand side. The x_*Savings columns clamp negatives at row level, so the two sides no longer reconcile.

Tile Symptom, measured on a production hub
Savings breakdown, monthly (e17346d1) + daily (f1ef29df) the three equations the tile literally displays don't hold: +25.9%, +53.8%, +40.2%
Total / Negotiated / Commitment (line 3074) components exceed the total they decompose by ~61%
Stacked savings area chart (line 2876) stack no longer sums to ListCost, overshoots by a tenant-dependent amount
Effective Savings Rate (e1bc2d51 + daily twin) clamped numerator over raw denominator: ESR +40% relative, one month +230%

One decision settles all four: either keep the raw subtractions on these tiles, or derive the displayed cost operands from the clamped columns so the arithmetic closes. I don't have a strong preference as long as each tile is internally consistent — a user reading two numbers and their stated difference shouldn't see a third number that isn't the difference.

ESR is the one I'd treat as separate from the arithmetic question: it's a headline KPI, and a step change of that size across an upgrade boundary makes the month-over-month series non-comparable. If the clamped definition is the one we want, that needs a release note rather than just a formula change.

Happy to re-measure whichever direction you pick.

@microsoft-github-policy-service microsoft-github-policy-service Bot added Needs: Attention 👋 Issue or PR needs to be reviewed by the author or it will be closed due to no activity and removed Needs: Review 👀 PR that is ready to be reviewed labels Aug 14, 2026

@MSBrett Brett Wilson (MSBrett) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes. I re-reviewed the current head (0e05f98b) from the base diff, traced the savings fields to their source definitions, checked all 21 changed queries, and independently reproduced the arithmetic failures below.

The replacement of aggregate subtraction with sums of row-level clamped savings is not semantics-preserving for visuals that still rely on exact accounting relationships:

  1. The monthly and daily savings-summary tiles display false equations (dashboard.json:3086 and :3092). They still show raw aggregate values for List, Contracted, and Effective cost, followed by subtraction and equals signs, but the displayed results now sum independently clamped row values. Whenever a row is clamped, the displayed operands no longer produce the displayed result.

  2. The Total / Negotiated / Commitment summary is no longer a valid decomposition (dashboard.json:3074). Independently clamped negotiated and commitment savings are not additive and can sum to more than total savings.

  3. The stacked Cost + savings chart no longer reconciles to list cost (dashboard.json:2876). It stacks raw effective cost with both independently clamped savings categories, so the stack can overstate the implied cost without discounts.

A minimal counterexample confirms all three failures: for rows (List, Contracted, Effective) = (100,120,80) and (100,80,60), the raw negotiated difference is 0 while summed clamped negotiated savings is 20; negotiated plus commitment savings is 80 while total savings is 60; and the stacked value is 220 against list cost of 200.

The clamped Effective Savings Rate formula is not inherently invalid—it matches the repository's savings-summary-report.kql precedent—but it is a material KPI-definition change rather than a mechanical refactor. If retained for ESR and standalone savings visuals, the changed semantics and historical discontinuity should be explicitly approved and documented in release notes.

Please either keep aggregate subtraction for visuals that present reconciled equations/components, or redesign those visuals and labels so the independently clamped metrics are not presented as arithmetic identities. Also update the PR title/body now that the FX change has been reverted and remove the claim that the savings portion was verified clean while these blocking threads remain unresolved.

…hboard tiles

Roland Krummenacher and MSBrett both flagged that summing clamped,
row-level savings columns (x_TotalSavings, x_CommitmentDiscountSavings,
x_NegotiatedDiscountSavings) for the result side of 6 tiles, while
leaving ListCost/ContractedCost/EffectiveCost as raw aggregates on the
operand side, broke the on-screen equations those tiles visually
promise (e.g. "List - Contracted = Negotiated savings"). Clamping
doesn't distribute over sums, so the two sides no longer reconciled.

Rather than reverting to raw subtraction (which would silently
reintroduce the original per-row clamping bug for just these tiles),
derive the displayed List/Contracted cost operands from the same
clamped basis as the results, anchored on EffectiveCost (the only
value that's never negative-clamped):

  ContractedCost' = EffectiveCost + sum(x_CommitmentDiscountSavings)
  ListCost'        = ContractedCost' + sum(x_NegotiatedDiscountSavings)
  TotalSavings'    = sum(x_NegotiatedDiscountSavings) + sum(x_CommitmentDiscountSavings)

Applied to:
- Savings breakdown waterfall, monthly (e17346d1) and daily (f1ef29df)
- Total/Negotiated/Commitment savings bar (f5f240a8)
- Cost + savings stacked area chart (dcc1f533) - already reconciled by
  construction; added a clarifying comment only
- Effective Savings Rate, monthly (e1bc2d51) and daily (cb34f22b)

This changes the ESR definition on the two ESR tiles, since TotalSavings
and ListCost are now derived consistently rather than mixing a clamped
numerator with a raw denominator. Added a release note under FinOps
hubs v15 in docs-mslearn/toolkit/changelog.md per review feedback.

Verified against the counterexample from review: rows (List, Contracted,
Effective) = (100,120,80) and (100,80,60) give NegotiatedDiscountSavings=20,
CommitmentDiscountSavings=60, Effective=140 -> ContractedCost'=200 ->
ListCost'=220; all displayed equations hold exactly.

The other ~15 tiles' clamped-sum fix from the original PR is unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@microsoft-github-policy-service microsoft-github-policy-service Bot added Needs: Review 👀 PR that is ready to be reviewed and removed Needs: Attention 👋 Issue or PR needs to be reviewed by the author or it will be closed due to no activity labels Aug 26, 2026
…on dashboard

- Cost summary derives Contracted/List from clamped savings so it reconciles with Savings summary
- Total savings = Negotiated + Commitment in pies, trends, resource tiles
- Savings by month grid uses the derived ESR basis (excludes commitment purchases)

Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@microsoft-github-policy-service microsoft-github-policy-service Bot added Needs: Review 👀 PR that is ready to be reviewed and removed Needs: Attention 👋 Issue or PR needs to be reviewed by the author or it will be closed due to no activity labels Oct 18, 2026
…/2093-dashboard-savings-formulas

# Conflicts:
#	docs-mslearn/toolkit/changelog.md

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed at 0c226999. All three items from my last round are implemented as described: Cost summary is on the derived basis and reconciles with Savings summary beside it; every displayed "Total savings" is now sum(neg) + sum(comm) (no x_TotalSavings left on screen); Savings breakdown by month derives List cost and ESR the same way as the ESR tiles. I re-ran the counterexample across all tiles and the Rate optimization page reconciles end to end (220 / 200 / 140 → 20 / 60 / 80, ESR 36.4%). The merge from v15-prep only touched markdown in dashboard.json.

Two things still need to be settled before this can go in, details inline:

  1. Commitment purchases still count as savings on the Summary page, which conflicts with #2352.
  2. Rate optimization uses three different filters for purchase rows.

Minor: the PR title and description are stale (FX fallback, x_TotalSavings, "6 tiles").

Given how visible these numbers are, I'll also walk through the deployed dashboard against a live hub (purchase month, non-USD, EA and MCA) before approving.

Comment thread src/templates/finops-hub/dashboard.json Outdated
Comment thread src/templates/finops-hub/dashboard.json Outdated
Comment thread src/templates/finops-hub/dashboard.json Outdated
Comment thread docs-mslearn/toolkit/changelog.md Outdated
…urchase filter; NaN guard

- Summary queries: sumif(x_*Savings, x_AmortizationClass != 'Principal') on the derived basis (combined form of #2352)
- Rate optimization: use x_AmortizationClass != 'Principal' everywhere; drop local re-derivations
- ESR: iff(ListCost == 0, real(null), ...) to avoid NaN
- Fix stale stacked-chart comment; move changelog entry to Unreleased v16 and fix tile names

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@flanakin Michael Flanakin (flanakin) changed the title Fix Data Explorer dashboard savings formulas and FX fallback Fix Data Explorer dashboard savings formulas and exclude commitment purchases Oct 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed at 5f764371. All four items from my last round are implemented as described:

  • All 9 Summary queries use sumif(x_*Savings, x_AmortizationClass != 'Principal'), and Effective cost stays unfiltered.
  • Every Rate optimization tile filters with x_AmortizationClass != 'Principal'. The local re-derivations in 3b3f0a58 and 0341b3e4 are gone, and so is the ChargeCategory == 'Usage' or isempty(CommitmentDiscountId) filter. Removing the re-derivation is safe because ingestion fills x_AmortizationClass when an export doesn't provide it.
  • The stale comment in dcc1f533 is fixed.
  • The changelog entry is under Unreleased → FinOps hubs v16, with the correct tile names.

The tiles that still use plain sum(x_*Savings) are fine: d7f31381 and 13cad52d already filter to ChargeCategory == 'Usage', and 3886b5cd only shows Effective cost. One nit inline. LGTM.

Comment thread src/templates/finops-hub/dashboard.json Outdated
… rate

Address PR #2248 feedback.

Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Address PR #2341 feedback.

Co-Authored-By: RolandKrummenacher <RolandKrummenacher@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs-mslearn/toolkit/changelog.md
…/2093-dashboard-savings-formulas

# Conflicts:
#	docs-mslearn/toolkit/changelog.md
#	src/templates/finops-hub/dashboard.json
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…log entry

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@flanakin Michael Flanakin (flanakin) changed the title Fix Data Explorer dashboard savings formulas and exclude commitment purchases fix(hubs): Fix Data Explorer dashboard savings formulas and exclude commitment purchases Oct 29, 2026
@flanakin

Copy link
Copy Markdown
Collaborator Author

🤖 [AI] Merged origin/dev (conflicts were stale v15-prep docs; took dev, then re-added the Unreleased v16 changelog entry). Added HubsDashboardSavings.Tests.ps1: guards against raw-aggregate savings, the legacy purchase filter, missing Principal exclusion on Summary queries, and ESR zero-list-cost guard. Brett Wilson (@MSBrett) your 3 blocking points (false equations, decomposition, stack) were addressed by the derived-operand basis in earlier commits (verified by Roland Krummenacher (@RolandKrummenacher), who approved at 5f76437); please re-review.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs: Review 👀 PR that is ready to be reviewed Tool: FinOps hubs Data pipeline solution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants