Skip to content

Commit 8b2db33

Browse files
committed
fix: Group alert histories by evaluation time
1 parent 892e43f commit 8b2db33

File tree

4 files changed

+503
-27
lines changed

4 files changed

+503
-27
lines changed
Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
import { ObjectId } from 'mongodb';
2+
3+
import { getRecentAlertHistories } from '@/controllers/alertHistory';
4+
import { clearDBCollections, closeDB, connectDB } from '@/fixtures';
5+
import Alert, { AlertState } from '@/models/alert';
6+
import AlertHistory from '@/models/alertHistory';
7+
import Team from '@/models/team';
8+
9+
describe('alertHistory controller', () => {
10+
beforeAll(async () => {
11+
await connectDB();
12+
});
13+
14+
afterEach(async () => {
15+
await clearDBCollections();
16+
});
17+
18+
afterAll(async () => {
19+
await closeDB();
20+
});
21+
22+
describe('getRecentAlertHistories', () => {
23+
it('should return alert histories grouped by createdAt', async () => {
24+
// Create test team and alert
25+
const team = await Team.create({
26+
name: 'Test Team',
27+
});
28+
29+
const alert = await Alert.create({
30+
channel: { type: null },
31+
interval: '15m',
32+
state: AlertState.OK,
33+
team: team._id,
34+
threshold: 10,
35+
thresholdType: 'above',
36+
});
37+
38+
// Create alert histories with 3 distinct createdAt values
39+
const date1 = new Date('2024-01-01T10:00:00Z');
40+
const date2 = new Date('2024-01-01T10:15:00Z');
41+
const date3 = new Date('2024-01-01T10:30:00Z');
42+
43+
await AlertHistory.create([
44+
{
45+
alert: alert._id,
46+
counts: 5,
47+
createdAt: date1,
48+
state: AlertState.OK,
49+
lastValues: [{ startTime: date1, count: 5 }],
50+
},
51+
{
52+
alert: alert._id,
53+
counts: 12,
54+
createdAt: date2,
55+
state: AlertState.ALERT,
56+
lastValues: [{ startTime: date2, count: 12 }],
57+
},
58+
{
59+
alert: alert._id,
60+
counts: 8,
61+
createdAt: date3,
62+
state: AlertState.OK,
63+
lastValues: [{ startTime: date3, count: 8 }],
64+
},
65+
]);
66+
67+
const results = await getRecentAlertHistories({
68+
alertId: new ObjectId(alert._id),
69+
createdAtLimit: 3,
70+
});
71+
72+
expect(results).toHaveLength(3);
73+
expect(results[0].counts).toBe(8); // Most recent
74+
expect(results[1].counts).toBe(12);
75+
expect(results[2].counts).toBe(5); // Oldest
76+
});
77+
78+
it('should respect createdAtLimit parameter', async () => {
79+
const team = await Team.create({
80+
name: 'Test Team',
81+
});
82+
83+
const alert = await Alert.create({
84+
channel: { type: null },
85+
interval: '15m',
86+
state: AlertState.OK,
87+
team: team._id,
88+
threshold: 10,
89+
thresholdType: 'above',
90+
});
91+
92+
// Create 5 alert histories with distinct createdAt values
93+
const dates = [
94+
new Date('2024-01-01T10:00:00Z'),
95+
new Date('2024-01-01T10:15:00Z'),
96+
new Date('2024-01-01T10:30:00Z'),
97+
new Date('2024-01-01T10:45:00Z'),
98+
new Date('2024-01-01T11:00:00Z'),
99+
];
100+
101+
for (const date of dates) {
102+
await AlertHistory.create({
103+
alert: alert._id,
104+
counts: 5,
105+
createdAt: date,
106+
state: AlertState.OK,
107+
lastValues: [{ startTime: date, count: 5 }],
108+
});
109+
}
110+
111+
const results = await getRecentAlertHistories({
112+
alertId: new ObjectId(alert._id),
113+
createdAtLimit: 2,
114+
});
115+
116+
expect(results).toHaveLength(2);
117+
// Should return only the 2 most recent
118+
expect(new Date(results[0].createdAt)).toEqual(dates[4]);
119+
expect(new Date(results[1].createdAt)).toEqual(dates[3]);
120+
});
121+
122+
it('should return multiple histories with the same createdAt', async () => {
123+
const team = await Team.create({
124+
name: 'Test Team',
125+
});
126+
127+
const alert = await Alert.create({
128+
channel: { type: null },
129+
interval: '15m',
130+
state: AlertState.OK,
131+
team: team._id,
132+
threshold: 10,
133+
thresholdType: 'above',
134+
});
135+
136+
const sharedDate = new Date('2024-01-01T10:00:00Z');
137+
138+
// Create 3 histories with the same createdAt (for group-by alerts)
139+
await AlertHistory.create([
140+
{
141+
alert: alert._id,
142+
counts: 5,
143+
createdAt: sharedDate,
144+
state: AlertState.ALERT,
145+
lastValues: [{ startTime: sharedDate, count: 5 }],
146+
group: 'group-a',
147+
},
148+
{
149+
alert: alert._id,
150+
counts: 10,
151+
createdAt: sharedDate,
152+
state: AlertState.ALERT,
153+
lastValues: [{ startTime: sharedDate, count: 10 }],
154+
group: 'group-b',
155+
},
156+
{
157+
alert: alert._id,
158+
counts: 15,
159+
createdAt: sharedDate,
160+
state: AlertState.ALERT,
161+
lastValues: [{ startTime: sharedDate, count: 15 }],
162+
group: 'group-c',
163+
},
164+
]);
165+
166+
const results = await getRecentAlertHistories({
167+
alertId: new ObjectId(alert._id),
168+
createdAtLimit: 1,
169+
});
170+
171+
// Should return all 3 histories since they share the same createdAt
172+
expect(results).toHaveLength(3);
173+
expect(
174+
results.every(r => r.createdAt.getTime() === sharedDate.getTime()),
175+
).toBe(true);
176+
});
177+
178+
it('should respect optional limit parameter', async () => {
179+
const team = await Team.create({
180+
name: 'Test Team',
181+
});
182+
183+
const alert = await Alert.create({
184+
channel: { type: null },
185+
interval: '15m',
186+
state: AlertState.OK,
187+
team: team._id,
188+
threshold: 10,
189+
thresholdType: 'above',
190+
});
191+
192+
const sharedDate = new Date('2024-01-01T10:00:00Z');
193+
194+
// Create 5 histories with the same createdAt
195+
for (let i = 0; i < 5; i++) {
196+
await AlertHistory.create({
197+
alert: alert._id,
198+
counts: i,
199+
createdAt: sharedDate,
200+
state: AlertState.ALERT,
201+
lastValues: [{ startTime: sharedDate, count: i }],
202+
group: `group-${i}`,
203+
});
204+
}
205+
206+
const results = await getRecentAlertHistories({
207+
alertId: new ObjectId(alert._id),
208+
createdAtLimit: 1,
209+
limit: 3,
210+
});
211+
212+
// Should return only 3 histories even though there are 5 with the same createdAt
213+
expect(results).toHaveLength(3);
214+
});
215+
216+
it('should return empty array when no histories exist', async () => {
217+
const team = await Team.create({
218+
name: 'Test Team',
219+
});
220+
221+
const alert = await Alert.create({
222+
channel: { type: null },
223+
interval: '15m',
224+
state: AlertState.OK,
225+
team: team._id,
226+
threshold: 10,
227+
thresholdType: 'above',
228+
});
229+
230+
const results = await getRecentAlertHistories({
231+
alertId: new ObjectId(alert._id),
232+
createdAtLimit: 5,
233+
});
234+
235+
expect(results).toHaveLength(0);
236+
});
237+
238+
it('should return empty array for non-existent alert', async () => {
239+
const fakeAlertId = new ObjectId().toString();
240+
241+
const results = await getRecentAlertHistories({
242+
alertId: new ObjectId(fakeAlertId),
243+
createdAtLimit: 5,
244+
});
245+
246+
expect(results).toHaveLength(0);
247+
});
248+
249+
it('should not include internal fields in results', async () => {
250+
const team = await Team.create({
251+
name: 'Test Team',
252+
});
253+
254+
const alert = await Alert.create({
255+
channel: { type: null },
256+
interval: '15m',
257+
state: AlertState.OK,
258+
team: team._id,
259+
threshold: 10,
260+
thresholdType: 'above',
261+
});
262+
263+
const date = new Date('2024-01-01T10:00:00Z');
264+
265+
await AlertHistory.create({
266+
alert: alert._id,
267+
counts: 5,
268+
createdAt: date,
269+
state: AlertState.OK,
270+
lastValues: [{ startTime: date, count: 5 }],
271+
});
272+
273+
const results = await getRecentAlertHistories({
274+
alertId: new ObjectId(alert._id),
275+
createdAtLimit: 1,
276+
});
277+
278+
expect(results).toHaveLength(1);
279+
// Should not include _id, __v, or alert fields
280+
expect(results[0]).not.toHaveProperty('_id');
281+
expect(results[0]).not.toHaveProperty('__v');
282+
expect(results[0]).not.toHaveProperty('alert');
283+
// Should include the actual data
284+
expect(results[0]).toHaveProperty('counts');
285+
expect(results[0]).toHaveProperty('createdAt');
286+
expect(results[0]).toHaveProperty('state');
287+
expect(results[0]).toHaveProperty('lastValues');
288+
});
289+
290+
it('should handle complex scenario with multiple createdAt values and groups', async () => {
291+
const team = await Team.create({
292+
name: 'Test Team',
293+
});
294+
295+
const alert = await Alert.create({
296+
channel: { type: null },
297+
interval: '15m',
298+
state: AlertState.OK,
299+
team: team._id,
300+
threshold: 10,
301+
thresholdType: 'above',
302+
});
303+
304+
const date1 = new Date('2024-01-01T10:00:00Z');
305+
const date2 = new Date('2024-01-01T10:15:00Z');
306+
const date3 = new Date('2024-01-01T10:30:00Z');
307+
308+
// Create histories:
309+
// - 2 at date1
310+
// - 3 at date2
311+
// - 1 at date3
312+
await AlertHistory.create([
313+
{
314+
alert: alert._id,
315+
counts: 5,
316+
createdAt: date1,
317+
state: AlertState.OK,
318+
lastValues: [{ startTime: date1, count: 5 }],
319+
group: 'group-a',
320+
},
321+
{
322+
alert: alert._id,
323+
counts: 6,
324+
createdAt: date1,
325+
state: AlertState.OK,
326+
lastValues: [{ startTime: date1, count: 6 }],
327+
group: 'group-b',
328+
},
329+
{
330+
alert: alert._id,
331+
counts: 10,
332+
createdAt: date2,
333+
state: AlertState.ALERT,
334+
lastValues: [{ startTime: date2, count: 10 }],
335+
group: 'group-a',
336+
},
337+
{
338+
alert: alert._id,
339+
counts: 11,
340+
createdAt: date2,
341+
state: AlertState.ALERT,
342+
lastValues: [{ startTime: date2, count: 11 }],
343+
group: 'group-b',
344+
},
345+
{
346+
alert: alert._id,
347+
counts: 12,
348+
createdAt: date2,
349+
state: AlertState.ALERT,
350+
lastValues: [{ startTime: date2, count: 12 }],
351+
group: 'group-c',
352+
},
353+
{
354+
alert: alert._id,
355+
counts: 7,
356+
createdAt: date3,
357+
state: AlertState.OK,
358+
lastValues: [{ startTime: date3, count: 7 }],
359+
},
360+
]);
361+
362+
const results = await getRecentAlertHistories({
363+
alertId: new ObjectId(alert._id),
364+
createdAtLimit: 2, // Should get date3 and date2
365+
limit: 3, // Limit total to 3 entries, so one of the date2 entries gets excluded
366+
});
367+
368+
// Should return 1 from date3 + 2 from date2 = 3 total
369+
expect(results).toHaveLength(3);
370+
371+
// Verify the dates (most recent first)
372+
const uniqueDates = [...new Set(results.map(r => r.createdAt.getTime()))];
373+
expect(uniqueDates).toHaveLength(2);
374+
expect(uniqueDates[0]).toBe(date3.getTime());
375+
expect(uniqueDates[1]).toBe(date2.getTime());
376+
});
377+
});
378+
});

0 commit comments

Comments
 (0)