@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 +43 -0
- package/package.json +6 -6
- package/src/request-collector.test.ts +11 -7
- package/src/request-collector.ts +29 -20
- package/src/strategy.test.ts +15 -10
- package/src/strategy.ts +19 -22
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
|
+
"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": "
|
|
13
|
-
"@checkstack/healthcheck-common": "
|
|
12
|
+
"@checkstack/backend-api": "0.5.2",
|
|
13
|
+
"@checkstack/healthcheck-common": "0.8.1",
|
|
14
14
|
"jsonpath-plus": "^10.3.0",
|
|
15
|
-
"@checkstack/common": "
|
|
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": "
|
|
22
|
-
"@checkstack/scripts": "
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/request-collector.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
181
|
+
mergeResult(
|
|
182
|
+
existing: RequestAggregatedResult | undefined,
|
|
183
|
+
newRun: HealthCheckRunForAggregation<RequestResult>,
|
|
167
184
|
): RequestAggregatedResult {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
.
|
|
171
|
-
|
|
172
|
-
const
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
123
|
-
|
|
120
|
+
mergeResult(
|
|
121
|
+
existing: HttpAggregatedMetadata | undefined,
|
|
122
|
+
newRun: HealthCheckRunForAggregation<HttpResultMetadata>,
|
|
124
123
|
): HttpAggregatedMetadata {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 {
|