@miragon/pulse-analytics-connector 0.0.0-bootstrap.0 → 0.21.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/LICENSE +21 -0
- package/README.md +86 -3
- package/dist/bpmn-name-lookup.d.ts +72 -0
- package/dist/bpmn-name-lookup.d.ts.map +1 -0
- package/dist/bpmn-name-lookup.js +123 -0
- package/dist/bpmn-name-lookup.js.map +1 -0
- package/dist/bpmn-name-lookup.test.d.ts +2 -0
- package/dist/bpmn-name-lookup.test.d.ts.map +1 -0
- package/dist/bpmn-name-lookup.test.js +287 -0
- package/dist/bpmn-name-lookup.test.js.map +1 -0
- package/dist/bpmn-names.d.ts +38 -0
- package/dist/bpmn-names.d.ts.map +1 -0
- package/dist/bpmn-names.js +22 -0
- package/dist/bpmn-names.js.map +1 -0
- package/dist/brand-gates.test.d.ts +2 -0
- package/dist/brand-gates.test.d.ts.map +1 -0
- package/dist/brand-gates.test.js +58 -0
- package/dist/brand-gates.test.js.map +1 -0
- package/dist/builder-texts.d.ts +4 -0
- package/dist/builder-texts.d.ts.map +1 -0
- package/dist/builder-texts.js +14 -0
- package/dist/builder-texts.js.map +1 -0
- package/dist/builder-texts.test.d.ts +2 -0
- package/dist/builder-texts.test.d.ts.map +1 -0
- package/dist/builder-texts.test.js +32 -0
- package/dist/builder-texts.test.js.map +1 -0
- package/dist/definition.d.ts +3 -0
- package/dist/definition.d.ts.map +1 -0
- package/dist/definition.js +151 -0
- package/dist/definition.js.map +1 -0
- package/dist/engine-ids.d.ts +42 -0
- package/dist/engine-ids.d.ts.map +1 -0
- package/dist/engine-ids.js +50 -0
- package/dist/engine-ids.js.map +1 -0
- package/dist/engine-scope.test.d.ts +2 -0
- package/dist/engine-scope.test.d.ts.map +1 -0
- package/dist/engine-scope.test.js +260 -0
- package/dist/engine-scope.test.js.map +1 -0
- package/dist/heatmap-data.d.ts +15 -0
- package/dist/heatmap-data.d.ts.map +1 -0
- package/dist/heatmap-data.js +2 -0
- package/dist/heatmap-data.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +8 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +33 -0
- package/dist/instructions.js.map +1 -0
- package/dist/messages/de.ask-ai.d.ts +9 -0
- package/dist/messages/de.ask-ai.d.ts.map +1 -0
- package/dist/messages/de.ask-ai.js +18 -0
- package/dist/messages/de.ask-ai.js.map +1 -0
- package/dist/messages/de.builder.d.ts +12 -0
- package/dist/messages/de.builder.d.ts.map +1 -0
- package/dist/messages/de.builder.js +53 -0
- package/dist/messages/de.builder.js.map +1 -0
- package/dist/messages/de.server.d.ts +8 -0
- package/dist/messages/de.server.d.ts.map +1 -0
- package/dist/messages/de.server.js +26 -0
- package/dist/messages/de.server.js.map +1 -0
- package/dist/messages/de.sweep.d.ts +10 -0
- package/dist/messages/de.sweep.d.ts.map +1 -0
- package/dist/messages/de.sweep.js +203 -0
- package/dist/messages/de.sweep.js.map +1 -0
- package/dist/messages/en.ask-ai.d.ts +25 -0
- package/dist/messages/en.ask-ai.d.ts.map +1 -0
- package/dist/messages/en.ask-ai.js +24 -0
- package/dist/messages/en.ask-ai.js.map +1 -0
- package/dist/messages/en.builder.d.ts +12 -0
- package/dist/messages/en.builder.d.ts.map +1 -0
- package/dist/messages/en.builder.js +53 -0
- package/dist/messages/en.builder.js.map +1 -0
- package/dist/messages/en.server.d.ts +8 -0
- package/dist/messages/en.server.d.ts.map +1 -0
- package/dist/messages/en.server.js +26 -0
- package/dist/messages/en.server.js.map +1 -0
- package/dist/messages/en.sweep.d.ts +11 -0
- package/dist/messages/en.sweep.d.ts.map +1 -0
- package/dist/messages/en.sweep.js +204 -0
- package/dist/messages/en.sweep.js.map +1 -0
- package/dist/messages/glossary.d.ts +26 -0
- package/dist/messages/glossary.d.ts.map +1 -0
- package/dist/messages/glossary.js +37 -0
- package/dist/messages/glossary.js.map +1 -0
- package/dist/messages/glossary.test.d.ts +2 -0
- package/dist/messages/glossary.test.d.ts.map +1 -0
- package/dist/messages/glossary.test.js +109 -0
- package/dist/messages/glossary.test.js.map +1 -0
- package/dist/messages/index.d.ts +12 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +23 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/messages/plural.d.ts +10 -0
- package/dist/messages/plural.d.ts.map +1 -0
- package/dist/messages/plural.js +17 -0
- package/dist/messages/plural.js.map +1 -0
- package/dist/messages/use-t.d.ts +10 -0
- package/dist/messages/use-t.d.ts.map +1 -0
- package/dist/messages/use-t.js +10 -0
- package/dist/messages/use-t.js.map +1 -0
- package/dist/module.d.ts +48 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +174 -0
- package/dist/module.js.map +1 -0
- package/dist/module.test.d.ts +2 -0
- package/dist/module.test.d.ts.map +1 -0
- package/dist/module.test.js +230 -0
- package/dist/module.test.js.map +1 -0
- package/dist/named-views.d.ts +27 -0
- package/dist/named-views.d.ts.map +1 -0
- package/dist/named-views.js +73 -0
- package/dist/named-views.js.map +1 -0
- package/dist/named-views.test.d.ts +2 -0
- package/dist/named-views.test.d.ts.map +1 -0
- package/dist/named-views.test.js +110 -0
- package/dist/named-views.test.js.map +1 -0
- package/dist/plugin.d.ts +44 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +52 -0
- package/dist/plugin.js.map +1 -0
- package/dist/prometheus-wiring.test.d.ts +2 -0
- package/dist/prometheus-wiring.test.d.ts.map +1 -0
- package/dist/prometheus-wiring.test.js +142 -0
- package/dist/prometheus-wiring.test.js.map +1 -0
- package/dist/server-locale.d.ts +48 -0
- package/dist/server-locale.d.ts.map +1 -0
- package/dist/server-locale.js +33 -0
- package/dist/server-locale.js.map +1 -0
- package/dist/settings-tools.d.ts +23 -0
- package/dist/settings-tools.d.ts.map +1 -0
- package/dist/settings-tools.js +100 -0
- package/dist/settings-tools.js.map +1 -0
- package/dist/settings.d.ts +77 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +99 -0
- package/dist/settings.js.map +1 -0
- package/dist/settings.test.d.ts +2 -0
- package/dist/settings.test.d.ts.map +1 -0
- package/dist/settings.test.js +299 -0
- package/dist/settings.test.js.map +1 -0
- package/dist/steps/app-config.d.ts +30 -0
- package/dist/steps/app-config.d.ts.map +1 -0
- package/dist/steps/app-config.js +46 -0
- package/dist/steps/app-config.js.map +1 -0
- package/dist/steps/dashboard.d.ts +12 -0
- package/dist/steps/dashboard.d.ts.map +1 -0
- package/dist/steps/dashboard.js +58 -0
- package/dist/steps/dashboard.js.map +1 -0
- package/dist/steps/failure-dashboard.d.ts +10 -0
- package/dist/steps/failure-dashboard.d.ts.map +1 -0
- package/dist/steps/failure-dashboard.js +32 -0
- package/dist/steps/failure-dashboard.js.map +1 -0
- package/dist/steps/index.d.ts +3 -0
- package/dist/steps/index.d.ts.map +1 -0
- package/dist/steps/index.js +3 -0
- package/dist/steps/index.js.map +1 -0
- package/dist/steps/steps.test.d.ts +2 -0
- package/dist/steps/steps.test.d.ts.map +1 -0
- package/dist/steps/steps.test.js +153 -0
- package/dist/steps/steps.test.js.map +1 -0
- package/dist/tool-names.d.ts +13 -0
- package/dist/tool-names.d.ts.map +1 -0
- package/dist/tool-names.js +27 -0
- package/dist/tool-names.js.map +1 -0
- package/dist/tools/cluster-compare.d.ts +8 -0
- package/dist/tools/cluster-compare.d.ts.map +1 -0
- package/dist/tools/cluster-compare.js +17 -0
- package/dist/tools/cluster-compare.js.map +1 -0
- package/dist/tools/element.d.ts +8 -0
- package/dist/tools/element.d.ts.map +1 -0
- package/dist/tools/element.js +25 -0
- package/dist/tools/element.js.map +1 -0
- package/dist/tools/engine-compare.d.ts +8 -0
- package/dist/tools/engine-compare.d.ts.map +1 -0
- package/dist/tools/engine-compare.js +18 -0
- package/dist/tools/engine-compare.js.map +1 -0
- package/dist/tools/engine-compare.test.d.ts +2 -0
- package/dist/tools/engine-compare.test.d.ts.map +1 -0
- package/dist/tools/engine-compare.test.js +166 -0
- package/dist/tools/engine-compare.test.js.map +1 -0
- package/dist/tools/engine-landscape.d.ts +7 -0
- package/dist/tools/engine-landscape.d.ts.map +1 -0
- package/dist/tools/engine-landscape.js +13 -0
- package/dist/tools/engine-landscape.js.map +1 -0
- package/dist/tools/engine-landscape.test.d.ts +2 -0
- package/dist/tools/engine-landscape.test.d.ts.map +1 -0
- package/dist/tools/engine-landscape.test.js +110 -0
- package/dist/tools/engine-landscape.test.js.map +1 -0
- package/dist/tools/failures.d.ts +7 -0
- package/dist/tools/failures.d.ts.map +1 -0
- package/dist/tools/failures.js +13 -0
- package/dist/tools/failures.js.map +1 -0
- package/dist/tools/health.d.ts +7 -0
- package/dist/tools/health.d.ts.map +1 -0
- package/dist/tools/health.js +23 -0
- package/dist/tools/health.js.map +1 -0
- package/dist/tools/health.test.d.ts +2 -0
- package/dist/tools/health.test.d.ts.map +1 -0
- package/dist/tools/health.test.js +119 -0
- package/dist/tools/health.test.js.map +1 -0
- package/dist/tools/index.d.ts +18 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +35 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/performance.d.ts +8 -0
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js +25 -0
- package/dist/tools/performance.js.map +1 -0
- package/dist/tools/performance.test.d.ts +2 -0
- package/dist/tools/performance.test.d.ts.map +1 -0
- package/dist/tools/performance.test.js +176 -0
- package/dist/tools/performance.test.js.map +1 -0
- package/dist/tools/version-compare.d.ts +8 -0
- package/dist/tools/version-compare.d.ts.map +1 -0
- package/dist/tools/version-compare.js +17 -0
- package/dist/tools/version-compare.js.map +1 -0
- package/dist/tools/version-compare.test.d.ts +2 -0
- package/dist/tools/version-compare.test.d.ts.map +1 -0
- package/dist/tools/version-compare.test.js +82 -0
- package/dist/tools/version-compare.test.js.map +1 -0
- package/dist/toolsets.d.ts +17 -0
- package/dist/toolsets.d.ts.map +1 -0
- package/dist/toolsets.js +47 -0
- package/dist/toolsets.js.map +1 -0
- package/dist/toolsets.test.d.ts +2 -0
- package/dist/toolsets.test.d.ts.map +1 -0
- package/dist/toolsets.test.js +91 -0
- package/dist/toolsets.test.js.map +1 -0
- package/dist/view-title.test.d.ts +2 -0
- package/dist/view-title.test.d.ts.map +1 -0
- package/dist/view-title.test.js +91 -0
- package/dist/view-title.test.js.map +1 -0
- package/dist/widget-tools/comparisons.d.ts +13 -0
- package/dist/widget-tools/comparisons.d.ts.map +1 -0
- package/dist/widget-tools/comparisons.js +177 -0
- package/dist/widget-tools/comparisons.js.map +1 -0
- package/dist/widget-tools/comparisons.test.d.ts +2 -0
- package/dist/widget-tools/comparisons.test.d.ts.map +1 -0
- package/dist/widget-tools/comparisons.test.js +123 -0
- package/dist/widget-tools/comparisons.test.js.map +1 -0
- package/dist/widget-tools/shared.d.ts +36 -0
- package/dist/widget-tools/shared.d.ts.map +1 -0
- package/dist/widget-tools/shared.js +39 -0
- package/dist/widget-tools/shared.js.map +1 -0
- package/dist/widget-tools.d.ts +25 -0
- package/dist/widget-tools.d.ts.map +1 -0
- package/dist/widget-tools.js +269 -0
- package/dist/widget-tools.js.map +1 -0
- package/dist/widget-tools.test.d.ts +2 -0
- package/dist/widget-tools.test.d.ts.map +1 -0
- package/dist/widget-tools.test.js +272 -0
- package/dist/widget-tools.test.js.map +1 -0
- package/package.json +91 -5
- package/src/bpmn-name-lookup.test.ts +333 -0
- package/src/bpmn-name-lookup.ts +197 -0
- package/src/bpmn-names.ts +51 -0
- package/src/brand-gates.test.ts +69 -0
- package/src/builder-texts.test.ts +40 -0
- package/src/builder-texts.ts +15 -0
- package/src/definition.ts +181 -0
- package/src/engine-ids.ts +85 -0
- package/src/engine-scope.test.ts +324 -0
- package/src/heatmap-data.ts +14 -0
- package/src/index.ts +4 -0
- package/src/instructions.ts +33 -0
- package/src/messages/GLOSSARY.md +88 -0
- package/src/messages/de.ask-ai.ts +28 -0
- package/src/messages/de.builder.ts +77 -0
- package/src/messages/de.server.ts +39 -0
- package/src/messages/de.sweep.ts +244 -0
- package/src/messages/en.ask-ai.ts +34 -0
- package/src/messages/en.builder.ts +76 -0
- package/src/messages/en.server.ts +39 -0
- package/src/messages/en.sweep.ts +240 -0
- package/src/messages/glossary.test.ts +122 -0
- package/src/messages/glossary.ts +62 -0
- package/src/messages/index.ts +24 -0
- package/src/messages/plural.ts +18 -0
- package/src/messages/use-t.ts +12 -0
- package/src/module.test.ts +319 -0
- package/src/module.ts +211 -0
- package/src/named-views.test.ts +141 -0
- package/src/named-views.ts +97 -0
- package/src/plugin.ts +94 -0
- package/src/prometheus-wiring.test.ts +174 -0
- package/src/server-locale.ts +59 -0
- package/src/settings-tools.ts +170 -0
- package/src/settings.test.ts +402 -0
- package/src/settings.ts +114 -0
- package/src/steps/app-config.ts +75 -0
- package/src/steps/dashboard.ts +73 -0
- package/src/steps/failure-dashboard.ts +38 -0
- package/src/steps/index.ts +2 -0
- package/src/steps/steps.test.ts +216 -0
- package/src/tool-names.ts +26 -0
- package/src/tools/cluster-compare.ts +28 -0
- package/src/tools/element.ts +35 -0
- package/src/tools/engine-compare.test.ts +210 -0
- package/src/tools/engine-compare.ts +30 -0
- package/src/tools/engine-landscape.test.ts +138 -0
- package/src/tools/engine-landscape.ts +21 -0
- package/src/tools/failures.ts +19 -0
- package/src/tools/health.test.ts +156 -0
- package/src/tools/health.ts +29 -0
- package/src/tools/index.ts +44 -0
- package/src/tools/performance.test.ts +210 -0
- package/src/tools/performance.ts +38 -0
- package/src/tools/version-compare.test.ts +97 -0
- package/src/tools/version-compare.ts +28 -0
- package/src/toolsets.test.ts +107 -0
- package/src/toolsets.ts +56 -0
- package/src/view-title.test.ts +123 -0
- package/src/widget-tools/comparisons.test.ts +170 -0
- package/src/widget-tools/comparisons.ts +229 -0
- package/src/widget-tools/shared.ts +67 -0
- package/src/widget-tools.test.ts +306 -0
- package/src/widget-tools.ts +375 -0
- package/src/widgets/analytics-dashboard/activity-bottleneck-table.tsx +179 -0
- package/src/widgets/analytics-dashboard/definition-breakdown.tsx +108 -0
- package/src/widgets/analytics-dashboard/execution-performance-kpi.tsx +94 -0
- package/src/widgets/analytics-dashboard/execution-summary-kpi.tsx +122 -0
- package/src/widgets/analytics-dashboard/lib.ts +43 -0
- package/src/widgets/bpmn-heatmap.test.tsx +75 -0
- package/src/widgets/bpmn-heatmap.tsx +127 -0
- package/src/widgets/catalogue-sync.test.ts +16 -0
- package/src/widgets/cluster-compare.tsx +148 -0
- package/src/widgets/comparison-shared.test.ts +237 -0
- package/src/widgets/comparison-shared.tsx +371 -0
- package/src/widgets/comparison-widgets.test.tsx +307 -0
- package/src/widgets/dashboard-widgets.test.tsx +397 -0
- package/src/widgets/engine-compare.tsx +87 -0
- package/src/widgets/engine-landscape.tsx +352 -0
- package/src/widgets/failure-dashboard/error-patterns-table.tsx +122 -0
- package/src/widgets/failure-dashboard/failure-rate-table.test.ts +80 -0
- package/src/widgets/failure-dashboard/failure-rate-table.tsx +170 -0
- package/src/widgets/failure-dashboard/lib.ts +32 -0
- package/src/widgets/failure-dashboard/summary-kpi.tsx +122 -0
- package/src/widgets/failure-landscape-widgets.test.tsx +393 -0
- package/src/widgets/figure.tsx +79 -0
- package/src/widgets/hand-off.test.ts +126 -0
- package/src/widgets/hand-off.ts +102 -0
- package/src/widgets/index.ts +124 -0
- package/src/widgets/model-descriptions.dashboard.test.ts +280 -0
- package/src/widgets/model-descriptions.test.ts +251 -0
- package/src/widgets/model-descriptions.ts +428 -0
- package/src/widgets/names.test.ts +32 -0
- package/src/widgets/names.ts +39 -0
- package/src/widgets/query-gate.test.tsx +57 -0
- package/src/widgets/query-gate.tsx +50 -0
- package/src/widgets/settings-section.test.tsx +174 -0
- package/src/widgets/settings-section.tsx +244 -0
- package/src/widgets/version-compare.test.ts +68 -0
- package/src/widgets/version-compare.tsx +96 -0
- package/src/widgets/view-meta.ts +18 -0
- package/src/widgets/widget-render.test-support.tsx +103 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PipelineStepDefinition } from "@miragon/mcp-toolkit-core"
|
|
2
|
+
import { queries } from "@miragon/pulse-analytics-client"
|
|
3
|
+
import { ENGINE_KEY_DECLARATION, stepEngines, type AnalyticsAppConfig } from "./app-config.js"
|
|
4
|
+
import { builderText } from "../builder-texts.js"
|
|
5
|
+
import { NO_NAMES } from "../bpmn-name-lookup.js"
|
|
6
|
+
import { namedFailureDashboard } from "../named-views.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Loads current failure / incident state from Prometheus for the
|
|
10
|
+
* failure-dashboard widgets. Point-in-time (live state gauges), so there is no
|
|
11
|
+
* time-window input. Reads the optional `analytics:engine` key (configured
|
|
12
|
+
* engine id(s); omitted = all of them).
|
|
13
|
+
*/
|
|
14
|
+
export const loadFailureDashboardStep: PipelineStepDefinition<AnalyticsAppConfig> = {
|
|
15
|
+
id: "analytics:load-failure-dashboard",
|
|
16
|
+
...builderText("step.load-failure-dashboard"),
|
|
17
|
+
description:
|
|
18
|
+
"The incidents open right now, grouped by incident type and process definition, plus each affected process's running instances and dead jobs. Powers the failure widgets (failure-summary-kpi, failure-rate-table, error-patterns-table).",
|
|
19
|
+
dataType: "analytics:failureDashboard",
|
|
20
|
+
requires: [],
|
|
21
|
+
optionalKeys: [ENGINE_KEY_DECLARATION],
|
|
22
|
+
produces: ["analytics:failureDashboardData"],
|
|
23
|
+
execute: async (context, appConfig) => {
|
|
24
|
+
const data = await namedFailureDashboard(
|
|
25
|
+
await queries.failureDashboardData(appConfig.client, {
|
|
26
|
+
engine: stepEngines(context.keys, appConfig.engineScope),
|
|
27
|
+
}),
|
|
28
|
+
appConfig.namesFor ?? NO_NAMES,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
data,
|
|
33
|
+
keys: { "analytics:failureDashboardData": data },
|
|
34
|
+
_app: "analytics",
|
|
35
|
+
_step: "load-failure-dashboard",
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import type { MCPServer } from "mcp-use"
|
|
3
|
+
import type { PipelineContext } from "@miragon/mcp-toolkit-core"
|
|
4
|
+
import { runWithMcpRequestInfo } from "@miragon/pulse-widget-shell/server"
|
|
5
|
+
import type { PrometheusClient, PromSample } from "@miragon/pulse-analytics-client"
|
|
6
|
+
import { createEngineScope } from "../engine-ids.js"
|
|
7
|
+
import { analyticsModule } from "../module.js"
|
|
8
|
+
import type { ProfileSource } from "../server-locale.js"
|
|
9
|
+
import type { AnalyticsAppConfig } from "./app-config.js"
|
|
10
|
+
import { loadDashboardStep, loadFailureDashboardStep } from "./index.js"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The analytics pipeline steps run under render-view / the dashboard builder,
|
|
14
|
+
* whose step keys are free-form (`z.record(z.string(), z.unknown())`) — no
|
|
15
|
+
* schema checks them before the step does. So the steps hold the line the
|
|
16
|
+
* tools' strict inputs hold: a malformed key is refused naming the key, never
|
|
17
|
+
* read as "omitted" (which for `analytics:engine` would silently widen one
|
|
18
|
+
* engine's view to the whole fleet).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function recordingClient() {
|
|
22
|
+
const sent: string[] = []
|
|
23
|
+
const ch: PrometheusClient = {
|
|
24
|
+
instant: (q) => {
|
|
25
|
+
sent.push(q)
|
|
26
|
+
return Promise.resolve<PromSample[]>([])
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
return { ch, sent }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A profile store holding one signed-in user's saved analytics period. */
|
|
33
|
+
const profileStore: ProfileSource = {
|
|
34
|
+
get: (key) =>
|
|
35
|
+
Promise.resolve(
|
|
36
|
+
key === "user-7" ? { modules: { analytics: { defaultPeriod: "30d" } } } : undefined,
|
|
37
|
+
),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function setup(withProfileStore = false) {
|
|
41
|
+
const { ch, sent } = recordingClient()
|
|
42
|
+
const appConfig: AnalyticsAppConfig = {
|
|
43
|
+
client: ch,
|
|
44
|
+
engineScope: createEngineScope(["prod-a", "prod-b"]),
|
|
45
|
+
...(withProfileStore ? { profileStore } : {}),
|
|
46
|
+
}
|
|
47
|
+
return { appConfig, sent }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const context = (keys: Record<string, unknown>): PipelineContext => ({
|
|
51
|
+
steps: {},
|
|
52
|
+
keys,
|
|
53
|
+
errors: [],
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const STEPS = [
|
|
57
|
+
["analytics:load-dashboard", loadDashboardStep],
|
|
58
|
+
["analytics:load-failure-dashboard", loadFailureDashboardStep],
|
|
59
|
+
] as const
|
|
60
|
+
|
|
61
|
+
/** Every engine id the `engine_id` matchers of `promql` admit. */
|
|
62
|
+
const enginesRead = (sent: string[]) =>
|
|
63
|
+
new Set(
|
|
64
|
+
sent.flatMap((q) =>
|
|
65
|
+
[...q.matchAll(/engine_id(=~?)"([^"]*)"/g)].flatMap((m) =>
|
|
66
|
+
m[1] === "=~" ? m[2].split("|") : [m[2]],
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
describe.each(STEPS)("%s — analytics:engine", (_id, step) => {
|
|
72
|
+
it("reads the whole configured fleet when the key is omitted", async () => {
|
|
73
|
+
const { appConfig, sent } = setup()
|
|
74
|
+
const out = await step.execute(context({}), appConfig)
|
|
75
|
+
|
|
76
|
+
expect((out.data as { engines: string[] }).engines).toEqual(["prod-a", "prod-b"])
|
|
77
|
+
expect([...enginesRead(sent)].sort()).toEqual(["prod-a", "prod-b"])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("reads exactly the configured engine(s) the key names", async () => {
|
|
81
|
+
const { appConfig, sent } = setup()
|
|
82
|
+
const out = await step.execute(context({ "analytics:engine": "prod-b" }), appConfig)
|
|
83
|
+
|
|
84
|
+
expect((out.data as { engines: string[] }).engines).toEqual(["prod-b"])
|
|
85
|
+
expect([...enginesRead(sent)]).toEqual(["prod-b"])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("refuses an engine this server is not configured for, before any query", async () => {
|
|
89
|
+
const { appConfig, sent } = setup()
|
|
90
|
+
await expect(
|
|
91
|
+
step.execute(context({ "analytics:engine": "tenant-x" }), appConfig),
|
|
92
|
+
).rejects.toThrow('Unknown engine "tenant-x"')
|
|
93
|
+
expect(sent).toEqual([])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it.each([
|
|
97
|
+
["a number", 7],
|
|
98
|
+
["a list with a non-string id", ["prod-a", 7]],
|
|
99
|
+
["null", null],
|
|
100
|
+
["an object", { id: "prod-a" }],
|
|
101
|
+
])("refuses %s instead of widening to the fleet", async (_label, raw) => {
|
|
102
|
+
const { appConfig, sent } = setup()
|
|
103
|
+
await expect(step.execute(context({ "analytics:engine": raw }), appConfig)).rejects.toThrow(
|
|
104
|
+
/analytics:engine must be an engine id or a list of engine ids/,
|
|
105
|
+
)
|
|
106
|
+
expect(sent).toEqual([])
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe("the plugin wires the steps' caller lookup", () => {
|
|
111
|
+
it("hands the steps the profile store and installs the ambient request info they read", () => {
|
|
112
|
+
const patterns: string[] = []
|
|
113
|
+
const server = {
|
|
114
|
+
tool: () => {},
|
|
115
|
+
use: (pattern: string) => {
|
|
116
|
+
patterns.push(pattern)
|
|
117
|
+
},
|
|
118
|
+
} as unknown as MCPServer
|
|
119
|
+
const plugin = analyticsModule.createPlugin(
|
|
120
|
+
{ url: "http://prometheus.invalid" },
|
|
121
|
+
{ profileStore, engineIds: ["prod-a"] },
|
|
122
|
+
)
|
|
123
|
+
plugin.registerTools?.(server)
|
|
124
|
+
|
|
125
|
+
expect((plugin.appConfig as unknown as AnalyticsAppConfig).profileStore).toBe(profileStore)
|
|
126
|
+
// A step gets no handler ctx: without this middleware its caller lookup
|
|
127
|
+
// finds no identity and every saved period silently reads as the default.
|
|
128
|
+
expect(patterns).toEqual(["mcp:*"])
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("hands the steps the name lookup over the injected BPMN lookup", async () => {
|
|
132
|
+
const xml = '<definitions><process id="order" name="Order fulfillment"/></definitions>'
|
|
133
|
+
const plugin = analyticsModule.createPlugin(
|
|
134
|
+
{ url: "http://prometheus.invalid" },
|
|
135
|
+
{ profileStore, engineIds: ["prod-a"], fetchBpmnXml: () => Promise.resolve(xml) },
|
|
136
|
+
)
|
|
137
|
+
const { namesFor } = plugin.appConfig as unknown as AnalyticsAppConfig
|
|
138
|
+
expect(await namesFor!(["order"])).toEqual({
|
|
139
|
+
order: { name: "Order fulfillment", elements: {} },
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe.each(STEPS)("%s — the names behind the ids", (_id, step) => {
|
|
145
|
+
it("passes its figures through the view's name lookup", async () => {
|
|
146
|
+
const names = { order: { name: "Order fulfillment", elements: {} } }
|
|
147
|
+
const { appConfig } = setup()
|
|
148
|
+
const out = await step.execute(context({}), {
|
|
149
|
+
...appConfig,
|
|
150
|
+
namesFor: () => Promise.resolve(names),
|
|
151
|
+
})
|
|
152
|
+
expect((out.data as { names: unknown }).names).toEqual(names)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it("shows ids when the server has no lookup", async () => {
|
|
156
|
+
const out = await step.execute(context({}), setup().appConfig)
|
|
157
|
+
expect((out.data as { names: unknown }).names).toEqual({})
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe("analytics:load-dashboard — analytics:period", () => {
|
|
162
|
+
// The windows read — not the hour the born-series check looks at (`[1h] offset …`).
|
|
163
|
+
const windows = (sent: string[]) =>
|
|
164
|
+
new Set(
|
|
165
|
+
sent.flatMap((q) => [...q.matchAll(/\[(\w+)\](?!(?: @ \d+)? offset)/g)].map((m) => m[1])),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
it("takes the caller's SAVED period when the key is omitted (N83/N116)", async () => {
|
|
169
|
+
const { appConfig, sent } = setup(true)
|
|
170
|
+
const out = await runWithMcpRequestInfo({ authUserId: "user-7" }, () =>
|
|
171
|
+
loadDashboardStep.execute(context({}), appConfig),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
expect((out.data as { period: string }).period).toBe("30d")
|
|
175
|
+
expect([...windows(sent)]).toEqual(["30d"])
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it("falls back to the module default without a caller or a saved period", async () => {
|
|
179
|
+
const { appConfig } = setup(true)
|
|
180
|
+
const anonymous = await runWithMcpRequestInfo({}, () =>
|
|
181
|
+
loadDashboardStep.execute(context({}), appConfig),
|
|
182
|
+
)
|
|
183
|
+
const storeless = await loadDashboardStep.execute(context({}), setup().appConfig)
|
|
184
|
+
|
|
185
|
+
expect((anonymous.data as { period: string }).period).toBe("7d")
|
|
186
|
+
expect((storeless.data as { period: string }).period).toBe("7d")
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it("lets an explicit period win over the saved one", async () => {
|
|
190
|
+
const { appConfig } = setup(true)
|
|
191
|
+
const out = await runWithMcpRequestInfo({ authUserId: "user-7" }, () =>
|
|
192
|
+
loadDashboardStep.execute(context({ "analytics:period": "1d" }), appConfig),
|
|
193
|
+
)
|
|
194
|
+
expect((out.data as { period: string }).period).toBe("1d")
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it.each([
|
|
198
|
+
["an unknown period", "90d"],
|
|
199
|
+
["a number", 7],
|
|
200
|
+
["null", null],
|
|
201
|
+
])("refuses %s instead of substituting a default", async (_label, raw) => {
|
|
202
|
+
const { appConfig, sent } = setup()
|
|
203
|
+
await expect(
|
|
204
|
+
loadDashboardStep.execute(context({ "analytics:period": raw }), appConfig),
|
|
205
|
+
).rejects.toThrow(/analytics:period must be one of 1d, 3d, 7d, 14d, 30d/)
|
|
206
|
+
expect(sent).toEqual([])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it("refuses a non-string process key, naming the key", async () => {
|
|
210
|
+
const { appConfig, sent } = setup()
|
|
211
|
+
await expect(
|
|
212
|
+
loadDashboardStep.execute(context({ "analytics:processDefinitionKey": 7 }), appConfig),
|
|
213
|
+
).rejects.toThrow(/analytics:processDefinitionKey must be a process definition key/)
|
|
214
|
+
expect(sent).toEqual([])
|
|
215
|
+
})
|
|
216
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names of the analytics widget tools referenced from widget code, so a tool
|
|
3
|
+
* rename in `widget-tools.ts` trips a TS error at every call site (same rule
|
|
4
|
+
* as the camunda7 module's tool-names.ts).
|
|
5
|
+
*/
|
|
6
|
+
// Per-view data-only feeds (no UI, app-only via `visibility: "app"`) backing
|
|
7
|
+
// each widget's own self-fetch. A widget tool (view-bound) can't be used for
|
|
8
|
+
// this: the host renders it instead of returning data to the in-widget
|
|
9
|
+
// callTool().
|
|
10
|
+
export const ANALYTICS_DASHBOARD_DATA = "analytics_dashboard_data"
|
|
11
|
+
export const ANALYTICS_FAILURE_DASHBOARD_DATA = "analytics_failure_dashboard_data"
|
|
12
|
+
// Cross-module consumers (camunda7's process-incidents flow) reference this
|
|
13
|
+
// feed by raw string with graceful degradation — keep the name stable.
|
|
14
|
+
export const ANALYTICS_BPMN_HEATMAP_DATA = "analytics_bpmn_heatmap_data"
|
|
15
|
+
// The latest deployed diagram of a process key, for the heatmap view: never
|
|
16
|
+
// inside the heat payload, so the model never reads the XML and the heat
|
|
17
|
+
// values refresh without it.
|
|
18
|
+
export const ANALYTICS_BPMN_XML_DATA = "analytics_bpmn_xml_data"
|
|
19
|
+
// Backs the cross-engine landscape widget's self-fetch. Also referenced by
|
|
20
|
+
// camunda7's cross-engine cockpit mode through the widget id (tier-2), so the
|
|
21
|
+
// section disappears with the module instead of erroring.
|
|
22
|
+
export const ANALYTICS_ENGINE_LANDSCAPE_DATA = "analytics_engine_landscape_data"
|
|
23
|
+
// Settings section: the widget self-fetches the feed and writes via the save
|
|
24
|
+
// tool; both live in settings-tools.ts.
|
|
25
|
+
export const ANALYTICS_SETTINGS_DATA = "analytics_settings_data"
|
|
26
|
+
export const ANALYTICS_SAVE_SETTINGS = "analytics_save_settings"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PrometheusClient } from "@miragon/pulse-analytics-client"
|
|
2
|
+
import { schemas, queries } from "@miragon/pulse-analytics-client"
|
|
3
|
+
import type { createToolRegistrar } from "@miragon/mcp-toolkit-core/tools"
|
|
4
|
+
import type { ProfileSource } from "../server-locale.js"
|
|
5
|
+
import { optionalMinBucketSize, settingsFor } from "../settings.js"
|
|
6
|
+
import { withEngineScope, type AnalyticsEngineScope } from "../engine-ids.js"
|
|
7
|
+
|
|
8
|
+
type Register = ReturnType<typeof createToolRegistrar<PrometheusClient>>
|
|
9
|
+
|
|
10
|
+
export function registerClusterCompareTools(
|
|
11
|
+
register: Register,
|
|
12
|
+
engineScope: AnalyticsEngineScope,
|
|
13
|
+
profileStore?: ProfileSource,
|
|
14
|
+
) {
|
|
15
|
+
register({
|
|
16
|
+
name: "analytics_cluster_compare",
|
|
17
|
+
category: "analytics",
|
|
18
|
+
description:
|
|
19
|
+
"Pre/Post deployment correlation from metrics. Given a deployment timestamp and windows before/after, compute instance KPIs per window and the delta, using PromQL historical windows. Windows are clamped to now and the 30-day retention and reported as measured (`windowDays`, `partial`) — right after a deploy the post window holds only the elapsed time; starts compare per day. Results are flagged `suppressed` if either window has fewer than minBucketSize started or completed instances. Typical flow: commit-hash → camunda7_get_deployment → deployment.timestamp → this tool.",
|
|
20
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
21
|
+
inputSchema: { ...schemas.clusterCompareInput.shape, minBucketSize: optionalMinBucketSize },
|
|
22
|
+
handler: async (ch, args, ctx) =>
|
|
23
|
+
queries.clusterCompare(ch, {
|
|
24
|
+
...withEngineScope(engineScope, args),
|
|
25
|
+
minBucketSize: args.minBucketSize ?? (await settingsFor(profileStore, ctx)).minBucketSize,
|
|
26
|
+
}),
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PrometheusClient } from "@miragon/pulse-analytics-client"
|
|
2
|
+
import { schemas, queries } from "@miragon/pulse-analytics-client"
|
|
3
|
+
import type { createToolRegistrar } from "@miragon/mcp-toolkit-core/tools"
|
|
4
|
+
import type { ProfileSource } from "../server-locale.js"
|
|
5
|
+
import { optionalMinBucketSize, optionalPeriod, settingsFor } from "../settings.js"
|
|
6
|
+
import { withEngineScope, type AnalyticsEngineScope } from "../engine-ids.js"
|
|
7
|
+
|
|
8
|
+
type Register = ReturnType<typeof createToolRegistrar<PrometheusClient>>
|
|
9
|
+
|
|
10
|
+
export function registerElementTools(
|
|
11
|
+
register: Register,
|
|
12
|
+
engineScope: AnalyticsEngineScope,
|
|
13
|
+
profileStore?: ProfileSource,
|
|
14
|
+
) {
|
|
15
|
+
register({
|
|
16
|
+
name: "analytics_element_bottleneck",
|
|
17
|
+
category: "analytics",
|
|
18
|
+
description:
|
|
19
|
+
"Rank activities by execution-time contribution and incident rate over a rolling window, from process metrics. Activities with fewer than `minBucketSize` executions are suppressed. Note: queue/wait time is not available from metrics.",
|
|
20
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
21
|
+
inputSchema: {
|
|
22
|
+
...schemas.elementBottleneckInput.shape,
|
|
23
|
+
period: optionalPeriod,
|
|
24
|
+
minBucketSize: optionalMinBucketSize,
|
|
25
|
+
},
|
|
26
|
+
handler: async (ch, args, ctx) => {
|
|
27
|
+
const settings = await settingsFor(profileStore, ctx)
|
|
28
|
+
return queries.elementBottleneck(ch, {
|
|
29
|
+
...withEngineScope(engineScope, args),
|
|
30
|
+
period: args.period ?? settings.defaultPeriod,
|
|
31
|
+
minBucketSize: args.minBucketSize ?? settings.minBucketSize,
|
|
32
|
+
})
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { increaseIn, type PrometheusClient } from "@miragon/pulse-analytics-client"
|
|
4
|
+
import type { RegisteredToolMeta, ToolConfig } from "@miragon/mcp-toolkit-core/tools"
|
|
5
|
+
import { createEngineScope } from "../engine-ids.js"
|
|
6
|
+
import { registerEngineCompareTools } from "./engine-compare.js"
|
|
7
|
+
|
|
8
|
+
const scope = createEngineScope(["prod-a", "prod-b"])
|
|
9
|
+
const register = (r: never, store?: never) => registerEngineCompareTools(r, scope, store)
|
|
10
|
+
|
|
11
|
+
type Config = ToolConfig<PrometheusClient>
|
|
12
|
+
|
|
13
|
+
/** Registrar stand-in that captures each tool's full config instead of an MCPServer. */
|
|
14
|
+
function captureConfigs(registerModule: (register: never) => void): Map<string, Config> {
|
|
15
|
+
const configs = new Map<string, Config>()
|
|
16
|
+
const register = Object.assign(
|
|
17
|
+
(config: Config) => {
|
|
18
|
+
configs.set(config.name, config)
|
|
19
|
+
},
|
|
20
|
+
{ getRegisteredTools: (): RegisteredToolMeta[] => [] },
|
|
21
|
+
)
|
|
22
|
+
registerModule(register as never)
|
|
23
|
+
return configs
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function captureHandlers(registerModule: (register: never) => void) {
|
|
27
|
+
const configs = captureConfigs(registerModule)
|
|
28
|
+
return new Map([...configs].map(([name, c]) => [name, c.handler]))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** PrometheusClient that records every instant PromQL string and returns no samples. */
|
|
32
|
+
function recordingClient(): { client: PrometheusClient; queries: string[] } {
|
|
33
|
+
const queries: string[] = []
|
|
34
|
+
return {
|
|
35
|
+
client: {
|
|
36
|
+
instant: (query: string) => {
|
|
37
|
+
queries.push(query)
|
|
38
|
+
return Promise.resolve([])
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
queries,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The five KPI queries per engine — no duplicate incident query without an element (N85). */
|
|
46
|
+
function kpiQueries(sel: string, completedSel: string, range: string) {
|
|
47
|
+
const count = (series: string) => increaseIn(series, { range })
|
|
48
|
+
const duration = "camunda_process_instance_duration_milliseconds"
|
|
49
|
+
return [
|
|
50
|
+
`sum(${count(`camunda_process_instance_started_total${sel}`)})`,
|
|
51
|
+
`sum(${count(`camunda_process_instance_ended_total${completedSel}`)})`,
|
|
52
|
+
`sum(${count(`camunda_incident_created_total${sel}`)})`,
|
|
53
|
+
`sum(${count(`${duration}_sum${sel}`)}) / sum(${count(`${duration}_count${sel}`)})`,
|
|
54
|
+
`histogram_quantile(0.95, sum by (le)(${count(`${duration}_bucket${sel}`)}))`,
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("analytics_engine_compare PromQL", () => {
|
|
59
|
+
it("partitions every query by engine_id and applies the shared window", async () => {
|
|
60
|
+
const handlers = captureHandlers(register)
|
|
61
|
+
const { client, queries } = recordingClient()
|
|
62
|
+
const key = 'process_definition_key="order"'
|
|
63
|
+
|
|
64
|
+
await handlers.get("analytics_engine_compare")!(client, {
|
|
65
|
+
processDefinitionKey: "order",
|
|
66
|
+
engineA: "prod-a",
|
|
67
|
+
engineB: "prod-b",
|
|
68
|
+
windowDays: 14,
|
|
69
|
+
minBucketSize: 10,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(queries).toEqual([
|
|
73
|
+
...kpiQueries(
|
|
74
|
+
`{${key},engine_id="prod-a"}`,
|
|
75
|
+
`{${key},state="COMPLETED",engine_id="prod-a"}`,
|
|
76
|
+
"14d",
|
|
77
|
+
),
|
|
78
|
+
...kpiQueries(
|
|
79
|
+
`{${key},engine_id="prod-b"}`,
|
|
80
|
+
`{${key},state="COMPLETED",engine_id="prod-b"}`,
|
|
81
|
+
"14d",
|
|
82
|
+
),
|
|
83
|
+
])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("scopes to processDefinitionKey everywhere and activityId only on the incident query", async () => {
|
|
87
|
+
const handlers = captureHandlers(register)
|
|
88
|
+
const { client, queries } = recordingClient()
|
|
89
|
+
|
|
90
|
+
await handlers.get("analytics_engine_compare")!(client, {
|
|
91
|
+
engineA: "prod-a",
|
|
92
|
+
engineB: "prod-b",
|
|
93
|
+
windowDays: 7,
|
|
94
|
+
processDefinitionKey: "order",
|
|
95
|
+
activityId: "Task_check",
|
|
96
|
+
minBucketSize: 1,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const key = 'process_definition_key="order"'
|
|
100
|
+
// Five KPI queries per engine plus the element-scoped incident count.
|
|
101
|
+
expect(queries).toHaveLength(12)
|
|
102
|
+
expect(queries.every((q) => q.includes(key))).toBe(true)
|
|
103
|
+
const elementScoped = queries.filter((q) => q.includes('activity_id="Task_check"'))
|
|
104
|
+
const incidents = (engine: string) =>
|
|
105
|
+
`sum(${increaseIn(`camunda_incident_created_total{${key},activity_id="Task_check",engine_id="${engine}"}`, { range: "7d" })})`
|
|
106
|
+
expect(elementScoped).toEqual([incidents("prod-a"), incidents("prod-b")])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("refuses an engine this server is not configured for, before any query (N138)", async () => {
|
|
110
|
+
const handlers = captureHandlers(register)
|
|
111
|
+
const { client, queries } = recordingClient()
|
|
112
|
+
|
|
113
|
+
await expect(
|
|
114
|
+
handlers.get("analytics_engine_compare")!(client, {
|
|
115
|
+
processDefinitionKey: "order",
|
|
116
|
+
engineA: "prod-a",
|
|
117
|
+
engineB: "tenant-x",
|
|
118
|
+
windowDays: 7,
|
|
119
|
+
}),
|
|
120
|
+
).rejects.toThrow(/Unknown engine "tenant-x" in engineB/)
|
|
121
|
+
expect(queries).toEqual([])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("reads 'nothing ended' as an unmeasured duration and suppresses its delta (N78)", async () => {
|
|
125
|
+
const handlers = captureHandlers(register)
|
|
126
|
+
// Both engines started 40 instances; only prod-a has completed any — prod-b
|
|
127
|
+
// has no duration sample at all.
|
|
128
|
+
const client: PrometheusClient = {
|
|
129
|
+
instant: (q) =>
|
|
130
|
+
Promise.resolve(
|
|
131
|
+
q.includes("duration_milliseconds") && q.includes("prod-b")
|
|
132
|
+
? []
|
|
133
|
+
: [{ metric: {}, value: q.includes("duration_milliseconds") ? 30_000 : 40 }],
|
|
134
|
+
),
|
|
135
|
+
}
|
|
136
|
+
const result = (await handlers.get("analytics_engine_compare")!(client, {
|
|
137
|
+
processDefinitionKey: "order",
|
|
138
|
+
engineA: "prod-a",
|
|
139
|
+
engineB: "prod-b",
|
|
140
|
+
windowDays: 7,
|
|
141
|
+
minBucketSize: 1,
|
|
142
|
+
})) as {
|
|
143
|
+
kpis: Array<{ avg_duration_sec: number | null; p95_duration_sec: number | null }>
|
|
144
|
+
delta: { avg_duration_delta_pct: number | null; p95_duration_delta_pct: number | null }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
expect(result.kpis[1]).toMatchObject({ avg_duration_sec: null, p95_duration_sec: null })
|
|
148
|
+
// Never a −100 % from a missing value.
|
|
149
|
+
expect(result.delta).toMatchObject({
|
|
150
|
+
avg_duration_delta_pct: null,
|
|
151
|
+
p95_duration_delta_pct: null,
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it("prefers an explicit minBucketSize over the resolved setting", async () => {
|
|
156
|
+
const handlers = captureHandlers(register)
|
|
157
|
+
const { client } = recordingClient()
|
|
158
|
+
|
|
159
|
+
const explicit = (await handlers.get("analytics_engine_compare")!(client, {
|
|
160
|
+
processDefinitionKey: "order",
|
|
161
|
+
engineA: "prod-a",
|
|
162
|
+
engineB: "prod-b",
|
|
163
|
+
windowDays: 7,
|
|
164
|
+
minBucketSize: 3,
|
|
165
|
+
})) as { minBucketSize: number }
|
|
166
|
+
const fallback = (await handlers.get("analytics_engine_compare")!(client, {
|
|
167
|
+
processDefinitionKey: "order",
|
|
168
|
+
engineA: "prod-a",
|
|
169
|
+
engineB: "prod-b",
|
|
170
|
+
windowDays: 7,
|
|
171
|
+
})) as { minBucketSize: number }
|
|
172
|
+
|
|
173
|
+
// "explicit arg > saved setting > schema default": without a store or a
|
|
174
|
+
// resolvable profile key the registrar path lands on the schema default.
|
|
175
|
+
expect(explicit.minBucketSize).toBe(3)
|
|
176
|
+
expect(fallback.minBucketSize).toBe(10)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
describe("analytics_engine_compare registration", () => {
|
|
181
|
+
const config = captureConfigs(register).get("analytics_engine_compare")!
|
|
182
|
+
|
|
183
|
+
it("registers under the analytics category as a read-only external read", () => {
|
|
184
|
+
expect(config.category).toBe("analytics")
|
|
185
|
+
// toolsets.test.ts derives admin-only status from these; a read that lost
|
|
186
|
+
// `readOnlyHint` would silently change the toolset surface.
|
|
187
|
+
expect(config.annotations).toEqual({
|
|
188
|
+
readOnlyHint: true,
|
|
189
|
+
idempotentHint: true,
|
|
190
|
+
openWorldHint: true,
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it("tells the model why the process scope is mandatory", () => {
|
|
195
|
+
// The description is the only place a model learns that an unscoped
|
|
196
|
+
// engine-vs-engine comparison measures the process mix — losing it turns
|
|
197
|
+
// the tool back into the apples-to-oranges comparison it replaced.
|
|
198
|
+
expect(config.description).toContain("processDefinitionKey is required")
|
|
199
|
+
expect(config.description).toContain("process mixes")
|
|
200
|
+
expect(config.description).toContain("analytics_engine_landscape")
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it("requires processDefinitionKey at the schema boundary", () => {
|
|
204
|
+
const parsed = z.object(config.inputSchema).safeParse({
|
|
205
|
+
engineA: "prod-a",
|
|
206
|
+
engineB: "prod-b",
|
|
207
|
+
})
|
|
208
|
+
expect(parsed.success).toBe(false)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PrometheusClient } from "@miragon/pulse-analytics-client"
|
|
2
|
+
import { schemas, queries } from "@miragon/pulse-analytics-client"
|
|
3
|
+
import type { createToolRegistrar } from "@miragon/mcp-toolkit-core/tools"
|
|
4
|
+
import type { ProfileSource } from "../server-locale.js"
|
|
5
|
+
import { optionalMinBucketSize, settingsFor } from "../settings.js"
|
|
6
|
+
import type { AnalyticsEngineScope } from "../engine-ids.js"
|
|
7
|
+
|
|
8
|
+
type Register = ReturnType<typeof createToolRegistrar<PrometheusClient>>
|
|
9
|
+
|
|
10
|
+
export function registerEngineCompareTools(
|
|
11
|
+
register: Register,
|
|
12
|
+
engineScope: AnalyticsEngineScope,
|
|
13
|
+
profileStore?: ProfileSource,
|
|
14
|
+
) {
|
|
15
|
+
register({
|
|
16
|
+
name: "analytics_engine_compare",
|
|
17
|
+
category: "analytics",
|
|
18
|
+
description:
|
|
19
|
+
"Compare ONE process definition as it runs on two configured CIB Seven engines (e.g. prod-a vs prod-b) over a shared rolling window, from metrics. processDefinitionKey is required: engines host different process mixes, so comparing their whole workloads would measure the mix rather than the engines — holding the process fixed is what makes the delta attributable. Returns per engine starts, completions, incidents per 100 starts (with activityId also at that element) and avg/p95 duration of the instances that ENDED, plus the deltas. Flagged `suppressed` when either engine has fewer than minBucketSize started or completed instances. For the unscoped cross-engine picture (what runs where, load, job backlog, which keys are deployed on several engines) use analytics_engine_landscape first; its `sharedProcessKeys` are the valid inputs here.",
|
|
20
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
21
|
+
inputSchema: { ...schemas.engineCompareInput.shape, minBucketSize: optionalMinBucketSize },
|
|
22
|
+
handler: async (ch, args, ctx) =>
|
|
23
|
+
queries.engineCompare(ch, {
|
|
24
|
+
...args,
|
|
25
|
+
engineA: engineScope.require(args.engineA, "engineA"),
|
|
26
|
+
engineB: engineScope.require(args.engineB, "engineB"),
|
|
27
|
+
minBucketSize: args.minBucketSize ?? (await settingsFor(profileStore, ctx)).minBucketSize,
|
|
28
|
+
}),
|
|
29
|
+
})
|
|
30
|
+
}
|