@checkstack/healthcheck-backend 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -0
- package/drizzle/0010_colorful_shinobi_shaw.sql +8 -0
- package/drizzle/meta/0010_snapshot.json +469 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +15 -14
- package/src/hooks.ts +10 -0
- package/src/index.ts +18 -4
- package/src/queue-executor.ts +27 -3
- package/src/realtime-aggregation.ts +12 -0
- package/src/router.test.ts +6 -5
- package/src/router.ts +43 -8
- package/src/schema.ts +31 -1
- package/src/service.ts +215 -144
- package/src/availability.test.ts +0 -236
package/src/availability.test.ts
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, mock, beforeEach } from "bun:test";
|
|
2
|
-
import { HealthCheckService } from "./service";
|
|
3
|
-
import { subDays, subHours } from "date-fns";
|
|
4
|
-
|
|
5
|
-
describe("HealthCheckService.getAvailabilityStats", () => {
|
|
6
|
-
// Mock database
|
|
7
|
-
let mockDb: ReturnType<typeof createMockDb>;
|
|
8
|
-
let service: HealthCheckService;
|
|
9
|
-
|
|
10
|
-
// Store mock data for different queries
|
|
11
|
-
let mockHourlyAggregates: Array<{
|
|
12
|
-
bucketStart: Date;
|
|
13
|
-
runCount: number;
|
|
14
|
-
healthyCount: number;
|
|
15
|
-
}> = [];
|
|
16
|
-
let mockDailyAggregates: Array<{
|
|
17
|
-
bucketStart: Date;
|
|
18
|
-
runCount: number;
|
|
19
|
-
healthyCount: number;
|
|
20
|
-
}> = [];
|
|
21
|
-
let mockRetentionConfig: { retentionConfig: unknown } | undefined = undefined;
|
|
22
|
-
|
|
23
|
-
function createMockDb() {
|
|
24
|
-
// Select call order for getAvailabilityStats:
|
|
25
|
-
// 1. getRetentionConfig (from systemHealthChecks) - uses .then() pattern
|
|
26
|
-
// 2. hourlyAggregates
|
|
27
|
-
// 3. dailyAggregates
|
|
28
|
-
let selectCallCount = 0;
|
|
29
|
-
|
|
30
|
-
const createSelectChain = () => {
|
|
31
|
-
const currentCall = selectCallCount++;
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
from: mock(() => ({
|
|
35
|
-
where: mock(() => {
|
|
36
|
-
// Call 0: retentionConfig - uses .then() pattern
|
|
37
|
-
if (currentCall === 0) {
|
|
38
|
-
const result = mockRetentionConfig ? [mockRetentionConfig] : [];
|
|
39
|
-
// Return a promise-like object with .then()
|
|
40
|
-
return Promise.resolve(result);
|
|
41
|
-
}
|
|
42
|
-
// Call 1: hourly aggregates
|
|
43
|
-
if (currentCall === 1) return Promise.resolve(mockHourlyAggregates);
|
|
44
|
-
// Call 2: daily aggregates
|
|
45
|
-
return Promise.resolve(mockDailyAggregates);
|
|
46
|
-
}),
|
|
47
|
-
})),
|
|
48
|
-
};
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
select: mock(createSelectChain),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
beforeEach(() => {
|
|
57
|
-
// Reset mock data
|
|
58
|
-
mockHourlyAggregates = [];
|
|
59
|
-
mockDailyAggregates = [];
|
|
60
|
-
mockRetentionConfig = undefined;
|
|
61
|
-
mockDb = createMockDb();
|
|
62
|
-
service = new HealthCheckService(mockDb as never, {} as never, {} as never);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe("with no data", () => {
|
|
66
|
-
it("returns null availability when no aggregates exist", async () => {
|
|
67
|
-
const result = await service.getAvailabilityStats({
|
|
68
|
-
systemId: "sys-1",
|
|
69
|
-
configurationId: "config-1",
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
expect(result.availability31Days).toBeNull();
|
|
73
|
-
expect(result.availability365Days).toBeNull();
|
|
74
|
-
expect(result.totalRuns31Days).toBe(0);
|
|
75
|
-
expect(result.totalRuns365Days).toBe(0);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe("with hourly aggregates (real-time incremental)", () => {
|
|
80
|
-
it("calculates 100% availability when all runs are healthy", async () => {
|
|
81
|
-
mockHourlyAggregates = [
|
|
82
|
-
{
|
|
83
|
-
bucketStart: subHours(new Date(), 2),
|
|
84
|
-
runCount: 100,
|
|
85
|
-
healthyCount: 100,
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
bucketStart: subHours(new Date(), 5),
|
|
89
|
-
runCount: 100,
|
|
90
|
-
healthyCount: 100,
|
|
91
|
-
},
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
const result = await service.getAvailabilityStats({
|
|
95
|
-
systemId: "sys-1",
|
|
96
|
-
configurationId: "config-1",
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
expect(result.availability31Days).toBe(100);
|
|
100
|
-
expect(result.availability365Days).toBe(100);
|
|
101
|
-
expect(result.totalRuns31Days).toBe(200);
|
|
102
|
-
expect(result.totalRuns365Days).toBe(200);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("calculates correct availability with mixed results", async () => {
|
|
106
|
-
mockHourlyAggregates = [
|
|
107
|
-
{
|
|
108
|
-
bucketStart: subHours(new Date(), 2),
|
|
109
|
-
runCount: 100,
|
|
110
|
-
healthyCount: 90,
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
bucketStart: subHours(new Date(), 5),
|
|
114
|
-
runCount: 100,
|
|
115
|
-
healthyCount: 80,
|
|
116
|
-
},
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
const result = await service.getAvailabilityStats({
|
|
120
|
-
systemId: "sys-1",
|
|
121
|
-
configurationId: "config-1",
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// 170 healthy / 200 total = 85%
|
|
125
|
-
expect(result.availability31Days).toBe(85);
|
|
126
|
-
expect(result.availability365Days).toBe(85);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("includes current hour data since aggregates are updated incrementally", async () => {
|
|
130
|
-
const currentHourStart = new Date();
|
|
131
|
-
currentHourStart.setMinutes(0, 0, 0);
|
|
132
|
-
|
|
133
|
-
mockHourlyAggregates = [
|
|
134
|
-
{ bucketStart: currentHourStart, runCount: 10, healthyCount: 9 },
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
const result = await service.getAvailabilityStats({
|
|
138
|
-
systemId: "sys-1",
|
|
139
|
-
configurationId: "config-1",
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
expect(result.totalRuns31Days).toBe(10);
|
|
143
|
-
expect(result.availability31Days).toBe(90);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe("with combined hourly and daily aggregates", () => {
|
|
148
|
-
it("combines hourly and daily data correctly", async () => {
|
|
149
|
-
mockHourlyAggregates = [
|
|
150
|
-
{
|
|
151
|
-
bucketStart: subHours(new Date(), 2),
|
|
152
|
-
runCount: 100,
|
|
153
|
-
healthyCount: 99,
|
|
154
|
-
},
|
|
155
|
-
];
|
|
156
|
-
|
|
157
|
-
mockDailyAggregates = [
|
|
158
|
-
{
|
|
159
|
-
bucketStart: subDays(new Date(), 60),
|
|
160
|
-
runCount: 100,
|
|
161
|
-
healthyCount: 50,
|
|
162
|
-
},
|
|
163
|
-
];
|
|
164
|
-
|
|
165
|
-
const result = await service.getAvailabilityStats({
|
|
166
|
-
systemId: "sys-1",
|
|
167
|
-
configurationId: "config-1",
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// 31 days: only hourly (99/100) = 99%
|
|
171
|
-
expect(result.availability31Days).toBe(99);
|
|
172
|
-
expect(result.totalRuns31Days).toBe(100);
|
|
173
|
-
|
|
174
|
-
// 365 days: (99+50)/200 = 74.5%
|
|
175
|
-
expect(result.availability365Days).toBe(74.5);
|
|
176
|
-
expect(result.totalRuns365Days).toBe(200);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
describe("99.9% availability calculation", () => {
|
|
181
|
-
it("calculates precise availability for SLA tracking", async () => {
|
|
182
|
-
mockHourlyAggregates = [
|
|
183
|
-
{
|
|
184
|
-
bucketStart: subHours(new Date(), 5),
|
|
185
|
-
runCount: 1000,
|
|
186
|
-
healthyCount: 999,
|
|
187
|
-
},
|
|
188
|
-
];
|
|
189
|
-
|
|
190
|
-
const result = await service.getAvailabilityStats({
|
|
191
|
-
systemId: "sys-1",
|
|
192
|
-
configurationId: "config-1",
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
expect(result.availability31Days).toBe(99.9);
|
|
196
|
-
expect(result.availability365Days).toBe(99.9);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("calculates very high availability correctly", async () => {
|
|
200
|
-
mockHourlyAggregates = [
|
|
201
|
-
{
|
|
202
|
-
bucketStart: subHours(new Date(), 5),
|
|
203
|
-
runCount: 10_000,
|
|
204
|
-
healthyCount: 9999,
|
|
205
|
-
},
|
|
206
|
-
];
|
|
207
|
-
|
|
208
|
-
const result = await service.getAvailabilityStats({
|
|
209
|
-
systemId: "sys-1",
|
|
210
|
-
configurationId: "config-1",
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
expect(result.availability31Days).toBe(99.99);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe("real-time incremental aggregation behavior", () => {
|
|
218
|
-
it("uses hourly aggregates directly without raw run queries", async () => {
|
|
219
|
-
mockHourlyAggregates = [
|
|
220
|
-
{
|
|
221
|
-
bucketStart: subHours(new Date(), 1),
|
|
222
|
-
runCount: 50,
|
|
223
|
-
healthyCount: 48,
|
|
224
|
-
},
|
|
225
|
-
];
|
|
226
|
-
|
|
227
|
-
const result = await service.getAvailabilityStats({
|
|
228
|
-
systemId: "sys-1",
|
|
229
|
-
configurationId: "config-1",
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
expect(result.availability31Days).toBe(96);
|
|
233
|
-
expect(result.totalRuns31Days).toBe(50);
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
});
|