@checkstack/healthcheck-http-backend 0.2.5 → 0.3.1

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/healthcheck-http-backend
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch 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
+ - Updated dependencies [869b4ab]
25
+ - @checkstack/backend-api@0.8.0
26
+
27
+ ## 0.3.0
28
+
29
+ ### Minor Changes
30
+
31
+ - 3dd1914: Migrate health check strategies to VersionedAggregated with \_type discriminator
32
+
33
+ All 13 health check strategies now use `VersionedAggregated` for their `aggregatedResult` property, enabling automatic bucket merging with 100% mathematical fidelity.
34
+
35
+ **Key changes:**
36
+
37
+ - **`_type` discriminator**: All aggregated state objects now include a required `_type` field (`"average"`, `"rate"`, `"counter"`, `"minmax"`) for reliable type detection
38
+ - The `HealthCheckStrategy` interface now requires `aggregatedResult` to be a `VersionedAggregated<AggregatedResultShape>`
39
+ - Strategy/collector `mergeResult` methods return state objects with `_type` (e.g., `{ _type: "average", _sum, _count, avg }`)
40
+ - `mergeAggregatedBucketResults`, `combineBuckets`, and `reaggregateBuckets` now require `registry` and `strategyId` parameters
41
+ - `HealthCheckService` constructor now requires both `registry` and `collectorRegistry` parameters
42
+ - Frontend `extractComputedValue` now uses `_type` discriminator for robust type detection
43
+
44
+ **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.
45
+
46
+ ### Patch Changes
47
+
48
+ - Updated dependencies [3dd1914]
49
+ - @checkstack/backend-api@0.7.0
50
+
3
51
  ## 0.2.5
4
52
 
5
53
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-http-backend",
3
- "version": "0.2.5",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
@@ -9,10 +9,10 @@
9
9
  "lint:code": "eslint . --max-warnings 0"
10
10
  },
11
11
  "dependencies": {
12
- "@checkstack/backend-api": "0.5.2",
13
- "@checkstack/healthcheck-common": "0.8.1",
12
+ "@checkstack/backend-api": "0.7.0",
13
+ "@checkstack/healthcheck-common": "0.8.2",
14
14
  "jsonpath-plus": "^10.3.0",
15
- "@checkstack/common": "0.6.1"
15
+ "@checkstack/common": "0.6.2"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@types/bun": "^1.0.0",
@@ -156,8 +156,8 @@ describe("RequestCollector", () => {
156
156
  let aggregated = collector.mergeResult(undefined, runs[0]);
157
157
  aggregated = collector.mergeResult(aggregated, runs[1]);
158
158
 
159
- expect(aggregated.avgResponseTimeMs).toBe(75);
160
- expect(aggregated.successRate).toBe(100);
159
+ expect(aggregated.avgResponseTimeMs.avg).toBe(75);
160
+ expect(aggregated.successRate.rate).toBe(100);
161
161
  });
162
162
 
163
163
  it("should calculate success rate correctly", () => {
@@ -199,7 +199,7 @@ describe("RequestCollector", () => {
199
199
  let aggregated = collector.mergeResult(undefined, runs[0]);
200
200
  aggregated = collector.mergeResult(aggregated, runs[1]);
201
201
 
202
- expect(aggregated.successRate).toBe(50);
202
+ expect(aggregated.successRate.rate).toBe(50);
203
203
  });
204
204
  });
205
205
 
@@ -7,8 +7,10 @@ import {
7
7
  type CollectorStrategy,
8
8
  mergeAverage,
9
9
  mergeRate,
10
- averageStateSchema,
11
- rateStateSchema,
10
+ VersionedAggregated,
11
+ aggregatedAverage,
12
+ aggregatedRate,
13
+ type InferAggregatedResult,
12
14
  } from "@checkstack/backend-api";
13
15
  import {
14
16
  healthResultNumber,
@@ -80,32 +82,24 @@ const requestResultSchema = healthResultSchema({
80
82
 
81
83
  export type RequestResult = z.infer<typeof requestResultSchema>;
82
84
 
83
- // UI-visible aggregated fields (for charts)
84
- const requestAggregatedDisplaySchema = healthResultSchema({
85
- avgResponseTimeMs: healthResultNumber({
85
+ // Aggregated result fields definition
86
+ const requestAggregatedFields = {
87
+ avgResponseTimeMs: aggregatedAverage({
86
88
  "x-chart-type": "line",
87
89
  "x-chart-label": "Avg Response Time",
88
90
  "x-chart-unit": "ms",
89
91
  }),
90
- successRate: healthResultNumber({
92
+ successRate: aggregatedRate({
91
93
  "x-chart-type": "gauge",
92
94
  "x-chart-label": "Success Rate",
93
95
  "x-chart-unit": "%",
94
96
  }),
95
- });
96
-
97
- // Internal state for incremental aggregation (not shown in charts)
98
- const requestAggregatedInternalSchema = z.object({
99
- _responseTime: averageStateSchema.optional(),
100
- _success: rateStateSchema.optional(),
101
- });
97
+ };
102
98
 
103
- // Combined schema for storage
104
- const requestAggregatedSchema = requestAggregatedDisplaySchema.and(
105
- requestAggregatedInternalSchema,
106
- );
107
-
108
- export type RequestAggregatedResult = z.infer<typeof requestAggregatedSchema>;
99
+ // Type inferred automatically from field definitions
100
+ export type RequestAggregatedResult = InferAggregatedResult<
101
+ typeof requestAggregatedFields
102
+ >;
109
103
 
110
104
  // ============================================================================
111
105
  // REQUEST COLLECTOR
@@ -131,9 +125,9 @@ export class RequestCollector implements CollectorStrategy<
131
125
 
132
126
  config = new Versioned({ version: 1, schema: requestConfigSchema });
133
127
  result = new Versioned({ version: 1, schema: requestResultSchema });
134
- aggregatedResult = new Versioned({
128
+ aggregatedResult = new VersionedAggregated({
135
129
  version: 1,
136
- schema: requestAggregatedSchema,
130
+ fields: requestAggregatedFields,
137
131
  });
138
132
 
139
133
  async execute({
@@ -182,17 +176,12 @@ export class RequestCollector implements CollectorStrategy<
182
176
  existing: RequestAggregatedResult | undefined,
183
177
  newRun: HealthCheckRunForAggregation<RequestResult>,
184
178
  ): RequestAggregatedResult {
185
- const responseTime = mergeAverage(
186
- existing?._responseTime,
187
- newRun.metadata?.responseTimeMs,
188
- );
189
- const success = mergeRate(existing?._success, newRun.metadata?.success);
190
-
191
179
  return {
192
- avgResponseTimeMs: responseTime.avg,
193
- successRate: success.rate,
194
- _responseTime: responseTime,
195
- _success: success,
180
+ avgResponseTimeMs: mergeAverage(
181
+ existing?.avgResponseTimeMs,
182
+ newRun.metadata?.responseTimeMs,
183
+ ),
184
+ successRate: mergeRate(existing?.successRate, newRun.metadata?.success),
196
185
  };
197
186
  }
198
187
  }
@@ -218,7 +218,7 @@ describe("HttpHealthCheckStrategy", () => {
218
218
  aggregated = strategy.mergeResult(aggregated, runs[1]);
219
219
  aggregated = strategy.mergeResult(aggregated, runs[2]);
220
220
 
221
- expect(aggregated.errorCount).toBe(2);
221
+ expect(aggregated.errorCount.count).toBe(2);
222
222
  });
223
223
 
224
224
  it("should return zero errors when all runs succeed", () => {
@@ -245,7 +245,7 @@ describe("HttpHealthCheckStrategy", () => {
245
245
  let aggregated = strategy.mergeResult(undefined, runs[0]);
246
246
  aggregated = strategy.mergeResult(aggregated, runs[1]);
247
247
 
248
- expect(aggregated.errorCount).toBe(0);
248
+ expect(aggregated.errorCount.count).toBe(0);
249
249
  });
250
250
  });
251
251
  });
package/src/strategy.ts CHANGED
@@ -2,12 +2,15 @@ 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
- mergeCounter,
11
+ baseStrategyConfigSchema,
8
12
  } from "@checkstack/backend-api";
9
13
  import {
10
- healthResultNumber,
11
14
  healthResultString,
12
15
  healthResultSchema,
13
16
  } from "@checkstack/healthcheck-common";
@@ -25,14 +28,7 @@ import type {
25
28
  * HTTP health check configuration schema.
26
29
  * Global defaults only - action params moved to RequestCollector.
27
30
  */
28
- export const httpHealthCheckConfigSchema = z.object({
29
- timeout: z
30
- .number()
31
- .int()
32
- .min(100)
33
- .default(30_000)
34
- .describe("Default request timeout in milliseconds"),
35
- });
31
+ export const httpHealthCheckConfigSchema = baseStrategyConfigSchema.extend({});
36
32
 
37
33
  export type HttpHealthCheckConfig = z.infer<typeof httpHealthCheckConfigSchema>;
38
34
 
@@ -58,15 +54,15 @@ const httpResultMetadataSchema = healthResultSchema({
58
54
 
59
55
  type HttpResultMetadata = z.infer<typeof httpResultMetadataSchema>;
60
56
 
61
- /** Aggregated metadata for buckets */
62
- const httpAggregatedMetadataSchema = healthResultSchema({
63
- errorCount: healthResultNumber({
57
+ /** Aggregated field definitions for bucket merging */
58
+ const httpAggregatedFields = {
59
+ errorCount: aggregatedCounter({
64
60
  "x-chart-type": "counter",
65
61
  "x-chart-label": "Errors",
66
62
  }),
67
- });
63
+ };
68
64
 
69
- type HttpAggregatedMetadata = z.infer<typeof httpAggregatedMetadataSchema>;
65
+ type HttpAggregatedResult = InferAggregatedResult<typeof httpAggregatedFields>;
70
66
 
71
67
  // ============================================================================
72
68
  // STRATEGY
@@ -76,7 +72,7 @@ export class HttpHealthCheckStrategy implements HealthCheckStrategy<
76
72
  HttpHealthCheckConfig,
77
73
  HttpTransportClient,
78
74
  HttpResultMetadata,
79
- HttpAggregatedMetadata
75
+ typeof httpAggregatedFields
80
76
  > {
81
77
  id = "http";
82
78
  displayName = "HTTP/HTTPS Health Check";
@@ -112,21 +108,18 @@ export class HttpHealthCheckStrategy implements HealthCheckStrategy<
112
108
  schema: httpResultMetadataSchema,
113
109
  });
114
110
 
115
- aggregatedResult: Versioned<HttpAggregatedMetadata> = new Versioned({
111
+ aggregatedResult = new VersionedAggregated({
116
112
  version: 1,
117
- schema: httpAggregatedMetadataSchema,
113
+ fields: httpAggregatedFields,
118
114
  });
119
115
 
120
116
  mergeResult(
121
- existing: HttpAggregatedMetadata | undefined,
117
+ existing: HttpAggregatedResult | undefined,
122
118
  newRun: HealthCheckRunForAggregation<HttpResultMetadata>,
123
- ): HttpAggregatedMetadata {
119
+ ): HttpAggregatedResult {
124
120
  const hasError = !!newRun.metadata?.error;
125
121
  return {
126
- errorCount: mergeCounter(
127
- existing ? { count: existing.errorCount } : undefined,
128
- hasError,
129
- ).count,
122
+ errorCount: mergeCounter(existing?.errorCount, hasError),
130
123
  };
131
124
  }
132
125
 
@@ -155,9 +148,10 @@ export class HttpHealthCheckStrategy implements HealthCheckStrategy<
155
148
  signal: controller.signal,
156
149
  });
157
150
 
158
- clearTimeout(timeoutId);
159
-
151
+ // Read body BEFORE clearing timeout - body streaming can also hang
160
152
  const body = await response.text();
153
+
154
+ clearTimeout(timeoutId);
161
155
  const headers: Record<string, string> = {};
162
156
 
163
157
  // eslint-disable-next-line unicorn/no-array-for-each