Skip to content

fix(recommendations): selecting one Azure row checkbox toggles two rows together #187

@cristim

Description

@cristim

Background

Reported by @cristim: clicking a single row checkbox on the Recommendations page sometimes ticks two Azure rows at once. Same UX on uncheck — unticking one unticks both. The two rows look like genuinely separate recommendations (different terms, or different subscriptions) but get coupled through the checkbox.

Likely root cause

internal/scheduler/scheduler.go:827 derives the recommendation ID from a 5-tuple:

key := fmt.Sprintf("%s:%s:%s:%s:%s", providerName, rec.Service, rec.Region, rec.ResourceType, rec.PaymentOption)
hash := sha256.Sum256([]byte(key))
recordID := hex.EncodeToString(hash[:])[:16]

The hash does not include Term or the cloud account / subscription ID. Two recs that differ only in those fields share an ID. Downstream:

  • The frontend renders both rows, each with data-rec-id="<same hash>" (frontend/src/recommendations.ts:1067-1069).
  • The selection toggle at recommendations.ts:1639-1660 walks input[data-rec-id] and matches by ID.
  • A click on one box flips the underlying selection set; the resync redraws both boxes from state.selectedRecommendations, so both end up "checked". Hence the two-rows-toggle-together behaviour.

This is most visible on Azure because Advisor frequently emits recs that vary only by subscription within the same provider/service/region/SKU/payment cell, but it's a generic bug — AWS and GCP can hit it too whenever 1yr+3yr recs exist for the same (provider, service, region, resource_type, payment) cell.

Suspected to share a root cause with the AWS-3yr-only bug — same broken hash drops one of two same-cell recs at storage time, while the surviving pairs collide on selection.

Repro

  1. On Azure, ensure at least one subscription emits two Advisor recs that share (provider, service, region, resource_type, payment) but differ in term or subscription.
  2. Open the Recommendations tab.
  3. Tick the checkbox on the first such row.
  4. Observe: the second row's checkbox ticks as well.

Acceptance criteria

  • Each rendered Recommendation row carries a globally unique data-rec-id.
  • The fix lives in the ID-hash function, not in the frontend (don't paper over it with row-index-based fallbacks).
  • A scheduler-level test seeds two recs that differ only in term and asserts they get different IDs.
  • A frontend test seeds two recs with distinct IDs in the same (provider, service, region, resource_type, payment) cell and asserts that ticking one does NOT flip the other.

Suggested fix path

Add Term and CloudAccountID (or whatever account-equivalent the rec carries) to the hash key:

key := fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s",
    providerName, accountID, rec.Service, rec.Region,
    rec.ResourceType, rec.Term, rec.PaymentOption)

Verify the hash is read at write time only — the column-filter machinery and the selection state both use rec.id opaquely, so changing the hash is safe as long as the same rec maps to the same ID in subsequent collections (deterministic across runs).

⚠ Verify before estimating effort: any persisted state keyed on the old IDs (account_credentials, purchase_executions.recommendations, plan rows) needs a re-key story or the next collection just produces fresh IDs and the old ones go stale gracefully — likely the latter, but confirm against internal/database/postgres/migrations/.

References

  • internal/scheduler/scheduler.go:817-846 — record construction + the broken hash
  • frontend/src/recommendations.ts:1067-1069 — row render
  • frontend/src/recommendations.ts:1639-1660 — selection toggle
  • frontend/src/state.ts:80-94 — selectedRecommendations Set keyed by string ID
  • Related: AWS-3yr-only bug filed separately (likely shared root cause)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions