Skip to content

feat: Support Alerting for PromQL Charts - #3114

Draft
Aryainguz wants to merge 5 commits into
hyperdxio:mainfrom
Aryainguz:feat/promql-tile-alerts
Draft

feat: Support Alerting for PromQL Charts#3114
Aryainguz wants to merge 5 commits into
hyperdxio:mainfrom
Aryainguz:feat/promql-tile-alerts

Conversation

@Aryainguz

Copy link
Copy Markdown
Contributor

Description

This PR introduces the ability to create and evaluate alerts based on PromQL charts. Previously, alerting was restricted to standard ClickHouse builder configurations, but this adds full end-to-end support for triggering alerts off of Prometheus queries.

Changes Made

  • Frontend: Enabled the 'Alert' configuration tab in the Chart Editor for PromQL based charts.
  • Schema Validation: Updated AlertChartConfigSchema and the displayTypeSupportsPromQLAlerts helper to permit PromQL chart configs in alert payloads.
  • Backend Evaluation:
    • Refactored checkAlerts/index.ts to cleanly separate the PromQL evaluation path from the standard ClickHouse builder path.
    • Added evaluatePromqlAlert which proxies the alert query directly to the Prometheus endpoint (if configured) or the prometheusQueryRange ClickHouse table function.
    • Added robust error handling and integrated cleanly into the existing alert history tracking and notification dispatching logic.
  • Testing: Added unit tests for evaluatePromqlAlert covering both the standard Prometheus API and ClickHouse proxy fallback scenarios.

Verification

  • Unit tests successfully pass (yarn ci:unit src/tasks/checkAlerts/__tests__/evaluatePromqlAlert.test.ts).
  • No lint or knip issues introduced.

Issue

Fixes #3113

@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9fae4d8

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

This PR includes changesets to release 4 packages
Name Type
@hyperdx/api Minor
@hyperdx/app Minor
@hyperdx/common-utils 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

@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

@Aryainguz is attempting to deploy a commit to the HyperDX Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds PromQL alert configuration, validation, scheduled evaluation, per-series state tracking, and Prometheus/ClickHouse query support.

Since the previous review, it:

  • Reuses the existing bounded ClickHouse PromQL range-query helper.
  • Parses ClickHouse tags in their tuple representation.
  • Records PromQL query failures with alert diagnostics and telemetry.
  • Treats unsuccessful Prometheus responses as query failures.
  • Adjusts external alert-config field classification.

Confidence Score: 5/5

The PR appears safe to merge; no new actionable issue remains after the latest changes, and all previous findings are resolved.

The latest changes correctly connect PromQL evaluation to the shared bounded query path, preserve multi-series handling, resolve source tables through loaded source metadata, and record query failures without advancing successful history.

Important Files Changed

Filename Overview
packages/api/src/tasks/checkAlerts/index.ts Adds PromQL evaluation, per-series state transitions, variable substitution, error recording, and notification integration.
packages/api/src/controllers/timeseriesEngine.ts Extracts the bounded ClickHouse PromQL range query into a reusable controller helper.
packages/api/src/routers/api/prometheus.ts Reuses the extracted ClickHouse range-query helper without changing the response path.
packages/common-utils/src/types.ts Adds PromQL chart configurations to the shared inline-alert schema.
packages/app/src/components/ChartEditor/utils.ts Preserves PromQL alert configuration when converting editor state into a saved chart.
packages/api/src/routers/external-api/v2/utils/alertChartConfig.ts Classifies the PromQL legend as evaluation-inert while retaining query fields as lossy for external round trips.
packages/api/src/tasks/checkAlerts/tests/evaluatePromqlAlert.test.ts Covers Prometheus and ClickHouse evaluation, multiple series, errors, tag conversion, and empty results.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[PromQL tile or inline alert] --> B[Scheduled alert worker]
  B --> C{Connection type}
  C -->|Prometheus endpoint| D[Prometheus query_range]
  C -->|ClickHouse| E[prometheusQueryRange table function]
  D --> F[Extract latest value per label set]
  E --> F
  F --> G[Compare each series with threshold]
  G --> H[Update grouped history and state]
  G --> I[Send firing or recovery notification]
  C -->|Query failure| J[Record retryable alert error]
Loading

Reviews (4): Last reviewed commit: "fix(promql-alerts): address PR review fe..." | Re-trigger Greptile

Comment thread packages/api/src/tasks/checkAlerts/index.ts
Comment thread packages/api/src/tasks/checkAlerts/index.ts Outdated
Comment thread packages/api/src/tasks/checkAlerts/index.ts Outdated
@Aryainguz
Aryainguz marked this pull request as draft September 11, 2026 17:41
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Deep Review

Scope: PromQL chart alerting — packages/api/src/tasks/checkAlerts/index.ts (new evaluatePromqlAlert + processAlert branch), timeseriesEngine.ts, prometheus.ts, schema widening in common-utils/src/types.ts and alertChartConfig.ts, and frontend gating. Base 3876d6b.

All twelve prior review findings (evaluator never called, wrong ClickHouse target, single-series evaluation, missing type import, schema drift-guard, return null return-type, query/client duplication, variable substitution, ungrouped-state keying, lost query-error context, ClickHouse test assertions) are addressed in the current head. No new critical issues surfaced.

✅ No critical issues found.

🟡 P2 -- recommended

  • packages/api/src/tasks/checkAlerts/index.ts:1034 -- the Prometheus fetch uses a hardcoded AbortSignal.timeout(30_000) while the ClickHouse branch of the same function uses the imported PROMETHEUS_CH_TIMEOUT_MS and the interactive proxy allows 90s, so a PromQL query that renders in the UI can consistently abort during alert evaluation and record an error every minute instead of firing.
    • Fix: Replace the literal with a shared named constant (reuse PROMETHEUS_CH_TIMEOUT_MS or a dedicated alert-HTTP timeout) so both backends and the proxy stay aligned.
    • reliability, maintainability
  • packages/api/src/tasks/checkAlerts/index.ts:1448 -- the new PromQL branch of processAlert (threshold crossing, consecutive-window firing, grouped multi-series state, missing-series auto-resolve, histories.size===0 fallback, and the error path) has no test coverage; the new unit tests exercise only evaluatePromqlAlert in isolation and never invoke processAlert.
    • Fix: Add processAlert integration tests covering firing after consecutive windows, per-group state divergence, missing-series auto-resolve, the empty-result fallback, and the error branch recording an ERROR history row.
    • testing, reliability, maintainability, previous-comments
  • packages/api/src/tasks/checkAlerts/index.ts:1475 -- the PromQL path issues one query_range over the whole window and reads only the last bucket while setting backfilledBuckets = 0, unlike the standard path which backfills each missed bucket, so after worker downtime intermediate windows that should have fired are silently skipped.
    • Fix: Evaluate each bucket in the returned series (or document and test the last-bucket-only behavior as an intentional limitation).
    • reliability
  • packages/api/src/tasks/checkAlerts/index.ts:1081 -- the ClickHouse branch constructs a new ClickhouseClient on every evaluation and never closes it, and this now runs inside the per-minute alert loop rather than a single request lifecycle, so keep-alive socket pools can accumulate.
    • Fix: Wrap the query in try/finally and close the client, or thread a shared client through the evaluator.
    • reliability
🔵 P3 nitpicks (6)
  • packages/api/src/tasks/checkAlerts/index.ts:1516 -- a fetch aborted by AbortSignal.timeout rejects with a TimeoutError, which isQueryTimeoutError/isClientTimeoutOrAbortError do not match (they check only ClickHouse client message strings, TIMEOUT_EXCEEDED, and ETIMEDOUT), so Prometheus HTTP-path timeouts are recorded as generic query errors and the error_type:'timeout' metric is wrong.
    • Fix: Extend the timeout classifier to also match e.name === 'TimeoutError'/AbortError.
  • packages/api/src/tasks/checkAlerts/index.ts:1104 -- the ClickHouse response is parsed as any (resp.json<any>(), ([k]: any), ([k, v]: any)) whereas the Prometheus branch declares a structural response type, leaving the value/tag shapes unchecked.
    • Fix: Type the ClickHouse response like the Prometheus branch and drop the : any callback annotations.
  • packages/api/src/tasks/checkAlerts/index.ts:1060 -- the Prometheus and ClickHouse parse loops duplicate last-point extraction and group-key formatting, differing only in field names and string-vs-number parsing.
    • Fix: Extract shared formatPromGroupKey/lastFinitePoint helpers used by both branches.
  • packages/api/src/tasks/checkAlerts/index.ts:1070 -- the PromQL group key is formatted as quoted ${k}:"${v}" while the standard time-series path (line 1888) builds unquoted ${k}:${v}, so PromQL alert history/labels read differently from every other alert type.
    • Fix: Align on one group-key formatter across alert paths.
  • packages/api/src/tasks/checkAlerts/index.ts:1507 -- the PromQL error path computes queryDurationMs from evalStartedAt (whole evaluation) rather than a query-scoped start as the standard path does, so the alerts.query duration metric means something different on this branch.
    • Fix: Capture a queryStartedAt before calling evaluatePromqlAlert and measure from it.
  • packages/api/src/tasks/checkAlerts/index.ts:1041 -- the real-Prometheus path buffers the full response via resp.json() with no series/size cap, unlike the ClickHouse branch which enforces max_result_rows; a high-cardinality expression is loaded entirely into the worker.
    • Fix: Apply a defensive series/response-size bound on the HTTP path.

Reviewers (5): security, testing, maintainability, reliability, previous-comments. Correctness, adversarial, API-contract, and TypeScript dimensions were verified directly against the code by the orchestrator: schema widening (AlertChartConfigSchema union), grouped-state keying (alertHasGroupBy now true for PromQL, consistent with computeHistoryMapKey read/write), error-path parity with the standard branch, team-scoped connection lookup, and variable substitution were all confirmed correct with no additional issues.

Testing gaps:

  • No processAlert integration test for either INLINE or TILE PromQL task types.
  • Non-finite/NaN/+Inf value filtering (Number.isFinite) is untested on both backends.
  • Prometheus !resp.ok transport-error branch and the AbortSignal.timeout path are untested.
  • Variable-substitution path (non-empty variables) and the connection == null guard are untested.
  • No injection-regression guard asserting promqlExpression/db/table reach ClickHouse strictly as bound query_params, and no cross-team isolation test.
  • SSRF via member-configured connection.host remains a pre-existing team-scoped trust boundary shared with the existing Prometheus proxy — not introduced by this diff, noted for awareness.

Comment thread packages/api/src/tasks/checkAlerts/index.ts
Comment thread packages/api/src/tasks/checkAlerts/index.ts
Comment thread packages/api/src/tasks/checkAlerts/index.ts Outdated
Comment thread packages/api/src/tasks/checkAlerts/index.ts Outdated
Comment thread packages/common-utils/src/types.ts
Comment thread packages/common-utils/src/core/utils.ts Outdated
Comment thread packages/api/src/tasks/checkAlerts/index.ts Outdated
const startMs = Math.floor(startSec * 1000);
const endMs = Math.floor(endSec * 1000);

const resp = await client.query({

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 prometheusQueryRange query, its client, and its timeouts are copied from the Prometheus router

The SQL string and query_params shape at lines 1045-1061 are a verbatim copy of queryRangeHandler in packages/api/src/routers/api/prometheus.ts:502-518, and 30_000/30 restate the exported PROMETHEUS_CH_TIMEOUT_MS / PROMETHEUS_MAX_EXECUTION_SEC (prometheus.ts:163, controllers/timeseriesEngine.ts:14). It also drops the router's max_result_rows cap. Extract one queryPrometheusRangeFromClickHouse helper (alongside formatMatrixResponse in prometheus.ts, or in controllers/timeseriesEngine.ts next to queryLabelValues) and call it from both. Related: the await import('@hyperdx/common-utils/dist/clickhouse/node') at line 1031 duplicates the static import already at line 10, and bypasses the API's ClickhouseClient wrapper in @/clickhouse that wires the pino logger — which is what prometheus.ts:491 uses.

}),
});

const result = await evaluatePromqlAlert({

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 ClickHouse test asserts around the two parameters that are actually wrong

expect.objectContaining({ expr, startMs, endMs, stepSec }) deliberately omits db and table — the only two params the function derives rather than passes through, and the two that are incorrect (a source ObjectId and a regex guess). The test passes while the query targets a nonexistent table. Assert the full query_params object, including db and table resolved from the alert's source.

Comment thread packages/api/src/tasks/checkAlerts/index.ts Outdated
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

PR Review

11 finding(s): 🔴 1 critical · 🟠 4 major · 🔵 6 minor

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

Findings outside the changed lines

  • 🔴 packages/api/src/tasks/checkAlerts/providers/default.ts:313The alert provider never loads a PromQL alert, so evaluatePromqlAlert is unreachablegetInlineAlertDetails (line 313) and getTileDetails (line 218) only special-case isRawSqlSavedChartConfig; every other config falls through to Source.findOne({ _id: config.source }). A PromQL config's source is optional (packages/common-utils/src/types.ts:1741) and is absent for the Prometheus-endpoint case, so the lookup returns null, the helper returns [], and loadAlert throws failed to fetch alert details — the alert silently never evaluates. Even when a source is set, the connection handed to processAlert is source.connection (line 339) rather than config.connection, which is what the chart itself queries (packages/app/src/components/ChartEditor/utils.ts:289). Add an isPromqlSavedChartConfig branch in both helpers that resolves Connection.findOne({ _id: config.connection }) and treats the source as optional metadata, mirroring the raw-SQL branch.
  • 🟠 packages/api/src/controllers/alerts.ts:191Creating an inline PromQL alert is rejected with "Invalid source ID"validateAlertInput routes any config that is not raw SQL into the builder else branch, which calls validateObjectId(chartConfig.source, 'Invalid source ID') — and validateObjectId throws Api400Error on undefined (line 53). A PromQL chart with no source (the normal shape for a Prometheus-endpoint connection) therefore fails POST/PUT /alerts, and a PromQL chart on a display type outside line/bar/number fails with the misleading "Inline chart alerts are only supported for Line, Stacked Bar, or Number display types". Add an isPromqlSavedChartConfig branch before the else that validates chartConfig.connection and only validates chartConfig.source when present, as the raw-SQL branch at line 143 already does.
1 minor
  • 🔵 packages/app/src/components/alerts/AlertDetailChart.tsx:146An alert on a PromQL chart has no detail previewbuildAlertChartConfig returns undefined for any PromQL config, so the alert detail page falls back to "This tile type can't be previewed here." for exactly the alerts this PR adds. Either render the PromQL config through the same Prometheus query path the chart uses, or give the fallback a PromQL-specific message like the SINGLE_VALUE_RAW_SQL_MESSAGE one at line 243.

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

Comment thread packages/api/src/tasks/checkAlerts/index.ts Outdated
Comment thread packages/api/src/tasks/checkAlerts/index.ts Outdated
Comment thread packages/api/src/tasks/checkAlerts/index.ts Outdated
- Remove unused displayTypeSupportsPromQLAlerts workaround and inline the boolean flag with explicit HDX-4636 tracker in UI
- Fix alertHasGroupBy comments to clarify PromQL multi-series behavior
- Remove invalid type leak (variables) in schedule offset normalizer
- Fix ISource type cast in provider fetching to safely use .from.databaseName
- Clean up unused imports and stale comments around PromQL in inline alerts
- Fix test fixture using 'any' by properly typing PromqlSavedChartConfig
@Aryainguz
Aryainguz marked this pull request as ready for review September 11, 2026 19:12
@Aryainguz
Aryainguz marked this pull request as draft September 11, 2026 19:22
'alignDateRangeToGranularity',
'alternateRowBackground',
// 'alert', // TODO: Support alerts on PromQL (HDX-4636)
'alert',

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.

🟠 major — PromQL configs now persist alert, but the PromQL editor renders no alert UI — the alert becomes invisible and uneditable

PromqlChartEditor (packages/app/src/components/ChartEditor/PromqlChartEditor.tsx) renders no "Add Alert" button and no alert form (only ChartEditorControls/RawSqlChartEditor do), and the display-type cleanup effect in EditTimeChartForm.tsx:294-303 only clears alert on a displayType change, not on a configType change. So configuring an alert on a builder/SQL chart and then switching the segmented control to PromQL now silently persists and evaluates that alert against the PromQL expression with no way to see or remove it. Either render the alert editor in PromqlChartEditor (the stated goal of this PR) or clear alert when configType becomes promql until that UI exists.

? displayTypeSupportsRawSqlAlerts(chart.config.displayType)
: isPromQL
? displayTypeSupportsPromQLAlerts(chart.config.displayType)
? false // PromQL alert UI not yet implemented (HDX-4636)

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.

🟠 major — Dashboard tile alert UI is hardcoded to false, so the new TILE PromQL evaluation path is unreachable

Both call sites replace displayTypeSupportsPromQLAlerts(...) with false // PromQL alert UI not yet implemented, which contradicts the changeset ("configuring and evaluating PromQL-based alerts directly from the chart explorer and dashboards") and leaves the whole AlertTaskType.TILE PromQL branch in checkAlerts/index.ts:1438 — including the dashboard-variable substitution wiring — dead. Either enable the tile alert affordance for PromQL display types or drop the TILE half of the backend branch from this PR.

const value = res.value;

const history = getOrCreateHistory(groupKey);
history.lastValues.push({ count: value, startTime: dateRange[1] });

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.

🟠 major — Only the last point of the window range is evaluated, so backfilled windows are silently skipped

getAlertEvaluationDateRange + calcAlertDateRange (packages/api/src/tasks/util.ts:47) return a range covering up to 50 missed windows, and the builder path iterates expectedBuckets to evaluate each one (index.ts:1751-1892). evaluatePromqlAlert takes only series.values[values.length - 1] and the branch then reports backfilledBuckets = 0, so after a worker outage every intermediate window (and any breach in it) is dropped, and shouldFireBasedOnConsecutiveWindows sees one history row for many windows. Iterate the query_range/time_series points bucket-by-bucket like the time-series path instead of keeping only the last.

await sendNotificationIfResolved(previous, history, groupKey);
}
}
} catch (e) {

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.

🟠 major — PromQL evaluation failures are swallowed: no alert error is recorded and no query metrics are emitted

The catch logs and returns without calling alertProvider.recordAlertErrors, unlike the ClickHouse path (index.ts:1662-1685), so a broken expression, unreachable Prometheus, or missing connection leaves the alert with no ERROR history row and nothing visible in the UI — the alert just appears to never evaluate. It also skips recordOperationOutcome({operation:'alerts.query'}), alertQueryFailuresCounter and evaluationAnalytics.queryDurationMs, so PromQL alerts are invisible to the alert-query SLI. Reuse makeQueryAlertError + recordAlertErrors and record the same query metrics around the PromQL call.

Comment thread packages/api/src/tasks/checkAlerts/index.ts Outdated
Comment thread packages/api/src/tasks/checkAlerts/index.ts
Comment thread packages/api/src/tasks/checkAlerts/index.ts Outdated
Comment thread packages/api/src/tasks/checkAlerts/index.ts
Comment thread packages/api/src/tasks/checkAlerts/index.ts
Comment thread packages/api/src/tasks/checkAlerts/index.ts
P0: Fix tags tuple iteration in ClickHouse path
- evaluatePromqlAlert now iterates series.tags as [key, value] tuples
  matching formatMatrixResponse, instead of Object.entries (which
  produced numeric indices and broke group key building entirely)

P2: Throw on degraded Prometheus status instead of returning null
- status !== 'success' now throws, preventing false auto-resolves
  from masking genuine Prometheus outages as empty results

P2: Route PromQL failures through standard error recording
- PromQL catch block now calls makeQueryAlertError,
  alertQueryFailuresCounter, recordOperationOutcome, and
  alertProvider.recordAlertErrors — matching the SQL path so failed
  windows get typed error history rows, operator metrics, and retry

P3: Extract queryPrometheusRangeFromClickHouse helper
- Moved to @/controllers/timeseriesEngine (with PROMETHEUS_MAX_RESULT_ROWS
  cap that was missing from the alert path); both prometheus router and
  evaluatePromqlAlert now use the same helper, eliminating duplication

P3: Use API ClickhouseClient in evaluatePromqlAlert
- Swapped common-utils direct ClickhouseClient for @/clickhouse wrapper
  so telemetry/logger wiring is included in the alert evaluation path

P3: Reuse PROMETHEUS_CH_TIMEOUT_MS constant
- Exported from prometheus router; evaluatePromqlAlert now imports and
  reuses it instead of hardcoding 30_000 / max_execution_time: 30

P3: Move legendTemplate to EVALUATION_INERT_CONFIG_KEYS
- Was erroneously classified as KNOWN_LOSSY; it is render-only so a
  blind external GET -> PUT should not fail on it

P3: Fix drift-guard test to label PromQL variant correctly
- i === 0 ? 'builder' : 'raw SQL' now handles i === 2 as 'promql'

Tests: Update evaluatePromqlAlert.test.ts
- Mock queryPrometheusRangeFromClickHouse (new helper) instead of
  raw ClickhouseClient; fix tags fixtures to use tuple arrays;
  add test asserting status !== success throws
@Aryainguz
Aryainguz marked this pull request as ready for review September 12, 2026 18:53
@Aryainguz
Aryainguz marked this pull request as draft September 12, 2026 19:00
throw new Error(`Connection ${connectionId} not found for PromQL alert`);
}

const endSec = dateRange[1].getTime() / 1000;

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.

🟠 major — PromQL evaluation reads only the last point, so backfilled windows are silently skipped

dateRange spans from the previous history's createdAt to now and can cover many windows (getAlertEvaluationDateRange at line 686 plus calcAlertDateRange, which only truncates at MAX_NUM_WINDOWS). The builder path iterates every expected bucket (line 1818) and reports backfilledBuckets; evaluatePromqlAlert takes series.values[series.values.length - 1] and the branch hardcodes evaluationAnalytics.backfilledBuckets = 0. After a worker delay or a run of failed evaluations, every intermediate window is dropped without ever being evaluated (and numConsecutiveWindows never accumulates), because the next run starts from this run's createdAt. Return all points per series and loop over timeBucketByGranularity(dateRange[0], dateRange[1], ${windowSizeInMins} minute) the way the time-series path does.

client,
// ISource always has `from` (it is on BaseSourceSchema); the nullable
// source param covers the case where no source is wired to the alert.
databaseName: source?.from.databaseName ?? 'default',

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.

🟠 major — ClickHouse PromQL path guesses default.otel_metrics_gauge when the alert has no source

source?.from.databaseName ?? 'default' / source?.from.tableName ?? 'otel_metrics_gauge' invents a table for prometheusQueryRange, which requires a TimeSeries-engine table — otel_metrics_gauge is the OTel gauge MergeTree table, so the query fails (or, worse, silently reads the wrong table if one by that name exists). The equivalent HTTP route refuses rather than guessing: packages/api/src/routers/api/prometheus.ts:476 returns 400 when table is missing. Throw a descriptive error when a non-Prometheus connection has no source from, instead of defaulting.

'alignDateRangeToGranularity',
'alternateRowBackground',
// 'alert', // TODO: Support alerts on PromQL (HDX-4636)
'alert',

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.

🟠 major — No UI exists to add or edit a PromQL alert; the only reachable path is a stale carry-over

PromqlChartEditor.tsx contains no alert affordance (no TileAlertEditor, no "Add alert" button — unlike RawSqlChartEditor.tsx:299), and DBDashboardPage.tsx:846,1057 now hardcode false for PromQL tiles, so nothing ever sets form.alert on a PromQL config deliberately. What persisting 'alert' here does enable is the leftover case: a user configures an alert in Builder/SQL mode and flips the configType SegmentedControl to PromQL (the clearing effect at EditTimeChartForm.tsx:294 only fires on displayType change), leaving "Save alert" live in ChartActionBar. With the isPromqlSavedChartConfig guard removed from buildInlineAlertPayload, that click now POSTs a PromQL payload that 400s (see the alerts.ts finding) instead of being a no-op. Either ship the alert editor for PromQL, or clear alert on configType change and keep the guard until the UI lands.

: undefined;

try {
const promqlResults = await evaluatePromqlAlert({

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 PromQL branch records only failures for the alerts.query SLI

The success path never calls recordOperationOutcome({ operation: 'alerts.query', outcome: 'success', ... }) and never sets evaluationAnalytics.queryDurationMs, while the catch at line 1509 does record errors — so the availability SLI for PromQL alerts reads as 0% success. Also, the error path measures from evalStartedAt (the start of the whole evaluation) rather than from a query-start timestamp, unlike queryStartedAt at line 1660. Capture a queryStartedAt around the evaluatePromqlAlert call and record both outcomes from it.

const parsed = parseFloat(lastPoint[1]);
if (Number.isFinite(parsed)) {
// Format Prometheus metric object as a HyperDX group key: key1:"val1", key2:"val2"
const group = series.metric

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 — Series labels are discarded: no notification attributes, and the group key format differs from every other alert type

The builder path builds group keys as k:v (line 1888) and passes the parsed labels through as attributes so templates can render {{attributes.*}} (line 1908). evaluatePromqlAlert formats k:"v" and returns only the joined string, so PromQL alert notifications render an inconsistent group label and always have empty attributes. Return the label map alongside group, format the key as k:v, and pass the map as attributes to trySendNotification.

const json = await resp.json<any>();
if (!Array.isArray(json?.data)) return null;

const results: Array<{ group: string; value: number }> = [];

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 ClickHouse result parsing duplicates formatMatrixResponse, and the two backend branches duplicate each other

packages/api/src/routers/api/prometheus.ts:122 (formatMatrixResponse) already converts { tags, time_series } rows into { metric, values }. Calling it on json.data would let both branches share a single "take the last point, build the group key" loop instead of maintaining two copies that already differ (the Prometheus branch parseFloats the value and skips non-finite points, the ClickHouse branch does neither).

@@ -0,0 +1,283 @@
import mongoose from 'mongoose';

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 ~150-line PromQL branch in processAlert has no test coverage

The new tests only exercise the query helper with both backends mocked; nothing covers threshold firing, PENDING/consecutive-window handling, auto-resolve, the missing-series recovery loop, or the variables substitution argument. checkAlerts.int.test.ts is where the equivalent builder/raw-SQL state machine is pinned — add a PromQL case there so a regression in the state transitions is caught.

import { ISavedSearch } from '@/models/savedSearch';
import { ISource } from '@/models/source';
import { IWebhook } from '@/models/webhook';
import {

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 check-alerts worker now imports an Express router for a constant and a URL helper

Importing @/routers/api/prometheus pulls express.Router(), express.urlencoded, and @/middleware/auth into the worker process just to reach PROMETHEUS_CH_TIMEOUT_MS and joinPrometheusUpstreamUrl (which is why the constant had to be exported). Move both to @/controllers/timeseriesEngine, where this PR already moved queryPrometheusRangeFromClickHouse, and have the router import them from there.

@pulpdrew
pulpdrew self-requested a review September 13, 2026 15:02
@pulpdrew

Copy link
Copy Markdown
Contributor

@Aryainguz please let me know when this is ready to review. Don't feel like you have to address every AI comment, some will push you to expand the scope of the PR unnecessarily and fix tangential bugs. We'd rather see a well-scoped, easy to review PR. We're still working on dialing in the agent reviewer.

And if you find the scope increasing too much, feel free to add a brief high level plan in the corresponding issue, we can discuss, and then tackle this in a few well-scoped PRs.

@Aryainguz

Copy link
Copy Markdown
Contributor Author

Hi @pulpdrew, I'm working on end to end implementation for same from dashboard to alerts for this, sharing recording of same on my local testing, should I include frontend changes for this in this PR only or created a well scoped subsequent PRs after this ?

Screen.Recording.2026-09-13.at.8.47.48.PM.mov

@pulpdrew

Copy link
Copy Markdown
Contributor

If the UI changes are small, feel free to include them here. If they're several hundred lines or more, it would be best to split them out. When taking a look at the alerts UI, please be sure to consider the new alert details page as well, in case there are any changes that need to be made there.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Alerting for PromQL Charts

2 participants