diff --git a/.changeset/wild-plants-drop.md b/.changeset/wild-plants-drop.md new file mode 100644 index 00000000..e1a64d6d --- /dev/null +++ b/.changeset/wild-plants-drop.md @@ -0,0 +1,6 @@ +--- +'@hyperdx/api': patch +'@hyperdx/app': patch +--- + +fix: GET alerts endpoint diff --git a/packages/api/src/controllers/alerts.ts b/packages/api/src/controllers/alerts.ts index 8a4815f4..6a403df8 100644 --- a/packages/api/src/controllers/alerts.ts +++ b/packages/api/src/controllers/alerts.ts @@ -3,6 +3,7 @@ import ms from 'ms'; import * as clickhouse from '@/clickhouse'; import { SQLSerializer } from '@/clickhouse/searchQueryParser'; +import type { ObjectId } from '@/models'; import Alert, { AlertChannel, AlertInterval, @@ -76,11 +77,11 @@ export const validateGroupByProperty = async ({ const makeAlert = (alert: AlertInput) => { return { - source: alert.source, channel: alert.channel, interval: alert.interval, - type: alert.type, + source: alert.source, threshold: alert.threshold, + type: alert.type, // Log alerts logView: alert.logViewId, groupBy: alert.groupBy, @@ -92,12 +93,20 @@ const makeAlert = (alert: AlertInput) => { }; }; -export const createAlert = async (alertInput: AlertInput) => { - return new Alert(makeAlert(alertInput)).save(); +export const createAlert = async (teamId: ObjectId, alertInput: AlertInput) => { + return new Alert({ + ...makeAlert(alertInput), + team: teamId, + }).save(); }; // create an update alert function based off of the above create alert function -export const updateAlert = async (id: string, alertInput: AlertInput) => { +export const updateAlert = async ( + id: string, + teamId: ObjectId, + alertInput: AlertInput, +) => { + // TODO: find by id and teamId // should consider clearing AlertHistory when updating an alert? return Alert.findByIdAndUpdate(id, makeAlert(alertInput), { returnDocument: 'after', diff --git a/packages/api/src/models/alert.ts b/packages/api/src/models/alert.ts index 6cd933e5..190c20e1 100644 --- a/packages/api/src/models/alert.ts +++ b/packages/api/src/models/alert.ts @@ -34,11 +34,12 @@ export interface IAlert { channel: AlertChannel; cron: string; interval: AlertInterval; + source?: AlertSource; state: AlertState; + team: ObjectId; threshold: number; timezone: string; type: AlertType; - source?: AlertSource; // Log alerts groupBy?: string; @@ -85,6 +86,10 @@ const AlertSchema = new Schema( required: false, default: 'LOG', }, + team: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Team', + }, // Log alerts logView: { diff --git a/packages/api/src/routers/api/alerts.ts b/packages/api/src/routers/api/alerts.ts index 82b8d3ad..50e8b349 100644 --- a/packages/api/src/routers/api/alerts.ts +++ b/packages/api/src/routers/api/alerts.ts @@ -11,8 +11,8 @@ import { import { getTeam } from '@/controllers/team'; import Alert from '@/models/alert'; import AlertHistory from '@/models/alertHistory'; -import { IDashboard } from '@/models/dashboard'; -import { ILogView } from '@/models/logView'; +import Dashboard, { IDashboard } from '@/models/dashboard'; +import LogView, { ILogView } from '@/models/logView'; const router = express.Router(); @@ -82,7 +82,27 @@ router.get('/', async (req, res, next) => { if (teamId == null) { return res.sendStatus(403); } - const alerts = await Alert.find({ team: teamId }).populate<{ + + // TODO: to use team field in the alert model + const [dashboards, logViews] = await Promise.all([ + Dashboard.find({ team: teamId }, { _id: 1 }), + LogView.find({ team: teamId }, { _id: 1 }), + ]); + + const alerts = await Alert.find({ + $or: [ + { + logView: { + $in: logViews.map(logView => logView._id), + }, + }, + { + dashboardId: { + $in: dashboards.map(dashboard => dashboard._id), + }, + }, + ], + }).populate<{ logView: ILogView; dashboardId: IDashboard; }>(['logView', 'dashboardId']); @@ -92,7 +112,6 @@ router.get('/', async (req, res, next) => { const history = await AlertHistory.find( { alert: alert._id, - team: teamId, }, { __v: 0, @@ -105,16 +124,30 @@ router.get('/', async (req, res, next) => { return { history, - dashboard: alert.dashboardId, + channel: _.pick(alert.channel, ['type']), + ...(alert.dashboardId && { + dashboard: { + charts: alert.dashboardId.charts + .filter(chart => chart.id === alert.chartId) + .map(chart => _.pick(chart, ['id', 'name'])), + ..._.pick(alert.dashboardId, ['_id', 'name', 'updatedAt']), + }, + }), + ...(alert.logView && { + logView: _.pick(alert.logView, [ + '_id', + 'createdAt', + 'name', + 'updatedAt', + ]), + }), ..._.pick(alert, [ '_id', - 'channel', 'interval', 'threshold', 'state', 'type', 'source', - 'logView', 'chartId', 'createdAt', 'updatedAt', @@ -135,10 +168,14 @@ router.post( validateRequest({ body: zAlert }), validateGroupBy, async (req, res, next) => { + const teamId = req.user?.team; + if (teamId == null) { + return res.sendStatus(403); + } try { const alertInput = req.body; return res.json({ - data: await createAlert(alertInput), + data: await createAlert(teamId, alertInput), }); } catch (e) { next(e); @@ -152,10 +189,14 @@ router.put( validateGroupBy, async (req, res, next) => { try { + const teamId = req.user?.team; + if (teamId == null) { + return res.sendStatus(403); + } const { id } = req.params; const alertInput = req.body; res.json({ - data: await updateAlert(id, alertInput), + data: await updateAlert(id, teamId, alertInput), }); } catch (e) { next(e); @@ -173,6 +214,7 @@ router.delete('/:id', async (req, res, next) => { if (!alertId) { return res.sendStatus(400); } + // FIXME: should add teamId to the find query await Alert.findByIdAndDelete(alertId); res.sendStatus(200); } catch (e) { diff --git a/packages/api/src/routers/api/logViews.ts b/packages/api/src/routers/api/logViews.ts index 8c9fe251..beb6fe13 100644 --- a/packages/api/src/routers/api/logViews.ts +++ b/packages/api/src/routers/api/logViews.ts @@ -71,6 +71,7 @@ router.patch('/:id', async (req, res, next) => { if (!logViewId || !query) { return res.sendStatus(400); } + // TODO: query teamId const logView = await LogView.findByIdAndUpdate( logViewId, { @@ -96,6 +97,7 @@ router.delete('/:id', async (req, res, next) => { if (!logViewId) { return res.sendStatus(400); } + // TODO: query teamId // delete all alerts await Alert.deleteMany({ logView: logViewId }); await LogView.findByIdAndDelete(logViewId); diff --git a/packages/api/src/tasks/__tests__/checkAlerts.test.ts b/packages/api/src/tasks/__tests__/checkAlerts.test.ts index cb6d6c05..3cd831e6 100644 --- a/packages/api/src/tasks/__tests__/checkAlerts.test.ts +++ b/packages/api/src/tasks/__tests__/checkAlerts.test.ts @@ -179,7 +179,7 @@ describe('checkAlerts', () => { url: 'https://hooks.slack.com/services/123', name: 'My Webhook', }).save(); - const alert = await createAlert({ + const alert = await createAlert(team._id, { source: 'LOG', channel: { type: 'webhook', @@ -353,7 +353,7 @@ describe('checkAlerts', () => { }, ], }).save(); - const alert = await createAlert({ + const alert = await createAlert(team._id, { source: 'CHART', channel: { type: 'webhook', @@ -586,7 +586,7 @@ describe('checkAlerts', () => { }, ], }).save(); - const alert = await createAlert({ + const alert = await createAlert(team._id, { source: 'CHART', channel: { type: 'webhook',