@checkstack/healthcheck-script-backend 0.1.12 → 0.2.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 +68 -0
- package/package.json +6 -6
- package/src/execute-collector.test.ts +5 -3
- package/src/execute-collector.ts +42 -28
- package/src/index.ts +2 -0
- package/src/inline-script-collector.test.ts +256 -0
- package/src/inline-script-collector.ts +364 -0
- package/src/strategy.test.ts +5 -3
- package/src/strategy.ts +58 -47
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
# @checkstack/healthcheck-script-backend
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f676e11: Add script execution support and migrate CodeEditor to Monaco
|
|
8
|
+
|
|
9
|
+
**Integration providers** (`@checkstack/integration-script-backend`):
|
|
10
|
+
|
|
11
|
+
- **Script** - Execute TypeScript/JavaScript with context object
|
|
12
|
+
- **Bash** - Execute shell scripts with environment variables ($EVENT*ID, $PAYLOAD*\*)
|
|
13
|
+
|
|
14
|
+
**Health check collectors** (`@checkstack/healthcheck-script-backend`):
|
|
15
|
+
|
|
16
|
+
- **InlineScriptCollector** - Run TypeScript directly for health checks
|
|
17
|
+
- **ExecuteCollector** - Bash syntax highlighting for command field
|
|
18
|
+
|
|
19
|
+
**CodeEditor migration to Monaco** (`@checkstack/ui`):
|
|
20
|
+
|
|
21
|
+
- Replaced CodeMirror with Monaco Editor (VS Code's editor)
|
|
22
|
+
- Full TypeScript/JavaScript IntelliSense with custom type definitions
|
|
23
|
+
- Added `generateTypeDefinitions()` for JSON Schema → TypeScript conversion
|
|
24
|
+
- Removed all CodeMirror dependencies
|
|
25
|
+
|
|
26
|
+
**Type updates** (`@checkstack/common`):
|
|
27
|
+
|
|
28
|
+
- Added `javascript`, `typescript`, and `bash` to `EditorType` union
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- 48c2080: Migrate aggregation from batch to incremental (`mergeResult`)
|
|
33
|
+
|
|
34
|
+
### Breaking Changes (Internal)
|
|
35
|
+
|
|
36
|
+
- Replaced `aggregateResult(runs[])` with `mergeResult(existing, run)` interface across all HealthCheckStrategy and CollectorStrategy implementations
|
|
37
|
+
|
|
38
|
+
### New Features
|
|
39
|
+
|
|
40
|
+
- Added incremental aggregation utilities in `@checkstack/backend-api`:
|
|
41
|
+
- `mergeCounter()` - track occurrences
|
|
42
|
+
- `mergeAverage()` - track sum/count, compute avg
|
|
43
|
+
- `mergeRate()` - track success/total, compute %
|
|
44
|
+
- `mergeMinMax()` - track min/max values
|
|
45
|
+
- Exported Zod schemas for internal state: `averageStateSchema`, `rateStateSchema`, `minMaxStateSchema`, `counterStateSchema`
|
|
46
|
+
|
|
47
|
+
### Improvements
|
|
48
|
+
|
|
49
|
+
- Enables O(1) storage overhead by maintaining incremental aggregation state
|
|
50
|
+
- Prepares for real-time hourly aggregation without batch accumulation
|
|
51
|
+
|
|
52
|
+
- Updated dependencies [f676e11]
|
|
53
|
+
- Updated dependencies [48c2080]
|
|
54
|
+
- @checkstack/common@0.6.2
|
|
55
|
+
- @checkstack/backend-api@0.6.0
|
|
56
|
+
- @checkstack/healthcheck-common@0.8.2
|
|
57
|
+
|
|
58
|
+
## 0.1.13
|
|
59
|
+
|
|
60
|
+
### Patch Changes
|
|
61
|
+
|
|
62
|
+
- 0b9fc58: Fix workspace:\* protocol resolution in published packages
|
|
63
|
+
|
|
64
|
+
Published packages now correctly have resolved dependency versions instead of `workspace:*` references. This is achieved by using `bun publish` which properly resolves workspace protocol references.
|
|
65
|
+
|
|
66
|
+
- Updated dependencies [0b9fc58]
|
|
67
|
+
- @checkstack/backend-api@0.5.2
|
|
68
|
+
- @checkstack/common@0.6.1
|
|
69
|
+
- @checkstack/healthcheck-common@0.8.1
|
|
70
|
+
|
|
3
71
|
## 0.1.12
|
|
4
72
|
|
|
5
73
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-script-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"scripts": {
|
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
"lint:code": "eslint . --max-warnings 0"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@checkstack/backend-api": "
|
|
13
|
-
"@checkstack/common": "
|
|
14
|
-
"@checkstack/healthcheck-common": "
|
|
12
|
+
"@checkstack/backend-api": "0.5.2",
|
|
13
|
+
"@checkstack/common": "0.6.1",
|
|
14
|
+
"@checkstack/healthcheck-common": "0.8.1"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@types/bun": "^1.0.0",
|
|
18
18
|
"typescript": "^5.0.0",
|
|
19
|
-
"@checkstack/tsconfig": "
|
|
20
|
-
"@checkstack/scripts": "
|
|
19
|
+
"@checkstack/tsconfig": "0.0.3",
|
|
20
|
+
"@checkstack/scripts": "0.1.1"
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -100,7 +100,7 @@ describe("ExecuteCollector", () => {
|
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
describe("
|
|
103
|
+
describe("mergeResult", () => {
|
|
104
104
|
it("should calculate average execution time and success rate", () => {
|
|
105
105
|
const collector = new ExecuteCollector();
|
|
106
106
|
const runs = [
|
|
@@ -136,7 +136,8 @@ describe("ExecuteCollector", () => {
|
|
|
136
136
|
},
|
|
137
137
|
];
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
140
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
140
141
|
|
|
141
142
|
expect(aggregated.avgExecutionTimeMs).toBe(75);
|
|
142
143
|
expect(aggregated.successRate).toBe(100);
|
|
@@ -177,7 +178,8 @@ describe("ExecuteCollector", () => {
|
|
|
177
178
|
},
|
|
178
179
|
];
|
|
179
180
|
|
|
180
|
-
|
|
181
|
+
let aggregated = collector.mergeResult(undefined, runs[0]);
|
|
182
|
+
aggregated = collector.mergeResult(aggregated, runs[1]);
|
|
181
183
|
|
|
182
184
|
expect(aggregated.successRate).toBe(50);
|
|
183
185
|
});
|
package/src/execute-collector.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Versioned,
|
|
3
3
|
z,
|
|
4
|
+
configString,
|
|
4
5
|
type HealthCheckRunForAggregation,
|
|
5
6
|
type CollectorResult,
|
|
6
7
|
type CollectorStrategy,
|
|
8
|
+
mergeAverage,
|
|
9
|
+
averageStateSchema,
|
|
10
|
+
mergeRate,
|
|
11
|
+
rateStateSchema,
|
|
12
|
+
type AverageState,
|
|
13
|
+
type RateState,
|
|
7
14
|
} from "@checkstack/backend-api";
|
|
8
15
|
import {
|
|
9
16
|
healthResultNumber,
|
|
@@ -19,7 +26,9 @@ import type { ScriptTransportClient } from "./transport-client";
|
|
|
19
26
|
// ============================================================================
|
|
20
27
|
|
|
21
28
|
const executeConfigSchema = z.object({
|
|
22
|
-
command:
|
|
29
|
+
command: configString({
|
|
30
|
+
"x-editor-types": ["shell"],
|
|
31
|
+
}).describe("Shell command or script to execute"),
|
|
23
32
|
args: z.array(z.string()).default([]).describe("Command arguments"),
|
|
24
33
|
cwd: z.string().optional().describe("Working directory"),
|
|
25
34
|
env: z
|
|
@@ -69,7 +78,7 @@ const executeResultSchema = healthResultSchema({
|
|
|
69
78
|
|
|
70
79
|
export type ExecuteResult = z.infer<typeof executeResultSchema>;
|
|
71
80
|
|
|
72
|
-
const
|
|
81
|
+
const executeAggregatedDisplaySchema = healthResultSchema({
|
|
73
82
|
avgExecutionTimeMs: healthResultNumber({
|
|
74
83
|
"x-chart-type": "line",
|
|
75
84
|
"x-chart-label": "Avg Execution Time",
|
|
@@ -82,6 +91,15 @@ const executeAggregatedSchema = healthResultSchema({
|
|
|
82
91
|
}),
|
|
83
92
|
});
|
|
84
93
|
|
|
94
|
+
const executeAggregatedInternalSchema = z.object({
|
|
95
|
+
_executionTime: averageStateSchema.optional(),
|
|
96
|
+
_success: rateStateSchema.optional(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const executeAggregatedSchema = executeAggregatedDisplaySchema.merge(
|
|
100
|
+
executeAggregatedInternalSchema,
|
|
101
|
+
);
|
|
102
|
+
|
|
85
103
|
export type ExecuteAggregatedResult = z.infer<typeof executeAggregatedSchema>;
|
|
86
104
|
|
|
87
105
|
// ============================================================================
|
|
@@ -92,15 +110,12 @@ export type ExecuteAggregatedResult = z.infer<typeof executeAggregatedSchema>;
|
|
|
92
110
|
* Built-in Script execute collector.
|
|
93
111
|
* Runs commands and checks results.
|
|
94
112
|
*/
|
|
95
|
-
export class ExecuteCollector
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
ExecuteAggregatedResult
|
|
102
|
-
>
|
|
103
|
-
{
|
|
113
|
+
export class ExecuteCollector implements CollectorStrategy<
|
|
114
|
+
ScriptTransportClient,
|
|
115
|
+
ExecuteConfig,
|
|
116
|
+
ExecuteResult,
|
|
117
|
+
ExecuteAggregatedResult
|
|
118
|
+
> {
|
|
104
119
|
id = "execute";
|
|
105
120
|
displayName = "Execute Script";
|
|
106
121
|
description = "Execute a command or script and check the result";
|
|
@@ -152,28 +167,27 @@ export class ExecuteCollector
|
|
|
152
167
|
};
|
|
153
168
|
}
|
|
154
169
|
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
mergeResult(
|
|
171
|
+
existing: ExecuteAggregatedResult | undefined,
|
|
172
|
+
run: HealthCheckRunForAggregation<ExecuteResult>,
|
|
157
173
|
): ExecuteAggregatedResult {
|
|
158
|
-
const
|
|
159
|
-
.map((r) => r.metadata?.executionTimeMs)
|
|
160
|
-
.filter((v): v is number => typeof v === "number");
|
|
174
|
+
const metadata = run.metadata;
|
|
161
175
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
const executionTimeState = mergeAverage(
|
|
177
|
+
existing?._executionTime as AverageState | undefined,
|
|
178
|
+
metadata?.executionTimeMs,
|
|
179
|
+
);
|
|
165
180
|
|
|
166
|
-
const
|
|
181
|
+
const successState = mergeRate(
|
|
182
|
+
existing?._success as RateState | undefined,
|
|
183
|
+
metadata?.success,
|
|
184
|
+
);
|
|
167
185
|
|
|
168
186
|
return {
|
|
169
|
-
avgExecutionTimeMs:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
successRate:
|
|
174
|
-
successes.length > 0
|
|
175
|
-
? Math.round((successCount / successes.length) * 100)
|
|
176
|
-
: 0,
|
|
187
|
+
avgExecutionTimeMs: executionTimeState.avg,
|
|
188
|
+
successRate: successState.rate,
|
|
189
|
+
_executionTime: executionTimeState,
|
|
190
|
+
_success: successState,
|
|
177
191
|
};
|
|
178
192
|
}
|
|
179
193
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
|
|
|
2
2
|
import { ScriptHealthCheckStrategy } from "./strategy";
|
|
3
3
|
import { pluginMetadata } from "./plugin-metadata";
|
|
4
4
|
import { ExecuteCollector } from "./execute-collector";
|
|
5
|
+
import { InlineScriptCollector } from "./inline-script-collector";
|
|
5
6
|
|
|
6
7
|
export default createBackendPlugin({
|
|
7
8
|
metadata: pluginMetadata,
|
|
@@ -17,6 +18,7 @@ export default createBackendPlugin({
|
|
|
17
18
|
const strategy = new ScriptHealthCheckStrategy();
|
|
18
19
|
healthCheckRegistry.register(strategy);
|
|
19
20
|
collectorRegistry.register(new ExecuteCollector());
|
|
21
|
+
collectorRegistry.register(new InlineScriptCollector());
|
|
20
22
|
},
|
|
21
23
|
});
|
|
22
24
|
},
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
InlineScriptCollector,
|
|
4
|
+
type InlineScriptConfig,
|
|
5
|
+
} from "./inline-script-collector";
|
|
6
|
+
import type { ScriptTransportClient } from "./transport-client";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Unit tests for the Inline Script Collector.
|
|
10
|
+
*
|
|
11
|
+
* Tests cover:
|
|
12
|
+
* - Basic script execution
|
|
13
|
+
* - Return value handling
|
|
14
|
+
* - Error handling
|
|
15
|
+
* - Timeout protection
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Mock client (not actually used by InlineScriptCollector, but required by interface)
|
|
19
|
+
const mockClient: ScriptTransportClient = {
|
|
20
|
+
exec: async () => ({
|
|
21
|
+
exitCode: 0,
|
|
22
|
+
stdout: "",
|
|
23
|
+
stderr: "",
|
|
24
|
+
timedOut: false,
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function createConfig(
|
|
29
|
+
overrides: Partial<InlineScriptConfig> = {},
|
|
30
|
+
): InlineScriptConfig {
|
|
31
|
+
return {
|
|
32
|
+
script: "return { success: true };",
|
|
33
|
+
timeout: 5000,
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("InlineScriptCollector", () => {
|
|
39
|
+
const collector = new InlineScriptCollector();
|
|
40
|
+
|
|
41
|
+
// ─────────────────────────────────────────────────────────────
|
|
42
|
+
// Metadata
|
|
43
|
+
// ─────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
describe("metadata", () => {
|
|
46
|
+
it("has correct basic metadata", () => {
|
|
47
|
+
expect(collector.id).toBe("inline-script");
|
|
48
|
+
expect(collector.displayName).toBe("Inline Script");
|
|
49
|
+
expect(collector.description).toContain("TypeScript");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("allows multiple instances", () => {
|
|
53
|
+
expect(collector.allowMultiple).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─────────────────────────────────────────────────────────────
|
|
58
|
+
// Basic Execution
|
|
59
|
+
// ─────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
describe("execute - basic", () => {
|
|
62
|
+
it("executes script returning success object", async () => {
|
|
63
|
+
const config = createConfig({
|
|
64
|
+
script: 'return { success: true, message: "All good" };',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const result = await collector.execute({
|
|
68
|
+
config,
|
|
69
|
+
client: mockClient,
|
|
70
|
+
pluginId: "script",
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.result.success).toBe(true);
|
|
74
|
+
expect(result.result.message).toBe("All good");
|
|
75
|
+
expect(result.error).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("executes script returning boolean", async () => {
|
|
79
|
+
const config = createConfig({
|
|
80
|
+
script: "return true;",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const result = await collector.execute({
|
|
84
|
+
config,
|
|
85
|
+
client: mockClient,
|
|
86
|
+
pluginId: "script",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.result.success).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("executes script with no return (defaults to success)", async () => {
|
|
93
|
+
const config = createConfig({
|
|
94
|
+
script: "const x = 1 + 1;",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = await collector.execute({
|
|
98
|
+
config,
|
|
99
|
+
client: mockClient,
|
|
100
|
+
pluginId: "script",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(result.result.success).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("executes async script", async () => {
|
|
107
|
+
const config = createConfig({
|
|
108
|
+
script: `
|
|
109
|
+
await Promise.resolve();
|
|
110
|
+
return { success: true, message: "async works" };
|
|
111
|
+
`,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const result = await collector.execute({
|
|
115
|
+
config,
|
|
116
|
+
client: mockClient,
|
|
117
|
+
pluginId: "script",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(result.result.success).toBe(true);
|
|
121
|
+
expect(result.result.message).toBe("async works");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("captures numeric value from script", async () => {
|
|
125
|
+
const config = createConfig({
|
|
126
|
+
script: "return { success: true, value: 42 };",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const result = await collector.execute({
|
|
130
|
+
config,
|
|
131
|
+
client: mockClient,
|
|
132
|
+
pluginId: "script",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result.result.value).toBe(42);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ─────────────────────────────────────────────────────────────
|
|
140
|
+
// Failure Handling
|
|
141
|
+
// ─────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
describe("execute - failures", () => {
|
|
144
|
+
it("handles script returning false", async () => {
|
|
145
|
+
const config = createConfig({
|
|
146
|
+
script: "return false;",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const result = await collector.execute({
|
|
150
|
+
config,
|
|
151
|
+
client: mockClient,
|
|
152
|
+
pluginId: "script",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(result.result.success).toBe(false);
|
|
156
|
+
expect(result.error).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("handles script returning success: false", async () => {
|
|
160
|
+
const config = createConfig({
|
|
161
|
+
script: 'return { success: false, message: "Check failed" };',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const result = await collector.execute({
|
|
165
|
+
config,
|
|
166
|
+
client: mockClient,
|
|
167
|
+
pluginId: "script",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(result.result.success).toBe(false);
|
|
171
|
+
expect(result.result.message).toBe("Check failed");
|
|
172
|
+
expect(result.error).toBe("Check failed");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("handles script errors", async () => {
|
|
176
|
+
const config = createConfig({
|
|
177
|
+
script: 'throw new Error("Something went wrong");',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const result = await collector.execute({
|
|
181
|
+
config,
|
|
182
|
+
client: mockClient,
|
|
183
|
+
pluginId: "script",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(result.result.success).toBe(false);
|
|
187
|
+
expect(result.error).toContain("Something went wrong");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("handles syntax errors", async () => {
|
|
191
|
+
const config = createConfig({
|
|
192
|
+
script: "const x = {",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const result = await collector.execute({
|
|
196
|
+
config,
|
|
197
|
+
client: mockClient,
|
|
198
|
+
pluginId: "script",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(result.result.success).toBe(false);
|
|
202
|
+
expect(result.error).toBeDefined();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ─────────────────────────────────────────────────────────────
|
|
207
|
+
// Timeout
|
|
208
|
+
// ─────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
describe("execute - timeout", () => {
|
|
211
|
+
it("times out long-running scripts", async () => {
|
|
212
|
+
const config = createConfig({
|
|
213
|
+
script: `
|
|
214
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
215
|
+
return { success: true };
|
|
216
|
+
`,
|
|
217
|
+
timeout: 1000,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const result = await collector.execute({
|
|
221
|
+
config,
|
|
222
|
+
client: mockClient,
|
|
223
|
+
pluginId: "script",
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(result.result.success).toBe(false);
|
|
227
|
+
expect(result.result.timedOut).toBe(true);
|
|
228
|
+
expect(result.error).toContain("timed out");
|
|
229
|
+
}, 10000);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ─────────────────────────────────────────────────────────────
|
|
233
|
+
// Aggregation
|
|
234
|
+
// ─────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
describe("mergeResult", () => {
|
|
237
|
+
it("aggregates execution time and success rate", () => {
|
|
238
|
+
const run1 = {
|
|
239
|
+
metadata: { success: true, executionTimeMs: 100, timedOut: false },
|
|
240
|
+
};
|
|
241
|
+
const run2 = {
|
|
242
|
+
metadata: { success: true, executionTimeMs: 200, timedOut: false },
|
|
243
|
+
};
|
|
244
|
+
const run3 = {
|
|
245
|
+
metadata: { success: false, executionTimeMs: 150, timedOut: false },
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
let aggregated = collector.mergeResult(undefined, run1 as never);
|
|
249
|
+
aggregated = collector.mergeResult(aggregated, run2 as never);
|
|
250
|
+
aggregated = collector.mergeResult(aggregated, run3 as never);
|
|
251
|
+
|
|
252
|
+
expect(aggregated.avgExecutionTimeMs).toBe(150); // (100+200+150)/3
|
|
253
|
+
expect(aggregated.successRate).toBeCloseTo(67, 0); // 2/3 * 100 = ~67
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Versioned,
|
|
3
|
+
z,
|
|
4
|
+
configString,
|
|
5
|
+
configNumber,
|
|
6
|
+
type HealthCheckRunForAggregation,
|
|
7
|
+
type CollectorResult,
|
|
8
|
+
type CollectorStrategy,
|
|
9
|
+
mergeAverage,
|
|
10
|
+
averageStateSchema,
|
|
11
|
+
mergeRate,
|
|
12
|
+
rateStateSchema,
|
|
13
|
+
type AverageState,
|
|
14
|
+
type RateState,
|
|
15
|
+
} from "@checkstack/backend-api";
|
|
16
|
+
import {
|
|
17
|
+
healthResultNumber,
|
|
18
|
+
healthResultString,
|
|
19
|
+
healthResultBoolean,
|
|
20
|
+
healthResultSchema,
|
|
21
|
+
} from "@checkstack/healthcheck-common";
|
|
22
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
23
|
+
import type { ScriptTransportClient } from "./transport-client";
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// SCRIPT EXECUTION UTILITIES (shared with integration-script-backend pattern)
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Context available to inline scripts.
|
|
31
|
+
*/
|
|
32
|
+
interface ScriptContext {
|
|
33
|
+
/** Health check configuration */
|
|
34
|
+
config: Record<string, unknown>;
|
|
35
|
+
/** Fetch API for HTTP requests */
|
|
36
|
+
fetch: typeof fetch;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Safe console interface for scripts.
|
|
41
|
+
*/
|
|
42
|
+
interface SafeConsole {
|
|
43
|
+
log: (...args: unknown[]) => void;
|
|
44
|
+
warn: (...args: unknown[]) => void;
|
|
45
|
+
error: (...args: unknown[]) => void;
|
|
46
|
+
info: (...args: unknown[]) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Expected return type from health check scripts.
|
|
51
|
+
*/
|
|
52
|
+
interface ScriptHealthResult {
|
|
53
|
+
/** Whether the health check passed */
|
|
54
|
+
success: boolean;
|
|
55
|
+
/** Optional message describing the result */
|
|
56
|
+
message?: string;
|
|
57
|
+
/** Optional numeric value for metrics */
|
|
58
|
+
value?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Execute an inline script with the given context.
|
|
63
|
+
*/
|
|
64
|
+
async function executeInlineScript({
|
|
65
|
+
script,
|
|
66
|
+
context,
|
|
67
|
+
safeConsole,
|
|
68
|
+
timeoutMs,
|
|
69
|
+
}: {
|
|
70
|
+
script: string;
|
|
71
|
+
context: ScriptContext;
|
|
72
|
+
safeConsole: SafeConsole;
|
|
73
|
+
timeoutMs: number;
|
|
74
|
+
}): Promise<{
|
|
75
|
+
result: ScriptHealthResult | undefined;
|
|
76
|
+
error?: string;
|
|
77
|
+
timedOut: boolean;
|
|
78
|
+
}> {
|
|
79
|
+
try {
|
|
80
|
+
// Create an async function from the script
|
|
81
|
+
const asyncFn = new Function(
|
|
82
|
+
"context",
|
|
83
|
+
"console",
|
|
84
|
+
"fetch",
|
|
85
|
+
`return (async () => { ${script} })();`,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Execute with timeout
|
|
89
|
+
const result = await Promise.race([
|
|
90
|
+
asyncFn(context, safeConsole, fetch),
|
|
91
|
+
new Promise<never>((_, reject) =>
|
|
92
|
+
setTimeout(() => reject(new Error("__TIMEOUT__")), timeoutMs),
|
|
93
|
+
),
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
// Normalize result
|
|
97
|
+
if (result === undefined || result === null) {
|
|
98
|
+
return { result: { success: true }, timedOut: false };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof result === "boolean") {
|
|
102
|
+
return { result: { success: result }, timedOut: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof result === "object") {
|
|
106
|
+
return {
|
|
107
|
+
result: {
|
|
108
|
+
success: Boolean(result.success ?? true),
|
|
109
|
+
message: result.message,
|
|
110
|
+
value: result.value,
|
|
111
|
+
},
|
|
112
|
+
timedOut: false,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
result: { success: true, message: String(result) },
|
|
118
|
+
timedOut: false,
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
+
if (message === "__TIMEOUT__") {
|
|
123
|
+
return {
|
|
124
|
+
result: undefined,
|
|
125
|
+
error: "Script execution timed out",
|
|
126
|
+
timedOut: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { result: undefined, error: message, timedOut: false };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// CONFIGURATION SCHEMA
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
const inlineScriptConfigSchema = z.object({
|
|
138
|
+
script: configString({
|
|
139
|
+
"x-editor-types": ["typescript"],
|
|
140
|
+
}).describe(
|
|
141
|
+
"TypeScript/JavaScript code to execute. Return { success: boolean, message?: string, value?: number }",
|
|
142
|
+
),
|
|
143
|
+
timeout: configNumber({})
|
|
144
|
+
.min(1000)
|
|
145
|
+
.max(60_000)
|
|
146
|
+
.default(10_000)
|
|
147
|
+
.describe("Maximum execution time in milliseconds"),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
export type InlineScriptConfig = z.infer<typeof inlineScriptConfigSchema>;
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// RESULT SCHEMAS
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
const inlineScriptResultSchema = healthResultSchema({
|
|
157
|
+
success: healthResultBoolean({
|
|
158
|
+
"x-chart-type": "boolean",
|
|
159
|
+
"x-chart-label": "Success",
|
|
160
|
+
}),
|
|
161
|
+
message: healthResultString({
|
|
162
|
+
"x-chart-type": "text",
|
|
163
|
+
"x-chart-label": "Message",
|
|
164
|
+
}).optional(),
|
|
165
|
+
value: healthResultNumber({
|
|
166
|
+
"x-chart-type": "line",
|
|
167
|
+
"x-chart-label": "Value",
|
|
168
|
+
}).optional(),
|
|
169
|
+
executionTimeMs: healthResultNumber({
|
|
170
|
+
"x-chart-type": "line",
|
|
171
|
+
"x-chart-label": "Execution Time",
|
|
172
|
+
"x-chart-unit": "ms",
|
|
173
|
+
}),
|
|
174
|
+
timedOut: healthResultBoolean({
|
|
175
|
+
"x-chart-type": "boolean",
|
|
176
|
+
"x-chart-label": "Timed Out",
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export type InlineScriptResult = z.infer<typeof inlineScriptResultSchema>;
|
|
181
|
+
|
|
182
|
+
const inlineScriptAggregatedDisplaySchema = healthResultSchema({
|
|
183
|
+
avgExecutionTimeMs: healthResultNumber({
|
|
184
|
+
"x-chart-type": "line",
|
|
185
|
+
"x-chart-label": "Avg Execution Time",
|
|
186
|
+
"x-chart-unit": "ms",
|
|
187
|
+
}),
|
|
188
|
+
successRate: healthResultNumber({
|
|
189
|
+
"x-chart-type": "gauge",
|
|
190
|
+
"x-chart-label": "Success Rate",
|
|
191
|
+
"x-chart-unit": "%",
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const inlineScriptAggregatedInternalSchema = z.object({
|
|
196
|
+
_executionTime: averageStateSchema.optional(),
|
|
197
|
+
_success: rateStateSchema.optional(),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const inlineScriptAggregatedSchema = inlineScriptAggregatedDisplaySchema.merge(
|
|
201
|
+
inlineScriptAggregatedInternalSchema,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
export type InlineScriptAggregatedResult = z.infer<
|
|
205
|
+
typeof inlineScriptAggregatedSchema
|
|
206
|
+
>;
|
|
207
|
+
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// INLINE SCRIPT COLLECTOR
|
|
210
|
+
// ============================================================================
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Inline Script collector for health checks.
|
|
214
|
+
* Executes TypeScript/JavaScript code directly and checks the result.
|
|
215
|
+
*
|
|
216
|
+
* Scripts should return an object with:
|
|
217
|
+
* - success: boolean - Whether the check passed
|
|
218
|
+
* - message?: string - Optional status message
|
|
219
|
+
* - value?: number - Optional numeric value for metrics
|
|
220
|
+
*
|
|
221
|
+
* Scripts have access to:
|
|
222
|
+
* - context.config - The collector configuration
|
|
223
|
+
* - console.log/warn/error - Logging functions
|
|
224
|
+
* - fetch - HTTP client for making requests
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```typescript
|
|
228
|
+
* // Simple check
|
|
229
|
+
* return { success: true, message: "All good!" };
|
|
230
|
+
*
|
|
231
|
+
* // HTTP health check
|
|
232
|
+
* const response = await fetch("https://api.example.com/health");
|
|
233
|
+
* return {
|
|
234
|
+
* success: response.ok,
|
|
235
|
+
* message: `Status: ${response.status}`,
|
|
236
|
+
* value: response.status
|
|
237
|
+
* };
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
export class InlineScriptCollector implements CollectorStrategy<
|
|
241
|
+
ScriptTransportClient,
|
|
242
|
+
InlineScriptConfig,
|
|
243
|
+
InlineScriptResult,
|
|
244
|
+
InlineScriptAggregatedResult
|
|
245
|
+
> {
|
|
246
|
+
id = "inline-script";
|
|
247
|
+
displayName = "Inline Script";
|
|
248
|
+
description = "Execute TypeScript/JavaScript code for health checking";
|
|
249
|
+
|
|
250
|
+
supportedPlugins = [pluginMetadata];
|
|
251
|
+
|
|
252
|
+
allowMultiple = true;
|
|
253
|
+
|
|
254
|
+
config = new Versioned({ version: 1, schema: inlineScriptConfigSchema });
|
|
255
|
+
result = new Versioned({ version: 1, schema: inlineScriptResultSchema });
|
|
256
|
+
aggregatedResult = new Versioned({
|
|
257
|
+
version: 1,
|
|
258
|
+
schema: inlineScriptAggregatedSchema,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
async execute({
|
|
262
|
+
config,
|
|
263
|
+
}: {
|
|
264
|
+
config: InlineScriptConfig;
|
|
265
|
+
client: ScriptTransportClient;
|
|
266
|
+
pluginId: string;
|
|
267
|
+
}): Promise<CollectorResult<InlineScriptResult>> {
|
|
268
|
+
const startTime = Date.now();
|
|
269
|
+
|
|
270
|
+
// Build context for the script
|
|
271
|
+
const scriptContext: ScriptContext = {
|
|
272
|
+
config: config as unknown as Record<string, unknown>,
|
|
273
|
+
fetch,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// Create a safe console that captures logs
|
|
277
|
+
const logs: string[] = [];
|
|
278
|
+
const safeConsole: SafeConsole = {
|
|
279
|
+
log: (...args) => {
|
|
280
|
+
logs.push(
|
|
281
|
+
args
|
|
282
|
+
.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a)))
|
|
283
|
+
.join(" "),
|
|
284
|
+
);
|
|
285
|
+
},
|
|
286
|
+
warn: (...args) => {
|
|
287
|
+
logs.push(
|
|
288
|
+
`[WARN] ${args.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))).join(" ")}`,
|
|
289
|
+
);
|
|
290
|
+
},
|
|
291
|
+
error: (...args) => {
|
|
292
|
+
logs.push(
|
|
293
|
+
`[ERROR] ${args.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))).join(" ")}`,
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
info: (...args) => {
|
|
297
|
+
logs.push(
|
|
298
|
+
`[INFO] ${args.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))).join(" ")}`,
|
|
299
|
+
);
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Execute the script
|
|
304
|
+
const { result, error, timedOut } = await executeInlineScript({
|
|
305
|
+
script: config.script,
|
|
306
|
+
context: scriptContext,
|
|
307
|
+
safeConsole,
|
|
308
|
+
timeoutMs: config.timeout,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const executionTimeMs = Date.now() - startTime;
|
|
312
|
+
|
|
313
|
+
if (error) {
|
|
314
|
+
return {
|
|
315
|
+
result: {
|
|
316
|
+
success: false,
|
|
317
|
+
message: error,
|
|
318
|
+
executionTimeMs,
|
|
319
|
+
timedOut,
|
|
320
|
+
},
|
|
321
|
+
error,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
result: {
|
|
327
|
+
success: result?.success ?? true,
|
|
328
|
+
message:
|
|
329
|
+
result?.message ?? (logs.length > 0 ? logs.join("\n") : undefined),
|
|
330
|
+
value: result?.value,
|
|
331
|
+
executionTimeMs,
|
|
332
|
+
timedOut: false,
|
|
333
|
+
},
|
|
334
|
+
error:
|
|
335
|
+
result?.success === false
|
|
336
|
+
? (result.message ?? "Check failed")
|
|
337
|
+
: undefined,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
mergeResult(
|
|
342
|
+
existing: InlineScriptAggregatedResult | undefined,
|
|
343
|
+
run: HealthCheckRunForAggregation<InlineScriptResult>,
|
|
344
|
+
): InlineScriptAggregatedResult {
|
|
345
|
+
const metadata = run.metadata;
|
|
346
|
+
|
|
347
|
+
const executionTimeState = mergeAverage(
|
|
348
|
+
existing?._executionTime as AverageState | undefined,
|
|
349
|
+
metadata?.executionTimeMs,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const successState = mergeRate(
|
|
353
|
+
existing?._success as RateState | undefined,
|
|
354
|
+
metadata?.success,
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
avgExecutionTimeMs: executionTimeState.avg,
|
|
359
|
+
successRate: successState.rate,
|
|
360
|
+
_executionTime: executionTimeState,
|
|
361
|
+
_success: successState,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
package/src/strategy.test.ts
CHANGED
|
@@ -138,7 +138,7 @@ describe("ScriptHealthCheckStrategy", () => {
|
|
|
138
138
|
});
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
-
describe("
|
|
141
|
+
describe("mergeResult", () => {
|
|
142
142
|
it("should calculate averages correctly", () => {
|
|
143
143
|
const strategy = new ScriptHealthCheckStrategy();
|
|
144
144
|
const runs = [
|
|
@@ -172,7 +172,8 @@ describe("ScriptHealthCheckStrategy", () => {
|
|
|
172
172
|
},
|
|
173
173
|
];
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
176
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
176
177
|
|
|
177
178
|
expect(aggregated.avgExecutionTime).toBe(75);
|
|
178
179
|
expect(aggregated.successRate).toBe(100);
|
|
@@ -213,7 +214,8 @@ describe("ScriptHealthCheckStrategy", () => {
|
|
|
213
214
|
},
|
|
214
215
|
];
|
|
215
216
|
|
|
216
|
-
|
|
217
|
+
let aggregated = strategy.mergeResult(undefined, runs[0]);
|
|
218
|
+
aggregated = strategy.mergeResult(aggregated, runs[1]);
|
|
217
219
|
|
|
218
220
|
expect(aggregated.errorCount).toBe(1);
|
|
219
221
|
expect(aggregated.timeoutCount).toBe(1);
|
package/src/strategy.ts
CHANGED
|
@@ -5,6 +5,15 @@ import {
|
|
|
5
5
|
Versioned,
|
|
6
6
|
z,
|
|
7
7
|
type ConnectedClient,
|
|
8
|
+
mergeAverage,
|
|
9
|
+
averageStateSchema,
|
|
10
|
+
mergeRate,
|
|
11
|
+
rateStateSchema,
|
|
12
|
+
mergeCounter,
|
|
13
|
+
counterStateSchema,
|
|
14
|
+
type AverageState,
|
|
15
|
+
type RateState,
|
|
16
|
+
type CounterState,
|
|
8
17
|
} from "@checkstack/backend-api";
|
|
9
18
|
import {
|
|
10
19
|
healthResultBoolean,
|
|
@@ -82,7 +91,7 @@ type ScriptResult = z.infer<typeof scriptResultSchema>;
|
|
|
82
91
|
/**
|
|
83
92
|
* Aggregated metadata for buckets.
|
|
84
93
|
*/
|
|
85
|
-
const
|
|
94
|
+
const scriptAggregatedDisplaySchema = healthResultSchema({
|
|
86
95
|
avgExecutionTime: healthResultNumber({
|
|
87
96
|
"x-chart-type": "line",
|
|
88
97
|
"x-chart-label": "Avg Execution Time",
|
|
@@ -103,6 +112,19 @@ const scriptAggregatedSchema = healthResultSchema({
|
|
|
103
112
|
}),
|
|
104
113
|
});
|
|
105
114
|
|
|
115
|
+
const scriptAggregatedInternalSchema = z.object({
|
|
116
|
+
_executionTime: averageStateSchema
|
|
117
|
+
.optional(),
|
|
118
|
+
_success: rateStateSchema
|
|
119
|
+
.optional(),
|
|
120
|
+
_errors: counterStateSchema.optional(),
|
|
121
|
+
_timeouts: counterStateSchema.optional(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const scriptAggregatedSchema = scriptAggregatedDisplaySchema.merge(
|
|
125
|
+
scriptAggregatedInternalSchema,
|
|
126
|
+
);
|
|
127
|
+
|
|
106
128
|
type ScriptAggregatedResult = z.infer<typeof scriptAggregatedSchema>;
|
|
107
129
|
|
|
108
130
|
// ============================================================================
|
|
@@ -182,15 +204,12 @@ const defaultScriptExecutor: ScriptExecutor = {
|
|
|
182
204
|
// STRATEGY
|
|
183
205
|
// ============================================================================
|
|
184
206
|
|
|
185
|
-
export class ScriptHealthCheckStrategy
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
ScriptAggregatedResult
|
|
192
|
-
>
|
|
193
|
-
{
|
|
207
|
+
export class ScriptHealthCheckStrategy implements HealthCheckStrategy<
|
|
208
|
+
ScriptConfig,
|
|
209
|
+
ScriptTransportClient,
|
|
210
|
+
ScriptResult,
|
|
211
|
+
ScriptAggregatedResult
|
|
212
|
+
> {
|
|
194
213
|
id = "script";
|
|
195
214
|
displayName = "Script Health Check";
|
|
196
215
|
description = "Execute local scripts or commands for health checking";
|
|
@@ -234,54 +253,46 @@ export class ScriptHealthCheckStrategy
|
|
|
234
253
|
schema: scriptAggregatedSchema,
|
|
235
254
|
});
|
|
236
255
|
|
|
237
|
-
|
|
238
|
-
|
|
256
|
+
mergeResult(
|
|
257
|
+
existing: ScriptAggregatedResult | undefined,
|
|
258
|
+
run: HealthCheckRunForAggregation<ScriptResult>,
|
|
239
259
|
): ScriptAggregatedResult {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
if (validRuns.length === 0) {
|
|
243
|
-
return {
|
|
244
|
-
avgExecutionTime: 0,
|
|
245
|
-
successRate: 0,
|
|
246
|
-
errorCount: 0,
|
|
247
|
-
timeoutCount: 0,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const executionTimes = validRuns
|
|
252
|
-
.map((r) => r.metadata?.executionTimeMs)
|
|
253
|
-
.filter((t): t is number => typeof t === "number");
|
|
260
|
+
const metadata = run.metadata;
|
|
254
261
|
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
)
|
|
260
|
-
: 0;
|
|
262
|
+
const executionTimeState = mergeAverage(
|
|
263
|
+
existing?._executionTime as AverageState | undefined,
|
|
264
|
+
metadata?.executionTimeMs,
|
|
265
|
+
);
|
|
261
266
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
267
|
+
const successState = mergeRate(
|
|
268
|
+
existing?._success as RateState | undefined,
|
|
269
|
+
metadata?.success,
|
|
270
|
+
);
|
|
266
271
|
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
272
|
+
const errorState = mergeCounter(
|
|
273
|
+
existing?._errors as CounterState | undefined,
|
|
274
|
+
metadata?.error !== undefined,
|
|
275
|
+
);
|
|
270
276
|
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
277
|
+
const timeoutState = mergeCounter(
|
|
278
|
+
existing?._timeouts as CounterState | undefined,
|
|
279
|
+
metadata?.timedOut === true,
|
|
280
|
+
);
|
|
274
281
|
|
|
275
282
|
return {
|
|
276
|
-
avgExecutionTime,
|
|
277
|
-
successRate,
|
|
278
|
-
errorCount,
|
|
279
|
-
timeoutCount,
|
|
283
|
+
avgExecutionTime: executionTimeState.avg,
|
|
284
|
+
successRate: successState.rate,
|
|
285
|
+
errorCount: errorState.count,
|
|
286
|
+
timeoutCount: timeoutState.count,
|
|
287
|
+
_executionTime: executionTimeState,
|
|
288
|
+
_success: successState,
|
|
289
|
+
_errors: errorState,
|
|
290
|
+
_timeouts: timeoutState,
|
|
280
291
|
};
|
|
281
292
|
}
|
|
282
293
|
|
|
283
294
|
async createClient(
|
|
284
|
-
_config: ScriptConfigInput
|
|
295
|
+
_config: ScriptConfigInput,
|
|
285
296
|
): Promise<ConnectedClient<ScriptTransportClient>> {
|
|
286
297
|
const client: ScriptTransportClient = {
|
|
287
298
|
exec: async (request: ScriptRequest): Promise<ScriptResultType> => {
|