@checkstack/backend-api 0.5.2 → 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 CHANGED
@@ -1,5 +1,59 @@
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
+
26
+ ## 0.6.0
27
+
28
+ ### Minor Changes
29
+
30
+ - 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
31
+
32
+ ### Breaking Changes (Internal)
33
+
34
+ - Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
35
+
36
+ ### New Features
37
+
38
+ - Added incremental aggregation utilities in `@checkstack/backend-api`:
39
+ - `mergeCounter()` - track occurrences
40
+ - `mergeAverage()` - track sum/count, compute avg
41
+ - `mergeRate()` - track success/total, compute %
42
+ - `mergeMinMax()` - track min/max values
43
+ - Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
44
+
45
+ ### Improvements
46
+
47
+ - Enables O(1) storage overhead by maintaining incremental aggregation state
48
+ - Prepares for real-time hourly aggregation without batch accumulation
49
+
50
+ ### Patch Changes
51
+
52
+ - Updated dependencies [f676e11]
53
+ - @checkstack/common@0.6.2
54
+ - @checkstack/signal-common@0.1.6
55
+ - @checkstack/queue-api@0.2.3
56
+
3
57
  ## 0.5.2
4
58
 
5
59
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/backend-api",
3
- "version": "0.5.2",
3
+ "version": "0.7.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.0",
13
- "@checkstack/queue-api": "0.2.1",
14
- "@checkstack/signal-common": "0.1.4",
12
+ "@checkstack/common": "0.6.1",
13
+ "@checkstack/healthcheck-common": "0.8.1",
14
+ "@checkstack/queue-api": "0.2.2",
15
+ "@checkstack/signal-common": "0.1.5",
15
16
  "@orpc/client": "^1.13.2",
16
17
  "@orpc/openapi": "^1.13.2",
17
18
  "@orpc/server": "^1.13.2",
@@ -23,8 +24,8 @@
23
24
  },
24
25
  "devDependencies": {
25
26
  "@types/bun": "latest",
26
- "@checkstack/tsconfig": "0.0.2",
27
- "@checkstack/scripts": "0.1.0"
27
+ "@checkstack/tsconfig": "0.0.3",
28
+ "@checkstack/scripts": "0.1.1"
28
29
  },
29
30
  "peerDependencies": {
30
31
  "hono": "^4.0.0",
@@ -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
+ });