Skip to content

feat(iac): offer dashboard tile alerts for Terraform import - #3076

Open
jordan-simonovski wants to merge 15 commits into
mainfrom
jordansimonovski/iac-export-tile-alerts
Open

feat(iac): offer dashboard tile alerts for Terraform import#3076
jordan-simonovski wants to merge 15 commits into
mainfrom
jordansimonovski/iac-export-tile-alerts

Conversation

@jordan-simonovski

@jordan-simonovski jordan-simonovski commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Terraform export skips every alert that is not a saved-search alert, because that was all the provider could model. terraform-provider-clickhouse#683 adds source = "tile" with dashboard_id and tile_id to clickhouse_clickstack_alert, and lands in provider 3.28.0, so this offers tile alerts for import too, minus the ones the provider could not address. Closes HDX-5285.

This depends on provider 3.28.0 publishing first. #683 merged on 9 September and is in the v3.28.0 release, but that release is still a draft, so the newest published version is 3.27.0 and a file carrying a tile alert asks for a >= 3.28.0 that cannot resolve — terraform init fails for the whole file. Please hold this until v3.28.0 is out. The floor this PR originally carried, >= 3.26.0, was wrong in a worse way than not resolving: 3.26.0 and 3.27.0 both published without source = "tile", so it would have let Terraform install a provider that accepts the file and then fails on apply, and a lockfile already pinned to 3.26.0 would have kept satisfying it.

What changed

isImportableAlert now accepts source === 'tile'; inline alerts stay excluded, since clickhouse_clickstack_alert has no attributes for the chart config one carries. A tile alert is withheld when the provider could not address its tile, and that judgement is made server-side in two places — the import manifest that Team Settings reads, and the alerts listing the row menu reads. Neither response ships a dashboard's sibling tile names, so the client cannot make the call itself: the alerts response filters dashboard.tiles down to the alert's own tile, which means a client-side duplicate check would pass for every alert.

The generated file's provider floor is per-file rather than global. Only an export that actually contains a tile alert asks for >= 3.28.0; anything else still installs on 3.25.x, so this does not force a provider upgrade on people exporting dashboards and saved searches. When a tile alert is present the file gains a note about the hand edit its generated config needs, and the per-alert popover carries a one-line version of the same caveat.

Key decisions

Withholding an unaddressable tile alert rather than exporting it with a warning. The import itself would succeed — the import id is the alert's own — but the config it generates can only pin the literal tile id the server holds today, and the durable form is a tile_ids["<tile name>"] reference that does not exist for a blank or duplicated name. The failure would land at apply time, on the alert, long after the export.

Tile alerts on provisioned dashboards are withheld for the same reason isImportableDashboard refuses the dashboard: ProvisionDashboardsTask rewrites those tiles wholesale, so the tile an alert is bound to can be replaced underneath Terraform. Alerts on a dashboard withheld for an unexportable tile are still offered, because that dashboard is not Terraform-managed at all and its tile ids therefore stay put.

Tile names are compared trimmed. If the provider trims its tile_ids keys then "Errors" and "Errors " collide, and treating them as distinct would emit a reference that cannot resolve; if it does not trim, the cost is withholding two alerts that would have worked.

Background

The provider cannot let you author a tile id, because the server assigns them. It exposes them instead as a computed tile_ids map on clickhouse_clickstack_dashboard, keyed by tile name, which is what makes a unique non-blank name load-bearing for a tile alert. Per #683 the provider's tile-id carry-forward matches unique names before position, so an alerted tile with a unique name keeps its id across dashboard applies — the condition this change gates on is the same one that keeps the reference stable.

Impact

Tile alerts appear in the Team Settings export and the alerts row menu once the provider ships. The skip copy names both exclusions. One extra bounded Mongo read on GET /iac/import-manifest, skipped entirely for teams with no tile alerts and drawing on the request's existing 10s budget rather than a fresh one, and the alerts listing gains an optional unaddressableTile marker.

Implementation detail

isTileAlertUnaddressable in packages/common-utils/src/iacEligibility.ts is the single definition both surfaces use, composed from isAddressableTile plus the provisioned check. The manifest's tile-name lookup lives in packages/api/src/utils/iacTileAlerts.ts — it needs alerts.items before it knows which dashboards to read, so it cannot join the existing Promise.all. It is team-scoped and bounded by its own $in, whose ids come from a listing already capped at IAC_MANIFEST_LIMIT. A dashboard that is gone, or belongs to another team, reads as unaddressable, which withholds an alert that would have worked rather than offering one that would not.

IacResourceRef.tileAlert only decides the version floor; the address and import id of a tile alert are unchanged. buildImportFile derives the floor from the resources rather than taking it as an argument, so a call site cannot forget to pass it.

Tests: isTileAlertUnaddressable and the trimmed-name rule in packages/common-utils/src/__tests__/iac.test.ts, plus a guard that the tile-alert floor never drops below the base floor; the manifest marker for duplicate, blank, dangling-tile, provisioned, cross-team and deleted-dashboard cases in iac.int.test.ts; the alerts-listing marker for duplicate names, provisioned dashboards and a deleted dashboard in alerts.int.test.ts; and the row menu now opens the export panel and pins the >= 3.28.0 floor, so the two surfaces cannot drift. That floor guard reads the provider's version line with an anchored match; unanchored, it hit required_version = ">= 1.5.0" first and compared 1.5.0 to itself, so it could not fail.

One accepted gap: during a rolling deploy a new bundle against a not-yet-upgraded API pod sees no unaddressableTile and offers every tile alert. The field's JSDoc documents it. The worst case is an import whose reference needs a hand fix, which did not seem worth a capability-handshake field on the manifest.

@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hyperdx-oss Ready Ready Preview Sep 11, 2026 10:41pm UTC
hyperdx-storybook Ready Ready Preview Sep 11, 2026 10:41pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ef53c21

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/common-utils Minor
@hyperdx/api Minor
@hyperdx/app Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@greptile-apps

greptile-apps Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR extends Terraform export eligibility to addressable dashboard-tile alerts while retaining fail-closed behavior for alerts whose tiles cannot be referenced safely.

  • Computes tile-alert addressability server-side for both the import manifest and alerts listing.
  • Applies the provider 3.28.0 floor only to exports containing tile alerts.
  • Adds generated-file and per-alert guidance for replacing imported literal tile identifiers with durable dashboard references.
  • Enforces the manifest’s remaining wall-clock budget around the additional dashboard lookup.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

The wall-clock race and spent-budget short circuit now keep the optional dashboard lookup within the manifest’s remaining deadline while returning the other manifest listings fail-closed.

Important Files Changed

Filename Overview
packages/api/src/utils/iacTileAlerts.ts Resolves tile-alert addressability within the remaining request budget and fails closed on timeout or lookup failure.
packages/api/src/routers/api/iac.ts Adds the bounded dashboard lookup and marks unaddressable tile alerts in the import manifest.
packages/api/src/routers/api/alerts.ts Adds the server-computed tile-addressability marker to alert-list responses.
packages/common-utils/src/iacEligibility.ts Extends alert eligibility to tile alerts and centralizes unique, nonblank tile-name validation.
packages/common-utils/src/iac.ts Generates tile-alert imports with the required provider floor and durability guidance.
packages/app/src/components/alerts/AlertRowMenu.tsx Offers Terraform export only for alerts that satisfy the shared eligibility contract.

Reviews (13): Last reviewed commit: "Merge branch 'main' into jordansimonovsk..." | Re-trigger Greptile

Comment thread packages/api/src/routers/api/iac.ts Outdated
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Deep Review

Scope: 18 files vs base 41eee7d3 — tile-alert Terraform import across common-utils (eligibility + HCL generation), api (manifest + alerts responses, new bounded Mongo read), and app (Team Settings export + alert row menu), with tests.
Intent: Make dashboard tile alerts importable behind a server-computed unaddressableTile marker; raise the generated provider floor to >= 3.28.0 only for files that carry a tile alert.

🔴 P0/P1 — must fix

  • packages/common-utils/src/iac.ts:41 — Exporting a tile alert emits a file pinned to version = ">= 3.28.0"; until that provider release is actually published, terraform init cannot resolve the constraint and fails for the whole file.
    • Fix: Block merge until the provider release carrying source = "tile" is published, and confirm TERRAFORM_PROVIDER_TILE_ALERT_VERSION_CONSTRAINT matches that published version.
    • previous-comments

🟡 P2 — recommended

  • packages/app/src/components/alerts/AlertRowMenu.tsx:113canExport hand-projects { source, savedSearchId, unaddressableTile } into isImportableAlert, while collectImportableResources passes the whole manifest entry; a future predicate input would arrive here as undefined with no compile error, letting the two eligibility surfaces silently diverge — the exact drift the shared predicate exists to prevent.
    • Fix: Pass the alert object whole (isImportableAlert(alert)), which type-checks since AlertsPageItem is structurally assignable to the parameter.
    • previous-comments
  • packages/common-utils/src/iac.ts:270 — The tile-alert notice instructs the user to import the alert's dashboard under Terraform when it is not already in the file, but a dashboard skipped as unexportableTiles (e.g. a PromQL tile alongside a uniquely-named alerted tile) is withheld while its tile alert is still offered; following the advice runs the apply that deletes the alerted tile.
    • Fix: Qualify the guidance to exclude dashboards listed as skipped, or scope the notice to dashboards that pass isImportableDashboard.
    • previous-comments
🔵 P3 nitpicks (4)
  • packages/common-utils/src/iac.ts:326 — When a tile alert is present the requires ${providerVersionConstraint(tileAlerts)} bullet reads >= 3.28.0 but keeps the base-floor rationale (server-only ids / provider fix: correct handling of gauge metrics in renderChartConfig #654), stating the wrong reason for the higher floor; the correct reason lives in the notice below.
    • Fix: Pin this bullet's version to TERRAFORM_PROVIDER_VERSION_CONSTRAINT and let tileAlertNotice own the tile-alert floor rationale.
  • packages/api/src/utils/iacTileAlerts.ts:135 — A transient read failure or spent budget marks perfectly addressable alerts unaddressableTile: true, which the UI and downloaded .tf header then explain as a tile-naming rule with no retry affordance; the degraded state is only visible server-side via the counter/log.
    • Fix: Carry a distinct transient signal (e.g. a manifest-level lookup-unavailable flag) so the notice can say the check could not run rather than blaming tile names.
  • packages/common-utils/src/iacEligibility.ts:50tileName re-implements normalizeName (packages/api/src/utils/alerts.ts) over the same untrusted Mongo values without naming the twin; the cross-package boundary may force duplication but the DRY convention wants a pointer.
    • Fix: Add a doc-comment referencing normalizeName, or lift the shared helper into common-utils.
  • packages/app/src/components/TeamSettings/__tests__/IacMigrationSection.test.tsx:137 — The download test asserts banner-form skip copy but nothing asserts the file-form (no "will be skipped") notice reaching buildImportFile, so passing { willBeSkipped: true } at that call site would regress silently.
    • Fix: Assert the generated .tf content contains the file-form skip notice.

Reviewers (8): correctness, reliability, kieran-typescript, api-contract, adversarial, testing, maintainability, previous-comments.

Testing gaps:

  • No test pins the AlertRowMenuisImportableAlert call site against a newly-added predicate input arriving undefined.
  • File-header (non-banner) rendering of skip notices is unasserted on both the app and buildImportFile sides.

Comment thread packages/api/src/routers/api/iac.ts
Comment thread packages/app/src/components/TeamSettings/IacMigrationSection.tsx Outdated
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

PR Review

2 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 2 minor

2 posted as inline comment(s) on the changed lines.


Severity is the reviewer's own estimate and is used for ordering, not filtering.

`isImportableAlert` only accepted saved-search alerts, because that was all
`clickhouse_clickstack_alert` modelled. Provider 3.26.0 adds `source = "tile"`
with `dashboard_id` and `tile_id`, so tile alerts are now offered too — unless
the provider could not address the tile: the `tile_ids` map it references is
keyed by tile name and omits any tile whose name is blank or shared, and a
provisioned dashboard's tiles are rewritten wholesale by
ProvisionDashboardsTask.

Both decisions are made server-side, on the import manifest and on the alerts
listing, because neither response carries a dashboard's sibling tile names —
the alerts response filters `dashboard.tiles` down to the alert's own tile, so
a client-side check could never see a duplicate.

A generated file that carries a tile alert asks for `>= 3.26.0` and explains
the hand edit its config needs; an export without one still installs on 3.25.x.
… manifest budget

The alerts listing computed `unaddressableTile` inside the `alert.dashboard`
spread, so a tile alert whose dashboard had been deleted came back with no
marker at all and the row menu offered it for import — the one case the
manifest already withheld. The marker now hangs off the alert's source, where a
null dashboard reaches `isTileAlertUnaddressable` and reads as unaddressable.

The tile-name read is bounded by its own `$in`, not by the `limit` it carried:
the ids come from a listing already capped at IAC_MANIFEST_LIMIT, so the cap
could never bind and the comment describing what happens past it described
nothing. It now takes what is left of the request's 10s budget rather than a
fresh one, so sequencing it after the six listings cannot double the worst case.

Also: the generated file's tile-alert notice said nothing about an export whose
dashboard is absent, `providerVersionConstraint` claimed to compare two floors
when it picks one (with a test to catch a base-floor bump past the tile-alert
floor), and the manifest's schema comment pointed at the wrong predicate.
The e2e alerts spec still asserted the Terraform export was hidden for a tile
alert. Its tile has a unique name on a dashboard nothing else manages, so the
export is offered now and the assertion was wrong, not the behaviour.

The dashboards read that resolves tile addressability decides one optional
marker, but it runs last on a shared budget and a MaxTimeMSExpired took the
whole manifest with it — the other six listings are the export. It now
withholds every tile alert instead of failing the request, with a counter and
a log line so the degraded export is visible rather than looking like a team
that has no tile alerts.

Also: the skip reasons lived in two places and had to be hand-edited in both
when tile alerts became eligible, so both renderings now come from one table;
the withheld counter described one of its three reasons; and the popover's doc
comment still said the provider models only saved-search alerts.
@jordan-simonovski
jordan-simonovski force-pushed the jordansimonovski/iac-export-tile-alerts branch from 188597a to 7c1f5a6 Compare September 6, 2026 23:38
@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 539 production lines changed (Tier 2 max: < 250)
  • Cross-layer change: touches frontend (packages/app) + backend (packages/api) + shared utils (packages/common-utils)
  • Touches API routes or data models — hidden complexity risk

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 10
  • Production lines changed: 539 (+ 710 in test files, excluded from tier calculation)
  • Branch: jordansimonovski/iac-export-tile-alerts
  • Author: jordan-simonovski

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

Comment thread packages/api/src/routers/api/iac.ts Outdated
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 357 passed • 1 skipped • 1488s

Status Count
✅ Passed 357
❌ Failed 0
⚠️ Flaky 1
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

// Withheld rather than offered unchecked: the export comes up short, which
// both the generated file and the UI report, instead of carrying an alert
// whose reference cannot resolve.
if (dashboards == null) return allTileAlertIds();

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.

🔵 minor — A failed tile-name lookup is reported to the user as a tile-naming problem

The fail-closed path reuses unaddressableTile: true, which is the only signal the manifest carries, so a transient Mongo error or MaxTimeMSExpired makes IacMigrationSection render "N alerts will be skipped — ... an alerted tile needs a unique, non-blank name on a dashboard Terraform can own" (packages/app/src/components/TeamSettings/IacMigrationSection.tsx:95) and bake that same false reason into the downloaded hyperdx-import.tf header, with no error banner and only a server-side counter to say otherwise. Add a distinct manifest-level marker (e.g. tileAlertCheckUnavailable: true alongside truncatedTypes) so the UI and the generated file can say the check could not run instead of blaming the user's tile names.

# generated for a dashboard, and applying that churns its tile ids and
# deletes the tile alerts attached to them.
# * Re-exporting later does NOT produce an additive file. It re-emits every
# requires ${providerVersionConstraint(tileAlerts)} — older providers write

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.

🔵 minor — Generated file gives the 3.25.0 rationale for the 3.26.0 floor

The bullet now interpolates providerVersionConstraint(tileAlerts) but keeps the reason clause that belongs to the base floor, so a file with a tile alert reads "requires >= 3.26.0 — older providers write server-only ids into the config generated for a dashboard" — which is the provider #654 / 3.24.1 issue, not why 3.26.0 is needed. Keep the base sentence pinned to TERRAFORM_PROVIDER_VERSION_CONSTRAINT and let the tileAlertNotice below it own the 3.26.0 rationale it already states.

// client cannot tell whether a sibling tile shares its name.
const canExport =
IS_IAC_EXPORT_ENABLED &&
isImportableAlert({

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.

🔵 minor — Row menu hand-picks the predicate's inputs instead of passing the alert

isImportableAlert used to receive alert whole; it now receives a three-field literal, so a fourth eligibility input added to the predicate would reach collectImportableResources (packages/common-utils/src/iac.ts:390, which still passes the manifest entry whole) but silently arrive as undefined here — exactly the drift the comment above this call says the shared predicate prevents. AlertsPageItem now carries unaddressableTile, so isImportableAlert(alert) type-checks; revert to passing alert.

The branch put api's eslint-disable count and common-utils' as-any count above
their baselines. The api one only silenced a warning on a test mock, and one of
the two common-utils hits was prose in a doc comment matching `as any` — the
phantom count the ratchet's own comment warns about.

AlertRowMenu's suite waited out Mantine's dropdown transition with 5s timeouts
and a 15s per-test budget, and still timed out under parallel workers. Turning
transitions and portals off with `env="test"` makes the dropdown present on
click, so the padding is gone. One assertion had to move ahead of its click:
it read the menu after picking an item, which only worked while the closing
transition kept the dropdown mounted.
Comment thread packages/common-utils/src/__tests__/iac.test.ts Outdated
// client cannot tell whether a sibling tile shares its name.
const canExport =
IS_IAC_EXPORT_ENABLED &&
isImportableAlert({

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.

🔵 minor — Hand-projecting the alert into isImportableAlert re-creates the sync burden the predicate exists to remove

isImportableAlert({ source, savedSearchId, unaddressableTile }) re-lists the predicate's inputs at the call site. AlertsPageItem is structurally assignable to the parameter type (source?: AlertSource widens to string?, and excess-property checking does not apply to a variable), so isImportableAlert(alert) still compiles — and it is the form that cannot go stale. As written, the next field added to the predicate arrives here as undefined with no compile error, which is precisely how a tile alert would be offered for import despite a server marker saying otherwise. Revert to passing alert.

The remaining budget was floored at a second, so a request that had already
spent the ten-second ceiling on the six concurrent listings still handed the
sequential dashboard read another second. The remainder is passed through
unclamped now, and the read is skipped when there is nothing left of it —
mongo reads `maxTimeMS: 0` as "no limit", so passing it through would have
removed the bound rather than tightened it.

Skipping withholds every tile alert, which is the answer an expired read
already gave, and the log and counter that report it are now one helper shared
by both paths.
…export-tile-alerts

# Conflicts:
#	packages/app/src/components/alerts/__tests__/AlertRowMenu.test.tsx
// passed through — and a floor under it would push the request past the
// ceiling this read is meant to stay inside. Skipped rather than run
// unbounded, which lands on the same answer an expired read gives.
if (maxTimeMS <= 0) {

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.

🔵 minor — A failed or budget-starved tile-alert lookup is reported to the user as a tile-naming rule

Both fail-closed paths here (maxTimeMS <= 0 after the six listings consume IAC_MANIFEST_MAX_TIME_MS, and the dashboards == null rejection at line 112) return every tile alert id, so packages/api/src/routers/api/iac.ts:244 stamps unaddressableTile: true on alerts that are perfectly addressable. The client cannot tell the two apart: IacMigrationSection.tsx:95 then tells the user "N alerts will be skipped — ... an alerted tile needs a unique, non-blank name on a dashboard Terraform can own", which is false, and the same wrong reason is baked into the downloaded .tf header. A retry produces a different file with no explanation. Carry a distinct signal for the transient case (e.g. a manifest-level tileAlertsUnresolved flag) and word that notice as a lookup failure the user can retry, rather than as an eligibility rule.

# generated for a dashboard, and applying that churns its tile ids and
# deletes the tile alerts attached to them.
# * Re-exporting later does NOT produce an additive file. It re-emits every
# requires ${providerVersionConstraint(tileAlerts)} — older providers write

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.

🔵 minor — Generated file states the 3.25.0 rationale against the 3.26.0 floor

The bullet now interpolates providerVersionConstraint(tileAlerts), so a file carrying a tile alert reads "check your own declaration requires >= 3.26.0 — older providers write server-only ids into the config generated for a dashboard...". That justification belongs to provider #654, fixed in 3.24.1 (per the comment at line 34), so it is untrue of 3.24.1–3.25.x — the actual reason for the higher floor is the tile-alert bullet immediately below. Keep this bullet pinned to TERRAFORM_PROVIDER_VERSION_CONSTRAINT and let tileAlertNotice own the 3.26.0 requirement, so each version is stated with its own reason.

// banner above is cached-manifest state, the file must describe what
// was actually written.
skipNotices: skipNoticesFor(freshSelection),
skipNotices: skippedRules(freshSelection).map(n => n.text),

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.

🔵 minor — The file-header rendering of the skip notices is untested

skippedRules now serves both renderings, and the only difference between them is the willBeSkipped flag. Every assertion covers the banner variant (IacMigrationSection.test.tsx:118, :246); nothing asserts what reaches buildImportFile's skipNotices, and iac.test.ts never passes skipNotices either. Passing { willBeSkipped: true } at this call site — the exact mistake the shared table invites — would emit "1 alert will be skipped" under the file's "Some resources were left out as ineligible:" heading with every test still green. Add an assertion in the download test that the generated content contains the file-form notice (no "will be skipped").

Comment thread packages/common-utils/src/iac.ts Outdated
# this file too, replace them with references —
# clickhouse_clickstack_dashboard.<name>.id and
# ...tile_ids["<tile name>"] — before you apply. If it is not, the literals
# hold only while nothing else applies that dashboard, so import it under

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.

🔵 minor — Tile-alert notice tells the user to import a dashboard the same file refuses to export

A tile alert is withheld only when its dashboard is missing or provisioned (isTileAlertUnaddressable, iacEligibility.ts:102) — not when the dashboard was skipped for unexportableTiles. So for a dashboard carrying a PromQL tile plus an alerted tile with a unique name, one file contains the alert's import block, the skip line "1 dashboard — a tile on them cannot be represented by the provider, and importing one would delete that tile on the next apply", and this bullet telling the user to "import it under Terraform before you do" — advice that, followed, deletes the very tile the skip line warns about. Qualify the sentence ("unless the dashboard is listed as skipped above"), or make the notice name only dashboards that passed isImportableDashboard.

* the cost is withholding two alerts that would have worked. Wrong in the safe
* direction.
*/
function tileName(tile: { config?: unknown } | undefined): string | undefined {

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.

🔵 minortileName re-implements the existing normalizeName without naming the twin

normalizeName at packages/api/src/utils/alerts.ts:53 is the same operation over the same untrusted-Mongo values (unknown → non-string is none, trim, blank is none), and is already used on tile.config.name at alerts.ts:118 for a tile alert. iacEligibility.ts cannot import from packages/api, so a twin may be necessary — but the DRY convention requires naming it: either move normalizeName into common-utils and have both call it, or doc-comment tileName with a pointer to packages/api/src/utils/alerts.ts:53.

@wrn14897

wrn14897 commented Sep 9, 2026

Copy link
Copy Markdown
Member

@jordan-simonovski Lmk if P0 is concerning

…export-tile-alerts

# Conflicts:
#	packages/app/src/components/alerts/__tests__/AlertRowMenu.test.tsx
Provider #683 published in 3.28.0, not 3.26.0 — 3.26.0 and 3.27.x went out
without it. A file asking for `>= 3.26.0` lets `terraform init` pick a provider
that then rejects `source = "tile"` at apply time.
@jordan-simonovski

Copy link
Copy Markdown
Contributor Author

@jordan-simonovski Lmk if P0 is concerning

@wrn14897 I've resolved the P0. Thanks for reviewing!

noun: 'alert',
count: c => c.skippedAlerts,
reason:
'the provider models saved-search and dashboard tile alerts, and an alerted tile needs a unique, non-blank name on a dashboard Terraform can own',

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.

🔵 minor — Skipped-alert reason no longer explains inline alerts, the other thing that lands in that count

collectImportableResources increments the single skippedAlerts counter for both unaddressable tile alerts and inline alerts (isImportableAlert in packages/common-utils/src/iacEligibility.ts:29 returns false for source: 'inline', and inline alerts are a shipped feature — see EditInlineAlertModal). The old copy ("the provider only supports saved-search alerts") covered both; the new single reason only describes the tile-name rule, so a team whose skipped alerts are all inline reads "an alerted tile needs a unique, non-blank name on a dashboard Terraform can own" — which does not apply to any of them. This text is rendered both in the banner and, via skipNotices, into the committed .tf header. Extend the reason to name inline alerts too ("...and an inline alert's chart config has no provider representation"), or split skippedAlerts into two counted rules.

* provider version the file asks for — the address and the import id of a
* tile alert match every other alert's.
*/
tileAlert?: boolean;

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.

🔵 minortileAlert on the shared IacResourceRef is representable on resource types where it is meaningless

{ type: 'dashboard', id, tileAlert: true } type-checks, and buildImportFile (line 263) would then raise the whole file's provider floor to >= 3.28.0 for an export containing no alert at all — failing terraform init until 3.28.0 publishes, for nothing. Make IacResourceRef a discriminated union on type so tileAlert only exists on the 'alert' variant, or drop the field and have buildImportFile derive the floor from the manifest's source the way collectImportableResources already does at line 403.

* the cost is withholding two alerts that would have worked. Wrong in the safe
* direction.
*/
function tileName(tile: { config?: unknown } | undefined): string | undefined {

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.

🔵 minortileName re-implements an existing untrusted-name normalizer and tile lookup

normalizeName at packages/api/src/utils/alerts.ts:53 is the same operation (unknown → trimmed non-blank string or nullish), and deriveAlertDisplayFields at packages/api/src/utils/alerts.ts:112-118 already performs the exact composition isAddressableTile opens with — Array.isArray(dashboard.tiles), tiles.find(t => t.id === tileId), then normalize tile?.config?.name. Per the repo's REQUIRED DRY rule, either lift normalizeName (and the tile lookup) into common-utils and have both sites import it, or doc-comment tileName naming its twin and why they must coexist across the package boundary.

A comment naming a release goes stale silently the next time the floor moves.
The constant and the assertions that check it still name the version — those
fail loudly instead — and the comments now say what holds regardless: #683 is
the provider change, and the shipping release is the thing to look up before
moving the floor.
Comment thread packages/api/src/routers/api/iac.ts
label: 'Provider setup',
collapsible: true,
hint: 'Add once per Terraform module. Skip if your project already declares the ClickHouse provider.',
hint: `Add once per Terraform module. Skip if your project already declares the ClickHouse provider${

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.

🔵 minor — The >= 3.28.0 requirement is only visible inside the collapsed "Provider setup" block, whose own copy tells the affected users to skip it

The tile-alert version caveat is appended to the Provider setup hint, and that hint is rendered inside <Collapse> in TerraformHelperPanel.tsx:40-46 — so it is hidden until the user clicks "Show provider setup". The users who hit the failure are exactly the ones who already declare the ClickHouse provider (possibly pinned at the old >= 3.25.0 floor) and therefore never expand that section; the always-visible "Import block" hint mentions the hand edit but not the version. Add the floor to the Import block hint (e.g. "Requires ClickHouse provider >= 3.28.0."), or move the hint outside the Collapse in CollapsibleSnippet. Note the new test at AlertRowMenu.test.tsx:170 asserts toBeInTheDocument(), which Mantine's always-mounted Collapse satisfies while collapsed, so it does not pin visibility.

maxTimeMS caps what mongo spends executing, not the wait for a pool
connection, server selection or the socket, so the sequential tile-alert
read could still push GET /iac/import-manifest past the ten seconds it
documents. A timer now races the read for what is left of the budget and
lands on the same fail-closed answer the error and spent-budget branches
already gave.

The failure counter dedupes so one request cannot count as two, but both
logs fire: when the read the timer walked away from rejects afterwards,
that late error names the failure the timeout could only guess at.
# generated for a dashboard, and applying that churns its tile ids and
# deletes the tile alerts attached to them.
# * Re-exporting later does NOT produce an additive file. It re-emits every
# requires ${providerVersionConstraint(tileAlerts)} — older providers write

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.

🔵 minor — Generated file justifies the 3.28.0 floor with the 3.25.0 rationale, which is false for 3.25–3.27

The bullet now reads "check your own declaration requires >= 3.28.0 — older providers write server-only ids into the config generated for a dashboard, and applying that churns its tile ids…", but that consequence only applies below 3.25.0 (provider #654, per the comment on TERRAFORM_PROVIDER_VERSION_CONSTRAINT at iac.ts:31-37). Every tile-alert export tells the reader that 3.26.0/3.27.0 churn tile ids, which they do not. Keep the interpolated floor but make the reason conditional too — the source = "tile" reason is already stated in tileAlertNotice, so the tile-alert case only needs the version, not the dashboard-id-churn clause.

// Withheld rather than offered unchecked: the export comes up short, which
// both the generated file and the UI report, instead of carrying an alert
// whose reference cannot resolve.
if (dashboards == null) return allTileAlertIds();

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.

🔵 minor — A failed or timed-out tile lookup is reported to the user as "the tile has no unique, non-blank name"

On a read failure, an exhausted budget, or the wall-clock timer (all reachable on a slow Mongo — this read runs last on what is left of IAC_MANIFEST_MAX_TIME_MS), every tile alert is returned with unaddressableTile: true. The client cannot distinguish that from a genuine verdict, so IacMigrationSection.tsx:95 renders "N alerts will be skipped — … an alerted tile needs a unique, non-blank name on a dashboard Terraform can own" for alerts that are perfectly addressable, and the downloaded file repeats it — the only accurate signal is the server-side counter. Return a separate manifest-level marker for "could not resolve" (like truncatedTypes) and give it its own copy, so the user is told to retry rather than to rename their tiles.

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

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants