@checkstack/healthcheck-http-backend 0.2.3 → 0.2.5

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,48 @@
1
1
  # @checkstack/healthcheck-http-backend
2
2
 
3
+ ## 0.2.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
8
+
9
+ ### Breaking Changes (Internal)
10
+
11
+ - Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
12
+
13
+ ### New Features
14
+
15
+ - Added incremental aggregation utilities in `@checkstack/backend-api`:
16
+ - `mergeCounter()` - track occurrences
17
+ - `mergeAverage()` - track sum/count, compute avg
18
+ - `mergeRate()` - track success/total, compute %
19
+ - `mergeMinMax()` - track min/max values
20
+ - Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
21
+
22
+ ### Improvements
23
+
24
+ - Enables O(1) storage overhead by maintaining incremental aggregation state
25
+ - Prepares for real-time hourly aggregation without batch accumulation
26
+
27
+ - Updated dependencies [f676e11]
28
+ - Updated dependencies [48c2080]
29
+ - @checkstack/common@0.6.2
30
+ - @checkstack/backend-api@0.6.0
31
+ - @checkstack/healthcheck-common@0.8.2
32
+
33
+ ## 0.2.4
34
+
35
+ ### Patch Changes
36
+
37
+ - 0b9fc58: Fix workspace:\* protocol resolution in published packages
38
+
39
+ Published packages now correctly have resolved dependency versions instead of `workspace:*` references. This is achieved by using `bun publish` which properly resolves workspace protocol references.
40
+
41
+ - Updated dependencies [0b9fc58]
42
+ - @checkstack/backend-api@0.5.2
43
+ - @checkstack/common@0.6.1
44
+ - @checkstack/healthcheck-common@0.8.1
45
+
3
46
  ## 0.2.3
4
47
 
5
48
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-http-backend",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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": "workspace:*",
13
- "@checkstack/healthcheck-common": "workspace:*",
12
+ "@checkstack/backend-api": "0.5.2",
13
+ "@checkstack/healthcheck-common": "0.8.1",
14
14
  "jsonpath-plus": "^10.3.0",
15
- "@checkstack/common": "workspace:*"
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": "workspace:*",
22
- "@checkstack/scripts": "workspace:*"
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,7 +152,9 @@ 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
159
  expect(aggregated.avgResponseTimeMs).toBe(75);
158
160
  expect(aggregated.successRate).toBe(100);
@@ -193,7 +195,9 @@ 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
202
  expect(aggregated.successRate).toBe(50);
199
203
  });
@@ -5,6 +5,10 @@ import {
5
5
  type HealthCheckRunForAggregation,
6
6
  type CollectorResult,
7
7
  type CollectorStrategy,
8
+ mergeAverage,
9
+ mergeRate,
10
+ averageStateSchema,
11
+ rateStateSchema,
8
12
  } from "@checkstack/backend-api";
9
13
  import {
10
14
  healthResultNumber,
@@ -76,7 +80,8 @@ const requestResultSchema = healthResultSchema({
76
80
 
77
81
  export type RequestResult = z.infer<typeof requestResultSchema>;
78
82
 
79
- const requestAggregatedSchema = healthResultSchema({
83
+ // UI-visible aggregated fields (for charts)
84
+ const requestAggregatedDisplaySchema = healthResultSchema({
80
85
  avgResponseTimeMs: healthResultNumber({
81
86
  "x-chart-type": "line",
82
87
  "x-chart-label": "Avg Response Time",
@@ -89,6 +94,17 @@ const requestAggregatedSchema = healthResultSchema({
89
94
  }),
90
95
  });
91
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
+ });
102
+
103
+ // Combined schema for storage
104
+ const requestAggregatedSchema = requestAggregatedDisplaySchema.and(
105
+ requestAggregatedInternalSchema,
106
+ );
107
+
92
108
  export type RequestAggregatedResult = z.infer<typeof requestAggregatedSchema>;
93
109
 
94
110
  // ============================================================================
@@ -162,28 +178,21 @@ export class RequestCollector implements CollectorStrategy<
162
178
  };
163
179
  }
164
180
 
165
- aggregateResult(
166
- runs: HealthCheckRunForAggregation<RequestResult>[],
181
+ mergeResult(
182
+ existing: RequestAggregatedResult | undefined,
183
+ newRun: HealthCheckRunForAggregation<RequestResult>,
167
184
  ): 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;
185
+ const responseTime = mergeAverage(
186
+ existing?._responseTime,
187
+ newRun.metadata?.responseTimeMs,
188
+ );
189
+ const success = mergeRate(existing?._success, newRun.metadata?.success);
177
190
 
178
191
  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,
192
+ avgResponseTimeMs: responseTime.avg,
193
+ successRate: success.rate,
194
+ _responseTime: responseTime,
195
+ _success: success,
187
196
  };
188
197
  }
189
198
  }
@@ -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,7 +213,10 @@ 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
221
  expect(aggregated.errorCount).toBe(2);
219
222
  });
@@ -238,7 +241,9 @@ 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
248
  expect(aggregated.errorCount).toBe(0);
244
249
  });
package/src/strategy.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  Versioned,
5
5
  z,
6
6
  type ConnectedClient,
7
+ mergeCounter,
7
8
  } from "@checkstack/backend-api";
8
9
  import {
9
10
  healthResultNumber,
@@ -71,15 +72,12 @@ type HttpAggregatedMetadata = z.infer<typeof httpAggregatedMetadataSchema>;
71
72
  // STRATEGY
72
73
  // ============================================================================
73
74
 
74
- export class HttpHealthCheckStrategy
75
- implements
76
- HealthCheckStrategy<
77
- HttpHealthCheckConfig,
78
- HttpTransportClient,
79
- HttpResultMetadata,
80
- HttpAggregatedMetadata
81
- >
82
- {
75
+ export class HttpHealthCheckStrategy implements HealthCheckStrategy<
76
+ HttpHealthCheckConfig,
77
+ HttpTransportClient,
78
+ HttpResultMetadata,
79
+ HttpAggregatedMetadata
80
+ > {
83
81
  id = "http";
84
82
  displayName = "HTTP/HTTPS Health Check";
85
83
  description = "HTTP endpoint health monitoring";
@@ -119,18 +117,17 @@ export class HttpHealthCheckStrategy
119
117
  schema: httpAggregatedMetadataSchema,
120
118
  });
121
119
 
122
- aggregateResult(
123
- runs: HealthCheckRunForAggregation<HttpResultMetadata>[]
120
+ mergeResult(
121
+ existing: HttpAggregatedMetadata | undefined,
122
+ newRun: HealthCheckRunForAggregation<HttpResultMetadata>,
124
123
  ): 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 };
124
+ const hasError = !!newRun.metadata?.error;
125
+ return {
126
+ errorCount: mergeCounter(
127
+ existing ? { count: existing.errorCount } : undefined,
128
+ hasError,
129
+ ).count,
130
+ };
134
131
  }
135
132
 
136
133
  /**
@@ -138,7 +135,7 @@ export class HttpHealthCheckStrategy
138
135
  * All request parameters come from the collector (RequestCollector).
139
136
  */
140
137
  async createClient(
141
- config: HttpHealthCheckConfig
138
+ config: HttpHealthCheckConfig,
142
139
  ): Promise<ConnectedClient<HttpTransportClient>> {
143
140
  const validatedConfig = this.config.validate(config);
144
141
 
@@ -147,7 +144,7 @@ export class HttpHealthCheckStrategy
147
144
  const controller = new AbortController();
148
145
  const timeoutId = setTimeout(
149
146
  () => controller.abort(),
150
- request.timeout ?? validatedConfig.timeout
147
+ request.timeout ?? validatedConfig.timeout,
151
148
  );
152
149
 
153
150
  try {