@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 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.6.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.1",
13
- "@checkstack/queue-api": "0.2.2",
14
- "@checkstack/signal-common": "0.1.5",
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
+ });