@checkstack/healthcheck-http-backend 0.2.4 → 0.3.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/healthcheck-http-backend
2
2
 
3
+ ## 0.3.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
+ - Updated dependencies [3dd1914]
25
+ - @checkstack/backend-api@0.7.0
26
+
27
+ ## 0.2.5
28
+
29
+ ### Patch Changes
30
+
31
+ - 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
32
+
33
+ ### Breaking Changes (Internal)
34
+
35
+ - Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
36
+
37
+ ### New Features
38
+
39
+ - Added incremental aggregation utilities in `@checkstack/backend-api`:
40
+ - `mergeCounter()` - track occurrences
41
+ - `mergeAverage()` - track sum/count, compute avg
42
+ - `mergeRate()` - track success/total, compute %
43
+ - `mergeMinMax()` - track min/max values
44
+ - Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
45
+
46
+ ### Improvements
47
+
48
+ - Enables O(1) storage overhead by maintaining incremental aggregation state
49
+ - Prepares for real-time hourly aggregation without batch accumulation
50
+
51
+ - Updated dependencies [f676e11]
52
+ - Updated dependencies [48c2080]
53
+ - @checkstack/common@0.6.2
54
+ - @checkstack/backend-api@0.6.0
55
+ - @checkstack/healthcheck-common@0.8.2
56
+
3
57
  ## 0.2.4
4
58
 
5
59
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-http-backend",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -9,16 +9,16 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/backend-api": "0.5.1",
13
- "@checkstack/healthcheck-common": "0.7.0",
12
+ "@checkstack/backend-api": "0.5.2",
13
+ "@checkstack/healthcheck-common": "0.8.1",
14
14
  "jsonpath-plus": "^10.3.0",
15
- "@checkstack/common": "0.6.0"
15
+ "@checkstack/common": "0.6.1"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@types/bun": "^1.0.0",
19
19
  "drizzle-kit": "^0.31.8",
20
20
  "typescript": "^5.0.0",
21
- "@checkstack/tsconfig": "0.0.2",
22
- "@checkstack/scripts": "0.1.0"
21
+ "@checkstack/tsconfig": "0.0.3",
22
+ "@checkstack/scripts": "0.1.1"
23
23
  }
24
24
  }
@@ -8,7 +8,7 @@ describe("RequestCollector", () => {
8
8
  statusCode?: number;
9
9
  statusText?: string;
10
10
  body?: string;
11
- } = {}
11
+ } = {},
12
12
  ): HttpTransportClient => ({
13
13
  exec: mock(() =>
14
14
  Promise.resolve({
@@ -16,7 +16,7 @@ describe("RequestCollector", () => {
16
16
  statusText: response.statusText ?? "OK",
17
17
  headers: {},
18
18
  body: response.body ?? "",
19
- })
19
+ }),
20
20
  ),
21
21
  });
22
22
 
@@ -89,7 +89,7 @@ describe("RequestCollector", () => {
89
89
  "Content-Type": "application/json",
90
90
  Authorization: "Bearer token",
91
91
  },
92
- })
92
+ }),
93
93
  );
94
94
  });
95
95
 
@@ -111,12 +111,12 @@ describe("RequestCollector", () => {
111
111
  expect(client.exec).toHaveBeenCalledWith(
112
112
  expect.objectContaining({
113
113
  body: '{"key":"value"}',
114
- })
114
+ }),
115
115
  );
116
116
  });
117
117
  });
118
118
 
119
- describe("aggregateResult", () => {
119
+ describe("mergeResult", () => {
120
120
  it("should calculate average response time", () => {
121
121
  const collector = new RequestCollector();
122
122
  const runs = [
@@ -152,10 +152,12 @@ describe("RequestCollector", () => {
152
152
  },
153
153
  ];
154
154
 
155
- const aggregated = collector.aggregateResult(runs);
155
+ // Merge runs incrementally
156
+ let aggregated = collector.mergeResult(undefined, runs[0]);
157
+ aggregated = collector.mergeResult(aggregated, runs[1]);
156
158
 
157
- expect(aggregated.avgResponseTimeMs).toBe(75);
158
- expect(aggregated.successRate).toBe(100);
159
+ expect(aggregated.avgResponseTimeMs.avg).toBe(75);
160
+ expect(aggregated.successRate.rate).toBe(100);
159
161
  });
160
162
 
161
163
  it("should calculate success rate correctly", () => {
@@ -193,9 +195,11 @@ describe("RequestCollector", () => {
193
195
  },
194
196
  ];
195
197
 
196
- const aggregated = collector.aggregateResult(runs);
198
+ // Merge runs incrementally
199
+ let aggregated = collector.mergeResult(undefined, runs[0]);
200
+ aggregated = collector.mergeResult(aggregated, runs[1]);
197
201
 
198
- expect(aggregated.successRate).toBe(50);
202
+ expect(aggregated.successRate.rate).toBe(50);
199
203
  });
200
204
  });
201
205
 
@@ -5,6 +5,12 @@ import {
5
5
  type HealthCheckRunForAggregation,
6
6
  type CollectorResult,
7
7
  type CollectorStrategy,
8
+ mergeAverage,
9
+ mergeRate,
10
+ VersionedAggregated,
11
+ aggregatedAverage,
12
+ aggregatedRate,
13
+ type InferAggregatedResult,
8
14
  } from "@checkstack/backend-api";
9
15
  import {
10
16
  healthResultNumber,
@@ -76,20 +82,24 @@ const requestResultSchema = healthResultSchema({
76
82
 
77
83
  export type RequestResult = z.infer<typeof requestResultSchema>;
78
84
 
79
- const requestAggregatedSchema = healthResultSchema({
80
- avgResponseTimeMs: healthResultNumber({
85
+ // Aggregated result fields definition
86
+ const requestAggregatedFields = {
87
+ avgResponseTimeMs: aggregatedAverage({
81
88
  "x-chart-type": "line",
82
89
  "x-chart-label": "Avg Response Time",
83
90
  "x-chart-unit": "ms",
84
91
  }),
85
- successRate: healthResultNumber({
92
+ successRate: aggregatedRate({
86
93
  "x-chart-type": "gauge",
87
94
  "x-chart-label": "Success Rate",
88
95
  "x-chart-unit": "%",
89
96
  }),
90
- });
97
+ };
91
98
 
92
- export type RequestAggregatedResult = z.infer<typeof requestAggregatedSchema>;
99
+ // Type inferred automatically from field definitions
100
+ export type RequestAggregatedResult = InferAggregatedResult<
101
+ typeof requestAggregatedFields
102
+ >;
93
103
 
94
104
  // ============================================================================
95
105
  // REQUEST COLLECTOR
@@ -115,9 +125,9 @@ export class RequestCollector implements CollectorStrategy<
115
125
 
116
126
  config = new Versioned({ version: 1, schema: requestConfigSchema });
117
127
  result = new Versioned({ version: 1, schema: requestResultSchema });
118
- aggregatedResult = new Versioned({
128
+ aggregatedResult = new VersionedAggregated({
119
129
  version: 1,
120
- schema: requestAggregatedSchema,
130
+ fields: requestAggregatedFields,
121
131
  });
122
132
 
123
133
  async execute({
@@ -162,28 +172,16 @@ export class RequestCollector implements CollectorStrategy<
162
172
  };
163
173
  }
164
174
 
165
- aggregateResult(
166
- runs: HealthCheckRunForAggregation<RequestResult>[],
175
+ mergeResult(
176
+ existing: RequestAggregatedResult | undefined,
177
+ newRun: HealthCheckRunForAggregation<RequestResult>,
167
178
  ): RequestAggregatedResult {
168
- const times = runs
169
- .map((r) => r.metadata?.responseTimeMs)
170
- .filter((v): v is number => typeof v === "number");
171
-
172
- const successes = runs
173
- .map((r) => r.metadata?.success)
174
- .filter((v): v is boolean => typeof v === "boolean");
175
-
176
- const successCount = successes.filter(Boolean).length;
177
-
178
179
  return {
179
- avgResponseTimeMs:
180
- times.length > 0
181
- ? Math.round(times.reduce((a, b) => a + b, 0) / times.length)
182
- : 0,
183
- successRate:
184
- successes.length > 0
185
- ? Math.round((successCount / successes.length) * 100)
186
- : 0,
180
+ avgResponseTimeMs: mergeAverage(
181
+ existing?.avgResponseTimeMs,
182
+ newRun.metadata?.responseTimeMs,
183
+ ),
184
+ successRate: mergeRate(existing?.successRate, newRun.metadata?.success),
187
185
  };
188
186
  }
189
187
  }
@@ -31,7 +31,7 @@ describe("HttpHealthCheckStrategy", () => {
31
31
  status: 200,
32
32
  statusText: "OK",
33
33
  headers: { "Content-Type": "application/json" },
34
- })
34
+ }),
35
35
  );
36
36
 
37
37
  const connectedClient = await strategy.createClient({ timeout: 5000 });
@@ -50,7 +50,7 @@ describe("HttpHealthCheckStrategy", () => {
50
50
 
51
51
  it("should return 404 status for not found", async () => {
52
52
  spyOn(globalThis, "fetch").mockResolvedValue(
53
- new Response(null, { status: 404, statusText: "Not Found" })
53
+ new Response(null, { status: 404, statusText: "Not Found" }),
54
54
  );
55
55
 
56
56
  const connectedClient = await strategy.createClient({ timeout: 5000 });
@@ -69,7 +69,7 @@ describe("HttpHealthCheckStrategy", () => {
69
69
  let capturedHeaders: Record<string, string> | undefined;
70
70
  spyOn(globalThis, "fetch").mockImplementation((async (
71
71
  _url: RequestInfo | URL,
72
- options?: RequestInit
72
+ options?: RequestInit,
73
73
  ) => {
74
74
  capturedHeaders = options?.headers as Record<string, string>;
75
75
  return new Response(null, { status: 200 });
@@ -99,7 +99,7 @@ describe("HttpHealthCheckStrategy", () => {
99
99
  new Response(JSON.stringify(responseBody), {
100
100
  status: 200,
101
101
  headers: { "Content-Type": "application/json" },
102
- })
102
+ }),
103
103
  );
104
104
 
105
105
  const connectedClient = await strategy.createClient({ timeout: 5000 });
@@ -119,7 +119,7 @@ describe("HttpHealthCheckStrategy", () => {
119
119
  new Response("Hello World", {
120
120
  status: 200,
121
121
  headers: { "Content-Type": "text/plain" },
122
- })
122
+ }),
123
123
  );
124
124
 
125
125
  const connectedClient = await strategy.createClient({ timeout: 5000 });
@@ -138,7 +138,7 @@ describe("HttpHealthCheckStrategy", () => {
138
138
  let capturedBody: string | undefined;
139
139
  spyOn(globalThis, "fetch").mockImplementation((async (
140
140
  _url: RequestInfo | URL,
141
- options?: RequestInit
141
+ options?: RequestInit,
142
142
  ) => {
143
143
  capturedBody = options?.body as string;
144
144
  return new Response(null, { status: 201 });
@@ -161,7 +161,7 @@ describe("HttpHealthCheckStrategy", () => {
161
161
  let capturedMethod: string | undefined;
162
162
  spyOn(globalThis, "fetch").mockImplementation((async (
163
163
  _url: RequestInfo | URL,
164
- options?: RequestInit
164
+ options?: RequestInit,
165
165
  ) => {
166
166
  capturedMethod = options?.method;
167
167
  return new Response(null, { status: 200 });
@@ -180,7 +180,7 @@ describe("HttpHealthCheckStrategy", () => {
180
180
  });
181
181
  });
182
182
 
183
- describe("aggregateResult", () => {
183
+ describe("mergeResult", () => {
184
184
  it("should count errors correctly", () => {
185
185
  const runs = [
186
186
  {
@@ -213,9 +213,12 @@ describe("HttpHealthCheckStrategy", () => {
213
213
  },
214
214
  ];
215
215
 
216
- const aggregated = strategy.aggregateResult(runs);
216
+ // Merge runs incrementally
217
+ let aggregated = strategy.mergeResult(undefined, runs[0]);
218
+ aggregated = strategy.mergeResult(aggregated, runs[1]);
219
+ aggregated = strategy.mergeResult(aggregated, runs[2]);
217
220
 
218
- expect(aggregated.errorCount).toBe(2);
221
+ expect(aggregated.errorCount.count).toBe(2);
219
222
  });
220
223
 
221
224
  it("should return zero errors when all runs succeed", () => {
@@ -238,9 +241,11 @@ describe("HttpHealthCheckStrategy", () => {
238
241
  },
239
242
  ];
240
243
 
241
- const aggregated = strategy.aggregateResult(runs);
244
+ // Merge runs incrementally
245
+ let aggregated = strategy.mergeResult(undefined, runs[0]);
246
+ aggregated = strategy.mergeResult(aggregated, runs[1]);
242
247
 
243
- expect(aggregated.errorCount).toBe(0);
248
+ expect(aggregated.errorCount.count).toBe(0);
244
249
  });
245
250
  });
246
251
  });
package/src/strategy.ts CHANGED
@@ -2,11 +2,14 @@ import {
2
2
  HealthCheckStrategy,
3
3
  HealthCheckRunForAggregation,
4
4
  Versioned,
5
+ VersionedAggregated,
6
+ aggregatedCounter,
7
+ mergeCounter,
5
8
  z,
9
+ type InferAggregatedResult,
6
10
  type ConnectedClient,
7
11
  } from "@checkstack/backend-api";
8
12
  import {
9
- healthResultNumber,
10
13
  healthResultString,
11
14
  healthResultSchema,
12
15
  } from "@checkstack/healthcheck-common";
@@ -57,29 +60,26 @@ const httpResultMetadataSchema = healthResultSchema({
57
60
 
58
61
  type HttpResultMetadata = z.infer<typeof httpResultMetadataSchema>;
59
62
 
60
- /** Aggregated metadata for buckets */
61
- const httpAggregatedMetadataSchema = healthResultSchema({
62
- errorCount: healthResultNumber({
63
+ /** Aggregated field definitions for bucket merging */
64
+ const httpAggregatedFields = {
65
+ errorCount: aggregatedCounter({
63
66
  "x-chart-type": "counter",
64
67
  "x-chart-label": "Errors",
65
68
  }),
66
- });
69
+ };
67
70
 
68
- type HttpAggregatedMetadata = z.infer<typeof httpAggregatedMetadataSchema>;
71
+ type HttpAggregatedResult = InferAggregatedResult<typeof httpAggregatedFields>;
69
72
 
70
73
  // ============================================================================
71
74
  // STRATEGY
72
75
  // ============================================================================
73
76
 
74
- export class HttpHealthCheckStrategy
75
- implements
76
- HealthCheckStrategy<
77
- HttpHealthCheckConfig,
78
- HttpTransportClient,
79
- HttpResultMetadata,
80
- HttpAggregatedMetadata
81
- >
82
- {
77
+ export class HttpHealthCheckStrategy implements HealthCheckStrategy<
78
+ HttpHealthCheckConfig,
79
+ HttpTransportClient,
80
+ HttpResultMetadata,
81
+ typeof httpAggregatedFields
82
+ > {
83
83
  id = "http";
84
84
  displayName = "HTTP/HTTPS Health Check";
85
85
  description = "HTTP endpoint health monitoring";
@@ -114,23 +114,19 @@ export class HttpHealthCheckStrategy
114
114
  schema: httpResultMetadataSchema,
115
115
  });
116
116
 
117
- aggregatedResult: Versioned<HttpAggregatedMetadata> = new Versioned({
117
+ aggregatedResult = new VersionedAggregated({
118
118
  version: 1,
119
- schema: httpAggregatedMetadataSchema,
119
+ fields: httpAggregatedFields,
120
120
  });
121
121
 
122
- aggregateResult(
123
- runs: HealthCheckRunForAggregation<HttpResultMetadata>[]
124
- ): HttpAggregatedMetadata {
125
- let errorCount = 0;
126
-
127
- for (const run of runs) {
128
- if (run.metadata?.error) {
129
- errorCount++;
130
- }
131
- }
132
-
133
- return { errorCount };
122
+ mergeResult(
123
+ existing: HttpAggregatedResult | undefined,
124
+ newRun: HealthCheckRunForAggregation<HttpResultMetadata>,
125
+ ): HttpAggregatedResult {
126
+ const hasError = !!newRun.metadata?.error;
127
+ return {
128
+ errorCount: mergeCounter(existing?.errorCount, hasError),
129
+ };
134
130
  }
135
131
 
136
132
  /**
@@ -138,7 +134,7 @@ export class HttpHealthCheckStrategy
138
134
  * All request parameters come from the collector (RequestCollector).
139
135
  */
140
136
  async createClient(
141
- config: HttpHealthCheckConfig
137
+ config: HttpHealthCheckConfig,
142
138
  ): Promise<ConnectedClient<HttpTransportClient>> {
143
139
  const validatedConfig = this.config.validate(config);
144
140
 
@@ -147,7 +143,7 @@ export class HttpHealthCheckStrategy
147
143
  const controller = new AbortController();
148
144
  const timeoutId = setTimeout(
149
145
  () => controller.abort(),
150
- request.timeout ?? validatedConfig.timeout
146
+ request.timeout ?? validatedConfig.timeout,
151
147
  );
152
148
 
153
149
  try {