@checkstack/healthcheck-http-backend 0.0.2 → 0.1.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 +82 -0
- package/package.json +1 -1
- package/src/index.ts +7 -5
- package/src/request-collector.test.ts +212 -0
- package/src/request-collector.ts +186 -0
- package/src/strategy.test.ts +182 -324
- package/src/strategy.ts +109 -404
- package/src/transport-client.ts +29 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# @checkstack/healthcheck-http-backend
|
|
2
2
|
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f5b1f49: Refactored health check strategies to use `createClient()` pattern with built-in collectors.
|
|
8
|
+
|
|
9
|
+
**Strategy Changes:**
|
|
10
|
+
|
|
11
|
+
- Replaced `execute()` with `createClient()` that returns a transport client
|
|
12
|
+
- Strategy configs now only contain connection parameters
|
|
13
|
+
- Collector configs handle what to do with the connection
|
|
14
|
+
|
|
15
|
+
**Built-in Collectors Added:**
|
|
16
|
+
|
|
17
|
+
- DNS: `LookupCollector` for hostname resolution
|
|
18
|
+
- gRPC: `HealthCollector` for gRPC health protocol
|
|
19
|
+
- HTTP: `RequestCollector` for HTTP requests
|
|
20
|
+
- MySQL: `QueryCollector` for database queries
|
|
21
|
+
- Ping: `PingCollector` for ICMP ping
|
|
22
|
+
- Postgres: `QueryCollector` for database queries
|
|
23
|
+
- Redis: `CommandCollector` for Redis commands
|
|
24
|
+
- Script: `ExecuteCollector` for script execution
|
|
25
|
+
- SSH: `CommandCollector` for SSH commands
|
|
26
|
+
- TCP: `BannerCollector` for TCP banner grabbing
|
|
27
|
+
- TLS: `CertificateCollector` for certificate inspection
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- f5b1f49: Added JSONPath assertions for response body validation and fully qualified strategy IDs.
|
|
32
|
+
|
|
33
|
+
**JSONPath Assertions:**
|
|
34
|
+
|
|
35
|
+
- Added `healthResultJSONPath()` factory in healthcheck-common for fields supporting JSONPath queries
|
|
36
|
+
- Extended AssertionBuilder with jsonpath field type showing path input (e.g., `$.data.status`)
|
|
37
|
+
- Added `jsonPath` field to `CollectorAssertionSchema` for persistence
|
|
38
|
+
- HTTP Request collector body field now supports JSONPath assertions
|
|
39
|
+
|
|
40
|
+
**Fully Qualified Strategy IDs:**
|
|
41
|
+
|
|
42
|
+
- HealthCheckRegistry now uses scoped factories like CollectorRegistry
|
|
43
|
+
- Strategies are stored with `pluginId.strategyId` format
|
|
44
|
+
- Added `getStrategiesWithMeta()` method to HealthCheckRegistry interface
|
|
45
|
+
- Router returns qualified IDs so frontend can correctly fetch collectors
|
|
46
|
+
|
|
47
|
+
**UI Improvements:**
|
|
48
|
+
|
|
49
|
+
- Save button disabled when collector configs have invalid required fields
|
|
50
|
+
- Fixed nested button warning in CollectorList accordion
|
|
51
|
+
|
|
52
|
+
- Updated dependencies [f5b1f49]
|
|
53
|
+
- Updated dependencies [f5b1f49]
|
|
54
|
+
- Updated dependencies [f5b1f49]
|
|
55
|
+
- Updated dependencies [f5b1f49]
|
|
56
|
+
- @checkstack/backend-api@0.1.0
|
|
57
|
+
- @checkstack/healthcheck-common@0.1.0
|
|
58
|
+
- @checkstack/common@0.0.3
|
|
59
|
+
|
|
60
|
+
## 0.0.3
|
|
61
|
+
|
|
62
|
+
### Patch Changes
|
|
63
|
+
|
|
64
|
+
- cb82e4d: Improved `counter` and `pie` auto-chart types to show frequency distributions instead of just the latest value. Both chart types now count occurrences of each unique value across all runs/buckets, making them more intuitive for visualizing data like HTTP status codes.
|
|
65
|
+
|
|
66
|
+
Changed HTTP health check chart annotations: `statusCode` now uses `pie` chart (distribution view), `contentType` now uses `counter` chart (frequency count).
|
|
67
|
+
|
|
68
|
+
Fixed scrollbar hopping when health check signals update the accordion content. All charts now update silently without layout shift or loading state flicker.
|
|
69
|
+
|
|
70
|
+
Refactored health check visualization architecture:
|
|
71
|
+
|
|
72
|
+
- `HealthCheckStatusTimeline` and `HealthCheckLatencyChart` now accept `HealthCheckDiagramSlotContext` directly, handling data transformation internally
|
|
73
|
+
- `HealthCheckDiagram` refactored to accept context from parent, ensuring all visualizations share the same data source and update together on signals
|
|
74
|
+
- `HealthCheckSystemOverview` simplified to use `useHealthCheckData` hook for consolidated data fetching with automatic signal-driven refresh
|
|
75
|
+
|
|
76
|
+
Added `silentRefetch()` method to `usePagination` hook for background data refreshes without showing loading indicators.
|
|
77
|
+
|
|
78
|
+
Fixed `useSignal` hook to use a ref pattern internally, preventing stale closure issues. Callbacks now always access the latest values without requiring manual memoization or refs in consumer components.
|
|
79
|
+
|
|
80
|
+
Added signal handling to `useHealthCheckData` hook for automatic chart refresh when health check runs complete.
|
|
81
|
+
|
|
82
|
+
- Updated dependencies [cb82e4d]
|
|
83
|
+
- @checkstack/healthcheck-common@0.0.3
|
|
84
|
+
|
|
3
85
|
## 0.0.2
|
|
4
86
|
|
|
5
87
|
### Patch Changes
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createBackendPlugin,
|
|
3
|
-
coreServices,
|
|
4
|
-
} from "@checkstack/backend-api";
|
|
1
|
+
import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
|
|
5
2
|
import { HttpHealthCheckStrategy } from "./strategy";
|
|
6
3
|
import { pluginMetadata } from "./plugin-metadata";
|
|
4
|
+
import { RequestCollector } from "./request-collector";
|
|
7
5
|
|
|
8
6
|
export default createBackendPlugin({
|
|
9
7
|
metadata: pluginMetadata,
|
|
@@ -11,13 +9,17 @@ export default createBackendPlugin({
|
|
|
11
9
|
env.registerInit({
|
|
12
10
|
deps: {
|
|
13
11
|
healthCheckRegistry: coreServices.healthCheckRegistry,
|
|
12
|
+
collectorRegistry: coreServices.collectorRegistry,
|
|
14
13
|
logger: coreServices.logger,
|
|
15
14
|
},
|
|
16
|
-
init: async ({ healthCheckRegistry, logger }) => {
|
|
15
|
+
init: async ({ healthCheckRegistry, collectorRegistry, logger }) => {
|
|
17
16
|
logger.debug("🔌 Registering HTTP Health Check Strategy...");
|
|
18
17
|
const strategy = new HttpHealthCheckStrategy();
|
|
19
18
|
healthCheckRegistry.register(strategy);
|
|
19
|
+
collectorRegistry.register(new RequestCollector());
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
22
|
},
|
|
23
23
|
});
|
|
24
|
+
|
|
25
|
+
export { pluginMetadata } from "./plugin-metadata";
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import { RequestCollector, type RequestConfig } from "./request-collector";
|
|
3
|
+
import type { HttpTransportClient } from "./transport-client";
|
|
4
|
+
|
|
5
|
+
describe("RequestCollector", () => {
|
|
6
|
+
const createMockClient = (
|
|
7
|
+
response: {
|
|
8
|
+
statusCode?: number;
|
|
9
|
+
statusText?: string;
|
|
10
|
+
body?: string;
|
|
11
|
+
} = {}
|
|
12
|
+
): HttpTransportClient => ({
|
|
13
|
+
exec: mock(() =>
|
|
14
|
+
Promise.resolve({
|
|
15
|
+
statusCode: response.statusCode ?? 200,
|
|
16
|
+
statusText: response.statusText ?? "OK",
|
|
17
|
+
headers: {},
|
|
18
|
+
body: response.body ?? "",
|
|
19
|
+
})
|
|
20
|
+
),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("execute", () => {
|
|
24
|
+
it("should execute HTTP request successfully", async () => {
|
|
25
|
+
const collector = new RequestCollector();
|
|
26
|
+
const client = createMockClient({
|
|
27
|
+
statusCode: 200,
|
|
28
|
+
statusText: "OK",
|
|
29
|
+
body: "Hello World",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = await collector.execute({
|
|
33
|
+
config: { url: "https://example.com", method: "GET", timeout: 5000 },
|
|
34
|
+
client,
|
|
35
|
+
pluginId: "test",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result.result.statusCode).toBe(200);
|
|
39
|
+
expect(result.result.statusText).toBe("OK");
|
|
40
|
+
expect(result.result.success).toBe(true);
|
|
41
|
+
expect(result.result.bodyLength).toBe(11);
|
|
42
|
+
expect(result.result.responseTimeMs).toBeGreaterThanOrEqual(0);
|
|
43
|
+
expect(result.error).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should return error for failed requests", async () => {
|
|
47
|
+
const collector = new RequestCollector();
|
|
48
|
+
const client = createMockClient({
|
|
49
|
+
statusCode: 500,
|
|
50
|
+
statusText: "Internal Server Error",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const result = await collector.execute({
|
|
54
|
+
config: {
|
|
55
|
+
url: "https://example.com/error",
|
|
56
|
+
method: "GET",
|
|
57
|
+
timeout: 5000,
|
|
58
|
+
},
|
|
59
|
+
client,
|
|
60
|
+
pluginId: "test",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result.result.statusCode).toBe(500);
|
|
64
|
+
expect(result.result.success).toBe(false);
|
|
65
|
+
expect(result.error).toContain("500");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should convert headers array to record", async () => {
|
|
69
|
+
const collector = new RequestCollector();
|
|
70
|
+
const client = createMockClient();
|
|
71
|
+
|
|
72
|
+
await collector.execute({
|
|
73
|
+
config: {
|
|
74
|
+
url: "https://example.com",
|
|
75
|
+
method: "POST",
|
|
76
|
+
timeout: 5000,
|
|
77
|
+
headers: [
|
|
78
|
+
{ name: "Content-Type", value: "application/json" },
|
|
79
|
+
{ name: "Authorization", value: "Bearer token" },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
client,
|
|
83
|
+
pluginId: "test",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(client.exec).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({
|
|
88
|
+
headers: {
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
Authorization: "Bearer token",
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should pass body to client", async () => {
|
|
97
|
+
const collector = new RequestCollector();
|
|
98
|
+
const client = createMockClient();
|
|
99
|
+
|
|
100
|
+
await collector.execute({
|
|
101
|
+
config: {
|
|
102
|
+
url: "https://example.com",
|
|
103
|
+
method: "POST",
|
|
104
|
+
timeout: 5000,
|
|
105
|
+
body: '{"key":"value"}',
|
|
106
|
+
},
|
|
107
|
+
client,
|
|
108
|
+
pluginId: "test",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(client.exec).toHaveBeenCalledWith(
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
body: '{"key":"value"}',
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("aggregateResult", () => {
|
|
120
|
+
it("should calculate average response time", () => {
|
|
121
|
+
const collector = new RequestCollector();
|
|
122
|
+
const runs = [
|
|
123
|
+
{
|
|
124
|
+
id: "1",
|
|
125
|
+
status: "healthy" as const,
|
|
126
|
+
latencyMs: 100,
|
|
127
|
+
checkId: "c1",
|
|
128
|
+
timestamp: new Date(),
|
|
129
|
+
metadata: {
|
|
130
|
+
statusCode: 200,
|
|
131
|
+
statusText: "OK",
|
|
132
|
+
responseTimeMs: 50,
|
|
133
|
+
body: "",
|
|
134
|
+
bodyLength: 100,
|
|
135
|
+
success: true,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "2",
|
|
140
|
+
status: "healthy" as const,
|
|
141
|
+
latencyMs: 150,
|
|
142
|
+
checkId: "c1",
|
|
143
|
+
timestamp: new Date(),
|
|
144
|
+
metadata: {
|
|
145
|
+
statusCode: 200,
|
|
146
|
+
statusText: "OK",
|
|
147
|
+
responseTimeMs: 100,
|
|
148
|
+
body: "",
|
|
149
|
+
bodyLength: 200,
|
|
150
|
+
success: true,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const aggregated = collector.aggregateResult(runs);
|
|
156
|
+
|
|
157
|
+
expect(aggregated.avgResponseTimeMs).toBe(75);
|
|
158
|
+
expect(aggregated.successRate).toBe(100);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should calculate success rate correctly", () => {
|
|
162
|
+
const collector = new RequestCollector();
|
|
163
|
+
const runs = [
|
|
164
|
+
{
|
|
165
|
+
id: "1",
|
|
166
|
+
status: "healthy" as const,
|
|
167
|
+
latencyMs: 100,
|
|
168
|
+
checkId: "c1",
|
|
169
|
+
timestamp: new Date(),
|
|
170
|
+
metadata: {
|
|
171
|
+
statusCode: 200,
|
|
172
|
+
statusText: "OK",
|
|
173
|
+
responseTimeMs: 50,
|
|
174
|
+
body: "",
|
|
175
|
+
bodyLength: 100,
|
|
176
|
+
success: true,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "2",
|
|
181
|
+
status: "unhealthy" as const,
|
|
182
|
+
latencyMs: 150,
|
|
183
|
+
checkId: "c1",
|
|
184
|
+
timestamp: new Date(),
|
|
185
|
+
metadata: {
|
|
186
|
+
statusCode: 500,
|
|
187
|
+
statusText: "Error",
|
|
188
|
+
responseTimeMs: 100,
|
|
189
|
+
body: "",
|
|
190
|
+
bodyLength: 0,
|
|
191
|
+
success: false,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const aggregated = collector.aggregateResult(runs);
|
|
197
|
+
|
|
198
|
+
expect(aggregated.successRate).toBe(50);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("metadata", () => {
|
|
203
|
+
it("should have correct static properties", () => {
|
|
204
|
+
const collector = new RequestCollector();
|
|
205
|
+
|
|
206
|
+
expect(collector.id).toBe("request");
|
|
207
|
+
expect(collector.displayName).toBe("HTTP Request");
|
|
208
|
+
expect(collector.allowMultiple).toBe(true);
|
|
209
|
+
expect(collector.supportedPlugins).toHaveLength(1);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Versioned,
|
|
3
|
+
z,
|
|
4
|
+
type HealthCheckRunForAggregation,
|
|
5
|
+
type CollectorResult,
|
|
6
|
+
type CollectorStrategy,
|
|
7
|
+
} from "@checkstack/backend-api";
|
|
8
|
+
import {
|
|
9
|
+
healthResultNumber,
|
|
10
|
+
healthResultString,
|
|
11
|
+
healthResultBoolean,
|
|
12
|
+
healthResultJSONPath,
|
|
13
|
+
} from "@checkstack/healthcheck-common";
|
|
14
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
15
|
+
import type { HttpTransportClient } from "./transport-client";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// CONFIGURATION SCHEMA
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
const requestConfigSchema = z.object({
|
|
22
|
+
url: z.string().url().describe("Full URL to request"),
|
|
23
|
+
method: z
|
|
24
|
+
.enum(["GET", "POST", "PUT", "DELETE", "HEAD"])
|
|
25
|
+
.default("GET")
|
|
26
|
+
.describe("HTTP method"),
|
|
27
|
+
headers: z
|
|
28
|
+
.array(z.object({ name: z.string(), value: z.string() }))
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Request headers"),
|
|
31
|
+
body: z.string().optional().describe("Request body"),
|
|
32
|
+
timeout: z
|
|
33
|
+
.number()
|
|
34
|
+
.min(100)
|
|
35
|
+
.default(30_000)
|
|
36
|
+
.describe("Timeout in milliseconds"),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export type RequestConfig = z.infer<typeof requestConfigSchema>;
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// RESULT SCHEMAS
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
const requestResultSchema = z.object({
|
|
46
|
+
statusCode: healthResultNumber({
|
|
47
|
+
"x-chart-type": "counter",
|
|
48
|
+
"x-chart-label": "Status Code",
|
|
49
|
+
}),
|
|
50
|
+
statusText: healthResultString({
|
|
51
|
+
"x-chart-type": "text",
|
|
52
|
+
"x-chart-label": "Status",
|
|
53
|
+
}),
|
|
54
|
+
responseTimeMs: healthResultNumber({
|
|
55
|
+
"x-chart-type": "line",
|
|
56
|
+
"x-chart-label": "Response Time",
|
|
57
|
+
"x-chart-unit": "ms",
|
|
58
|
+
}),
|
|
59
|
+
body: healthResultJSONPath({}),
|
|
60
|
+
bodyLength: healthResultNumber({
|
|
61
|
+
"x-chart-type": "counter",
|
|
62
|
+
"x-chart-label": "Body Length",
|
|
63
|
+
"x-chart-unit": "bytes",
|
|
64
|
+
}),
|
|
65
|
+
success: healthResultBoolean({
|
|
66
|
+
"x-chart-type": "boolean",
|
|
67
|
+
"x-chart-label": "Success",
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export type RequestResult = z.infer<typeof requestResultSchema>;
|
|
72
|
+
|
|
73
|
+
const requestAggregatedSchema = z.object({
|
|
74
|
+
avgResponseTimeMs: healthResultNumber({
|
|
75
|
+
"x-chart-type": "line",
|
|
76
|
+
"x-chart-label": "Avg Response Time",
|
|
77
|
+
"x-chart-unit": "ms",
|
|
78
|
+
}),
|
|
79
|
+
successRate: healthResultNumber({
|
|
80
|
+
"x-chart-type": "gauge",
|
|
81
|
+
"x-chart-label": "Success Rate",
|
|
82
|
+
"x-chart-unit": "%",
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export type RequestAggregatedResult = z.infer<typeof requestAggregatedSchema>;
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// REQUEST COLLECTOR
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Built-in HTTP request collector.
|
|
94
|
+
* Allows users to make HTTP requests and check responses.
|
|
95
|
+
*/
|
|
96
|
+
export class RequestCollector
|
|
97
|
+
implements
|
|
98
|
+
CollectorStrategy<
|
|
99
|
+
HttpTransportClient,
|
|
100
|
+
RequestConfig,
|
|
101
|
+
RequestResult,
|
|
102
|
+
RequestAggregatedResult
|
|
103
|
+
>
|
|
104
|
+
{
|
|
105
|
+
id = "request";
|
|
106
|
+
displayName = "HTTP Request";
|
|
107
|
+
description = "Make an HTTP request and check the response";
|
|
108
|
+
|
|
109
|
+
supportedPlugins = [pluginMetadata];
|
|
110
|
+
|
|
111
|
+
allowMultiple = true;
|
|
112
|
+
|
|
113
|
+
config = new Versioned({ version: 1, schema: requestConfigSchema });
|
|
114
|
+
result = new Versioned({ version: 1, schema: requestResultSchema });
|
|
115
|
+
aggregatedResult = new Versioned({
|
|
116
|
+
version: 1,
|
|
117
|
+
schema: requestAggregatedSchema,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
async execute({
|
|
121
|
+
config,
|
|
122
|
+
client,
|
|
123
|
+
}: {
|
|
124
|
+
config: RequestConfig;
|
|
125
|
+
client: HttpTransportClient;
|
|
126
|
+
pluginId: string;
|
|
127
|
+
}): Promise<CollectorResult<RequestResult>> {
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
|
|
130
|
+
// Convert headers array to record
|
|
131
|
+
const headers: Record<string, string> = {};
|
|
132
|
+
for (const h of config.headers ?? []) {
|
|
133
|
+
headers[h.name] = h.value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const response = await client.exec({
|
|
137
|
+
url: config.url,
|
|
138
|
+
method: config.method,
|
|
139
|
+
headers,
|
|
140
|
+
body: config.body,
|
|
141
|
+
timeout: config.timeout,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const responseTimeMs = Date.now() - startTime;
|
|
145
|
+
const success = response.statusCode >= 200 && response.statusCode < 400;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
result: {
|
|
149
|
+
statusCode: response.statusCode,
|
|
150
|
+
statusText: response.statusText,
|
|
151
|
+
responseTimeMs,
|
|
152
|
+
body: response.body ?? "",
|
|
153
|
+
bodyLength: response.body?.length ?? 0,
|
|
154
|
+
success,
|
|
155
|
+
},
|
|
156
|
+
error: success
|
|
157
|
+
? undefined
|
|
158
|
+
: `HTTP ${response.statusCode}: ${response.statusText}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
aggregateResult(
|
|
163
|
+
runs: HealthCheckRunForAggregation<RequestResult>[]
|
|
164
|
+
): RequestAggregatedResult {
|
|
165
|
+
const times = runs
|
|
166
|
+
.map((r) => r.metadata?.responseTimeMs)
|
|
167
|
+
.filter((v): v is number => typeof v === "number");
|
|
168
|
+
|
|
169
|
+
const successes = runs
|
|
170
|
+
.map((r) => r.metadata?.success)
|
|
171
|
+
.filter((v): v is boolean => typeof v === "boolean");
|
|
172
|
+
|
|
173
|
+
const successCount = successes.filter(Boolean).length;
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
avgResponseTimeMs:
|
|
177
|
+
times.length > 0
|
|
178
|
+
? Math.round(times.reduce((a, b) => a + b, 0) / times.length)
|
|
179
|
+
: 0,
|
|
180
|
+
successRate:
|
|
181
|
+
successes.length > 0
|
|
182
|
+
? Math.round((successCount / successes.length) * 100)
|
|
183
|
+
: 0,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|