@checkstack/healthcheck-common 0.4.1 → 0.5.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 +74 -0
- package/package.json +1 -1
- package/src/access.ts +6 -13
- package/src/rpc-contract.ts +32 -25
- package/src/schemas.ts +6 -1
- package/src/zod-health-result.test.ts +105 -0
- package/src/zod-health-result.ts +60 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# @checkstack/healthcheck-common
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ac3a4cf: ### Dynamic Bucket Sizing for Health Check Visualization
|
|
8
|
+
|
|
9
|
+
Implements industry-standard dynamic bucket sizing for health check data aggregation, following patterns from Grafana/VictoriaMetrics.
|
|
10
|
+
|
|
11
|
+
**What changed:**
|
|
12
|
+
|
|
13
|
+
- Replaced fixed `bucketSize: "hourly" | "daily" | "auto"` with dynamic `targetPoints` parameter (default: 500)
|
|
14
|
+
- Bucket interval is now calculated as `(endDate - startDate) / targetPoints` with a minimum of 1 second
|
|
15
|
+
- Added `bucketIntervalSeconds` to aggregated response and individual buckets
|
|
16
|
+
- Updated chart components to use dynamic time formatting based on bucket interval
|
|
17
|
+
|
|
18
|
+
**Why:**
|
|
19
|
+
|
|
20
|
+
- A 24-hour view with 1-second health checks previously returned 86,400+ data points, causing lag
|
|
21
|
+
- Now returns ~500 data points regardless of timeframe, ensuring consistent chart performance
|
|
22
|
+
- Charts still preserve visual fidelity through proper aggregation
|
|
23
|
+
|
|
24
|
+
**Breaking Change:**
|
|
25
|
+
|
|
26
|
+
- `bucketSize` parameter removed from `getAggregatedHistory` and `getDetailedAggregatedHistory` endpoints
|
|
27
|
+
- Use `targetPoints` instead (defaults to 500 if not specified)
|
|
28
|
+
|
|
29
|
+
***
|
|
30
|
+
|
|
31
|
+
### Collector Aggregated Charts Fix
|
|
32
|
+
|
|
33
|
+
Fixed issue where collector auto-charts (like HTTP request response time charts) were not showing in aggregated data mode.
|
|
34
|
+
|
|
35
|
+
**What changed:**
|
|
36
|
+
|
|
37
|
+
- Added `aggregatedResultSchema` to `CollectorDtoSchema`
|
|
38
|
+
- Backend now returns collector aggregated schemas via `getCollectors` endpoint
|
|
39
|
+
- Frontend `useStrategySchemas` hook now merges collector aggregated schemas
|
|
40
|
+
- Service now calls each collector's `aggregateResult()` when building buckets
|
|
41
|
+
- Aggregated collector data stored in `aggregatedResult.collectors[uuid]`
|
|
42
|
+
|
|
43
|
+
**Why:**
|
|
44
|
+
|
|
45
|
+
- Previously only strategy-level aggregated results were computed
|
|
46
|
+
- Collectors like HTTP Request Collector have their own `aggregateResult` method
|
|
47
|
+
- Without calling these, fields like `avgResponseTimeMs` and `successRate` were missing from aggregated buckets
|
|
48
|
+
|
|
49
|
+
- db1f56f: Add ephemeral field stripping to reduce database storage for health checks
|
|
50
|
+
|
|
51
|
+
- Added `x-ephemeral` metadata flag to `HealthResultMeta` for marking fields that should not be persisted
|
|
52
|
+
- All health result factory functions (`healthResultString`, `healthResultNumber`, `healthResultBoolean`, `healthResultArray`, `healthResultJSONPath`) now accept `x-ephemeral`
|
|
53
|
+
- Added `stripEphemeralFields()` utility to remove ephemeral fields before database storage
|
|
54
|
+
- Integrated ephemeral field stripping into `queue-executor.ts` for all collector results
|
|
55
|
+
- HTTP Request collector now explicitly marks `body` as ephemeral
|
|
56
|
+
|
|
57
|
+
This significantly reduces database storage for health checks with large response bodies, while still allowing assertions to run against the full response at execution time.
|
|
58
|
+
|
|
59
|
+
### Patch Changes
|
|
60
|
+
|
|
61
|
+
- Updated dependencies [db1f56f]
|
|
62
|
+
- @checkstack/common@0.6.0
|
|
63
|
+
- @checkstack/signal-common@0.1.4
|
|
64
|
+
|
|
65
|
+
## 0.4.2
|
|
66
|
+
|
|
67
|
+
### Patch Changes
|
|
68
|
+
|
|
69
|
+
- 8a87cd4: Updated access rules to use new `accessPair` interface
|
|
70
|
+
|
|
71
|
+
Migrated to the new `accessPair` interface with per-level options objects for cleaner access rule definitions.
|
|
72
|
+
|
|
73
|
+
- Updated dependencies [8a87cd4]
|
|
74
|
+
- @checkstack/common@0.5.0
|
|
75
|
+
- @checkstack/signal-common@0.1.3
|
|
76
|
+
|
|
3
77
|
## 0.4.1
|
|
4
78
|
|
|
5
79
|
### Patch Changes
|
package/package.json
CHANGED
package/src/access.ts
CHANGED
|
@@ -8,6 +8,9 @@ export const healthCheckAccess = {
|
|
|
8
8
|
* Status-only access for viewing health check status.
|
|
9
9
|
* Enabled by default for anonymous and authenticated users.
|
|
10
10
|
* Uses system-level instance access for team-based filtering.
|
|
11
|
+
*
|
|
12
|
+
* Bulk endpoints should use the same access rule with instanceAccess
|
|
13
|
+
* override at the contract level.
|
|
11
14
|
*/
|
|
12
15
|
status: access("healthcheck.status", "read", "View Health Check Status", {
|
|
13
16
|
idParam: "systemId",
|
|
@@ -15,22 +18,12 @@ export const healthCheckAccess = {
|
|
|
15
18
|
isPublic: true,
|
|
16
19
|
}),
|
|
17
20
|
|
|
18
|
-
/**
|
|
19
|
-
* Bulk status access for viewing health check status for multiple systems.
|
|
20
|
-
* Uses recordKey for filtering the output record by accessible system IDs.
|
|
21
|
-
*/
|
|
22
|
-
bulkStatus: access("healthcheck.status", "read", "View Health Check Status", {
|
|
23
|
-
recordKey: "statuses",
|
|
24
|
-
isDefault: true,
|
|
25
|
-
isPublic: true,
|
|
26
|
-
}),
|
|
27
|
-
|
|
28
21
|
/**
|
|
29
22
|
* Configuration access for viewing and managing health check configurations.
|
|
30
23
|
*/
|
|
31
24
|
configuration: accessPair("healthcheck", {
|
|
32
|
-
read: "Read Health Check Configurations",
|
|
33
|
-
manage: "Full management of Health Check Configurations",
|
|
25
|
+
read: { description: "Read Health Check Configurations" },
|
|
26
|
+
manage: { description: "Full management of Health Check Configurations" },
|
|
34
27
|
}),
|
|
35
28
|
|
|
36
29
|
/**
|
|
@@ -40,7 +33,7 @@ export const healthCheckAccess = {
|
|
|
40
33
|
details: access(
|
|
41
34
|
"healthcheck.details",
|
|
42
35
|
"read",
|
|
43
|
-
"View Detailed Health Check Run Data (Warning: This may expose sensitive data, depending on the health check strategy)"
|
|
36
|
+
"View Detailed Health Check Run Data (Warning: This may expose sensitive data, depending on the health check strategy)",
|
|
44
37
|
),
|
|
45
38
|
};
|
|
46
39
|
|
package/src/rpc-contract.ts
CHANGED
|
@@ -67,7 +67,7 @@ export const healthCheckContract = {
|
|
|
67
67
|
userType: "authenticated",
|
|
68
68
|
access: [healthCheckAccess.configuration.read],
|
|
69
69
|
}).output(
|
|
70
|
-
z.object({ configurations: z.array(HealthCheckConfigurationSchema) })
|
|
70
|
+
z.object({ configurations: z.array(HealthCheckConfigurationSchema) }),
|
|
71
71
|
),
|
|
72
72
|
|
|
73
73
|
createConfiguration: proc({
|
|
@@ -87,7 +87,7 @@ export const healthCheckContract = {
|
|
|
87
87
|
z.object({
|
|
88
88
|
id: z.string(),
|
|
89
89
|
body: UpdateHealthCheckConfigurationSchema,
|
|
90
|
-
})
|
|
90
|
+
}),
|
|
91
91
|
)
|
|
92
92
|
.output(HealthCheckConfigurationSchema),
|
|
93
93
|
|
|
@@ -124,8 +124,8 @@ export const healthCheckContract = {
|
|
|
124
124
|
configurationName: z.string(),
|
|
125
125
|
enabled: z.boolean(),
|
|
126
126
|
stateThresholds: StateThresholdsSchema.optional(),
|
|
127
|
-
})
|
|
128
|
-
)
|
|
127
|
+
}),
|
|
128
|
+
),
|
|
129
129
|
),
|
|
130
130
|
|
|
131
131
|
associateSystem: proc({
|
|
@@ -137,7 +137,7 @@ export const healthCheckContract = {
|
|
|
137
137
|
z.object({
|
|
138
138
|
systemId: z.string(),
|
|
139
139
|
body: AssociateHealthCheckSchema,
|
|
140
|
-
})
|
|
140
|
+
}),
|
|
141
141
|
)
|
|
142
142
|
.output(z.void()),
|
|
143
143
|
|
|
@@ -150,7 +150,7 @@ export const healthCheckContract = {
|
|
|
150
150
|
z.object({
|
|
151
151
|
systemId: z.string(),
|
|
152
152
|
configId: z.string(),
|
|
153
|
-
})
|
|
153
|
+
}),
|
|
154
154
|
)
|
|
155
155
|
.output(z.void()),
|
|
156
156
|
|
|
@@ -167,12 +167,12 @@ export const healthCheckContract = {
|
|
|
167
167
|
z.object({
|
|
168
168
|
systemId: z.string(),
|
|
169
169
|
configurationId: z.string(),
|
|
170
|
-
})
|
|
170
|
+
}),
|
|
171
171
|
)
|
|
172
172
|
.output(
|
|
173
173
|
z.object({
|
|
174
174
|
retentionConfig: RetentionConfigSchema.nullable(),
|
|
175
|
-
})
|
|
175
|
+
}),
|
|
176
176
|
),
|
|
177
177
|
|
|
178
178
|
updateRetentionConfig: proc({
|
|
@@ -185,7 +185,7 @@ export const healthCheckContract = {
|
|
|
185
185
|
systemId: z.string(),
|
|
186
186
|
configurationId: z.string(),
|
|
187
187
|
retentionConfig: RetentionConfigSchema.nullable(),
|
|
188
|
-
})
|
|
188
|
+
}),
|
|
189
189
|
)
|
|
190
190
|
.output(z.void()),
|
|
191
191
|
|
|
@@ -206,13 +206,13 @@ export const healthCheckContract = {
|
|
|
206
206
|
endDate: z.date().optional(),
|
|
207
207
|
limit: z.number().optional().default(10),
|
|
208
208
|
offset: z.number().optional().default(0),
|
|
209
|
-
})
|
|
209
|
+
}),
|
|
210
210
|
)
|
|
211
211
|
.output(
|
|
212
212
|
z.object({
|
|
213
213
|
runs: z.array(HealthCheckRunPublicSchema),
|
|
214
214
|
total: z.number(),
|
|
215
|
-
})
|
|
215
|
+
}),
|
|
216
216
|
),
|
|
217
217
|
|
|
218
218
|
getDetailedHistory: proc({
|
|
@@ -228,13 +228,13 @@ export const healthCheckContract = {
|
|
|
228
228
|
endDate: z.date().optional(),
|
|
229
229
|
limit: z.number().optional().default(10),
|
|
230
230
|
offset: z.number().optional().default(0),
|
|
231
|
-
})
|
|
231
|
+
}),
|
|
232
232
|
)
|
|
233
233
|
.output(
|
|
234
234
|
z.object({
|
|
235
235
|
runs: z.array(HealthCheckRunSchema),
|
|
236
236
|
total: z.number(),
|
|
237
|
-
})
|
|
237
|
+
}),
|
|
238
238
|
),
|
|
239
239
|
|
|
240
240
|
getAggregatedHistory: proc({
|
|
@@ -248,13 +248,16 @@ export const healthCheckContract = {
|
|
|
248
248
|
configurationId: z.string(),
|
|
249
249
|
startDate: z.date(),
|
|
250
250
|
endDate: z.date(),
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
/** Target number of data points (default: 500). Bucket interval is calculated as (endDate - startDate) / targetPoints */
|
|
252
|
+
targetPoints: z.number().min(10).max(2000).default(500),
|
|
253
|
+
}),
|
|
253
254
|
)
|
|
254
255
|
.output(
|
|
255
256
|
z.object({
|
|
256
257
|
buckets: z.array(AggregatedBucketBaseSchema),
|
|
257
|
-
|
|
258
|
+
/** The calculated bucket interval in seconds */
|
|
259
|
+
bucketIntervalSeconds: z.number(),
|
|
260
|
+
}),
|
|
258
261
|
),
|
|
259
262
|
|
|
260
263
|
getDetailedAggregatedHistory: proc({
|
|
@@ -268,13 +271,16 @@ export const healthCheckContract = {
|
|
|
268
271
|
configurationId: z.string(),
|
|
269
272
|
startDate: z.date(),
|
|
270
273
|
endDate: z.date(),
|
|
271
|
-
|
|
272
|
-
|
|
274
|
+
/** Target number of data points (default: 500). Bucket interval is calculated as (endDate - startDate) / targetPoints */
|
|
275
|
+
targetPoints: z.number().min(10).max(2000).default(500),
|
|
276
|
+
}),
|
|
273
277
|
)
|
|
274
278
|
.output(
|
|
275
279
|
z.object({
|
|
276
280
|
buckets: z.array(AggregatedBucketSchema),
|
|
277
|
-
|
|
281
|
+
/** The calculated bucket interval in seconds */
|
|
282
|
+
bucketIntervalSeconds: z.number(),
|
|
283
|
+
}),
|
|
278
284
|
),
|
|
279
285
|
|
|
280
286
|
getSystemHealthStatus: proc({
|
|
@@ -288,13 +294,14 @@ export const healthCheckContract = {
|
|
|
288
294
|
getBulkSystemHealthStatus: proc({
|
|
289
295
|
operationType: "query",
|
|
290
296
|
userType: "public",
|
|
291
|
-
access: [healthCheckAccess.
|
|
297
|
+
access: [healthCheckAccess.status],
|
|
298
|
+
instanceAccess: { recordKey: "statuses" },
|
|
292
299
|
})
|
|
293
300
|
.input(z.object({ systemIds: z.array(z.string()) }))
|
|
294
301
|
.output(
|
|
295
302
|
z.object({
|
|
296
303
|
statuses: z.record(z.string(), SystemHealthStatusResponseSchema),
|
|
297
|
-
})
|
|
304
|
+
}),
|
|
298
305
|
),
|
|
299
306
|
|
|
300
307
|
getSystemHealthOverview: proc({
|
|
@@ -320,11 +327,11 @@ export const healthCheckContract = {
|
|
|
320
327
|
id: z.string(),
|
|
321
328
|
status: HealthCheckStatusSchema,
|
|
322
329
|
timestamp: z.date(),
|
|
323
|
-
})
|
|
330
|
+
}),
|
|
324
331
|
),
|
|
325
|
-
})
|
|
332
|
+
}),
|
|
326
333
|
),
|
|
327
|
-
})
|
|
334
|
+
}),
|
|
328
335
|
),
|
|
329
336
|
};
|
|
330
337
|
|
|
@@ -335,5 +342,5 @@ export type HealthCheckContract = typeof healthCheckContract;
|
|
|
335
342
|
// Use: const client = rpcApi.forPlugin(HealthCheckApi);
|
|
336
343
|
export const HealthCheckApi = createClientDefinition(
|
|
337
344
|
healthCheckContract,
|
|
338
|
-
pluginMetadata
|
|
345
|
+
pluginMetadata,
|
|
339
346
|
);
|
package/src/schemas.ts
CHANGED
|
@@ -32,6 +32,8 @@ export const CollectorDtoSchema = z.object({
|
|
|
32
32
|
configSchema: z.record(z.string(), z.unknown()),
|
|
33
33
|
/** JSON Schema for per-run result metadata (with chart annotations) */
|
|
34
34
|
resultSchema: z.record(z.string(), z.unknown()),
|
|
35
|
+
/** JSON Schema for aggregated result metadata (with chart annotations) */
|
|
36
|
+
aggregatedResultSchema: z.record(z.string(), z.unknown()).optional(),
|
|
35
37
|
/** Whether multiple instances of this collector are allowed per config */
|
|
36
38
|
allowMultiple: z.boolean(),
|
|
37
39
|
});
|
|
@@ -276,7 +278,10 @@ export const DEFAULT_RETENTION_CONFIG: RetentionConfig = {
|
|
|
276
278
|
*/
|
|
277
279
|
export const AggregatedBucketBaseSchema = z.object({
|
|
278
280
|
bucketStart: z.date(),
|
|
279
|
-
|
|
281
|
+
/** @deprecated Use bucketIntervalSeconds instead. Kept for backward compatibility. */
|
|
282
|
+
bucketSize: z.enum(["hourly", "daily"]).optional(),
|
|
283
|
+
/** Bucket interval in seconds (e.g., 7 for 7-second buckets) */
|
|
284
|
+
bucketIntervalSeconds: z.number(),
|
|
280
285
|
runCount: z.number(),
|
|
281
286
|
healthyCount: z.number(),
|
|
282
287
|
degradedCount: z.number(),
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
healthResultSchema,
|
|
5
|
+
healthResultNumber,
|
|
6
|
+
healthResultString,
|
|
7
|
+
healthResultJSONPath,
|
|
8
|
+
stripEphemeralFields,
|
|
9
|
+
getHealthResultMeta,
|
|
10
|
+
} from "./zod-health-result";
|
|
11
|
+
|
|
12
|
+
describe("stripEphemeralFields", () => {
|
|
13
|
+
it("should strip fields marked with x-ephemeral", () => {
|
|
14
|
+
const schema = healthResultSchema({
|
|
15
|
+
statusCode: healthResultNumber({ "x-chart-type": "counter" }),
|
|
16
|
+
body: healthResultJSONPath({ "x-ephemeral": true }), // Explicitly marked ephemeral
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const result = {
|
|
20
|
+
statusCode: 200,
|
|
21
|
+
body: '{"large":"response body that should not be stored"}',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const stripped = stripEphemeralFields(result, schema);
|
|
25
|
+
|
|
26
|
+
expect(stripped).toEqual({ statusCode: 200 });
|
|
27
|
+
expect(stripped).not.toHaveProperty("body");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should preserve non-ephemeral fields", () => {
|
|
31
|
+
const schema = healthResultSchema({
|
|
32
|
+
responseTimeMs: healthResultNumber({ "x-chart-type": "line" }),
|
|
33
|
+
statusText: healthResultString({ "x-chart-type": "text" }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = {
|
|
37
|
+
responseTimeMs: 150,
|
|
38
|
+
statusText: "OK",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const stripped = stripEphemeralFields(result, schema);
|
|
42
|
+
|
|
43
|
+
expect(stripped).toEqual(result);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should preserve unknown fields like _collectorId", () => {
|
|
47
|
+
const schema = healthResultSchema({
|
|
48
|
+
value: healthResultNumber({ "x-chart-type": "counter" }),
|
|
49
|
+
body: healthResultJSONPath({ "x-ephemeral": true }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const result = {
|
|
53
|
+
_collectorId: "http.request",
|
|
54
|
+
_assertionFailed: undefined,
|
|
55
|
+
value: 42,
|
|
56
|
+
body: "large body content",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const stripped = stripEphemeralFields(result, schema);
|
|
60
|
+
|
|
61
|
+
expect(stripped).toEqual({
|
|
62
|
+
_collectorId: "http.request",
|
|
63
|
+
_assertionFailed: undefined,
|
|
64
|
+
value: 42,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should return original result for non-ZodObject schemas", () => {
|
|
69
|
+
const schema = z.string();
|
|
70
|
+
const result = { foo: "bar" };
|
|
71
|
+
|
|
72
|
+
const stripped = stripEphemeralFields(result, schema);
|
|
73
|
+
|
|
74
|
+
expect(stripped).toEqual(result);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should handle empty result objects", () => {
|
|
78
|
+
const schema = healthResultSchema({
|
|
79
|
+
body: healthResultJSONPath({ "x-ephemeral": true }),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const result = {};
|
|
83
|
+
const stripped = stripEphemeralFields(result, schema);
|
|
84
|
+
|
|
85
|
+
expect(stripped).toEqual({});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("healthResultJSONPath", () => {
|
|
90
|
+
it("should allow explicitly marking fields as ephemeral", () => {
|
|
91
|
+
const field = healthResultJSONPath({ "x-ephemeral": true });
|
|
92
|
+
const meta = getHealthResultMeta(field);
|
|
93
|
+
|
|
94
|
+
expect(meta?.["x-ephemeral"]).toBe(true);
|
|
95
|
+
expect(meta?.["x-jsonpath"]).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should not be ephemeral by default", () => {
|
|
99
|
+
const field = healthResultJSONPath({});
|
|
100
|
+
const meta = getHealthResultMeta(field);
|
|
101
|
+
|
|
102
|
+
expect(meta?.["x-ephemeral"]).toBeUndefined();
|
|
103
|
+
expect(meta?.["x-jsonpath"]).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
package/src/zod-health-result.ts
CHANGED
|
@@ -86,7 +86,7 @@ export type HealthResultShape = Record<string, HealthResultFieldType>;
|
|
|
86
86
|
* ```
|
|
87
87
|
*/
|
|
88
88
|
export function healthResultSchema<T extends HealthResultShape>(
|
|
89
|
-
shape: T
|
|
89
|
+
shape: T,
|
|
90
90
|
): z.ZodObject<T> {
|
|
91
91
|
return z.object(shape);
|
|
92
92
|
}
|
|
@@ -111,7 +111,7 @@ type ChartMeta = Omit<HealthResultMeta, "x-jsonpath">;
|
|
|
111
111
|
* ```
|
|
112
112
|
*/
|
|
113
113
|
export function healthResultString(
|
|
114
|
-
meta: ChartMeta
|
|
114
|
+
meta: ChartMeta,
|
|
115
115
|
): HealthResultField<z.ZodString> {
|
|
116
116
|
const schema = z.string();
|
|
117
117
|
schema.register(healthResultRegistry, meta);
|
|
@@ -122,7 +122,7 @@ export function healthResultString(
|
|
|
122
122
|
* Create a health result number field with typed chart metadata.
|
|
123
123
|
*/
|
|
124
124
|
export function healthResultNumber(
|
|
125
|
-
meta: ChartMeta
|
|
125
|
+
meta: ChartMeta,
|
|
126
126
|
): HealthResultField<z.ZodNumber> {
|
|
127
127
|
const schema = z.number();
|
|
128
128
|
schema.register(healthResultRegistry, meta);
|
|
@@ -133,7 +133,7 @@ export function healthResultNumber(
|
|
|
133
133
|
* Create a health result boolean field with typed chart metadata.
|
|
134
134
|
*/
|
|
135
135
|
export function healthResultBoolean(
|
|
136
|
-
meta: ChartMeta
|
|
136
|
+
meta: ChartMeta,
|
|
137
137
|
): HealthResultField<z.ZodBoolean> {
|
|
138
138
|
const schema = z.boolean();
|
|
139
139
|
schema.register(healthResultRegistry, meta);
|
|
@@ -152,7 +152,7 @@ export function healthResultBoolean(
|
|
|
152
152
|
* ```
|
|
153
153
|
*/
|
|
154
154
|
export function healthResultArray(
|
|
155
|
-
meta: ChartMeta
|
|
155
|
+
meta: ChartMeta,
|
|
156
156
|
): HealthResultField<z.ZodArray<z.ZodString>> {
|
|
157
157
|
const schema = z.array(z.string());
|
|
158
158
|
schema.register(healthResultRegistry, meta);
|
|
@@ -173,7 +173,7 @@ export function healthResultArray(
|
|
|
173
173
|
* ```
|
|
174
174
|
*/
|
|
175
175
|
export function healthResultJSONPath(
|
|
176
|
-
meta: ChartMeta
|
|
176
|
+
meta: ChartMeta,
|
|
177
177
|
): HealthResultField<z.ZodString> {
|
|
178
178
|
const schema = z.string();
|
|
179
179
|
schema.register(healthResultRegistry, { ...meta, "x-jsonpath": true });
|
|
@@ -213,7 +213,60 @@ function unwrapSchema(schema: z.ZodTypeAny): z.ZodTypeAny {
|
|
|
213
213
|
* Automatically unwraps Optional/Default/Nullable wrappers.
|
|
214
214
|
*/
|
|
215
215
|
export function getHealthResultMeta(
|
|
216
|
-
schema: z.ZodTypeAny
|
|
216
|
+
schema: z.ZodTypeAny,
|
|
217
217
|
): HealthResultMeta | undefined {
|
|
218
218
|
return healthResultRegistry.get(unwrapSchema(schema));
|
|
219
219
|
}
|
|
220
|
+
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// EPHEMERAL FIELD STRIPPING - For storage optimization
|
|
223
|
+
// ============================================================================
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Strip ephemeral fields from a result object before database storage.
|
|
227
|
+
*
|
|
228
|
+
* Ephemeral fields (marked with x-ephemeral: true) are used during health check
|
|
229
|
+
* execution for assertions but should not be persisted to save storage space.
|
|
230
|
+
* Common example: HTTP response bodies used for JSONPath assertions.
|
|
231
|
+
*
|
|
232
|
+
* @param result - The full result object from collector execution
|
|
233
|
+
* @param schema - The Zod schema with health result metadata
|
|
234
|
+
* @returns A new object with ephemeral fields removed
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const stripped = stripEphemeralFields(collectorResult.result, collector.result.schema);
|
|
239
|
+
* // The 'body' field (marked with x-ephemeral) is removed before storage
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
export function stripEphemeralFields<T extends Record<string, unknown>>(
|
|
243
|
+
result: T,
|
|
244
|
+
schema: z.ZodTypeAny,
|
|
245
|
+
): Partial<T> {
|
|
246
|
+
// Handle ZodObject schemas
|
|
247
|
+
if (!(schema instanceof z.ZodObject)) {
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const shape = schema.shape as Record<string, z.ZodTypeAny>;
|
|
252
|
+
const stripped: Record<string, unknown> = {};
|
|
253
|
+
|
|
254
|
+
for (const [key, value] of Object.entries(result)) {
|
|
255
|
+
const fieldSchema = shape[key];
|
|
256
|
+
if (!fieldSchema) {
|
|
257
|
+
// Keep unknown fields (e.g., _collectorId, _assertionFailed)
|
|
258
|
+
stripped[key] = value;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const meta = getHealthResultMeta(fieldSchema);
|
|
263
|
+
if (meta?.["x-ephemeral"]) {
|
|
264
|
+
// Skip ephemeral fields
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
stripped[key] = value;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return stripped as Partial<T>;
|
|
272
|
+
}
|