@checkstack/healthcheck-http-backend 0.0.3 → 0.1.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,86 @@
1
1
  # @checkstack/healthcheck-http-backend
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 97c5a6b: Add UUID-based collector identification for better multiple collector support
8
+
9
+ **Breaking Change**: Existing health check configurations with collectors need to be recreated.
10
+
11
+ - Each collector instance now has a unique UUID assigned on creation
12
+ - Collector results are stored under the UUID key with `_collectorId` and `_assertionFailed` metadata
13
+ - Auto-charts correctly display separate charts for each collector instance
14
+ - Charts are now grouped by collector instance with clear headings
15
+ - Assertion status card shows pass/fail for each collector
16
+ - Renamed "Success" to "HTTP Success" to clarify it's about HTTP request success
17
+ - Fixed deletion of collectors not persisting to database
18
+ - Fixed duplicate React key warnings in auto-chart grid
19
+
20
+ - Updated dependencies [97c5a6b]
21
+ - Updated dependencies [8e43507]
22
+ - Updated dependencies [97c5a6b]
23
+ - @checkstack/backend-api@0.2.0
24
+ - @checkstack/common@0.1.0
25
+ - @checkstack/healthcheck-common@0.2.0
26
+
27
+ ## 0.1.0
28
+
29
+ ### Minor Changes
30
+
31
+ - f5b1f49: Refactored health check strategies to use `createClient()` pattern with built-in collectors.
32
+
33
+ **Strategy Changes:**
34
+
35
+ - Replaced `execute()` with `createClient()` that returns a transport client
36
+ - Strategy configs now only contain connection parameters
37
+ - Collector configs handle what to do with the connection
38
+
39
+ **Built-in Collectors Added:**
40
+
41
+ - DNS: `LookupCollector` for hostname resolution
42
+ - gRPC: `HealthCollector` for gRPC health protocol
43
+ - HTTP: `RequestCollector` for HTTP requests
44
+ - MySQL: `QueryCollector` for database queries
45
+ - Ping: `PingCollector` for ICMP ping
46
+ - Postgres: `QueryCollector` for database queries
47
+ - Redis: `CommandCollector` for Redis commands
48
+ - Script: `ExecuteCollector` for script execution
49
+ - SSH: `CommandCollector` for SSH commands
50
+ - TCP: `BannerCollector` for TCP banner grabbing
51
+ - TLS: `CertificateCollector` for certificate inspection
52
+
53
+ ### Patch Changes
54
+
55
+ - f5b1f49: Added JSONPath assertions for response body validation and fully qualified strategy IDs.
56
+
57
+ **JSONPath Assertions:**
58
+
59
+ - Added `healthResultJSONPath()` factory in healthcheck-common for fields supporting JSONPath queries
60
+ - Extended AssertionBuilder with jsonpath field type showing path input (e.g., `$.data.status`)
61
+ - Added `jsonPath` field to `CollectorAssertionSchema` for persistence
62
+ - HTTP Request collector body field now supports JSONPath assertions
63
+
64
+ **Fully Qualified Strategy IDs:**
65
+
66
+ - HealthCheckRegistry now uses scoped factories like CollectorRegistry
67
+ - Strategies are stored with `pluginId.strategyId` format
68
+ - Added `getStrategiesWithMeta()` method to HealthCheckRegistry interface
69
+ - Router returns qualified IDs so frontend can correctly fetch collectors
70
+
71
+ **UI Improvements:**
72
+
73
+ - Save button disabled when collector configs have invalid required fields
74
+ - Fixed nested button warning in CollectorList accordion
75
+
76
+ - Updated dependencies [f5b1f49]
77
+ - Updated dependencies [f5b1f49]
78
+ - Updated dependencies [f5b1f49]
79
+ - Updated dependencies [f5b1f49]
80
+ - @checkstack/backend-api@0.1.0
81
+ - @checkstack/healthcheck-common@0.1.0
82
+ - @checkstack/common@0.0.3
83
+
3
84
  ## 0.0.3
4
85
 
5
86
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-http-backend",
3
- "version": "0.0.3",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "scripts": {
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": "HTTP 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
+ }