Skip to content

Commit 859f9df

Browse files
committed
feat: add the /report health report (get_report MCP tool + CLI + API)
1 parent 0dfaec2 commit 859f9df

22 files changed

Lines changed: 3105 additions & 3 deletions

.changeset/report-health.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@trigger.dev/core": patch
3+
"trigger.dev": patch
4+
---
5+
6+
Add the `get_report` MCP tool, a `trigger report` CLI command, and `GET /api/v1/reports/:key`, starting with the `health` report: a deterministic verdict on whether work is flowing, whether the runs that start are healthy, and whether telemetry is fresh — rendered as text with unicode sparklines (coloured in a terminal). Also adds a `report` MCP prompt, surfaced as a slash command in hosts that support prompts.
7+
8+
Also: `trigger mcp` now always starts the server — the install wizard is gated behind `trigger mcp --install`. A TTY previously launched the wizard, so MCP hosts spawning the server over a PTY timed out.
2.99 KB
Binary file not shown.
24.6 KB
Binary file not shown.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/**
2+
* The `health` report's PROSE — the single place its codes resolve to strings. Registered
3+
* under the "health" title so the generic renderer can look it up by `vm.title` without ever
4+
* importing health vocabulary. A future report (Cost, Regression) ships its own catalog the
5+
* same way; `report-messages.ts` stays report-agnostic infrastructure.
6+
*
7+
* Some strings carry {tokens} (e.g. {age}, {rate}) the renderer fills from the finding's
8+
* metrics / evidence — meaning lives here, numbers stay facts.
9+
*/
10+
11+
import { registerReportMessages, type ReportMessages } from "../report-messages";
12+
import { type ReasonCode, type Severity } from "../report-view-model";
13+
14+
/** Metric id -> expanded display label. */
15+
const METRIC_LABELS: Record<string, string> = {
16+
start_latency_p95: "start latency",
17+
pending: "pending",
18+
throughput: "throughput",
19+
failures: "failures",
20+
dur_p95: "p95 dur",
21+
liveness: "liveness",
22+
concurrency: "concurrency",
23+
throttled: "throttled",
24+
triggered: "triggered",
25+
};
26+
27+
/**
28+
* Finding headline — keyed by `${findingType}/${reason}`. Degraded = the cause,
29+
* healthy = reassurance. `@expanded` variants show a healthy finding expanded
30+
* (e.g. execution while flow is degraded).
31+
*/
32+
const FINDING_REASONS: Record<string, string> = {
33+
// flow — causes
34+
"flow/env_limit_saturation": "at your env concurrency limit",
35+
"flow/dequeue_stall": "capacity is free but nothing is dequeuing",
36+
"flow/queue_limit_throttling": "a queue is throttling at its own limit",
37+
"flow/trigger_spike": "a trigger spike is backing up the queue",
38+
"flow/trigger_surge": "a surge of new triggers is backing up the queue",
39+
// flow — fallback symptoms (v1)
40+
"flow/start_latency": "runs are slow to start",
41+
"flow/backlog": "backlog is growing",
42+
"flow/throughput_lag": "completion is falling behind triggers",
43+
"flow/degraded": "flow is degraded",
44+
// flow — healthy (collapsed)
45+
"flow/healthy": "starting normally",
46+
// execution
47+
"execution/failures_up": "runs are failing more than usual",
48+
"execution/slow_runs": "runs are slower than usual",
49+
"execution/degraded": "execution is degraded",
50+
"execution/unknown": "execution can't be assessed — the telemetry is stale",
51+
"flow/unknown": "flow can't be assessed — the telemetry is stale",
52+
"execution/healthy": "completing normally", // collapsed
53+
"execution/healthy@expanded": "the runs that DO start are fine",
54+
// liveness = telemetry freshness ({age} filled by the renderer)
55+
"liveness/fresh": "fresh — telemetry current, updated {age} ago",
56+
"liveness/lagging": "lagging — telemetry last updated {age} ago",
57+
"liveness/stale": "stale — no telemetry in {age}",
58+
"liveness/freshness_unknown": "freshness unknown — no telemetry signal to check",
59+
};
60+
61+
/** The "read:" causal-chain line — keyed by code. */
62+
const READS: Record<string, string> = {
63+
// cause chains
64+
saturation_chain: "limit saturated → incoming work exceeds capacity → backlog grows",
65+
capacity_free_not_dequeuing: "capacity is free but work isn't being picked up — on our side",
66+
queue_throttle_chain: "queue at its limit → its runs wait → backlog grows",
67+
spike_chain: "triggers jumped {mult}× → queue fills faster than it drains",
68+
surge_chain: "new triggers arriving with no prior baseline → queue fills faster than it drains",
69+
// fallback symptoms (v1)
70+
starting_normally: "runs are starting on time",
71+
lag_while_triggering_normal: "triggering normally, but starts lag → work is backing up",
72+
lag_and_failures: "runs are lagging AND failing — check the code path",
73+
degraded_generic: "flow is degraded",
74+
not_a_code_problem: "NOT a code problem",
75+
runs_are_fine: "runs are completing normally",
76+
failures_elevated: "failures are elevated — check the code path",
77+
data_stale: "data is stale — the verdict cannot be trusted",
78+
};
79+
80+
/** Exclusion (ruled-out cause) — rendered under `read:`. {tokens} filled from evidence. */
81+
const EXCLUSIONS: Record<string, string> = {
82+
not_env_limit: "env concurrency limit is not the bottleneck",
83+
not_your_code: "not your code — failures and durations normal",
84+
not_your_config: "not your config — limits aren't the bottleneck",
85+
};
86+
87+
/** Observation (supporting fact, not a ruled-out cause) — rendered under `read:` after exclusions. */
88+
const OBSERVATIONS: Record<string, string> = {
89+
not_workers_platform: "runs are completing at ~{rate}/min",
90+
execution_healthy: "runs that start are completing normally",
91+
nothing_dead_lettered: "nothing dead-lettered",
92+
};
93+
94+
/** Metric annotation shown on a cause line INSTEAD of "(normal ~x)". {tokens} filled by the renderer. */
95+
const ANNOTATIONS: Record<string, string> = {
96+
pinned_minutes: "pinned {value} of last {window} min",
97+
idle_share: "idle — {value} running of {limit}",
98+
throttled_minutes: "throttled {value} of last {window} min",
99+
spike_mult: "{value}× the normal rate",
100+
surge_rate: "{value}/min, no prior baseline",
101+
};
102+
103+
/** Headline statement — keyed by `${findingType}/${severity}`. */
104+
const STATEMENTS: Record<string, string> = {
105+
"flow/ok": "Flow healthy",
106+
"flow/warn": "Flow slowing",
107+
"flow/crit": "Flow stalled",
108+
"execution/ok": "Execution healthy",
109+
"execution/warn": "Execution degraded",
110+
"execution/crit": "Execution failing",
111+
"liveness/ok": "data fresh",
112+
"liveness/warn": "data lagging",
113+
"liveness/crit": "data stale",
114+
};
115+
116+
/** Recommendation / footer codes -> calm, jargon-free action text. */
117+
const ACTIONS: Record<string, string> = {
118+
// Review = open concrete data · Check = system state · Raise = a settings change
119+
review_start_latency: "Review start latency",
120+
review_failing_tasks: "Review failing tasks",
121+
review_slow_runs: "Review slow runs",
122+
review_trigger_source: "Review what's triggering the spike",
123+
check_queue_health: "Check queue health",
124+
check_worker_availability: "Check worker availability",
125+
check_control_plane: "Check control plane",
126+
check_platform_status: "Check status.trigger.dev — no action needed on yours",
127+
raise_env_limit: "Raise the env concurrency limit",
128+
raise_queue_limit: "Raise the queue's concurrency limit",
129+
do_nothing_drains: "or do nothing — backlog drains in ~{value} min once triggers ease",
130+
region_failover: "region move? ask your agent — depends on your failover setup",
131+
nothing_to_do: "nothing to do",
132+
};
133+
134+
function findingReason(
135+
findingType: string,
136+
reason: ReasonCode,
137+
opts?: { expanded?: boolean }
138+
): string {
139+
if (opts?.expanded) {
140+
const expanded = FINDING_REASONS[`${findingType}/${reason}@expanded`];
141+
if (expanded) return expanded;
142+
}
143+
return FINDING_REASONS[`${findingType}/${reason}`] ?? reason;
144+
}
145+
146+
function statementMessage(findingType: string, severity: Severity, reason?: ReasonCode): string {
147+
// Stale telemetry makes a CH-derived verdict untrustworthy — say so, don't show a severity.
148+
if (reason === "unknown") {
149+
const label = findingType.charAt(0).toUpperCase() + findingType.slice(1);
150+
return `${label} unknown — data stale`;
151+
}
152+
// No freshness signal is NOT "data lagging" (a real severity) — it's genuinely unknown.
153+
if (findingType === "liveness" && reason === "freshness_unknown") {
154+
return "data freshness unknown";
155+
}
156+
return STATEMENTS[`${findingType}/${severity}`] ?? `${findingType} ${severity}`;
157+
}
158+
159+
export const healthMessages: ReportMessages = {
160+
metricLabel: (id) => METRIC_LABELS[id] ?? id,
161+
findingReason,
162+
readMessage: (code) => READS[code] ?? code,
163+
exclusionMessage: (code) => EXCLUSIONS[code] ?? code,
164+
observationMessage: (code) => OBSERVATIONS[code] ?? code,
165+
annotationMessage: (code) => ANNOTATIONS[code] ?? code,
166+
statementMessage,
167+
actionMessage: (code) => ACTIONS[code] ?? code,
168+
};
169+
170+
registerReportMessages("health", healthMessages);

0 commit comments

Comments
 (0)