From 573149a482560f27de4f4a234bef0de16a31306e Mon Sep 17 00:00:00 2001 From: Jordan Simonovski Date: Tue, 25 Aug 2026 10:55:36 +1000 Subject: [PATCH] fix(alerts): show every notification channel on the summary line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AlertPropertiesSummary read `alert.channel` — the legacy single-value mirror of channels[0] — so an alert configured with several notification targets rendered as though it had one, with nothing to say the others existed. Dispatch was always correct, so this is a reporting gap rather than a delivery one. It points the wrong way, though: someone checking which targets an alert notifies is shown one, and would reasonably conclude a channel never saved. A single channel still names its webhook, so the common case is unchanged. Several channels render an icon each plus a count, which stays bounded at the ten channels the API allows. Both surfaces that share the component — the alerts page rows and the alert detail page — are fixed together. --- .changeset/alert-properties-all-channels.md | 5 +++ .../alerts/AlertPropertiesSummary.tsx | 23 ++++++++++- .../__tests__/AlertDetailProperties.test.tsx | 38 +++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 .changeset/alert-properties-all-channels.md diff --git a/.changeset/alert-properties-all-channels.md b/.changeset/alert-properties-all-channels.md new file mode 100644 index 0000000000..15021dc71a --- /dev/null +++ b/.changeset/alert-properties-all-channels.md @@ -0,0 +1,5 @@ +--- +'@hyperdx/app': patch +--- + +Show every notification channel on an alert's summary line, rather than only the first. `AlertPropertiesSummary` read `alert.channel` — the legacy single-value mirror of `channels[0]` — so an alert configured with several targets rendered as though it notified one, with nothing to indicate the others existed. Dispatch was always correct; this was a reporting gap, and it pointed the wrong way: someone checking which targets an alert notifies was shown one and would reasonably conclude a channel had not saved. A single-channel alert still names its webhook exactly as before; several channels now render an icon each plus a count. Affects both surfaces that share the component, the alerts page rows and the alert detail page. diff --git a/packages/app/src/components/alerts/AlertPropertiesSummary.tsx b/packages/app/src/components/alerts/AlertPropertiesSummary.tsx index 51b3f867e3..acdf1c2801 100644 --- a/packages/app/src/components/alerts/AlertPropertiesSummary.tsx +++ b/packages/app/src/components/alerts/AlertPropertiesSummary.tsx @@ -16,6 +16,8 @@ type AlertPropertiesSummaryProps = { showSchedule?: boolean; /** * Display name of the notification webhook (the alert only stores its id). + * Used only when the alert has a single channel; several channels render as + * a count, since one name cannot stand for all of them. * The detail page resolves and passes it; the alerts-page rows keep the * generic "Webhook" label. */ @@ -36,6 +38,14 @@ export function AlertPropertiesSummary({ TILE_ALERT_THRESHOLD_TYPE_OPTIONS[alert.thresholdType] ?? alert.thresholdType; + // `channels` is canonical; `channel` is its legacy single-value mirror of + // channels[0]. Reading the mirror meant an alert with several notification + // targets rendered as though it had one, with nothing to say the others + // existed. Falling back to the mirror keeps rows written before + // multi-channel (and the null-typed channel of an alert with no target) + // rendering exactly as they did. + const channels = alert.channels?.length ? alert.channels : [alert.channel]; + return (
@@ -65,8 +75,17 @@ export function AlertPropertiesSummary({ )} · - Notify via {getWebhookChannelIcon(alert.channel.type)} - {webhookName ?? 'Webhook'} + Notify via{' '} + {channels.map((channel, index) => ( + + {getWebhookChannelIcon(channel.type)} + + ))} + + {channels.length > 1 + ? `${channels.length} channels` + : (webhookName ?? 'Webhook')} + {alert.createdBy && ( <> diff --git a/packages/app/src/components/alerts/__tests__/AlertDetailProperties.test.tsx b/packages/app/src/components/alerts/__tests__/AlertDetailProperties.test.tsx index ac4ba9dfab..aa4ee6c112 100644 --- a/packages/app/src/components/alerts/__tests__/AlertDetailProperties.test.tsx +++ b/packages/app/src/components/alerts/__tests__/AlertDetailProperties.test.tsx @@ -43,6 +43,44 @@ const baseAlert = { } as unknown as AlertsPageItem; describe('AlertDetailProperties', () => { + // `channel` is a single-value mirror of channels[0], so reading it rendered + // a multi-channel alert as though it notified one target. + it('reports the channel count when an alert has several', () => { + renderWithMantine( + , + ); + + expect(screen.getByText('2 channels')).toBeInTheDocument(); + }); + + // One channel keeps naming its webhook — a count would be a regression for + // the overwhelmingly common case. + it('still names the webhook when an alert has a single channel', () => { + renderWithMantine( + , + ); + + expect(screen.getByText('Team Slack')).toBeInTheDocument(); + expect(screen.queryByText('1 channels')).not.toBeInTheDocument(); + }); + it('renders all persisted metadata fields when set', () => { renderWithMantine(