@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,174 @@
|
|
|
1
|
+
import http from "node:http"
|
|
2
|
+
import type { AddressInfo } from "node:net"
|
|
3
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
|
4
|
+
import type { MCPServer } from "mcp-use"
|
|
5
|
+
import { analyticsModule } from "./module.js"
|
|
6
|
+
import { ANALYTICS_ENGINE_LANDSCAPE_DATA } from "./tool-names.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* #325 through the module: env → config → plugin → Prometheus client → tool.
|
|
10
|
+
* A real local HTTP server plays Prometheus, so the auth headers, the
|
|
11
|
+
* deadline and the caller's cancellation are observed on the wire.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type ToolCallback = (
|
|
15
|
+
args: Record<string, unknown>,
|
|
16
|
+
ctx?: unknown,
|
|
17
|
+
) => Promise<{ isError?: boolean; content: Array<{ type: string; text: string }> }>
|
|
18
|
+
|
|
19
|
+
const seenAuth: Array<string | undefined> = []
|
|
20
|
+
const seenTenant: Array<string | string[] | undefined> = []
|
|
21
|
+
let hang = false
|
|
22
|
+
|
|
23
|
+
const prometheus = http.createServer((req, res) => {
|
|
24
|
+
seenAuth.push(req.headers.authorization)
|
|
25
|
+
seenTenant.push(req.headers["x-scope-orgid"])
|
|
26
|
+
if (hang) return
|
|
27
|
+
res.writeHead(200, { "Content-Type": "application/json" })
|
|
28
|
+
res.end(JSON.stringify({ status: "success", data: { resultType: "vector", result: [] } }))
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
let url = ""
|
|
32
|
+
|
|
33
|
+
beforeAll(async () => {
|
|
34
|
+
await new Promise<void>((resolve) => prometheus.listen(0, "127.0.0.1", resolve))
|
|
35
|
+
url = `http://127.0.0.1:${(prometheus.address() as AddressInfo).port}`
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterAll(async () => {
|
|
39
|
+
prometheus.closeAllConnections()
|
|
40
|
+
await new Promise<void>((resolve) => prometheus.close(() => resolve()))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/** A standalone analytics boot names the engines it may read (`engine-ids.ts`). */
|
|
44
|
+
const ENGINES = { ANALYTICS_ENGINE_IDS: "prod-a,prod-b" }
|
|
45
|
+
|
|
46
|
+
/** Boots the module from env the way the composition root does; returns its tools. */
|
|
47
|
+
function boot(env: NodeJS.ProcessEnv) {
|
|
48
|
+
const tools = new Map<string, ToolCallback>()
|
|
49
|
+
const server = {
|
|
50
|
+
tool: (definition: { name: string }, callback: ToolCallback) => {
|
|
51
|
+
tools.set(definition.name, callback)
|
|
52
|
+
},
|
|
53
|
+
use: () => {},
|
|
54
|
+
} as unknown as MCPServer
|
|
55
|
+
const plugin = analyticsModule.createPlugin(
|
|
56
|
+
{
|
|
57
|
+
...analyticsModule.configFromEnv({ PROMETHEUS_URL: url, ...ENGINES, ...env }),
|
|
58
|
+
toolset: "read-only",
|
|
59
|
+
},
|
|
60
|
+
{},
|
|
61
|
+
)
|
|
62
|
+
plugin.registerTools?.(server)
|
|
63
|
+
plugin.registerWidgetTools?.(server)
|
|
64
|
+
return tools
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("Prometheus config reaches the wire", () => {
|
|
68
|
+
it("PROMETHEUS_BEARER_TOKEN authenticates every query of a registrar tool", async () => {
|
|
69
|
+
hang = false
|
|
70
|
+
seenAuth.length = 0
|
|
71
|
+
const tools = boot({ PROMETHEUS_BEARER_TOKEN: "tok-123" })
|
|
72
|
+
const result = await tools.get("analytics_engine_landscape")!({})
|
|
73
|
+
expect(result.isError).toBeFalsy()
|
|
74
|
+
expect(seenAuth.length).toBeGreaterThan(0)
|
|
75
|
+
expect(seenAuth.every((auth) => auth === "Bearer tok-123")).toBe(true)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("PROMETHEUS_HEADERS and PROMETHEUS_USERNAME/PROMETHEUS_PASSWORD reach every query", async () => {
|
|
79
|
+
hang = false
|
|
80
|
+
seenAuth.length = 0
|
|
81
|
+
seenTenant.length = 0
|
|
82
|
+
const tools = boot({
|
|
83
|
+
PROMETHEUS_HEADERS: '{"X-Scope-OrgID":"tenant-a"}',
|
|
84
|
+
PROMETHEUS_USERNAME: "grafana",
|
|
85
|
+
PROMETHEUS_PASSWORD: "hunter2-pass",
|
|
86
|
+
})
|
|
87
|
+
const result = await tools.get("analytics_engine_landscape")!({})
|
|
88
|
+
expect(result.isError).toBeFalsy()
|
|
89
|
+
expect(seenTenant.length).toBeGreaterThan(0)
|
|
90
|
+
expect(seenTenant.every((tenant) => tenant === "tenant-a")).toBe(true)
|
|
91
|
+
const basic = `Basic ${Buffer.from("grafana:hunter2-pass").toString("base64")}`
|
|
92
|
+
expect(seenAuth.every((auth) => auth === basic)).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("PROMETHEUS_TIMEOUT_MS bounds a hung Prometheus", async () => {
|
|
96
|
+
hang = true
|
|
97
|
+
const tools = boot({ PROMETHEUS_TIMEOUT_MS: "60" })
|
|
98
|
+
const result = await tools.get("analytics_engine_landscape")!({})
|
|
99
|
+
expect(result.isError).toBe(true)
|
|
100
|
+
expect(result.content[0].text).toBe("Prometheus did not respond within 60 ms (timeout)")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("EVERY widget tool and feed hands ctx.signal to its queries", async () => {
|
|
104
|
+
hang = false
|
|
105
|
+
const tools = new Map<string, ToolCallback>()
|
|
106
|
+
const server = {
|
|
107
|
+
tool: (definition: { name: string }, callback: ToolCallback) => {
|
|
108
|
+
tools.set(definition.name, callback)
|
|
109
|
+
},
|
|
110
|
+
} as unknown as MCPServer
|
|
111
|
+
analyticsModule
|
|
112
|
+
.createPlugin(
|
|
113
|
+
{
|
|
114
|
+
...analyticsModule.configFromEnv({ PROMETHEUS_URL: url, ...ENGINES }),
|
|
115
|
+
toolset: "read-only",
|
|
116
|
+
},
|
|
117
|
+
{},
|
|
118
|
+
)
|
|
119
|
+
.registerWidgetTools?.(server)
|
|
120
|
+
// Each handler runs twice with args every tool accepts: unsignalled it
|
|
121
|
+
// must reach Prometheus, with an already-aborted ctx.signal it must not —
|
|
122
|
+
// a handler that drops ctx would still query. Tools that never query are
|
|
123
|
+
// named.
|
|
124
|
+
const args = {
|
|
125
|
+
processDefinitionKey: "order",
|
|
126
|
+
// Handlers called directly get no schema defaults: the compare windows.
|
|
127
|
+
windowDays: 7,
|
|
128
|
+
windowBeforeDays: 3,
|
|
129
|
+
windowAfterDays: 3,
|
|
130
|
+
// Relative to now: compare windows are held inside the retention.
|
|
131
|
+
deploymentTimestamp: new Date(Date.now() - 3 * 86_400_000).toISOString(),
|
|
132
|
+
engineA: "prod-a",
|
|
133
|
+
engineB: "prod-b",
|
|
134
|
+
}
|
|
135
|
+
const queriesOf = async (name: string, ctx: { signal?: AbortSignal }) => {
|
|
136
|
+
const before = seenAuth.length
|
|
137
|
+
await tools.get(name)!(args, ctx)
|
|
138
|
+
return seenAuth.length - before
|
|
139
|
+
}
|
|
140
|
+
const neverQuery: string[] = []
|
|
141
|
+
for (const name of tools.keys()) {
|
|
142
|
+
if ((await queriesOf(name, {})) === 0) {
|
|
143
|
+
neverQuery.push(name)
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
expect({ name, queries: await queriesOf(name, { signal: AbortSignal.abort() }) }).toEqual({
|
|
147
|
+
name,
|
|
148
|
+
queries: 0,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
// The settings and the heatmap's diagram (read from the engine) never ask Prometheus.
|
|
152
|
+
expect(neverQuery.sort()).toEqual([
|
|
153
|
+
"analytics_bpmn_xml_data",
|
|
154
|
+
"analytics_settings_data",
|
|
155
|
+
"analytics_show_settings",
|
|
156
|
+
])
|
|
157
|
+
expect(tools.size - neverQuery.length).toBeGreaterThanOrEqual(11)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("a widget feed's ctx.signal cancels its queries before the deadline", async () => {
|
|
161
|
+
hang = true
|
|
162
|
+
const tools = boot({ PROMETHEUS_TIMEOUT_MS: "10000" })
|
|
163
|
+
const controller = new AbortController()
|
|
164
|
+
setTimeout(() => controller.abort(), 30)
|
|
165
|
+
const started = Date.now()
|
|
166
|
+
const result = await tools.get(ANALYTICS_ENGINE_LANDSCAPE_DATA)!(
|
|
167
|
+
{},
|
|
168
|
+
{ signal: controller.signal },
|
|
169
|
+
)
|
|
170
|
+
expect(Date.now() - started).toBeLessThan(5_000)
|
|
171
|
+
expect(result.isError).toBe(true)
|
|
172
|
+
expect(result.content[0].text).toBe("Prometheus query cancelled by the caller")
|
|
173
|
+
})
|
|
174
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createLocalizeFor,
|
|
3
|
+
createLocalizeViewFor,
|
|
4
|
+
resolveAuthUserId,
|
|
5
|
+
resolveProfileKey,
|
|
6
|
+
} from "@miragon/pulse-widget-shell/server"
|
|
7
|
+
import { translator } from "./messages/index.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The profile port + key resolution come from the shared server kit, NOT from a
|
|
11
|
+
* module-local copy: every module reads and writes the same profile record, so
|
|
12
|
+
* a divergent key precedence (or a differently-spelled anonymous fallback)
|
|
13
|
+
* would silently split a user's settings across two records. Re-exported under
|
|
14
|
+
* this module's names so the call sites read in analytics vocabulary.
|
|
15
|
+
*
|
|
16
|
+
* `ProfileSource` is the minimal read/write view a module needs — the locale
|
|
17
|
+
* plus its own slice under `modules.analytics`. camunda7's `ProfileStore`
|
|
18
|
+
* satisfies it structurally, without analytics depending on the camunda7
|
|
19
|
+
* module; `save` merges per module key on the store side, so writing
|
|
20
|
+
* `modules.analytics` never touches other modules' slices.
|
|
21
|
+
*/
|
|
22
|
+
export type { ProfileSlice, ProfileSource } from "@miragon/pulse-widget-shell/server"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the profile key for the in-flight request: the OAuth caller (off the
|
|
26
|
+
* tool-handler `ctx`), else `anonymous` for an explicitly declared local
|
|
27
|
+
* caller (stdio, tests). A request without OAuth resolves `undefined` — reads
|
|
28
|
+
* fall back to defaults, saves fail visibly, so unrelated callers never
|
|
29
|
+
* cross-share one record.
|
|
30
|
+
*/
|
|
31
|
+
export const resolveSettingsKey = resolveProfileKey
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Just the OAuth half of {@link resolveSettingsKey} — the save path stamps it
|
|
35
|
+
* onto the record as its owner (`opts.userId`).
|
|
36
|
+
*/
|
|
37
|
+
export const resolveSettingsAuthUserId = resolveAuthUserId
|
|
38
|
+
|
|
39
|
+
/** A locale-bound translate for summaries; a show tool's summary translate + view title. */
|
|
40
|
+
export type { ServerT, ViewLocale } from "@miragon/pulse-widget-shell/server"
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the request locale and return a translate bound to it + the analytics
|
|
44
|
+
* catalogs — `const t = await localizeFor(store, ctx); … summary: t("key", { … })`.
|
|
45
|
+
* Pass the tool-handler `ctx` so an auth user id resolves the same record the
|
|
46
|
+
* save path writes. Locale resolution + the fail-soft rules (no store, no key,
|
|
47
|
+
* store OUTAGE → English) come from the shared `createLocalizeFor`.
|
|
48
|
+
*/
|
|
49
|
+
export const localizeFor = createLocalizeFor(translator)
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* {@link localizeFor} plus the view title, from ONE profile read — the kit's
|
|
53
|
+
* one title rule for every module's show tools: a view is titled only in a
|
|
54
|
+
* language the profile names; with "system" (the default), no caller or a
|
|
55
|
+
* store outage it carries no title, and the widget's own heading names it in
|
|
56
|
+
* the host's language (#322 U3). Summaries stay `t`:
|
|
57
|
+
* `const { t, title } = await localizeViewFor(store, ctx); … title: title("key")`.
|
|
58
|
+
*/
|
|
59
|
+
export const localizeViewFor = createLocalizeViewFor(translator)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { MCPServer } from "mcp-use"
|
|
2
|
+
import {
|
|
3
|
+
appOnly,
|
|
4
|
+
buildDataFeedResult,
|
|
5
|
+
buildSingleWidgetView,
|
|
6
|
+
requireProfileKey,
|
|
7
|
+
saveModuleSlice,
|
|
8
|
+
showToolBinding,
|
|
9
|
+
withToolErrors,
|
|
10
|
+
strictToolInput,
|
|
11
|
+
} from "@miragon/pulse-widget-shell/server"
|
|
12
|
+
import { ANALYTICS_SAVE_SETTINGS, ANALYTICS_SETTINGS_DATA } from "./tool-names.js"
|
|
13
|
+
import {
|
|
14
|
+
localizeFor,
|
|
15
|
+
localizeViewFor,
|
|
16
|
+
resolveSettingsAuthUserId,
|
|
17
|
+
resolveSettingsKey,
|
|
18
|
+
type ProfileSource,
|
|
19
|
+
type ServerT,
|
|
20
|
+
} from "./server-locale.js"
|
|
21
|
+
import {
|
|
22
|
+
ANALYTICS_MODULE_KEY,
|
|
23
|
+
analyticsSettingsSaveInput,
|
|
24
|
+
parseAnalyticsSettings,
|
|
25
|
+
settingsFor,
|
|
26
|
+
type AnalyticsSettings,
|
|
27
|
+
} from "./settings.js"
|
|
28
|
+
import { allowsDurableWrites, type AnalyticsToolset } from "./toolsets.js"
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The analytics module's settings section — its own show/data/save tool triple
|
|
32
|
+
* (the three render paths), so the module plugs into the settings page without
|
|
33
|
+
* camunda7 owning any analytics vocabulary. The slice persists under
|
|
34
|
+
* `modules.analytics` in the shared profile record; the shared store merges
|
|
35
|
+
* per module key, so this never touches other modules' slices.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** What the settings widget renders: the effective slice + whether saving is wired. */
|
|
39
|
+
export interface AnalyticsSettingsView {
|
|
40
|
+
settings: AnalyticsSettings
|
|
41
|
+
/**
|
|
42
|
+
* False without a writable store, in the `read-only` toolset (also what a
|
|
43
|
+
* missing toolset resolves to), or when the request carries no caller
|
|
44
|
+
* identity (no OAuth — see `resolveProfileKey`) — the widget hides Save.
|
|
45
|
+
*/
|
|
46
|
+
canSave: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function registerSettingsTools(
|
|
50
|
+
server: MCPServer,
|
|
51
|
+
profileStore?: ProfileSource,
|
|
52
|
+
toolset?: AnalyticsToolset,
|
|
53
|
+
): void {
|
|
54
|
+
// The save tool is a durable write registered OUTSIDE the tool registrar, so
|
|
55
|
+
// it gates itself against the module's toolset (see `allowsDurableWrites`):
|
|
56
|
+
// only `standard` saves. A missing toolset resolves to the `read-only`
|
|
57
|
+
// floor, and an unknown name from an untyped caller warns and degrades to it.
|
|
58
|
+
const store = profileStore
|
|
59
|
+
const save = allowsDurableWrites(toolset) ? store?.save?.bind(store) : undefined
|
|
60
|
+
|
|
61
|
+
const loadView = async (ctx: unknown): Promise<AnalyticsSettingsView> => ({
|
|
62
|
+
settings: await settingsFor(store, ctx),
|
|
63
|
+
// Store/toolset gate AND per-request identity: without a profile key the
|
|
64
|
+
// save tool refuses, so the section must render read-only instead of a
|
|
65
|
+
// Save button whose click errors.
|
|
66
|
+
canSave: Boolean(save) && resolveSettingsKey(ctx) !== undefined,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const summarize = (t: ServerT, view: AnalyticsSettingsView): string =>
|
|
70
|
+
t("aSum.settings", {
|
|
71
|
+
period: view.settings.defaultPeriod,
|
|
72
|
+
minBucketSize: view.settings.minBucketSize,
|
|
73
|
+
changeHint: view.canSave ? t("aSum.settingsChangeHint") : "",
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
server.tool(
|
|
77
|
+
{
|
|
78
|
+
name: "analytics_show_settings",
|
|
79
|
+
title: "Analytics Settings",
|
|
80
|
+
description:
|
|
81
|
+
"Open the analytics settings section: the default look-back period and the minimum comparison bucket size applied when analytics calls omit them.",
|
|
82
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
83
|
+
inputSchema: strictToolInput({}),
|
|
84
|
+
...showToolBinding("analytics_show_settings", "Analytics Settings"),
|
|
85
|
+
},
|
|
86
|
+
withToolErrors(async (_params, ctx) => {
|
|
87
|
+
const view = await loadView(ctx)
|
|
88
|
+
const { t, title } = await localizeViewFor(store, ctx)
|
|
89
|
+
return buildSingleWidgetView({
|
|
90
|
+
widget: "analytics:settings",
|
|
91
|
+
app: "analytics",
|
|
92
|
+
dataType: "analytics:settings",
|
|
93
|
+
data: { ...view },
|
|
94
|
+
// The view title in the language the profile names (the section
|
|
95
|
+
// heading's own key); none with "system", where the heading names it.
|
|
96
|
+
title: title("aSettings.heading"),
|
|
97
|
+
summary: summarize(t, view),
|
|
98
|
+
})
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
server.tool(
|
|
103
|
+
{
|
|
104
|
+
name: ANALYTICS_SETTINGS_DATA,
|
|
105
|
+
title: "Analytics settings data (internal)",
|
|
106
|
+
description:
|
|
107
|
+
"Internal JSON feed (no UI) for the analytics settings section's self-fetch. Prefer analytics_show_settings.",
|
|
108
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
109
|
+
inputSchema: strictToolInput({}),
|
|
110
|
+
...appOnly,
|
|
111
|
+
},
|
|
112
|
+
withToolErrors(async (_params, ctx) => buildDataFeedResult({ ...(await loadView(ctx)) })),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// Without a writable store (or on the `read-only` floor) there is nothing to
|
|
116
|
+
// save into — the section stays read-only (canSave: false) and the tool
|
|
117
|
+
// surface honestly reflects that.
|
|
118
|
+
if (!save || !store) return
|
|
119
|
+
|
|
120
|
+
server.tool(
|
|
121
|
+
{
|
|
122
|
+
name: ANALYTICS_SAVE_SETTINGS,
|
|
123
|
+
title: "Save analytics settings",
|
|
124
|
+
description:
|
|
125
|
+
"Update the caller's analytics defaults. Only the provided fields change; omitted fields keep their value. The saved defaults apply whenever an analytics call omits `period` or `minBucketSize`.",
|
|
126
|
+
// The module's only non-read-only tool, and explicitly NOT destructive
|
|
127
|
+
// (MCP presumes a write destructive unless told otherwise): it merges
|
|
128
|
+
// the caller's own slice, nothing is deleted or overwritten wholesale.
|
|
129
|
+
annotations: {
|
|
130
|
+
readOnlyHint: false,
|
|
131
|
+
destructiveHint: false,
|
|
132
|
+
idempotentHint: true,
|
|
133
|
+
openWorldHint: false,
|
|
134
|
+
},
|
|
135
|
+
inputSchema: strictToolInput(analyticsSettingsSaveInput.shape),
|
|
136
|
+
// No view binding / app visibility: a normal model-visible tool; the
|
|
137
|
+
// settings widget also calls it and reads the saved slice back from
|
|
138
|
+
// structuredContent.
|
|
139
|
+
},
|
|
140
|
+
withToolErrors(async (params, ctx) => {
|
|
141
|
+
// Keyless refusal + a patch-only slice save are the shared slice-write
|
|
142
|
+
// contract (`@miragon/pulse-widget-shell/server`) — every module's save
|
|
143
|
+
// tool uses the same pair. The store merges the patch under its per-key
|
|
144
|
+
// lock, so a concurrent save of the other field is never reverted.
|
|
145
|
+
const key = requireProfileKey(ctx)
|
|
146
|
+
// The OAuth caller is stamped as the record's owner.
|
|
147
|
+
const savedSlice = await saveModuleSlice(
|
|
148
|
+
{ get: (k) => store.get(k), save },
|
|
149
|
+
key,
|
|
150
|
+
ANALYTICS_MODULE_KEY,
|
|
151
|
+
params,
|
|
152
|
+
{ userId: resolveSettingsAuthUserId(ctx) },
|
|
153
|
+
)
|
|
154
|
+
const effective = parseAnalyticsSettings({ [ANALYTICS_MODULE_KEY]: savedSlice })
|
|
155
|
+
const t = await localizeFor(store, ctx)
|
|
156
|
+
return {
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: "text" as const,
|
|
160
|
+
text: t("aSum.settingsSaved", {
|
|
161
|
+
period: effective.defaultPeriod,
|
|
162
|
+
minBucketSize: effective.minBucketSize,
|
|
163
|
+
}),
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
structuredContent: effective as unknown as Record<string, unknown>,
|
|
167
|
+
}
|
|
168
|
+
}),
|
|
169
|
+
)
|
|
170
|
+
}
|