@checkstack/backend-api 0.6.0 → 0.8.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 +48 -0
- package/package.json +5 -4
- package/src/aggregated-result.test.ts +277 -0
- package/src/aggregated-result.ts +473 -0
- package/src/base-strategy-config.ts +26 -0
- package/src/collector-strategy.ts +6 -2
- package/src/health-check.ts +23 -44
- package/src/incremental-aggregation.test.ts +118 -21
- package/src/incremental-aggregation.ts +152 -22
- package/src/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# @checkstack/backend-api
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 869b4ab: ## Health Check Execution Improvements
|
|
8
|
+
|
|
9
|
+
### Breaking Changes (backend-api)
|
|
10
|
+
|
|
11
|
+
- `HealthCheckStrategy.createClient()` now accepts `unknown` instead of `TConfig` due to TypeScript contravariance constraints. Implementations should use `this.config.validate(config)` to narrow the type.
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- **Platform-level hard timeout**: The executor now wraps the entire health check execution (connection + all collectors) in a single timeout, ensuring checks never hang indefinitely.
|
|
16
|
+
- **Parallel collector execution**: Collectors now run in parallel using `Promise.allSettled()`, improving performance while ensuring all collectors complete regardless of individual failures.
|
|
17
|
+
- **Base strategy config schema**: All strategy configs now extend `baseStrategyConfigSchema` which provides a standardized `timeout` field with sensible defaults (30s, min 100ms).
|
|
18
|
+
|
|
19
|
+
### Fixes
|
|
20
|
+
|
|
21
|
+
- Fixed HTTP and Jenkins strategies clearing timeouts before reading the full response body.
|
|
22
|
+
- Simplified registry type signatures by using default type parameters.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- @checkstack/queue-api@0.2.5
|
|
27
|
+
|
|
28
|
+
## 0.7.0
|
|
29
|
+
|
|
30
|
+
### Minor Changes
|
|
31
|
+
|
|
32
|
+
- 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
|
|
33
|
+
|
|
34
|
+
All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
|
|
35
|
+
|
|
36
|
+
**Key changes:**
|
|
37
|
+
|
|
38
|
+
- **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
|
|
39
|
+
- The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
|
|
40
|
+
- Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
|
|
41
|
+
- `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
|
|
42
|
+
- `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
|
|
43
|
+
- Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
|
|
44
|
+
|
|
45
|
+
**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.
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- @checkstack/queue-api@0.2.4
|
|
50
|
+
|
|
3
51
|
## 0.6.0
|
|
4
52
|
|
|
5
53
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/backend-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
"lint:code": "eslint . --max-warnings 0"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@checkstack/common": "0.6.
|
|
13
|
-
"@checkstack/
|
|
14
|
-
"@checkstack/
|
|
12
|
+
"@checkstack/common": "0.6.2",
|
|
13
|
+
"@checkstack/healthcheck-common": "0.8.2",
|
|
14
|
+
"@checkstack/queue-api": "0.2.4",
|
|
15
|
+
"@checkstack/signal-common": "0.1.6",
|
|
15
16
|
"@orpc/client": "^1.13.2",
|
|
16
17
|
"@orpc/openapi": "^1.13.2",
|
|
17
18
|
"@orpc/server": "^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
|
+
});
|