@checkstack/healthcheck-backend 1.13.0 → 1.14.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 CHANGED
@@ -1,5 +1,50 @@
1
1
  # @checkstack/healthcheck-backend
2
2
 
3
+ ## 1.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e819276: Fix JSONPath collector assertions: the executor previously evaluated every
8
+ assertion with a flat field lookup, so a `Body (JSONPath)` assertion compared
9
+ against `undefined` and the configured path was silently ignored (`Exists`
10
+ always failed, `Not Exists` always passed). The executor now parses the source
11
+ field as JSON and extracts the configured path via `jsonpath-plus` (with
12
+ expression evaluation disabled - filter/script expressions are rejected).
13
+ Fail-closed: a non-JSON body, missing expression, or invalid path fails the
14
+ assertion with a diagnostic, never the collection.
15
+
16
+ Also adds `isEmpty` / `isNotEmpty` to the JSONPath operator set (and the
17
+ AssertionBuilder), treating `[]`, `{}`, `""`, and missing values as empty - so
18
+ "no errors reported" is a single `$.errors Is Empty` assertion, and "key exists
19
+ but is empty" is `Exists` + `Is Empty` on the same path.
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [e819276]
24
+ - Updated dependencies [e819276]
25
+ - @checkstack/ai-backend@0.10.5
26
+ - @checkstack/backend-api@0.28.0
27
+ - @checkstack/automation-backend@0.10.7
28
+ - @checkstack/catalog-backend@1.6.5
29
+ - @checkstack/incident-backend@1.9.3
30
+ - @checkstack/satellite-backend@0.7.8
31
+ - @checkstack/command-backend@0.2.17
32
+ - @checkstack/gitops-backend@0.5.17
33
+ - @checkstack/script-packages-backend@0.3.21
34
+ - @checkstack/secrets-backend@0.2.17
35
+ - @checkstack/status-page-backend@0.4.4
36
+
37
+ ## 1.13.1
38
+
39
+ ### Patch Changes
40
+
41
+ - Updated dependencies [b4e0832]
42
+ - @checkstack/ai-backend@0.10.4
43
+ - @checkstack/automation-backend@0.10.6
44
+ - @checkstack/catalog-backend@1.6.4
45
+ - @checkstack/incident-backend@1.9.2
46
+ - @checkstack/satellite-backend@0.7.7
47
+
3
48
  ## 1.13.0
4
49
 
5
50
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-backend",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -14,45 +14,46 @@
14
14
  "lint:code": "eslint . --max-warnings 0"
15
15
  },
16
16
  "dependencies": {
17
- "@checkstack/backend-api": "0.27.1",
18
- "@checkstack/ai-backend": "0.10.3",
17
+ "@checkstack/ai-backend": "0.10.5",
19
18
  "@checkstack/ai-common": "0.6.3",
20
- "@checkstack/script-packages-backend": "0.3.20",
19
+ "@checkstack/automation-backend": "0.10.7",
20
+ "@checkstack/backend-api": "0.28.0",
21
21
  "@checkstack/cache-api": "0.3.16",
22
22
  "@checkstack/cache-utils": "0.2.21",
23
- "@checkstack/catalog-backend": "1.6.3",
23
+ "@checkstack/catalog-backend": "1.6.5",
24
24
  "@checkstack/catalog-common": "2.6.0",
25
- "@checkstack/command-backend": "0.2.16",
25
+ "@checkstack/command-backend": "0.2.17",
26
26
  "@checkstack/common": "0.19.0",
27
- "@checkstack/gitops-backend": "0.5.16",
27
+ "@checkstack/gitops-backend": "0.5.17",
28
28
  "@checkstack/gitops-common": "0.7.0",
29
29
  "@checkstack/healthcheck-common": "1.11.0",
30
- "@checkstack/secrets-common": "0.2.8",
31
- "@checkstack/secrets-backend": "0.2.16",
32
- "@checkstack/incident-backend": "1.9.1",
30
+ "@checkstack/incident-backend": "1.9.3",
33
31
  "@checkstack/incident-common": "1.7.0",
34
- "@checkstack/automation-backend": "0.10.5",
35
32
  "@checkstack/maintenance-common": "1.8.0",
36
33
  "@checkstack/notification-common": "1.5.0",
37
34
  "@checkstack/queue-api": "0.3.16",
38
- "@checkstack/satellite-backend": "0.7.6",
39
- "@checkstack/sdk": "0.119.1",
35
+ "@checkstack/satellite-backend": "0.7.8",
36
+ "@checkstack/script-packages-backend": "0.3.21",
37
+ "@checkstack/sdk": "0.121.0",
38
+ "@checkstack/secrets-backend": "0.2.17",
39
+ "@checkstack/secrets-common": "0.2.8",
40
40
  "@checkstack/signal-common": "0.2.14",
41
- "@checkstack/status-page-backend": "0.4.3",
41
+ "@checkstack/status-page-backend": "0.4.4",
42
42
  "@checkstack/status-page-common": "0.5.0",
43
43
  "@hono/zod-validator": "^0.7.6",
44
+ "@orpc/contract": "^1.14.4",
45
+ "@orpc/server": "^1.14.4",
44
46
  "drizzle-orm": "^0.45.0",
45
47
  "hono": "^4.12.25",
48
+ "jsonpath-plus": "^10.4.0",
46
49
  "tdigest": "^0.1.2",
47
50
  "yaml": "^2.6.1",
48
- "zod": "^4.2.1",
49
- "@orpc/contract": "^1.14.4",
50
- "@orpc/server": "^1.14.4"
51
+ "zod": "^4.2.1"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@checkstack/drizzle-helper": "0.0.6",
54
55
  "@checkstack/scripts": "0.7.0",
55
- "@checkstack/test-utils-backend": "0.1.50",
56
+ "@checkstack/test-utils-backend": "0.1.51",
56
57
  "@checkstack/tsconfig": "0.0.7",
57
58
  "@types/bun": "^1.0.0",
58
59
  "@types/tdigest": "^0.1.5",
@@ -0,0 +1,276 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { CollectorAssertion } from "@checkstack/healthcheck-common";
3
+ import { evaluateCollectorAssertions } from "./collector-assertions";
4
+
5
+ /** The HTTP Request collector's result shape, as the executor sees it. */
6
+ const httpResult = (body: string): Record<string, unknown> => ({
7
+ statusCode: 200,
8
+ statusText: "OK",
9
+ responseTimeMs: 12,
10
+ body,
11
+ bodyLength: body.length,
12
+ success: true,
13
+ });
14
+
15
+ const jsonPathAssertion = (
16
+ jsonPath: string,
17
+ operator: string,
18
+ value?: unknown,
19
+ ): CollectorAssertion => ({
20
+ field: "body.$",
21
+ jsonPath,
22
+ operator,
23
+ ...(value === undefined ? {} : { value }),
24
+ });
25
+
26
+ describe("evaluateCollectorAssertions", () => {
27
+ describe("plain field assertions (regression)", () => {
28
+ it("passes and fails on a direct result field", () => {
29
+ const result = httpResult("{}");
30
+
31
+ expect(
32
+ evaluateCollectorAssertions({
33
+ assertions: [{ field: "statusCode", operator: "equals", value: 200 }],
34
+ result,
35
+ }),
36
+ ).toBeUndefined();
37
+
38
+ expect(
39
+ evaluateCollectorAssertions({
40
+ assertions: [{ field: "statusCode", operator: "equals", value: 404 }],
41
+ result,
42
+ }),
43
+ ).toBe("statusCode equals 404");
44
+ });
45
+
46
+ it("returns undefined for no assertions", () => {
47
+ expect(
48
+ evaluateCollectorAssertions({
49
+ assertions: undefined,
50
+ result: httpResult(""),
51
+ }),
52
+ ).toBeUndefined();
53
+ expect(
54
+ evaluateCollectorAssertions({ assertions: [], result: httpResult("") }),
55
+ ).toBeUndefined();
56
+ });
57
+
58
+ it("returns the FIRST failing assertion in configured order", () => {
59
+ const failure = evaluateCollectorAssertions({
60
+ assertions: [
61
+ { field: "success", operator: "isFalse" },
62
+ { field: "statusCode", operator: "equals", value: 404 },
63
+ ],
64
+ result: httpResult("{}"),
65
+ });
66
+ expect(failure).toBe("success isFalse");
67
+ });
68
+ });
69
+
70
+ describe("JSONPath assertions: extraction actually happens", () => {
71
+ it("exists fails when the key is missing and passes when present", () => {
72
+ expect(
73
+ evaluateCollectorAssertions({
74
+ assertions: [jsonPathAssertion("$.error", "exists")],
75
+ result: httpResult(`{"status":"ok"}`),
76
+ }),
77
+ ).toBe("body.$ $.error exists");
78
+
79
+ expect(
80
+ evaluateCollectorAssertions({
81
+ assertions: [jsonPathAssertion("$.error", "exists")],
82
+ result: httpResult(`{"error":""}`),
83
+ }),
84
+ ).toBeUndefined();
85
+ });
86
+
87
+ it("notExists passes for a missing key and fails for a present one", () => {
88
+ expect(
89
+ evaluateCollectorAssertions({
90
+ assertions: [jsonPathAssertion("$.error", "notExists")],
91
+ result: httpResult(`{"status":"ok"}`),
92
+ }),
93
+ ).toBeUndefined();
94
+
95
+ expect(
96
+ evaluateCollectorAssertions({
97
+ assertions: [jsonPathAssertion("$.error", "notExists")],
98
+ result: httpResult(`{"error":"boom"}`),
99
+ }),
100
+ ).toBe("body.$ $.error notExists");
101
+ });
102
+
103
+ it("equals compares the EXTRACTED value, not the whole body", () => {
104
+ expect(
105
+ evaluateCollectorAssertions({
106
+ assertions: [jsonPathAssertion("$.data.id", "equals", "42")],
107
+ result: httpResult(`{"data":{"id":42},"noise":"x"}`),
108
+ }),
109
+ ).toBeUndefined();
110
+
111
+ expect(
112
+ evaluateCollectorAssertions({
113
+ assertions: [jsonPathAssertion("$.data.id", "equals", "43")],
114
+ result: httpResult(`{"data":{"id":42}}`),
115
+ }),
116
+ ).toBe("body.$ $.data.id equals 43");
117
+ });
118
+
119
+ it('asserts "error exists but is empty" via exists + isEmpty', () => {
120
+ const assertions = [
121
+ jsonPathAssertion("$.error", "exists"),
122
+ jsonPathAssertion("$.error", "isEmpty"),
123
+ ];
124
+
125
+ // Present and empty string: both pass.
126
+ expect(
127
+ evaluateCollectorAssertions({
128
+ assertions,
129
+ result: httpResult(`{"error":""}`),
130
+ }),
131
+ ).toBeUndefined();
132
+
133
+ // Missing entirely: exists fails (isEmpty alone would have passed).
134
+ expect(
135
+ evaluateCollectorAssertions({
136
+ assertions,
137
+ result: httpResult(`{"status":"ok"}`),
138
+ }),
139
+ ).toBe("body.$ $.error exists");
140
+
141
+ // Present and non-empty: isEmpty fails.
142
+ expect(
143
+ evaluateCollectorAssertions({
144
+ assertions,
145
+ result: httpResult(`{"error":"boom"}`),
146
+ }),
147
+ ).toBe("body.$ $.error isEmpty");
148
+ });
149
+ });
150
+
151
+ describe("empty arrays in JSON responses", () => {
152
+ it("isEmpty passes for [] and fails for a populated array", () => {
153
+ expect(
154
+ evaluateCollectorAssertions({
155
+ assertions: [jsonPathAssertion("$.errors", "isEmpty")],
156
+ result: httpResult(`{"errors":[]}`),
157
+ }),
158
+ ).toBeUndefined();
159
+
160
+ expect(
161
+ evaluateCollectorAssertions({
162
+ assertions: [jsonPathAssertion("$.errors", "isEmpty")],
163
+ result: httpResult(`{"errors":["boom"]}`),
164
+ }),
165
+ ).toBe("body.$ $.errors isEmpty");
166
+ });
167
+
168
+ it("isNotEmpty passes for a populated array and fails for []", () => {
169
+ expect(
170
+ evaluateCollectorAssertions({
171
+ assertions: [jsonPathAssertion("$.items", "isNotEmpty")],
172
+ result: httpResult(`{"items":[1]}`),
173
+ }),
174
+ ).toBeUndefined();
175
+
176
+ expect(
177
+ evaluateCollectorAssertions({
178
+ assertions: [jsonPathAssertion("$.items", "isNotEmpty")],
179
+ result: httpResult(`{"items":[]}`),
180
+ }),
181
+ ).toBe("body.$ $.items isNotEmpty");
182
+ });
183
+
184
+ it("isEmpty handles empty objects and nested paths", () => {
185
+ expect(
186
+ evaluateCollectorAssertions({
187
+ assertions: [jsonPathAssertion("$.meta.warnings", "isEmpty")],
188
+ result: httpResult(`{"meta":{"warnings":{}}}`),
189
+ }),
190
+ ).toBeUndefined();
191
+
192
+ expect(
193
+ evaluateCollectorAssertions({
194
+ assertions: [jsonPathAssertion("$.meta.warnings", "isEmpty")],
195
+ result: httpResult(`{"meta":{"warnings":{"w1":"x"}}}`),
196
+ }),
197
+ ).toBe("body.$ $.meta.warnings isEmpty");
198
+ });
199
+
200
+ it("array length is assertable via the .length property", () => {
201
+ expect(
202
+ evaluateCollectorAssertions({
203
+ assertions: [
204
+ jsonPathAssertion("$.items.length", "greaterThan", "2"),
205
+ ],
206
+ result: httpResult(`{"items":[1,2,3]}`),
207
+ }),
208
+ ).toBeUndefined();
209
+
210
+ expect(
211
+ evaluateCollectorAssertions({
212
+ assertions: [
213
+ jsonPathAssertion("$.items.length", "greaterThan", "5"),
214
+ ],
215
+ result: httpResult(`{"items":[1,2,3]}`),
216
+ }),
217
+ ).toBe("body.$ $.items.length greaterThan 5");
218
+ });
219
+ });
220
+
221
+ describe("fail-closed diagnostics", () => {
222
+ it("fails with a diagnostic when the body is not valid JSON", () => {
223
+ expect(
224
+ evaluateCollectorAssertions({
225
+ assertions: [jsonPathAssertion("$.error", "notExists")],
226
+ result: httpResult("<html>not json</html>"),
227
+ }),
228
+ ).toBe('body.$ $.error notExists (field "body" is not valid JSON)');
229
+ });
230
+
231
+ it("fails with a diagnostic when the source field is absent", () => {
232
+ expect(
233
+ evaluateCollectorAssertions({
234
+ assertions: [jsonPathAssertion("$.error", "exists")],
235
+ result: { statusCode: 200 },
236
+ }),
237
+ ).toBe('body.$ $.error exists (field "body" has no value)');
238
+ });
239
+
240
+ it("fails with a diagnostic when the JSONPath expression is missing", () => {
241
+ expect(
242
+ evaluateCollectorAssertions({
243
+ assertions: [{ field: "body.$", jsonPath: " ", operator: "exists" }],
244
+ result: httpResult("{}"),
245
+ }),
246
+ ).toBe("body.$ exists (missing JSONPath expression)");
247
+ });
248
+
249
+ it("rejects filter/script expressions instead of evaluating them", () => {
250
+ const failure = evaluateCollectorAssertions({
251
+ assertions: [
252
+ jsonPathAssertion(`$.items[?(@.x==1)]`, "exists"),
253
+ ],
254
+ result: httpResult(`{"items":[{"x":1}]}`),
255
+ });
256
+ expect(failure).toContain("invalid JSONPath");
257
+ });
258
+ });
259
+
260
+ describe("structured (non-string) source fields", () => {
261
+ it("evaluates JSONPath against an already-parsed object field", () => {
262
+ expect(
263
+ evaluateCollectorAssertions({
264
+ assertions: [
265
+ {
266
+ field: "details.$",
267
+ jsonPath: "$.nodes",
268
+ operator: "isEmpty",
269
+ },
270
+ ],
271
+ result: { details: { nodes: [] } },
272
+ }),
273
+ ).toBeUndefined();
274
+ });
275
+ });
276
+ });
@@ -0,0 +1,169 @@
1
+ import {
2
+ evaluateAssertions,
3
+ evaluateJsonPathAssertions,
4
+ } from "@checkstack/backend-api";
5
+ import type { CollectorAssertion } from "@checkstack/healthcheck-common";
6
+ import { extractErrorMessage } from "@checkstack/common";
7
+ import { JSONPath } from "jsonpath-plus";
8
+
9
+ /**
10
+ * Suffix the AssertionBuilder appends to a JSONPath field's path: an
11
+ * `x-jsonpath` result field `body` is offered as the assertable field
12
+ * `body.$`, with the actual expression stored in `assertion.jsonPath`.
13
+ */
14
+ const JSONPATH_FIELD_SUFFIX = ".$";
15
+
16
+ /** Whether an assertion targets a JSONPath into a field, not the field itself. */
17
+ function isJsonPathAssertion(assertion: CollectorAssertion): boolean {
18
+ return (
19
+ assertion.field.endsWith(JSONPATH_FIELD_SUFFIX) ||
20
+ (typeof assertion.jsonPath === "string" && assertion.jsonPath.trim() !== "")
21
+ );
22
+ }
23
+
24
+ /**
25
+ * Extract a JSONPath from a parsed JSON value. `wrap: false` returns the
26
+ * single matched value directly (or `undefined` for no match), so `exists` /
27
+ * `isEmpty` operate on the value itself, not on a match array. `eval: false`
28
+ * rejects script/filter expressions outright - assertion paths are authored
29
+ * by users and must never evaluate code on the core.
30
+ */
31
+ /**
32
+ * Narrow an `unknown` to the input type `JSONPath` accepts. Parsed JSON is
33
+ * always one of these by construction; the guard exists because the shared
34
+ * `evaluateJsonPathAssertions` signature hands the json through as `unknown`.
35
+ */
36
+ function isJsonPathInput(
37
+ value: unknown,
38
+ ): value is string | number | boolean | object | null {
39
+ return (
40
+ value === null ||
41
+ typeof value === "string" ||
42
+ typeof value === "number" ||
43
+ typeof value === "boolean" ||
44
+ typeof value === "object"
45
+ );
46
+ }
47
+
48
+ function extractJsonPath(path: string, json: unknown): unknown {
49
+ if (!isJsonPathInput(json)) return undefined;
50
+ return JSONPath({ path, json, wrap: false, eval: false });
51
+ }
52
+
53
+ /** Human-readable failure string, stored on the run as `_assertionFailed`. */
54
+ function formatFailure({
55
+ assertion,
56
+ detail,
57
+ }: {
58
+ assertion: CollectorAssertion;
59
+ detail?: string;
60
+ }): string {
61
+ const path = assertion.jsonPath?.trim();
62
+ const parts = [
63
+ assertion.field,
64
+ ...(isJsonPathAssertion(assertion) && path ? [path] : []),
65
+ assertion.operator,
66
+ ...(assertion.value === undefined ? [] : [String(assertion.value)]),
67
+ ];
68
+ const base = parts.join(" ");
69
+ return detail ? `${base} (${detail})` : base;
70
+ }
71
+
72
+ /**
73
+ * Evaluate a collector's assertions - plain field assertions AND JSONPath
74
+ * assertions - against its result, in the order they were configured.
75
+ *
76
+ * Plain assertions compare `result[field]` directly (unchanged behaviour).
77
+ * JSONPath assertions parse the SOURCE field (e.g. `body` for the field
78
+ * `body.$`) as JSON when it is a string, extract `assertion.jsonPath`, and
79
+ * apply the operator to the extracted value. Fail-closed: a missing
80
+ * expression, a non-JSON source value, or an invalid/eval-blocked path fails
81
+ * the assertion (with a diagnostic suffix) - it never fails the collector.
82
+ *
83
+ * Returns the failure message of the FIRST failing assertion, or `undefined`
84
+ * when all pass.
85
+ */
86
+ export function evaluateCollectorAssertions({
87
+ assertions,
88
+ result,
89
+ }: {
90
+ assertions: CollectorAssertion[] | undefined;
91
+ result: Record<string, unknown>;
92
+ }): string | undefined {
93
+ if (!assertions?.length) return undefined;
94
+
95
+ // Parse each JSON source field at most once, not once per assertion.
96
+ const parsedSources = new Map<string, { json?: unknown; error?: string }>();
97
+ const parseSource = (sourceField: string) => {
98
+ const cached = parsedSources.get(sourceField);
99
+ if (cached) return cached;
100
+
101
+ const raw = result[sourceField];
102
+ let entry: { json?: unknown; error?: string };
103
+ if (raw === undefined || raw === null) {
104
+ entry = { error: `field "${sourceField}" has no value` };
105
+ } else if (typeof raw === "string") {
106
+ try {
107
+ entry = { json: JSON.parse(raw) };
108
+ } catch {
109
+ entry = { error: `field "${sourceField}" is not valid JSON` };
110
+ }
111
+ } else {
112
+ // Already-structured value (object/array/number) - use as-is.
113
+ entry = { json: raw };
114
+ }
115
+ parsedSources.set(sourceField, entry);
116
+ return entry;
117
+ };
118
+
119
+ for (const assertion of assertions) {
120
+ if (!isJsonPathAssertion(assertion)) {
121
+ const failed = evaluateAssertions([assertion], result);
122
+ if (failed) return formatFailure({ assertion: failed });
123
+ continue;
124
+ }
125
+
126
+ const path = assertion.jsonPath?.trim();
127
+ if (!path) {
128
+ return formatFailure({
129
+ assertion,
130
+ detail: "missing JSONPath expression",
131
+ });
132
+ }
133
+
134
+ const sourceField = assertion.field.endsWith(JSONPATH_FIELD_SUFFIX)
135
+ ? assertion.field.slice(0, -JSONPATH_FIELD_SUFFIX.length)
136
+ : assertion.field;
137
+ const source = parseSource(sourceField);
138
+ if (source.error) {
139
+ return formatFailure({ assertion, detail: source.error });
140
+ }
141
+
142
+ try {
143
+ const failed = evaluateJsonPathAssertions(
144
+ [
145
+ {
146
+ path,
147
+ operator: assertion.operator,
148
+ value:
149
+ assertion.value === undefined
150
+ ? undefined
151
+ : String(assertion.value),
152
+ },
153
+ ],
154
+ source.json,
155
+ extractJsonPath,
156
+ );
157
+ if (failed) return formatFailure({ assertion });
158
+ } catch (error) {
159
+ // jsonpath-plus rejects malformed paths and (with eval disabled)
160
+ // filter/script expressions by throwing.
161
+ return formatFailure({
162
+ assertion,
163
+ detail: `invalid JSONPath: ${extractErrorMessage(error)}`,
164
+ });
165
+ }
166
+ }
167
+
168
+ return undefined;
169
+ }
@@ -3,7 +3,6 @@ import {
3
3
  Logger,
4
4
  type EmitHookFn,
5
5
  type CollectorRegistry,
6
- evaluateAssertions,
7
6
  type SafeDatabase,
8
7
  type BaseStrategyConfig,
9
8
  type ConnectedClient,
@@ -57,6 +56,7 @@ import {
57
56
  shouldNotifyTransition,
58
57
  } from "./notification-policy";
59
58
  import { recordStateTransition } from "./state-transitions";
59
+ import { evaluateCollectorAssertions } from "./collector-assertions";
60
60
  import {
61
61
  writeHealthEntity,
62
62
  createHealthEntitySerializer,
@@ -901,21 +901,14 @@ async function executeHealthCheckJob(props: {
901
901
  collectorError = collectorResult.error;
902
902
  }
903
903
 
904
- // Evaluate per-collector assertions
904
+ // Evaluate per-collector assertions (plain fields + JSONPath)
905
905
  let assertionFailed: string | undefined;
906
- if (
907
- collectorEntry.assertions &&
908
- collectorEntry.assertions.length > 0 &&
909
- collectorResult.result
910
- ) {
911
- const failedAssertion = evaluateAssertions(
912
- collectorEntry.assertions,
913
- collectorResult.result as Record<string, unknown>,
914
- );
915
- if (failedAssertion) {
916
- assertionFailed = `${failedAssertion.field} ${
917
- failedAssertion.operator
918
- } ${failedAssertion.value ?? ""}`;
906
+ if (collectorResult.result) {
907
+ assertionFailed = evaluateCollectorAssertions({
908
+ assertions: collectorEntry.assertions,
909
+ result: collectorResult.result as Record<string, unknown>,
910
+ });
911
+ if (assertionFailed) {
919
912
  logger.debug(
920
913
  `Collector ${storageKey} assertion failed: ${assertionFailed}`,
921
914
  );