@miragon/pulse-analytics-connector 0.0.0-stage → 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 +87 -2
- 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 -3
- 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,229 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appOnly,
|
|
3
|
+
buildDataFeedResult,
|
|
4
|
+
buildSingleWidgetView,
|
|
5
|
+
showToolBinding,
|
|
6
|
+
withToolErrors,
|
|
7
|
+
strictToolInput,
|
|
8
|
+
} from "@miragon/pulse-widget-shell/server"
|
|
9
|
+
import { queries, schemas, withCallerSignal } from "@miragon/pulse-analytics-client"
|
|
10
|
+
import { ANALYTICS_ENGINE_LANDSCAPE_DATA } from "../tool-names.js"
|
|
11
|
+
import { localizeViewFor } from "../server-locale.js"
|
|
12
|
+
import { optionalMinBucketSize, settingsFor } from "../settings.js"
|
|
13
|
+
import { isFleetRequest, withEngineScope } from "../engine-ids.js"
|
|
14
|
+
import { namedLandscape } from "../named-views.js"
|
|
15
|
+
import { NO_NAMES } from "../bpmn-name-lookup.js"
|
|
16
|
+
import {
|
|
17
|
+
compareDeltaSummary,
|
|
18
|
+
engineScopeSummary,
|
|
19
|
+
suppressedNote,
|
|
20
|
+
type AnalyticsWidgetToolsContext,
|
|
21
|
+
} from "./shared.js"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The comparison family plus the cross-engine landscape they hang off.
|
|
25
|
+
*
|
|
26
|
+
* The three compare views answer "did this get worse?" along one axis at a
|
|
27
|
+
* time — across a deployment (cluster), across two versions of a process
|
|
28
|
+
* (version), across two engines running the SAME process (engine). The
|
|
29
|
+
* landscape is deliberately not a fourth comparison: engines host different
|
|
30
|
+
* process mixes, so it reports what runs where plus mix-independent signals
|
|
31
|
+
* and names the definitions for which an engine comparison actually holds.
|
|
32
|
+
*/
|
|
33
|
+
export function registerComparisonWidgetTools(ctx: AnalyticsWidgetToolsContext) {
|
|
34
|
+
const { server, ch, engineScope, profileStore } = ctx
|
|
35
|
+
const namesFor = ctx.namesFor ?? NO_NAMES
|
|
36
|
+
|
|
37
|
+
// --- Cluster Compare (Pre/Post deployment diff) ---
|
|
38
|
+
server.tool(
|
|
39
|
+
{
|
|
40
|
+
name: "analytics_show_cluster_compare",
|
|
41
|
+
title: "Pre/Post Deployment Comparison",
|
|
42
|
+
description:
|
|
43
|
+
"Visualize before/after KPI deltas around a deployment timestamp. Windows are clamped to now and the retention (flagged `partial`; starts compare per day). Results are flagged `suppressed` when either window has fewer than minBucketSize started or completed instances.",
|
|
44
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
45
|
+
inputSchema: strictToolInput({
|
|
46
|
+
...schemas.clusterCompareInput.shape,
|
|
47
|
+
minBucketSize: optionalMinBucketSize,
|
|
48
|
+
}),
|
|
49
|
+
...showToolBinding("analytics_show_cluster_compare", "Pre/Post Deployment Comparison"),
|
|
50
|
+
},
|
|
51
|
+
withToolErrors(async (args, toolCtx) => {
|
|
52
|
+
const { t, title } = await localizeViewFor(profileStore, toolCtx)
|
|
53
|
+
const minBucketSize =
|
|
54
|
+
args.minBucketSize ?? (await settingsFor(profileStore, toolCtx)).minBucketSize
|
|
55
|
+
const data = await queries.clusterCompare(withCallerSignal(ch, toolCtx.signal), {
|
|
56
|
+
...withEngineScope(engineScope, args),
|
|
57
|
+
minBucketSize,
|
|
58
|
+
})
|
|
59
|
+
return buildSingleWidgetView({
|
|
60
|
+
widget: "analytics:cluster-compare",
|
|
61
|
+
app: "analytics",
|
|
62
|
+
dataType: "analytics:clusterCompare",
|
|
63
|
+
data,
|
|
64
|
+
// The view title in the language the profile names (the widget heading's
|
|
65
|
+
// own key); none with "system", where the heading names the view.
|
|
66
|
+
title: title("aClusterCompare.title"),
|
|
67
|
+
summary: t("aSum.clusterCompare", {
|
|
68
|
+
scope: data.processDefinitionKey
|
|
69
|
+
? t("aSum.scopeForProcess", { key: data.processDefinitionKey })
|
|
70
|
+
: "",
|
|
71
|
+
engines: engineScopeSummary(t, data.engines, isFleetRequest(args.engine)),
|
|
72
|
+
deploymentTimestamp: data.deploymentTimestamp,
|
|
73
|
+
before: data.windowDays.before,
|
|
74
|
+
after: data.windowDays.after,
|
|
75
|
+
partial: data.partial ? t("aSum.clusterComparePartial") : "",
|
|
76
|
+
delta: compareDeltaSummary(data.delta),
|
|
77
|
+
suppressed: suppressedNote(data.suppressed),
|
|
78
|
+
}),
|
|
79
|
+
})
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// --- Version Compare (v1 vs v2 of one process) ---
|
|
84
|
+
server.tool(
|
|
85
|
+
{
|
|
86
|
+
name: "analytics_show_version_compare",
|
|
87
|
+
title: "Process Version Comparison",
|
|
88
|
+
description:
|
|
89
|
+
"Visualize KPI deltas between two deployed versions of the same processDefinitionKey within a shared time window. Instance counts and durations are exact per version; the incident rates show as n/a — the incident metric carries no version label, so they are not measured per version (never read them as zero). Results are flagged `suppressed` when either version has fewer than minBucketSize started or completed instances.",
|
|
90
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
91
|
+
inputSchema: strictToolInput({
|
|
92
|
+
...schemas.versionCompareInput.shape,
|
|
93
|
+
minBucketSize: optionalMinBucketSize,
|
|
94
|
+
}),
|
|
95
|
+
...showToolBinding("analytics_show_version_compare", "Process Version Comparison"),
|
|
96
|
+
},
|
|
97
|
+
withToolErrors(async (args, toolCtx) => {
|
|
98
|
+
const { t, title } = await localizeViewFor(profileStore, toolCtx)
|
|
99
|
+
const minBucketSize =
|
|
100
|
+
args.minBucketSize ?? (await settingsFor(profileStore, toolCtx)).minBucketSize
|
|
101
|
+
const data = await queries.versionCompare(withCallerSignal(ch, toolCtx.signal), {
|
|
102
|
+
...withEngineScope(engineScope, args),
|
|
103
|
+
minBucketSize,
|
|
104
|
+
})
|
|
105
|
+
return buildSingleWidgetView({
|
|
106
|
+
widget: "analytics:version-compare",
|
|
107
|
+
app: "analytics",
|
|
108
|
+
dataType: "analytics:versionCompare",
|
|
109
|
+
data,
|
|
110
|
+
title: title("aVersionCompare.title"),
|
|
111
|
+
summary: t("aSum.versionCompare", {
|
|
112
|
+
key: data.processDefinitionKey,
|
|
113
|
+
versionA: data.versionA,
|
|
114
|
+
versionB: data.versionB,
|
|
115
|
+
windowDays: data.windowDays,
|
|
116
|
+
engines: engineScopeSummary(t, data.engines, isFleetRequest(args.engine)),
|
|
117
|
+
delta: compareDeltaSummary(data.delta),
|
|
118
|
+
suppressed: suppressedNote(data.suppressed),
|
|
119
|
+
}),
|
|
120
|
+
})
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
// --- Engine Compare (ONE process, engine A vs engine B) ---
|
|
125
|
+
server.tool(
|
|
126
|
+
{
|
|
127
|
+
name: "analytics_show_engine_compare",
|
|
128
|
+
title: "Engine Comparison",
|
|
129
|
+
description:
|
|
130
|
+
"Visualize KPI deltas for ONE process definition as it runs on two configured CIB Seven engines (e.g. prod-a vs prod-b) over a shared time window. processDefinitionKey is required — engines host different process mixes, so an unscoped engine-vs-engine comparison would measure the mix, not the engines. Results are flagged `suppressed` when either engine has fewer than minBucketSize started or completed instances. For the cross-engine picture use analytics_show_engine_landscape.",
|
|
131
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
132
|
+
inputSchema: strictToolInput({
|
|
133
|
+
...schemas.engineCompareInput.shape,
|
|
134
|
+
minBucketSize: optionalMinBucketSize,
|
|
135
|
+
}),
|
|
136
|
+
...showToolBinding("analytics_show_engine_compare", "Engine Comparison"),
|
|
137
|
+
},
|
|
138
|
+
withToolErrors(async (args, toolCtx) => {
|
|
139
|
+
const { t, title } = await localizeViewFor(profileStore, toolCtx)
|
|
140
|
+
const minBucketSize =
|
|
141
|
+
args.minBucketSize ?? (await settingsFor(profileStore, toolCtx)).minBucketSize
|
|
142
|
+
const data = await queries.engineCompare(withCallerSignal(ch, toolCtx.signal), {
|
|
143
|
+
...args,
|
|
144
|
+
engineA: engineScope.require(args.engineA, "engineA"),
|
|
145
|
+
engineB: engineScope.require(args.engineB, "engineB"),
|
|
146
|
+
minBucketSize,
|
|
147
|
+
})
|
|
148
|
+
return buildSingleWidgetView({
|
|
149
|
+
widget: "analytics:engine-compare",
|
|
150
|
+
app: "analytics",
|
|
151
|
+
dataType: "analytics:engineCompare",
|
|
152
|
+
data,
|
|
153
|
+
title: title("aEngineCompare.title"),
|
|
154
|
+
summary: t("aSum.engineCompare", {
|
|
155
|
+
engineA: data.engineA,
|
|
156
|
+
engineB: data.engineB,
|
|
157
|
+
key: data.processDefinitionKey,
|
|
158
|
+
windowDays: data.windowDays,
|
|
159
|
+
delta: compareDeltaSummary(data.delta),
|
|
160
|
+
suppressed: suppressedNote(data.suppressed),
|
|
161
|
+
}),
|
|
162
|
+
})
|
|
163
|
+
}),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
// --- Engine Landscape (cross-engine overview, NOT an engine ranking) ---
|
|
167
|
+
server.tool(
|
|
168
|
+
{
|
|
169
|
+
name: "analytics_show_engine_landscape",
|
|
170
|
+
title: "Cross-Engine Landscape",
|
|
171
|
+
description:
|
|
172
|
+
"Show the cross-engine process landscape of the configured engines: which process definitions run on which engine, the absolute load per engine (running instances, open incidents, failed jobs) and the engine-owned job backlog. Counts, not rates — engines host different process mixes, so per-engine rates would measure the mix. Highlights the definitions deployed on several engines, the only sound targets for analytics_show_engine_compare.",
|
|
173
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
174
|
+
inputSchema: strictToolInput(schemas.engineLandscapeInput.shape),
|
|
175
|
+
...showToolBinding("analytics_show_engine_landscape", "Cross-Engine Landscape"),
|
|
176
|
+
},
|
|
177
|
+
withToolErrors(async (args, toolCtx) => {
|
|
178
|
+
const { t, title } = await localizeViewFor(profileStore, toolCtx)
|
|
179
|
+
const data = await namedLandscape(
|
|
180
|
+
await queries.engineLandscape(
|
|
181
|
+
withCallerSignal(ch, toolCtx.signal),
|
|
182
|
+
withEngineScope(engineScope, args),
|
|
183
|
+
),
|
|
184
|
+
namesFor,
|
|
185
|
+
)
|
|
186
|
+
return buildSingleWidgetView({
|
|
187
|
+
widget: "analytics:engine-landscape",
|
|
188
|
+
app: "analytics",
|
|
189
|
+
dataType: "analytics:engineLandscape",
|
|
190
|
+
data,
|
|
191
|
+
title: title("aLandscape.heading"),
|
|
192
|
+
summary: t("aSum.engineLandscape", {
|
|
193
|
+
engineCount: data.totals.engineCount,
|
|
194
|
+
reportingEngineCount: data.totals.reportingEngineCount,
|
|
195
|
+
processKeyCount: data.totals.processKeyCount,
|
|
196
|
+
sharedProcessKeyCount: data.totals.sharedProcessKeyCount,
|
|
197
|
+
// Unknown while no engine reports: "n/a", never a plausible 0.
|
|
198
|
+
runningInstances: data.totals.runningInstances ?? "n/a",
|
|
199
|
+
openIncidents: data.totals.openIncidents ?? "n/a",
|
|
200
|
+
shared: data.sharedProcessKeys.length
|
|
201
|
+
? t("aSum.sharedKeys", { keys: data.sharedProcessKeys.join(", ") })
|
|
202
|
+
: "",
|
|
203
|
+
}),
|
|
204
|
+
})
|
|
205
|
+
}),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
server.tool(
|
|
209
|
+
{
|
|
210
|
+
name: ANALYTICS_ENGINE_LANDSCAPE_DATA,
|
|
211
|
+
title: "Engine landscape data (internal)",
|
|
212
|
+
description:
|
|
213
|
+
"Internal JSON feed (no UI) for the cross-engine landscape widget's self-fetch — also consumed by the camunda7 cockpit's cross-engine mode. Prefer analytics_show_engine_landscape.",
|
|
214
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
215
|
+
inputSchema: strictToolInput(schemas.engineLandscapeInput.shape),
|
|
216
|
+
...appOnly,
|
|
217
|
+
},
|
|
218
|
+
withToolErrors(async (args, toolCtx) => {
|
|
219
|
+
const data = await namedLandscape(
|
|
220
|
+
await queries.engineLandscape(
|
|
221
|
+
withCallerSignal(ch, toolCtx.signal),
|
|
222
|
+
withEngineScope(engineScope, args),
|
|
223
|
+
),
|
|
224
|
+
namesFor,
|
|
225
|
+
)
|
|
226
|
+
return buildDataFeedResult({ ...data })
|
|
227
|
+
}),
|
|
228
|
+
)
|
|
229
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { MCPServer } from "mcp-use"
|
|
2
|
+
import type { CompareKpiDelta, PrometheusClient } from "@miragon/pulse-analytics-client"
|
|
3
|
+
import type { ProfileSource, ServerT } from "../server-locale.js"
|
|
4
|
+
import type { AnalyticsEngineScope } from "../engine-ids.js"
|
|
5
|
+
import type { NameLookup } from "../bpmn-name-lookup.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Everything the per-domain widget-tool registrars need, handed over by the
|
|
9
|
+
* `widget-tools.ts` entry — mirrors camunda7's `WidgetToolsContext`. The
|
|
10
|
+
* `_meta`/view bindings are NOT passed through: each registration spreads
|
|
11
|
+
* `showToolBinding()` / `appOnly` from `@miragon/pulse-widget-shell/server`
|
|
12
|
+
* itself, so the dual-protocol contract stays in one place.
|
|
13
|
+
*/
|
|
14
|
+
export interface AnalyticsWidgetToolsContext {
|
|
15
|
+
server: MCPServer
|
|
16
|
+
ch: PrometheusClient
|
|
17
|
+
/** The server's configured engine ids — every query resolves `engine` through it. */
|
|
18
|
+
engineScope: AnalyticsEngineScope
|
|
19
|
+
/** Locale for model-facing summaries plus the saved analytics defaults. */
|
|
20
|
+
profileStore?: ProfileSource
|
|
21
|
+
/** The process names behind the ids a view lists (`bpmn-name-lookup.ts`); absent → ids. */
|
|
22
|
+
namesFor?: NameLookup
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Formats a nullable comparison delta ("+12.5%", "-3pp", "n/a") for summaries. */
|
|
26
|
+
function fmtDelta(value: number | null, unit: string): string {
|
|
27
|
+
if (value == null) return "n/a"
|
|
28
|
+
return `${value > 0 ? "+" : ""}${value}${unit}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Shared delta shape of the three compare queries, for one-line summaries. A
|
|
33
|
+
* null delta reads "n/a" — a side was not measured (nothing ended, nothing
|
|
34
|
+
* started) or the baseline is 0 — never a −100 %.
|
|
35
|
+
*/
|
|
36
|
+
export function compareDeltaSummary(delta: CompareKpiDelta): string {
|
|
37
|
+
return [
|
|
38
|
+
`starts/day ${fmtDelta(delta.started_per_day_delta_pct, "%")}`,
|
|
39
|
+
`incident rate ${fmtDelta(delta.incident_rate_delta_pp, "pp")}`,
|
|
40
|
+
...(delta.element_incident_rate_delta_pp === null
|
|
41
|
+
? []
|
|
42
|
+
: [`element incident rate ${fmtDelta(delta.element_incident_rate_delta_pp, "pp")}`]),
|
|
43
|
+
`avg duration ${fmtDelta(delta.avg_duration_delta_pct, "%")}`,
|
|
44
|
+
`p95 ${fmtDelta(delta.p95_duration_delta_pct, "%")}`,
|
|
45
|
+
].join(", ")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const suppressedNote = (suppressed: boolean) =>
|
|
49
|
+
suppressed ? "; flagged suppressed (sample below minBucketSize)" : ""
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The engine scope of a result, for summaries: one engine by name, or — the
|
|
53
|
+
* deliberate fleet aggregate — every engine it adds up, so a fleet-wide figure
|
|
54
|
+
* never reads as one engine's. `fleet` = the caller named no engine.
|
|
55
|
+
*/
|
|
56
|
+
export function engineScopeSummary(
|
|
57
|
+
t: ServerT,
|
|
58
|
+
engines: readonly string[] | null,
|
|
59
|
+
fleet: boolean,
|
|
60
|
+
): string {
|
|
61
|
+
if (!engines || engines.length === 0) return ""
|
|
62
|
+
const ids = engines.map((id) => `"${id}"`).join(", ")
|
|
63
|
+
if (fleet) return t("aSum.engineScopeFleet", { count: engines.length, ids })
|
|
64
|
+
return engines.length === 1
|
|
65
|
+
? t("aSum.engineScopeOne", { id: engines[0] })
|
|
66
|
+
: t("aSum.engineScopeSubset", { ids })
|
|
67
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { parseToolResult } from "@miragon/mcp-toolkit-ui"
|
|
3
|
+
import type { MCPServer } from "mcp-use"
|
|
4
|
+
import type { PrometheusClient } from "@miragon/pulse-analytics-client"
|
|
5
|
+
import { registerWidgetTools } from "./widget-tools.js"
|
|
6
|
+
import { createEngineScope } from "./engine-ids.js"
|
|
7
|
+
import type { ProfileSource } from "./server-locale.js"
|
|
8
|
+
import type { FetchBpmnXml, NameRequest } from "./bpmn-name-lookup.js"
|
|
9
|
+
|
|
10
|
+
/** A feed's payload as the widget reads it (feeds answer in JSON text only). */
|
|
11
|
+
const feedData = <T = Record<string, unknown>>(result: unknown): T => parseToolResult<T>(result)
|
|
12
|
+
|
|
13
|
+
type Cell = { widget: string; props?: Record<string, unknown> }
|
|
14
|
+
type ShowResult = { structuredContent: { layout: Array<{ row: Cell[] }> } }
|
|
15
|
+
type Handler = (args: Record<string, unknown>, ctx: unknown) => Promise<ShowResult>
|
|
16
|
+
|
|
17
|
+
/** Registers the widget tools on a stand-in server and returns their handlers. */
|
|
18
|
+
function handlers(ch: PrometheusClient): Map<string, Handler> {
|
|
19
|
+
const byName = new Map<string, Handler>()
|
|
20
|
+
const server = {
|
|
21
|
+
tool: (definition: { name: string }, handler: Handler) => {
|
|
22
|
+
byName.set(definition.name, handler)
|
|
23
|
+
},
|
|
24
|
+
} as unknown as MCPServer
|
|
25
|
+
registerWidgetTools(server, ch, { engineScope: createEngineScope(["prod-a", "prod-b"]) })
|
|
26
|
+
return byName
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** PrometheusClient that records every instant PromQL string and returns no samples. */
|
|
30
|
+
function recordingClient(): { client: PrometheusClient; queries: string[] } {
|
|
31
|
+
const queries: string[] = []
|
|
32
|
+
return {
|
|
33
|
+
client: {
|
|
34
|
+
instant: (query: string) => {
|
|
35
|
+
queries.push(query)
|
|
36
|
+
return Promise.resolve([])
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
queries,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cells = (result: ShowResult) => result.structuredContent.layout.flatMap((r) => r.row)
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The engine-health hand-off renders `analytics_show_failure_dashboard` for
|
|
47
|
+
* ONE engine: the cells must know that scope, or their hand-offs and model
|
|
48
|
+
* contexts send the model to every engine (or the saved default) instead.
|
|
49
|
+
*/
|
|
50
|
+
describe("analytics_show_failure_dashboard engine scope", () => {
|
|
51
|
+
it("queries the engine and hands it to every cell", async () => {
|
|
52
|
+
const { client, queries } = recordingClient()
|
|
53
|
+
const show = handlers(client).get("analytics_show_failure_dashboard")!
|
|
54
|
+
const result = await show({ engine: "prod-b" }, {})
|
|
55
|
+
|
|
56
|
+
expect(queries.length).toBeGreaterThan(0)
|
|
57
|
+
for (const query of queries) expect(query).toContain('engine_id="prod-b"')
|
|
58
|
+
// The cells' self-fetches keep their scope through these props; their
|
|
59
|
+
// hand-offs and model contexts name the data's `engines` echo
|
|
60
|
+
// (model-descriptions.test.ts pins the `engine` id they then carry).
|
|
61
|
+
expect(cells(result)).toEqual([
|
|
62
|
+
{ widget: "analytics:failure-summary-kpi", props: { engine: "prod-b" } },
|
|
63
|
+
{ widget: "analytics:error-patterns-table", props: { engine: "prod-b" } },
|
|
64
|
+
{ widget: "analytics:failure-rate-table", props: { engine: "prod-b" } },
|
|
65
|
+
])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// The aggregate is the CONFIGURED fleet (#336), never the whole Prometheus:
|
|
69
|
+
// the query names every configured engine, and the data echoes them.
|
|
70
|
+
it("reads the configured fleet for the aggregate and leaves the cells unscoped", async () => {
|
|
71
|
+
const { client, queries } = recordingClient()
|
|
72
|
+
const result = await handlers(client).get("analytics_show_failure_dashboard")!({}, {})
|
|
73
|
+
|
|
74
|
+
expect(queries.length).toBeGreaterThan(0)
|
|
75
|
+
for (const query of queries) expect(query).toContain('engine_id=~"prod-a|prod-b"')
|
|
76
|
+
expect(cells(result).every((cell) => cell.props === undefined)).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe("view titles (U3) and the heatmap's as-of stamp (U7)", () => {
|
|
81
|
+
const CTX = { auth: { user: { id: "user-1" } } }
|
|
82
|
+
function localizedHandlers(language: string): Map<string, Handler> {
|
|
83
|
+
const byName = new Map<string, Handler>()
|
|
84
|
+
const server = {
|
|
85
|
+
tool: (definition: { name: string }, handler: Handler) => {
|
|
86
|
+
byName.set(definition.name, handler)
|
|
87
|
+
},
|
|
88
|
+
} as unknown as MCPServer
|
|
89
|
+
const profileStore: ProfileSource = { get: () => Promise.resolve({ language }) }
|
|
90
|
+
registerWidgetTools(server, recordingClient().client, {
|
|
91
|
+
engineScope: createEngineScope(["prod-a"]),
|
|
92
|
+
profileStore,
|
|
93
|
+
})
|
|
94
|
+
return byName
|
|
95
|
+
}
|
|
96
|
+
const SHOWS: Array<[string, Record<string, unknown>]> = [
|
|
97
|
+
["analytics_show_dashboard", {}],
|
|
98
|
+
["analytics_show_failure_dashboard", {}],
|
|
99
|
+
["analytics_show_bpmn_heatmap", { processDefinitionKey: "order" }],
|
|
100
|
+
]
|
|
101
|
+
const titles = async (language: string) => {
|
|
102
|
+
const tools = localizedHandlers(language)
|
|
103
|
+
const results = await Promise.all(SHOWS.map(([name, args]) => tools.get(name)!(args, CTX)))
|
|
104
|
+
return results.map((r) => (r.structuredContent as unknown as { title?: string }).title)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
it("titles each view in the caller's language, like the widget heading", async () => {
|
|
108
|
+
expect(await titles("de")).toEqual(["Prozessanalyse", "Fehleranalyse", "BPMN-Heatmap"])
|
|
109
|
+
expect(await titles("en")).toEqual(["Process analytics", "Failure analysis", "BPMN heatmap"])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("stamps the heatmap with the time its values were read, in the view and the feed", async () => {
|
|
113
|
+
const tools = localizedHandlers("en")
|
|
114
|
+
const args = { processDefinitionKey: "order" }
|
|
115
|
+
const show = (await tools.get("analytics_show_bpmn_heatmap")!(args, CTX)) as unknown as {
|
|
116
|
+
structuredContent: { context: { stepData: { result: { data: { asOf?: string } } } } }
|
|
117
|
+
}
|
|
118
|
+
const feed = await tools.get("analytics_bpmn_heatmap_data")!(args, CTX)
|
|
119
|
+
const iso = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
|
120
|
+
expect(show.structuredContent.context.stepData.result.data.asOf).toMatch(iso)
|
|
121
|
+
expect(feedData<{ asOf?: string }>(feed).asOf).toMatch(iso)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The heatmap says WHY it has no diagram only when the server knows it: no
|
|
127
|
+
* lookup injected (camunda7 not active), or a lookup that failed or found
|
|
128
|
+
* nothing. The widget picks its text from that and never guesses.
|
|
129
|
+
*/
|
|
130
|
+
describe("the heatmap's missing-diagram cause", () => {
|
|
131
|
+
type Diagram = { bpmnXml?: string | null; bpmnMissing?: string }
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The diagram fields of the show tool's payload (a cause at most, never the
|
|
135
|
+
* XML: the model reads that payload on some hosts), of the heat feed (none)
|
|
136
|
+
* and of the diagram feed the widget reads the XML from.
|
|
137
|
+
*/
|
|
138
|
+
async function payloads(fetchBpmnXml?: FetchBpmnXml, engines = ["prod-a"]) {
|
|
139
|
+
const byName = new Map<string, Handler>()
|
|
140
|
+
const server = {
|
|
141
|
+
tool: (definition: { name: string }, handler: Handler) => {
|
|
142
|
+
byName.set(definition.name, handler)
|
|
143
|
+
},
|
|
144
|
+
} as unknown as MCPServer
|
|
145
|
+
registerWidgetTools(server, recordingClient().client, {
|
|
146
|
+
engineScope: createEngineScope(engines),
|
|
147
|
+
fetchBpmnXml,
|
|
148
|
+
})
|
|
149
|
+
const args = { processDefinitionKey: "order" }
|
|
150
|
+
const show = (await byName.get("analytics_show_bpmn_heatmap")!(args, {})) as unknown as {
|
|
151
|
+
structuredContent: { context: { stepData: { result: { data: Diagram } } } }
|
|
152
|
+
}
|
|
153
|
+
const heat = feedData<Diagram>(await byName.get("analytics_bpmn_heatmap_data")!(args, {}))
|
|
154
|
+
const diagram = feedData<Diagram>(await byName.get("analytics_bpmn_xml_data")!(args, {}))
|
|
155
|
+
const fields = ({ bpmnXml, bpmnMissing }: Diagram) => ({ bpmnXml, bpmnMissing })
|
|
156
|
+
return {
|
|
157
|
+
show: fields(show.structuredContent.context.stepData.result.data),
|
|
158
|
+
heat: fields(heat),
|
|
159
|
+
diagram: fields(diagram),
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
it("names the missing camunda7 module only when no lookup was injected", async () => {
|
|
164
|
+
const { show, heat, diagram } = await payloads()
|
|
165
|
+
expect(show).toEqual({ bpmnXml: undefined, bpmnMissing: "no-camunda7" })
|
|
166
|
+
expect(heat).toEqual({ bpmnXml: undefined, bpmnMissing: undefined })
|
|
167
|
+
expect(diagram).toEqual({ bpmnXml: null, bpmnMissing: "no-camunda7" })
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it("says the lookup found nothing when it failed or returned null", async () => {
|
|
171
|
+
const failing: FetchBpmnXml = () => Promise.reject(new Error("ECONNREFUSED"))
|
|
172
|
+
const empty: FetchBpmnXml = () => Promise.resolve(null)
|
|
173
|
+
for (const fetcher of [failing, empty]) {
|
|
174
|
+
const { show, diagram } = await payloads(fetcher)
|
|
175
|
+
expect(show).toEqual({ bpmnXml: undefined, bpmnMissing: "not-loaded" })
|
|
176
|
+
expect(diagram).toEqual({ bpmnXml: null, bpmnMissing: "not-loaded" })
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it("keeps the XML out of the show payload and the heat feed when the diagram is there", async () => {
|
|
181
|
+
const xml = '<?xml version="1.0"?><definitions />'
|
|
182
|
+
const { show, heat, diagram } = await payloads(() => Promise.resolve(xml))
|
|
183
|
+
expect(show).toEqual({ bpmnXml: undefined, bpmnMissing: undefined })
|
|
184
|
+
expect(heat).toEqual({ bpmnXml: undefined, bpmnMissing: undefined })
|
|
185
|
+
expect(diagram).toEqual({ bpmnXml: xml, bpmnMissing: undefined })
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// The process may run on a later engine only: the first engine of the
|
|
189
|
+
// scope (configuration order) that has the model draws it.
|
|
190
|
+
it("reads the diagram from the engines of its scope in order", async () => {
|
|
191
|
+
const xml = '<?xml version="1.0"?><definitions />'
|
|
192
|
+
const asked: Array<[string, string | undefined]> = []
|
|
193
|
+
const fetcher: FetchBpmnXml = (key, engine) => {
|
|
194
|
+
asked.push([key, engine])
|
|
195
|
+
if (engine === "prod-a") return Promise.reject(new Error("503"))
|
|
196
|
+
return Promise.resolve(engine === "prod-c" ? xml : null)
|
|
197
|
+
}
|
|
198
|
+
const { diagram } = await payloads(fetcher, ["prod-a", "prod-b", "prod-c"])
|
|
199
|
+
expect(diagram).toEqual({ bpmnXml: xml, bpmnMissing: undefined })
|
|
200
|
+
expect(asked.slice(0, 3)).toEqual([
|
|
201
|
+
["order", "prod-a"],
|
|
202
|
+
["order", "prod-b"],
|
|
203
|
+
["order", "prod-c"],
|
|
204
|
+
])
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The names behind the ids (U13): the dashboard, the failure dashboard and
|
|
210
|
+
* the landscape carry the process (and, for the activity breakdown, the
|
|
211
|
+
* element) names the lookup knows, in the show tool's payload and in its
|
|
212
|
+
* feed alike. The dashboard judges its rate against the caller's saved
|
|
213
|
+
* comparison floor.
|
|
214
|
+
*/
|
|
215
|
+
describe("the names behind the ids", () => {
|
|
216
|
+
const ORDER = { process_definition_key: "order" }
|
|
217
|
+
/** One process with one activity, an open incident group and a deployment. */
|
|
218
|
+
const client: PrometheusClient = {
|
|
219
|
+
instant: (query: string) => {
|
|
220
|
+
if (query.includes("by (process_definition_key, activity_id, activity_type)")) {
|
|
221
|
+
return Promise.resolve([
|
|
222
|
+
{ metric: { ...ORDER, activity_id: "Task_A", activity_type: "userTask" }, value: 2 },
|
|
223
|
+
])
|
|
224
|
+
}
|
|
225
|
+
if (query.includes("by (process_definition_key, incident_type)")) {
|
|
226
|
+
return Promise.resolve([{ metric: { ...ORDER, incident_type: "failedJob" }, value: 1 }])
|
|
227
|
+
}
|
|
228
|
+
if (query.includes("by (engine_id, process_definition_key)")) {
|
|
229
|
+
return Promise.resolve([{ metric: { ...ORDER, engine_id: "prod-a" }, value: 1 }])
|
|
230
|
+
}
|
|
231
|
+
if (query.includes("by (process_definition_key)")) {
|
|
232
|
+
return Promise.resolve([{ metric: ORDER, value: 3 }])
|
|
233
|
+
}
|
|
234
|
+
if (query.includes("process_instance_started_total")) {
|
|
235
|
+
return Promise.resolve([{ metric: {}, value: 4 }])
|
|
236
|
+
}
|
|
237
|
+
return Promise.resolve([])
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
const NAMES = { order: { name: "Order fulfillment", elements: { Task_A: "Pack" } } }
|
|
241
|
+
|
|
242
|
+
async function run(name: string) {
|
|
243
|
+
const byName = new Map<string, Handler>()
|
|
244
|
+
const server = {
|
|
245
|
+
tool: (definition: { name: string }, handler: Handler) => {
|
|
246
|
+
byName.set(definition.name, handler)
|
|
247
|
+
},
|
|
248
|
+
} as unknown as MCPServer
|
|
249
|
+
const calls: Array<[string[], NameRequest | undefined]> = []
|
|
250
|
+
const profileStore: ProfileSource = {
|
|
251
|
+
get: () => Promise.resolve({ modules: { analytics: { minBucketSize: 3 } } }),
|
|
252
|
+
}
|
|
253
|
+
registerWidgetTools(server, client, {
|
|
254
|
+
engineScope: createEngineScope(["prod-a"]),
|
|
255
|
+
profileStore,
|
|
256
|
+
namesFor: (keys, request) => {
|
|
257
|
+
calls.push([[...keys], request])
|
|
258
|
+
return Promise.resolve(NAMES)
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
const ctx = { auth: { user: { id: "user-1" } } }
|
|
262
|
+
const result = await byName.get(name)!({}, ctx)
|
|
263
|
+
return { content: feedData(result), calls }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The payload of a show tool (its step data) or of a feed (the content itself). */
|
|
267
|
+
const payloadOf = (content: Record<string, unknown>) =>
|
|
268
|
+
((content.context as { stepData?: { result?: { data?: unknown } } } | undefined)?.stepData
|
|
269
|
+
?.result?.data ?? content) as Record<string, unknown>
|
|
270
|
+
|
|
271
|
+
it.each([
|
|
272
|
+
"analytics_show_dashboard",
|
|
273
|
+
"analytics_dashboard_data",
|
|
274
|
+
"analytics_show_failure_dashboard",
|
|
275
|
+
"analytics_failure_dashboard_data",
|
|
276
|
+
"analytics_show_engine_landscape",
|
|
277
|
+
"analytics_engine_landscape_data",
|
|
278
|
+
])("%s carries the names", async (name) => {
|
|
279
|
+
const { content, calls } = await run(name)
|
|
280
|
+
expect(JSON.stringify(content)).toContain('"names":{"order":{"name":"Order fulfillment"')
|
|
281
|
+
expect(calls).toHaveLength(1)
|
|
282
|
+
expect(calls[0][0]).toContain("order")
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it("asks for the elements of the activity breakdown only", async () => {
|
|
286
|
+
const { calls } = await run("analytics_dashboard_data")
|
|
287
|
+
expect(calls[0][1]?.elementIds).toEqual({ order: ["Task_A"] })
|
|
288
|
+
const failures = await run("analytics_failure_dashboard_data")
|
|
289
|
+
expect(failures.calls[0][1]?.elementIds).toEqual({ order: [] })
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
// A process that runs only on a later engine is named too: each view says
|
|
293
|
+
// which engines its rows come from, the landscape per process.
|
|
294
|
+
it("reads the models on the engines the rows come from", async () => {
|
|
295
|
+
const failures = await run("analytics_failure_dashboard_data")
|
|
296
|
+
expect(failures.calls[0][1]?.engines).toEqual(["prod-a"])
|
|
297
|
+
const landscape = await run("analytics_engine_landscape_data")
|
|
298
|
+
expect(landscape.calls[0][1]?.enginesByKey).toEqual({ order: ["prod-a"] })
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it("judges the dashboard's rate against the caller's saved comparison floor", async () => {
|
|
302
|
+
const { content } = await run("analytics_dashboard_data")
|
|
303
|
+
// 4 starts in each window, the saved floor 3: a change of the rate is judged.
|
|
304
|
+
expect(payloadOf(content).previous).toMatchObject({ totalCount: 4, suppressed: false })
|
|
305
|
+
})
|
|
306
|
+
})
|