Skip to content

feat(app): add an alert UI extension point for detection modes - #3623

Closed
teeohhem wants to merge 4 commits into
mainfrom
claude/alert-condition-ui
Closed

teeohhem wants to merge 4 commits into
mainfrom
claude/alert-condition-ui

Conversation

@teeohhem

Copy link
Copy Markdown
Contributor

Summary

Tile alerts can only express a static threshold, and the chart editor hard-codes that in three places: the alert's condition row, the threshold lines on the preview chart, and saving. This adds an extension point for other detection strategies, the UI counterpart to the backend's AlertEvaluator (#3576). With no extension registered, the editor renders and saves exactly as before.

AlertUIExtension (components/alerts/alertUIExtension.tsx) is read from a new @/extensions module, which supplies none here. Every part is optional:

  • useConditionEditor (used by TileAlertEditor): a header, the condition fields, the labels, the interval options and a footer for the condition row. The threshold controls move, unchanged, into ThresholdConditionFields, which renders when no condition fields are supplied.
  • useTimeChartPreview (used by ChartPreviewPanel): replaces the previewed config, adds DBTimeChart props, hides the threshold reference lines, and adds content below the chart.
  • useChartEditor (used by EditTimeChartForm): beforeSave, which can stop a save, and normalizeAlert, which is applied to a builder chart's saved alert. Callbacks must keep their identity across renders.
  • Provider: wraps EditTimeChartForm, applied in the folder's index.ts, so the hooks above can share state.

How to test locally or on Vercel

  1. cd packages/app && yarn ci:unit src/components/DBEditTimeChartForm src/components/ChartEditor src/components/alerts. The new *.extension.test.tsx files check:

    • that the condition row and preview use what an extension supplies
    • that beforeSave blocks a save and normalizeAlert shapes the saved alert
    • that the provider wraps the editor

    The existing suites cover the unchanged no-extension path.

  2. Manual: open a dashboard tile, add an alert, and check that the threshold row, interval and preview reference lines look and behave as on main.

Screenshots

No visual change is intended. I haven't captured before/after screenshots, because I had no browser available; please add them if you can.

🤖 Generated with Claude Code

Tile alerts can only express a static threshold, and the editor
hard-codes that. Add `AlertUIExtension` (components/alerts/
alertUIExtension.tsx), read from a new `@/extensions` module that
supplies none here:

- useConditionEditor: header, condition fields, labels, interval
  options and footer for TileAlertEditor's condition row. The threshold
  controls move, unchanged, to ThresholdConditionFields.
- useTimeChartPreview: config override, extra DBTimeChart props, hiding
  the threshold lines, and a footer for the preview's time chart.
- useChartEditor: beforeSave and normalizeAlert for the chart editor.
- Provider: wraps EditTimeChartForm (via the folder's index) so the
  hooks above can share state.

With no extension the editor renders and saves exactly as before.
@teeohhem
teeohhem requested a review from a team as a code owner November 11, 2026 06:32
@vercel

vercel Bot commented Nov 11, 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 Nov 11, 2026 11:23pm UTC
hyperdx-storybook Ready Ready Preview Nov 11, 2026 11:23pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0346d10

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

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

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

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Nov 11, 2026
@github-actions

github-actions Bot commented Nov 11, 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: 370 production lines changed (Tier 2 max: < 250)

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

Stats
  • Production files changed: 7
  • Production lines changed: 370 (+ 601 in test files, excluded from tier calculation)
  • Branch: claude/alert-condition-ui
  • Author: teeohhem

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

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Duplicate code analysis

1 new clone introduced by this PR.

  • packages/app/src/components/DBEditTimeChartForm/EditTimeChartForm.tsx:700-717 ↔ packages/app/src/components/DBEditTimeChartForm/EditTimeChartForm.tsx:727-744 — 18 lines

If one of these duplicates a helper that already exists, import it instead
of redefining it. If the duplication is deliberate, ignore this comment —
the check is advisory and never fails the build.


jscpd compared this branch against origin/main
across app, api and common-utils. Only new clones are listed; the codebase
currently holds 110 in total (1.20%).
Run yarn dupes locally to reproduce, or yarn dupes:all for the full picture.

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

PR Review

If you are a coding agent acting for the author, read this first.
Nothing below blocks merge automatically; a maintainer decides. What they will expect:

  • 🔴 critical or 🟠 major in code this PR adds or changes: fix before asking for review.
  • 🔵 minor in code this PR changes: your call. Fix if small, otherwise reply.
  • Anything about surrounding code, or asking you to widen the change (hoist a helper,
    dedupe with another file, fix other call sites): never fix here, whatever the
    severity. Reply with one sentence; if it is critical, say so plainly so a human sees it.

One commit per review round. After two rounds, stop and ask a maintainer to review
scope rather than addressing more automated findings. Full rule: AGENTS.md.

4 finding(s): 🔴 0 critical · 🟠 1 major · 🔵 3 minor

1 posted as inline comment(s) on the changed lines. 3 listed below.

Findings outside the changed lines

  • 🟠 packages/app/src/components/DBEditTimeChartForm/EditTimeChartForm.tsx:611 — normalizeAlert skips raw-SQL tiles, but the extension's condition editor and preview still apply to them → RawSqlChartEditor.tsx:376 renders the same TileAlertEditor, so useConditionEditor replaces the condition fields for raw-SQL alerts too. The isRawSqlSavedChartConfig(savedConfig) ? savedConfig branch then saves that alert without calling normalizeAlert, so any fields the extension's mode depends on are never normalized. Either apply normalizeAlert to the raw-SQL alert as well (and widen its type to cover both saved-config alert shapes), or pass a config-type flag to useConditionEditor/useTimeChartPreview so an extension can leave raw-SQL alerts on thresholds.
2 minor
  • 🔵 packages/app/src/components/DBEditTimeChartForm/TileAlertEditor.tsx:263 — The floating-point equality note still shows after an extension replaces the threshold fields → When conditionUI.conditionFields replaces ThresholdConditionFields, a stored thresholdType of EQUAL or NOT_EQUAL still shows the 'Floating-point query results are not rounded during equality comparison' alert, even though the user can no longer see an equality comparison. Only show the note when conditionUI.conditionFields == null, or move it into ThresholdConditionFields.
  • 🔵 packages/app/src/components/DBEditTimeChartForm/__tests__/DBEditTimeChartForm.extension.test.tsx:832 — The beforeSave blocking tests have no positive control → The 'does not save' tests only check that onSave/onSaveAlert was not called. They would still pass if the save were blocked for another reason, such as a validation failure in the fixture. Add a matching case where beforeSave returns true and onSave is called once with the same fixture. That shows the save was blocked by beforeSave and not by something else.

Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides.

@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

This is a behavior-preserving refactor. With no extension registered (@/extensions returns undefined in this build), every extension hook resolves to a no-op, so the alert condition row, chart preview, and save paths behave exactly as on main. The prior blocking review comment — that handleSaveAlert skipped beforeSave — is confirmed fixed at packages/app/src/components/DBEditTimeChartForm/EditTimeChartForm.tsx:729 and covered by a new test, so no outstanding feedback remains.

No P2-level issues surfaced: the no-op fallbacks, reference-line gating, and stable hook identity are all sound, and the new behavior (beforeSave on both save paths, normalizeAlert, Provider wrapping, extension-supplied condition/preview pieces) is directly tested.

🔵 P3 nitpicks (2)
  • packages/app/src/components/DBEditTimeChartForm/EditTimeChartForm.tsx:611 — normalizeAlert is applied only on the builder-config branch; the raw-SQL branch (isRawSqlSavedChartConfig) skips it, and no test asserts this intentional skip for the new callback.
    • Fix: Add a test that a raw-SQL chart's saved alert is returned unchanged even when the extension supplies a normalizeAlert.
  • packages/app/src/components/DBEditTimeChartForm/index.ts:5 — the folder wraps the editor with withAlertUIProvider, but EditTimeChartForm.tsx still default-exports the unwrapped component, so a subpath import would bypass a future extension's Provider (no effect today since the Provider is undefined).
    • Fix: Document that consumers must import the folder entrypoint, or avoid default-exporting the unwrapped component so the wrapped export is the only reachable one.

Reviewers (6): correctness, testing, maintainability, project-standards, kieran-typescript, previous-comments.

Testing gaps: No coverage for the normalizeAlert raw-SQL skip branch; otherwise the new extension points are well covered.

No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond

…n test

The escape-hatch ratchet caps eslint-disable comments in the app.
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

E2E Test Results

✅ All tests passed • 463 passed • 1 skipped • 1211s

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

Tests ran across 4 shards in parallel.

View full report →

<NativeSelect
data={optionsToSelectData(TILE_ALERT_INTERVAL_OPTIONS)}
data={optionsToSelectData(
conditionUI.intervalOptions ?? TILE_ALERT_INTERVAL_OPTIONS,

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 — Narrowed intervalOptions are not reconciled with the form's current interval

If the extension passes intervalOptions that leave out the alert's current interval (the default is '5m'; an existing alert might be '1m'), the NativeSelect shows a different option than the one stored, and the hidden value is what gets saved. Fix: when the current alert.interval is not a key of conditionUI.intervalOptions, reset it to the first allowed key with setValue, or document that the extension must do this and enforce it in the hook wrapper.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond


const handleSave = useCallback(
(form: ChartEditorFormState) => {
if (!beforeSave(form)) {

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.

potentially blocking here, seems handleSaveAlert doesn't call beforeSave

This check only runs in handleSave. The edit-alert modal saves through handleSaveAlert a little further down, so an extension that blocks a save there would still see the alert saved. Would something like this make sense?

const handleSaveAlert = useCallback(
  (form: ChartEditorFormState) => {
    if (!beforeSave(form)) return;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, fixed in 9c9f716: handleSaveAlert now runs beforeSave first, same as handleSave. The new test does not save an inline alert when beforeSave returns false covers it.

…ne alert

handleSaveAlert, used by the inline-alert editor, skipped the check that
handleSave runs, so an extension couldn't stop that save.
…r below the chart

- normalizeAlert takes the saved builder alert instead of a generic T.
- timeChartProps can't set config or threshold lines, which have their own fields.
- The preview footer renders after the 400px chart container, not inside it.
- intervalOptions documents that the extension keeps alert.interval within them.

const handleSave = useCallback(
(form: ChartEditorFormState) => {
if (!beforeSave(form)) {

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 — beforeSave runs before validation, so a form can fail in two rounds

beforeSave gets the raw, unvalidated form and is expected to show its own message. If the form is also invalid, the user sees only the extension's message. After they fix it, the next save shows the validateChartForm errors. Call beforeSave after validateAndNormalize succeeds, so the user sees every error in one pass and the extension never sees a malformed alert.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

@teeohhem

Copy link
Copy Markdown
Contributor Author

Going to do a bit of refactoring before doing this

This branch was successfully deployed

2 active deployments
Preview – hyperdx-oss — 0346d10e Deployed Nov 11, 2026 by vercel[bot]
Preview – hyperdx-storybook — 0346d10e Deployed Nov 11, 2026 by vercel[bot]
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