Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/alert-properties-all-channels.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 21 additions & 2 deletions packages/app/src/components/alerts/AlertPropertiesSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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 (
<div className="fs-8 d-flex gap-2 align-items-center">
<span>
Expand Down Expand Up @@ -65,8 +75,17 @@ export function AlertPropertiesSummary({
)}
<span>&middot;</span>
<Group gap={5}>
Notify via {getWebhookChannelIcon(alert.channel.type)}
<span>{webhookName ?? 'Webhook'}</span>
Notify via{' '}
{channels.map((channel, index) => (
<React.Fragment key={`${channel.type}-${channel.webhookId ?? index}`}>
{getWebhookChannelIcon(channel.type)}
</React.Fragment>
))}
<span>
{channels.length > 1
? `${channels.length} channels`
: (webhookName ?? 'Webhook')}
</span>
</Group>
{alert.createdBy && (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<AlertDetailProperties
alert={
{
...baseAlert,
channels: [
{ type: 'webhook', webhookId: 'webhook-id' },
{ type: 'webhook', webhookId: 'other-webhook' },
],
} as unknown as AlertsPageItem
}
/>,
);

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(
<AlertDetailProperties
alert={
{
...baseAlert,
channels: [{ type: 'webhook', webhookId: 'webhook-id' }],
} as unknown as AlertsPageItem
}
/>,
);

expect(screen.getByText('Team Slack')).toBeInTheDocument();
expect(screen.queryByText('1 channels')).not.toBeInTheDocument();
});

it('renders all persisted metadata fields when set', () => {
renderWithMantine(
<AlertDetailProperties
Expand Down
Loading