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(