@checkstack/healthcheck-backend 1.9.2 → 1.10.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 +112 -0
- package/package.json +30 -30
- package/src/ai/healthcheck-projection.test.ts +36 -0
- package/src/health-entity-id.test.ts +73 -0
- package/src/index.ts +28 -0
- package/src/queue-executor.ts +42 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,117 @@
|
|
|
1
1
|
# @checkstack/healthcheck-backend
|
|
2
2
|
|
|
3
|
+
## 1.10.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [748dc50]
|
|
8
|
+
- @checkstack/automation-backend@0.10.0
|
|
9
|
+
- @checkstack/ai-backend@0.9.0
|
|
10
|
+
- @checkstack/incident-backend@1.8.4
|
|
11
|
+
- @checkstack/satellite-backend@0.7.1
|
|
12
|
+
- @checkstack/sdk@0.113.1
|
|
13
|
+
- @checkstack/catalog-backend@1.5.4
|
|
14
|
+
- @checkstack/script-packages-backend@0.3.16
|
|
15
|
+
|
|
16
|
+
## 1.10.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 8cad340: Add an AI assistant tool that lists the health checks assigned to a system.
|
|
21
|
+
|
|
22
|
+
The assistant previously had `healthcheck.status` (every check globally) but no
|
|
23
|
+
way to map a check to a system, so it had to guess which check monitored a given
|
|
24
|
+
system. It now projects `getSystemConfigurations` as the read-only tool
|
|
25
|
+
`healthcheck.listSystemChecks`: given a `systemId` (resolved from a name via
|
|
26
|
+
`catalog.listSystems`), it returns the checks assigned to that system - id, name,
|
|
27
|
+
strategy, interval, collectors/assertions, and paused state. The tool inherits
|
|
28
|
+
the source procedure's system-scoped `configuration.read` gate, so it stays
|
|
29
|
+
team-scoped and needs no new permission.
|
|
30
|
+
|
|
31
|
+
- 8cad340: Add a finer per-run transport timing breakdown to health checks.
|
|
32
|
+
|
|
33
|
+
Each run now records an optional structured `metadata.timings` (DNS, connect,
|
|
34
|
+
TLS, wait/time-to-first-byte, transfer, and a `processing` catch-all for
|
|
35
|
+
non-HTTP operation time). The run-detail view renders the phases it has, in
|
|
36
|
+
transport order, and falls back to the previous Connection + Processing split
|
|
37
|
+
for older runs that lack the finer data.
|
|
38
|
+
|
|
39
|
+
For HTTP the request is issued verbatim through `fetch` (original URL, headers,
|
|
40
|
+
and body), so request behavior is identical to a plain `fetch`. The timing is
|
|
41
|
+
measured around it: `fetch` resolves at the response headers, so wait
|
|
42
|
+
(time-to-first-byte) and transfer (body) are measured exactly on the request,
|
|
43
|
+
DNS is timed at the resolve step, and connect/TLS come from a short-lived,
|
|
44
|
+
best-effort raw `net`/`tls` probe to the same already-validated IP (the request
|
|
45
|
+
socket exposes no connect/handshake events on the Bun runtime). The probe is
|
|
46
|
+
timing-only and never fails the check. The probe validates the TLS certificate
|
|
47
|
+
(against the original hostname via SNI) like the real request does - it does not
|
|
48
|
+
disable certificate validation; an unverifiable cert simply yields no TLS-phase
|
|
49
|
+
timing rather than aborting. Other transports surface the connect and operation
|
|
50
|
+
times they already measure.
|
|
51
|
+
|
|
52
|
+
The SSRF guard now validates the resolved host (rejecting cloud-metadata /
|
|
53
|
+
link-local and operator-denied ranges) as a pre-flight check and no longer pins
|
|
54
|
+
the request to the resolved IP. Pinning rewrote the URL to the IP literal and
|
|
55
|
+
moved the host to the `Host` header, which breaks HTTP/2 origins (their
|
|
56
|
+
authority comes from the URL's `:authority`, not `Host`) - that is why real
|
|
57
|
+
hosts such as `google.com` started answering 404/429 instead of 200. The
|
|
58
|
+
pre-flight validation keeps blocking static metadata/link-local targets and
|
|
59
|
+
direct denied IP literals; the only thing dropped is DNS-rebind TOCTOU
|
|
60
|
+
protection (a narrow window that pinning closed at the cost of breaking
|
|
61
|
+
legitimate HTTP/2 requests).
|
|
62
|
+
|
|
63
|
+
The run-detail "slowest" badge no longer collides with the timing bar, and a
|
|
64
|
+
genuinely sub-millisecond phase reads as "<1 ms" instead of a bare "0 ms".
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- Updated dependencies [8cad340]
|
|
69
|
+
- Updated dependencies [8cad340]
|
|
70
|
+
- Updated dependencies [8cad340]
|
|
71
|
+
- Updated dependencies [8cad340]
|
|
72
|
+
- Updated dependencies [8cad340]
|
|
73
|
+
- Updated dependencies [8cad340]
|
|
74
|
+
- Updated dependencies [8cad340]
|
|
75
|
+
- Updated dependencies [8cad340]
|
|
76
|
+
- Updated dependencies [8cad340]
|
|
77
|
+
- Updated dependencies [8cad340]
|
|
78
|
+
- Updated dependencies [8cad340]
|
|
79
|
+
- Updated dependencies [8cad340]
|
|
80
|
+
- Updated dependencies [8cad340]
|
|
81
|
+
- Updated dependencies [8cad340]
|
|
82
|
+
- Updated dependencies [8cad340]
|
|
83
|
+
- Updated dependencies [8cad340]
|
|
84
|
+
- Updated dependencies [8cad340]
|
|
85
|
+
- Updated dependencies [8cad340]
|
|
86
|
+
- Updated dependencies [8cad340]
|
|
87
|
+
- Updated dependencies [8cad340]
|
|
88
|
+
- @checkstack/ai-backend@0.8.0
|
|
89
|
+
- @checkstack/ai-common@0.6.0
|
|
90
|
+
- @checkstack/automation-backend@0.9.3
|
|
91
|
+
- @checkstack/status-page-backend@0.3.0
|
|
92
|
+
- @checkstack/satellite-backend@0.7.0
|
|
93
|
+
- @checkstack/gitops-backend@0.5.12
|
|
94
|
+
- @checkstack/secrets-backend@0.2.12
|
|
95
|
+
- @checkstack/script-packages-backend@0.3.15
|
|
96
|
+
- @checkstack/backend-api@0.25.0
|
|
97
|
+
- @checkstack/notification-common@1.4.0
|
|
98
|
+
- @checkstack/healthcheck-common@1.8.0
|
|
99
|
+
- @checkstack/common@0.17.0
|
|
100
|
+
- @checkstack/incident-backend@1.8.3
|
|
101
|
+
- @checkstack/command-backend@0.2.12
|
|
102
|
+
- @checkstack/catalog-backend@1.5.3
|
|
103
|
+
- @checkstack/status-page-common@0.3.0
|
|
104
|
+
- @checkstack/catalog-common@2.4.2
|
|
105
|
+
- @checkstack/incident-common@1.6.2
|
|
106
|
+
- @checkstack/maintenance-common@1.7.2
|
|
107
|
+
- @checkstack/sdk@0.112.1
|
|
108
|
+
- @checkstack/cache-api@0.3.14
|
|
109
|
+
- @checkstack/gitops-common@0.6.5
|
|
110
|
+
- @checkstack/queue-api@0.3.14
|
|
111
|
+
- @checkstack/secrets-common@0.2.5
|
|
112
|
+
- @checkstack/signal-common@0.2.11
|
|
113
|
+
- @checkstack/cache-utils@0.2.19
|
|
114
|
+
|
|
3
115
|
## 1.9.2
|
|
4
116
|
|
|
5
117
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-backend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.1",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -14,32 +14,32 @@
|
|
|
14
14
|
"lint:code": "eslint . --max-warnings 0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@checkstack/backend-api": "0.
|
|
18
|
-
"@checkstack/ai-backend": "0.
|
|
19
|
-
"@checkstack/ai-common": "0.
|
|
20
|
-
"@checkstack/script-packages-backend": "0.3.
|
|
21
|
-
"@checkstack/cache-api": "0.3.
|
|
22
|
-
"@checkstack/cache-utils": "0.2.
|
|
23
|
-
"@checkstack/catalog-backend": "1.5.
|
|
24
|
-
"@checkstack/catalog-common": "2.4.
|
|
25
|
-
"@checkstack/command-backend": "0.2.
|
|
26
|
-
"@checkstack/common": "0.
|
|
27
|
-
"@checkstack/gitops-backend": "0.5.
|
|
28
|
-
"@checkstack/gitops-common": "0.6.
|
|
29
|
-
"@checkstack/healthcheck-common": "1.
|
|
30
|
-
"@checkstack/secrets-common": "0.2.
|
|
31
|
-
"@checkstack/secrets-backend": "0.2.
|
|
32
|
-
"@checkstack/incident-backend": "1.8.
|
|
33
|
-
"@checkstack/incident-common": "1.6.
|
|
34
|
-
"@checkstack/automation-backend": "0.
|
|
35
|
-
"@checkstack/maintenance-common": "1.7.
|
|
36
|
-
"@checkstack/notification-common": "1.
|
|
37
|
-
"@checkstack/queue-api": "0.3.
|
|
38
|
-
"@checkstack/satellite-backend": "0.
|
|
39
|
-
"@checkstack/sdk": "0.
|
|
40
|
-
"@checkstack/signal-common": "0.2.
|
|
41
|
-
"@checkstack/status-page-backend": "0.
|
|
42
|
-
"@checkstack/status-page-common": "0.
|
|
17
|
+
"@checkstack/backend-api": "0.25.0",
|
|
18
|
+
"@checkstack/ai-backend": "0.9.0",
|
|
19
|
+
"@checkstack/ai-common": "0.6.0",
|
|
20
|
+
"@checkstack/script-packages-backend": "0.3.16",
|
|
21
|
+
"@checkstack/cache-api": "0.3.14",
|
|
22
|
+
"@checkstack/cache-utils": "0.2.19",
|
|
23
|
+
"@checkstack/catalog-backend": "1.5.4",
|
|
24
|
+
"@checkstack/catalog-common": "2.4.2",
|
|
25
|
+
"@checkstack/command-backend": "0.2.12",
|
|
26
|
+
"@checkstack/common": "0.17.0",
|
|
27
|
+
"@checkstack/gitops-backend": "0.5.12",
|
|
28
|
+
"@checkstack/gitops-common": "0.6.5",
|
|
29
|
+
"@checkstack/healthcheck-common": "1.8.0",
|
|
30
|
+
"@checkstack/secrets-common": "0.2.5",
|
|
31
|
+
"@checkstack/secrets-backend": "0.2.12",
|
|
32
|
+
"@checkstack/incident-backend": "1.8.4",
|
|
33
|
+
"@checkstack/incident-common": "1.6.2",
|
|
34
|
+
"@checkstack/automation-backend": "0.10.0",
|
|
35
|
+
"@checkstack/maintenance-common": "1.7.2",
|
|
36
|
+
"@checkstack/notification-common": "1.4.0",
|
|
37
|
+
"@checkstack/queue-api": "0.3.14",
|
|
38
|
+
"@checkstack/satellite-backend": "0.7.1",
|
|
39
|
+
"@checkstack/sdk": "0.113.1",
|
|
40
|
+
"@checkstack/signal-common": "0.2.11",
|
|
41
|
+
"@checkstack/status-page-backend": "0.3.0",
|
|
42
|
+
"@checkstack/status-page-common": "0.3.0",
|
|
43
43
|
"@hono/zod-validator": "^0.7.6",
|
|
44
44
|
"drizzle-orm": "^0.45.0",
|
|
45
45
|
"hono": "^4.12.25",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"@orpc/server": "^1.14.4"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@checkstack/drizzle-helper": "0.0.
|
|
54
|
-
"@checkstack/scripts": "0.6.
|
|
55
|
-
"@checkstack/test-utils-backend": "0.1.
|
|
53
|
+
"@checkstack/drizzle-helper": "0.0.6",
|
|
54
|
+
"@checkstack/scripts": "0.6.3",
|
|
55
|
+
"@checkstack/test-utils-backend": "0.1.46",
|
|
56
56
|
"@checkstack/tsconfig": "0.0.7",
|
|
57
57
|
"@types/bun": "^1.0.0",
|
|
58
58
|
"@types/tdigest": "^0.1.5",
|
|
@@ -35,6 +35,42 @@ describe("healthcheck.status projection", () => {
|
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
// The per-system projection is how the assistant maps a check to a system:
|
|
39
|
+
// `healthcheck.status` lists every check globally with no system attribution, so
|
|
40
|
+
// it must call this with a `systemId` (resolved via catalog.listSystems) to know
|
|
41
|
+
// which checks are assigned to that system. Gated by the system-scoped
|
|
42
|
+
// `configuration.read` rule, NOT the chat transport gate.
|
|
43
|
+
describe("healthcheck.listSystemChecks projection", () => {
|
|
44
|
+
const tool = buildProjectedTool({
|
|
45
|
+
procedure: healthCheckContract.getSystemConfigurations,
|
|
46
|
+
sourcePluginMetadata: pluginMetadata,
|
|
47
|
+
procedureKey: "getSystemConfigurations",
|
|
48
|
+
name: "healthcheck.listSystemChecks",
|
|
49
|
+
description: "List the health checks assigned to a system. Read-only.",
|
|
50
|
+
effect: "read",
|
|
51
|
+
execute: deferredProjectionExecute,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("uses the overridden tool name", () => {
|
|
55
|
+
expect(tool.name).toBe("healthcheck.listSystemChecks");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("is classified as a read-only effect", () => {
|
|
59
|
+
expect(tool.effect).toBe("read");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("exposes the systemId input to the model", () => {
|
|
63
|
+
const inputShape = tool.input as { shape?: Record<string, unknown> };
|
|
64
|
+
expect(inputShape.shape).toBeDefined();
|
|
65
|
+
expect(inputShape.shape?.["systemId"]).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("inherits the source procedure's access rules, not the chat gate", () => {
|
|
69
|
+
expect(tool.requiredAccessRules.length).toBeGreaterThan(0);
|
|
70
|
+
expect(tool.requiredAccessRules).not.toEqual(["ai.chat.read"]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
38
74
|
// The run-history projection lets the assistant answer timeline / root-cause
|
|
39
75
|
// questions ("what issues did system X have between T1 and T2"). It must expose
|
|
40
76
|
// the filter inputs (systemId, date window, statusFilter) and be gated by the
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
HEALTH_ENTITY_ID_SEPARATOR,
|
|
4
|
+
encodeHealthEntityId,
|
|
5
|
+
parseHealthEntityId,
|
|
6
|
+
} from "./health-entity-id";
|
|
7
|
+
|
|
8
|
+
describe("encodeHealthEntityId", () => {
|
|
9
|
+
it("yields the bare system id when environmentId is undefined", () => {
|
|
10
|
+
expect(encodeHealthEntityId({ systemId: "sys-1" })).toBe("sys-1");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("yields the bare system id when environmentId is null", () => {
|
|
14
|
+
expect(encodeHealthEntityId({ systemId: "sys-1", environmentId: null })).toBe(
|
|
15
|
+
"sys-1",
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("joins system and environment with the double-colon separator", () => {
|
|
20
|
+
expect(
|
|
21
|
+
encodeHealthEntityId({ systemId: "sys-1", environmentId: "prod" }),
|
|
22
|
+
).toBe(`sys-1${HEALTH_ENTITY_ID_SEPARATOR}prod`);
|
|
23
|
+
expect(HEALTH_ENTITY_ID_SEPARATOR).toBe("::");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("treats an empty-string environmentId as concrete (not a rollup)", () => {
|
|
27
|
+
// "" is neither null nor undefined, so it encodes as a per-env id.
|
|
28
|
+
expect(encodeHealthEntityId({ systemId: "sys-1", environmentId: "" })).toBe(
|
|
29
|
+
"sys-1::",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("parseHealthEntityId", () => {
|
|
35
|
+
it("decodes a bare id as the system rollup (environmentId null)", () => {
|
|
36
|
+
expect(parseHealthEntityId("sys-1")).toEqual({
|
|
37
|
+
systemId: "sys-1",
|
|
38
|
+
environmentId: null,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("decodes a per-env id into system and environment", () => {
|
|
43
|
+
expect(parseHealthEntityId("sys-1::prod")).toEqual({
|
|
44
|
+
systemId: "sys-1",
|
|
45
|
+
environmentId: "prod",
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("splits on the FIRST separator so a later '::' stays in the env id", () => {
|
|
50
|
+
expect(parseHealthEntityId("sys-1::a::b")).toEqual({
|
|
51
|
+
systemId: "sys-1",
|
|
52
|
+
environmentId: "a::b",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("yields an empty environmentId for a trailing separator", () => {
|
|
57
|
+
expect(parseHealthEntityId("sys-1::")).toEqual({
|
|
58
|
+
systemId: "sys-1",
|
|
59
|
+
environmentId: "",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("round-trips an encoded per-env id", () => {
|
|
64
|
+
const encoded = encodeHealthEntityId({
|
|
65
|
+
systemId: "sys-9",
|
|
66
|
+
environmentId: "staging",
|
|
67
|
+
});
|
|
68
|
+
expect(parseHealthEntityId(encoded)).toEqual({
|
|
69
|
+
systemId: "sys-9",
|
|
70
|
+
environmentId: "staging",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -314,6 +314,34 @@ export default createBackendPlugin({
|
|
|
314
314
|
execute: deferredProjectionExecute,
|
|
315
315
|
});
|
|
316
316
|
|
|
317
|
+
// The per-system ASSIGNMENT of checks: given a `systemId`, list the
|
|
318
|
+
// health checks assigned to that system (id, name, strategy, interval,
|
|
319
|
+
// collectors/assertions, paused state). This is the mapping
|
|
320
|
+
// `healthcheck.status` lacks - `getConfigurations` lists every check
|
|
321
|
+
// GLOBALLY with no system attribution, so from it alone the assistant
|
|
322
|
+
// cannot tell WHICH check monitors a given system (it would have to
|
|
323
|
+
// guess). The assistant resolves a system NAME to its id via
|
|
324
|
+
// catalog.listSystems, then calls this to attribute a named/failing
|
|
325
|
+
// check to the right system. Same system-scoped `configuration.read`
|
|
326
|
+
// gate as the dashboard's per-system configuration view.
|
|
327
|
+
env.getExtensionPoint(aiToolProjectionExtensionPoint).expose({
|
|
328
|
+
procedure: healthCheckContract.getSystemConfigurations,
|
|
329
|
+
sourcePluginMetadata: pluginMetadata,
|
|
330
|
+
procedureKey: "getSystemConfigurations",
|
|
331
|
+
name: "healthcheck.listSystemChecks",
|
|
332
|
+
description:
|
|
333
|
+
"List the health checks ASSIGNED to a specific system, given its " +
|
|
334
|
+
"`systemId` (resolve a system NAME to its id with " +
|
|
335
|
+
"catalog.listSystems first). Returns each assigned check's id, " +
|
|
336
|
+
"name, strategy, interval, collectors/assertions, and paused " +
|
|
337
|
+
"state. Use this to answer 'which checks monitor system X' or to " +
|
|
338
|
+
"attribute a specific or failing check to a system - " +
|
|
339
|
+
"healthcheck.status lists ALL checks globally WITHOUT the " +
|
|
340
|
+
"system mapping. Read-only.",
|
|
341
|
+
effect: "read",
|
|
342
|
+
execute: deferredProjectionExecute,
|
|
343
|
+
});
|
|
344
|
+
|
|
317
345
|
// Historical run-by-run check results, filterable by system, time
|
|
318
346
|
// window, and status. Complements `healthcheck.status` (which is only
|
|
319
347
|
// CURRENT state): this is how the assistant answers "what issues did
|
package/src/queue-executor.ts
CHANGED
|
@@ -8,10 +8,12 @@ import {
|
|
|
8
8
|
type BaseStrategyConfig,
|
|
9
9
|
type ConnectedClient,
|
|
10
10
|
type TransportClient,
|
|
11
|
+
type TransportTimings,
|
|
11
12
|
type CollectorRunContext,
|
|
12
13
|
type AdvisoryLockService,
|
|
13
14
|
renderTemplatableConfig,
|
|
14
15
|
} from "@checkstack/backend-api";
|
|
16
|
+
import type { RunTimings } from "@checkstack/healthcheck-common";
|
|
15
17
|
import { QueueManager } from "@checkstack/queue-api";
|
|
16
18
|
import {
|
|
17
19
|
healthCheckConfigurations,
|
|
@@ -91,6 +93,39 @@ function toHealthEntityView(state: AggregatedHealth): HealthEntityState {
|
|
|
91
93
|
};
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
/** The known transport timing phase keys, in transport order. */
|
|
97
|
+
const RUN_TIMING_KEYS = [
|
|
98
|
+
"dnsMs",
|
|
99
|
+
"connectMs",
|
|
100
|
+
"tlsMs",
|
|
101
|
+
"waitMs",
|
|
102
|
+
"transferMs",
|
|
103
|
+
"processingMs",
|
|
104
|
+
] as const;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build the run's `metadata.timings` from a connected client's surfaced
|
|
108
|
+
* transport timings, keeping only present, finite, non-negative phases. Returns
|
|
109
|
+
* `undefined` when no usable phase was measured so the field is omitted (old
|
|
110
|
+
* runs and single-phase strategies stay on the coarse fallback in the UI).
|
|
111
|
+
*/
|
|
112
|
+
function extractRunTimings(
|
|
113
|
+
connectedClient: ConnectedClient<TransportClient<never, unknown>> | undefined,
|
|
114
|
+
): RunTimings | undefined {
|
|
115
|
+
const raw: TransportTimings | undefined = connectedClient?.timings;
|
|
116
|
+
if (!raw) return undefined;
|
|
117
|
+
const result: RunTimings = {};
|
|
118
|
+
let any = false;
|
|
119
|
+
for (const key of RUN_TIMING_KEYS) {
|
|
120
|
+
const value = raw[key];
|
|
121
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
122
|
+
result[key] = value;
|
|
123
|
+
any = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return any ? result : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
94
129
|
/**
|
|
95
130
|
* Emit the checkCompleted hook if available, plus the narrower
|
|
96
131
|
* `checkFailed` hook when the result wasn't `healthy` (so operators
|
|
@@ -1003,6 +1038,12 @@ async function executeHealthCheckJob(props: {
|
|
|
1003
1038
|
const status = hasCollectorError ? "unhealthy" : "healthy";
|
|
1004
1039
|
const totalLatencyMs = Math.round(performance.now() - start);
|
|
1005
1040
|
|
|
1041
|
+
// Lift the strategy's structured transport timings (DNS / connect / TLS /
|
|
1042
|
+
// wait / transfer / processing) into the run metadata when the connected
|
|
1043
|
+
// client surfaced any. Strategies that cannot measure sub-phases leave this
|
|
1044
|
+
// undefined and the frontend falls back to the coarse connection split.
|
|
1045
|
+
const timings = extractRunTimings(connectedClient);
|
|
1046
|
+
|
|
1006
1047
|
const result = {
|
|
1007
1048
|
status: status as "healthy" | "unhealthy",
|
|
1008
1049
|
latencyMs: totalLatencyMs,
|
|
@@ -1012,6 +1053,7 @@ async function executeHealthCheckJob(props: {
|
|
|
1012
1053
|
metadata: {
|
|
1013
1054
|
connected: true,
|
|
1014
1055
|
connectionTimeMs,
|
|
1056
|
+
...(timings ? { timings } : {}),
|
|
1015
1057
|
collectors: collectorResults,
|
|
1016
1058
|
},
|
|
1017
1059
|
};
|