fix: Show alerts on a tile only when dashboard matches (#2048)

## Summary

This PR fixes a bug that caused alerts created on other dashboards to be displayed on tiles with IDs that match the other dashboard. This in turn led to failures updating the alert on the "duplicate" dashboard.

The included integration test demonstrates the case.

### Screenshots or video

### How to test locally or on Vercel

### References



- Linear Issue: Closes HDX-3918
- Related PRs:
This commit is contained in:
Drew Davis 2026-04-03 12:40:11 -04:00 committed by GitHub
parent b4e1498eb3
commit 59b1f46fd7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 57 additions and 5 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/api": patch
---
fix: Show alerts on a tile only when dashboard matches

View file

@ -203,12 +203,14 @@ export const getAlertById = async (
});
};
export const getTeamDashboardAlertsByTile = async (teamId: ObjectId) => {
export const getTeamDashboardAlertsByDashboardAndTile = async (
teamId: ObjectId,
) => {
const alerts = await Alert.find({
source: AlertSource.TILE,
team: teamId,
}).populate('createdBy', 'email name');
return groupBy(alerts, 'tileId');
return groupBy(alerts, a => `${a.dashboard?.toString()}:${a.tileId}`);
};
export const getDashboardAlertsByTile = async (

View file

@ -11,7 +11,7 @@ import {
createOrUpdateDashboardAlerts,
deleteDashboardAlerts,
getDashboardAlertsByTile,
getTeamDashboardAlertsByTile,
getTeamDashboardAlertsByDashboardAndTile,
} from '@/controllers/alerts';
import type { ObjectId } from '@/models';
import type { AlertDocument, IAlert } from '@/models/alert';
@ -96,7 +96,7 @@ async function syncDashboardAlerts(
export async function getDashboards(teamId: ObjectId) {
const [_dashboards, alerts] = await Promise.all([
Dashboard.find({ team: teamId }),
getTeamDashboardAlertsByTile(teamId),
getTeamDashboardAlertsByDashboardAndTile(teamId),
]);
const dashboards = _dashboards
@ -105,7 +105,10 @@ export async function getDashboards(teamId: ObjectId) {
...d,
tiles: d.tiles.map(t => ({
...t,
config: { ...t.config, alert: alerts[t.id]?.[0] },
config: {
...t.config,
alert: alerts[`${d._id.toString()}:${t.id}`]?.[0],
},
})),
}));

View file

@ -341,6 +341,48 @@ describe('dashboard router', () => {
expect(allTilesPostDelete).toEqual(alertsPostDeleteTiles);
});
it('alert on a tile only appears on the dashboard that owns it, not on another dashboard with the same tile ID', async () => {
const sharedTileId = new mongoose.Types.ObjectId().toHexString();
const mockAlert = makeMockAlert(webhook._id.toString());
// Create dashboard A with an alert on the tile
const dashboardA = await agent
.post('/dashboards')
.send({
name: 'Dashboard A',
tiles: [makeTile({ id: sharedTileId, alert: mockAlert })],
tags: [],
})
.expect(200);
// Create dashboard B with a tile that has the same ID, but no alert
const dashboardB = await agent
.post('/dashboards')
.send({
name: 'Dashboard B',
tiles: [makeTile({ id: sharedTileId })],
tags: [],
})
.expect(200);
// Fetch all dashboards
const dashboards = await agent.get('/dashboards').expect(200);
const fetchedA = dashboards.body.find(
(d: any) => d._id === dashboardA.body.id,
);
const fetchedB = dashboards.body.find(
(d: any) => d._id === dashboardB.body.id,
);
// The alert should appear on dashboard A's tile
expect(fetchedA.tiles[0].config.alert).toBeTruthy();
expect(fetchedA.tiles[0].config.alert.tileId).toBe(sharedTileId);
// The alert should NOT appear on dashboard B's tile
expect(fetchedB.tiles[0].config.alert).toBeUndefined();
});
it('preserves alert creator when different user updates dashboard', async () => {
const mockAlert = makeMockAlert(webhook._id.toString());
const currentUser = user;