@checkstack/backend-api 0.6.0 → 0.7.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 +23 -0
- package/package.json +2 -1
- package/src/aggregated-result.test.ts +277 -0
- package/src/aggregated-result.ts +473 -0
- package/src/collector-strategy.ts +6 -2
- package/src/health-check.ts +14 -10
- package/src/incremental-aggregation.test.ts +118 -21
- package/src/incremental-aggregation.ts +152 -22
- package/src/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @checkstack/backend-api
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
|
|
8
|
+
|
|
9
|
+
All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
|
|
10
|
+
|
|
11
|
+
**Key changes:**
|
|
12
|
+
|
|
13
|
+
- **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
|
|
14
|
+
- The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
|
|
15
|
+
- Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
|
|
16
|
+
- `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
|
|
17
|
+
- `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
|
|
18
|
+
- Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
|
|
19
|
+
|
|
20
|
+
**Breaking Change**: State objects now require `_type`. Merge functions automatically add `_type` to output. The bucket merging functions and `HealthCheckService` now require additional required parameters.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- @checkstack/queue-api@0.2.4
|
|
25
|
+
|
|
3
26
|
## 0.6.0
|
|
4
27
|
|
|
5
28
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/backend-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@checkstack/common": "0.6.1",
|
|
13
|
+
"@checkstack/healthcheck-common": "0.8.1",
|
|
13
14
|
"@checkstack/queue-api": "0.2.2",
|
|
14
15
|
"@checkstack/signal-common": "0.1.5",
|
|
15
16
|
"@orpc/client": "^1.13.2",
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
aggregatedAverage,
|
|
4
|
+
aggregatedRate,
|
|
5
|
+
aggregatedCounter,
|
|
6
|
+
aggregatedMinMax,
|
|
7
|
+
buildAggregatedResultSchema,
|
|
8
|
+
} from "./aggregated-result";
|
|
9
|
+
import { getHealthResultMeta } from "@checkstack/healthcheck-common";
|
|
10
|
+
|
|
11
|
+
describe("aggregatedAverage", () => {
|
|
12
|
+
it("creates field with correct type and merge function", () => {
|
|
13
|
+
const field = aggregatedAverage({
|
|
14
|
+
"x-chart-type": "line",
|
|
15
|
+
"x-chart-label": "Avg Response Time",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(field.type).toBe("average");
|
|
19
|
+
expect(field.meta["x-chart-label"]).toBe("Avg Response Time");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("mergeStates correctly merges two average states", () => {
|
|
23
|
+
const field = aggregatedAverage({});
|
|
24
|
+
const merged = field.mergeStates(
|
|
25
|
+
{ _sum: 100, _count: 2, avg: 50 },
|
|
26
|
+
{ _sum: 200, _count: 2, avg: 100 },
|
|
27
|
+
);
|
|
28
|
+
expect(merged).toEqual({ _type: "average", _sum: 300, _count: 4, avg: 75 });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("getDisplayValue returns avg", () => {
|
|
32
|
+
const field = aggregatedAverage({});
|
|
33
|
+
expect(field.getDisplayValue({ _sum: 100, _count: 2, avg: 50 })).toBe(50);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("aggregatedRate", () => {
|
|
38
|
+
it("creates field with correct type", () => {
|
|
39
|
+
const field = aggregatedRate({
|
|
40
|
+
"x-chart-type": "gauge",
|
|
41
|
+
"x-chart-label": "Success Rate",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(field.type).toBe("rate");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("mergeStates correctly merges two rate states", () => {
|
|
48
|
+
const field = aggregatedRate({});
|
|
49
|
+
const merged = field.mergeStates(
|
|
50
|
+
{ _success: 3, _total: 4, rate: 75 },
|
|
51
|
+
{ _success: 7, _total: 10, rate: 70 },
|
|
52
|
+
);
|
|
53
|
+
expect(merged.rate).toBe(71);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("getDisplayValue returns rate", () => {
|
|
57
|
+
const field = aggregatedRate({});
|
|
58
|
+
expect(field.getDisplayValue({ _success: 5, _total: 10, rate: 50 })).toBe(
|
|
59
|
+
50,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("aggregatedCounter", () => {
|
|
65
|
+
it("creates field with correct type", () => {
|
|
66
|
+
const field = aggregatedCounter({
|
|
67
|
+
"x-chart-type": "counter",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(field.type).toBe("counter");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("mergeStates correctly merges two counter states", () => {
|
|
74
|
+
const field = aggregatedCounter({});
|
|
75
|
+
const merged = field.mergeStates({ count: 5 }, { count: 3 });
|
|
76
|
+
expect(merged).toEqual({ _type: "counter", count: 8 });
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("aggregatedMinMax", () => {
|
|
81
|
+
it("creates field with correct type", () => {
|
|
82
|
+
const field = aggregatedMinMax({
|
|
83
|
+
"x-chart-type": "line",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(field.type).toBe("minmax");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("mergeStates correctly merges two minmax states", () => {
|
|
90
|
+
const field = aggregatedMinMax({});
|
|
91
|
+
const merged = field.mergeStates(
|
|
92
|
+
{ min: 10, max: 50 },
|
|
93
|
+
{ min: 5, max: 100 },
|
|
94
|
+
);
|
|
95
|
+
expect(merged).toEqual({ _type: "minmax", min: 5, max: 100 });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("buildAggregatedResultSchema", () => {
|
|
100
|
+
it("creates schema with field keys mapping to state types", () => {
|
|
101
|
+
const { schema } = buildAggregatedResultSchema({
|
|
102
|
+
avgResponseTimeMs: aggregatedAverage({}),
|
|
103
|
+
successRate: aggregatedRate({}),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Should parse valid data with _type - field keys now map directly to state objects
|
|
107
|
+
const result = schema.parse({
|
|
108
|
+
avgResponseTimeMs: { _type: "average", _sum: 100, _count: 1, avg: 100 },
|
|
109
|
+
successRate: { _type: "rate", _success: 95, _total: 100, rate: 95 },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(result.avgResponseTimeMs.avg).toBe(100);
|
|
113
|
+
expect(result.successRate.rate).toBe(95);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("mergeAggregatedResults correctly merges two results", () => {
|
|
117
|
+
const { mergeAggregatedResults } = buildAggregatedResultSchema({
|
|
118
|
+
avgResponseTimeMs: aggregatedAverage({}),
|
|
119
|
+
successRate: aggregatedRate({}),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const a = {
|
|
123
|
+
avgResponseTimeMs: {
|
|
124
|
+
_type: "average" as const,
|
|
125
|
+
_sum: 100,
|
|
126
|
+
_count: 2,
|
|
127
|
+
avg: 50,
|
|
128
|
+
},
|
|
129
|
+
successRate: {
|
|
130
|
+
_type: "rate" as const,
|
|
131
|
+
_success: 3,
|
|
132
|
+
_total: 4,
|
|
133
|
+
rate: 75,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const b = {
|
|
138
|
+
avgResponseTimeMs: {
|
|
139
|
+
_type: "average" as const,
|
|
140
|
+
_sum: 200,
|
|
141
|
+
_count: 2,
|
|
142
|
+
avg: 100,
|
|
143
|
+
},
|
|
144
|
+
successRate: {
|
|
145
|
+
_type: "rate" as const,
|
|
146
|
+
_success: 7,
|
|
147
|
+
_total: 10,
|
|
148
|
+
rate: 70,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const merged = mergeAggregatedResults(a, b);
|
|
153
|
+
|
|
154
|
+
// Average: (100 + 200) / 4 = 75
|
|
155
|
+
expect(merged.avgResponseTimeMs.avg).toBe(75);
|
|
156
|
+
expect(merged.avgResponseTimeMs).toEqual({
|
|
157
|
+
_type: "average",
|
|
158
|
+
_sum: 300,
|
|
159
|
+
_count: 4,
|
|
160
|
+
avg: 75,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Rate: 10/14 ≈ 71%
|
|
164
|
+
expect(merged.successRate.rate).toBe(71);
|
|
165
|
+
expect(merged.successRate).toEqual({
|
|
166
|
+
_type: "rate",
|
|
167
|
+
_success: 10,
|
|
168
|
+
_total: 14,
|
|
169
|
+
rate: 71,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("mergeAggregatedResults handles undefined first argument", () => {
|
|
174
|
+
const { mergeAggregatedResults } = buildAggregatedResultSchema({
|
|
175
|
+
avgResponseTimeMs: aggregatedAverage({}),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const b = {
|
|
179
|
+
avgResponseTimeMs: {
|
|
180
|
+
_type: "average" as const,
|
|
181
|
+
_sum: 100,
|
|
182
|
+
_count: 1,
|
|
183
|
+
avg: 100,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const merged = mergeAggregatedResults(undefined, b);
|
|
188
|
+
|
|
189
|
+
expect(merged.avgResponseTimeMs.avg).toBe(100);
|
|
190
|
+
expect(merged.avgResponseTimeMs).toEqual({
|
|
191
|
+
_type: "average",
|
|
192
|
+
_sum: 100,
|
|
193
|
+
_count: 1,
|
|
194
|
+
avg: 100,
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("chart metadata registration", () => {
|
|
200
|
+
it("aggregatedAverage schema has chart metadata registered", () => {
|
|
201
|
+
const field = aggregatedAverage({
|
|
202
|
+
"x-chart-type": "line",
|
|
203
|
+
"x-chart-label": "Avg Response Time",
|
|
204
|
+
"x-chart-unit": "ms",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// The stateSchema should have metadata registered via healthResultRegistry
|
|
208
|
+
// We can verify by checking that the schema is registered
|
|
209
|
+
const meta = getHealthResultMeta(field.stateSchema);
|
|
210
|
+
|
|
211
|
+
expect(meta).toBeDefined();
|
|
212
|
+
expect(meta?.["x-chart-type"]).toBe("line");
|
|
213
|
+
expect(meta?.["x-chart-label"]).toBe("Avg Response Time");
|
|
214
|
+
expect(meta?.["x-chart-unit"]).toBe("ms");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("aggregatedRate schema has chart metadata registered", () => {
|
|
218
|
+
const field = aggregatedRate({
|
|
219
|
+
"x-chart-type": "gauge",
|
|
220
|
+
"x-chart-label": "Success Rate",
|
|
221
|
+
"x-chart-unit": "%",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const meta = getHealthResultMeta(field.stateSchema);
|
|
225
|
+
|
|
226
|
+
expect(meta).toBeDefined();
|
|
227
|
+
expect(meta?.["x-chart-type"]).toBe("gauge");
|
|
228
|
+
expect(meta?.["x-chart-label"]).toBe("Success Rate");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("aggregatedCounter schema has chart metadata registered", () => {
|
|
232
|
+
const field = aggregatedCounter({
|
|
233
|
+
"x-chart-type": "counter",
|
|
234
|
+
"x-chart-label": "Error Count",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const meta = getHealthResultMeta(field.stateSchema);
|
|
238
|
+
|
|
239
|
+
expect(meta).toBeDefined();
|
|
240
|
+
expect(meta?.["x-chart-type"]).toBe("counter");
|
|
241
|
+
expect(meta?.["x-chart-label"]).toBe("Error Count");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("aggregatedMinMax schema has chart metadata registered", () => {
|
|
245
|
+
const field = aggregatedMinMax({
|
|
246
|
+
"x-chart-type": "line",
|
|
247
|
+
"x-chart-label": "Latency Range",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const meta = getHealthResultMeta(field.stateSchema);
|
|
251
|
+
|
|
252
|
+
expect(meta).toBeDefined();
|
|
253
|
+
expect(meta?.["x-chart-type"]).toBe("line");
|
|
254
|
+
expect(meta?.["x-chart-label"]).toBe("Latency Range");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("each field instance has unique metadata", () => {
|
|
258
|
+
const field1 = aggregatedAverage({
|
|
259
|
+
"x-chart-type": "line",
|
|
260
|
+
"x-chart-label": "First Field",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const field2 = aggregatedAverage({
|
|
264
|
+
"x-chart-type": "gauge",
|
|
265
|
+
"x-chart-label": "Second Field",
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const meta1 = getHealthResultMeta(field1.stateSchema);
|
|
269
|
+
const meta2 = getHealthResultMeta(field2.stateSchema);
|
|
270
|
+
|
|
271
|
+
// Each field should have its own metadata
|
|
272
|
+
expect(meta1?.["x-chart-label"]).toBe("First Field");
|
|
273
|
+
expect(meta2?.["x-chart-label"]).toBe("Second Field");
|
|
274
|
+
expect(meta1?.["x-chart-type"]).toBe("line");
|
|
275
|
+
expect(meta2?.["x-chart-type"]).toBe("gauge");
|
|
276
|
+
});
|
|
277
|
+
});
|