import type { ConditionKind } from '../../api/queries/alertRules'; export type VariableType = | 'string' | 'Instant' | 'number' | 'boolean' | 'url' | 'uuid'; export interface AlertVariable { path: string; // e.g. "alert.firedAt" type: VariableType; description: string; sampleValue: string; // rendered as a faint suggestion preview availableForKinds: 'always' | ConditionKind[]; mayBeNull?: boolean; // show "may be null" badge in UI } /** Variables the spec ยง8 context map exposes. Add to this registry whenever * NotificationContextBuilder (backend) gains a new leaf. */ export const ALERT_VARIABLES: AlertVariable[] = [ // Always available { path: 'env.slug', type: 'string', description: 'Environment slug', sampleValue: 'prod', availableForKinds: 'always' }, { path: 'env.id', type: 'uuid', description: 'Environment UUID', sampleValue: '00000000-0000-0000-0000-000000000001', availableForKinds: 'always' }, { path: 'rule.id', type: 'uuid', description: 'Rule UUID', sampleValue: '11111111-...', availableForKinds: 'always' }, { path: 'rule.name', type: 'string', description: 'Rule display name', sampleValue: 'Order API error rate', availableForKinds: 'always' }, { path: 'rule.severity', type: 'string', description: 'Rule severity', sampleValue: 'CRITICAL', availableForKinds: 'always' }, { path: 'rule.description', type: 'string', description: 'Rule description', sampleValue: 'Paging ops if error rate >5%', availableForKinds: 'always' }, { path: 'alert.id', type: 'uuid', description: 'Alert instance UUID', sampleValue: '22222222-...', availableForKinds: 'always' }, { path: 'alert.state', type: 'string', description: 'Alert state', sampleValue: 'FIRING', availableForKinds: 'always' }, { path: 'alert.firedAt', type: 'Instant', description: 'When the alert fired', sampleValue: '2026-04-20T14:33:10Z', availableForKinds: 'always' }, { path: 'alert.resolvedAt', type: 'Instant', description: 'When the alert resolved', sampleValue: '2026-04-20T14:45:00Z', availableForKinds: 'always', mayBeNull: true }, { path: 'alert.ackedBy', type: 'string', description: 'User who ack\'d the alert', sampleValue: 'alice', availableForKinds: 'always', mayBeNull: true }, { path: 'alert.link', type: 'url', description: 'UI link to this alert', sampleValue: 'https://cameleer.example.com/alerts/inbox/2222...', availableForKinds: 'always' }, { path: 'alert.currentValue', type: 'number', description: 'Observed metric value', sampleValue: '0.12', availableForKinds: 'always', mayBeNull: true }, { path: 'alert.threshold', type: 'number', description: 'Rule threshold', sampleValue: '0.05', availableForKinds: 'always', mayBeNull: true }, // App subtree โ€” populated on every kind except env-wide rules { path: 'app.slug', type: 'string', description: 'App slug', sampleValue: 'orders', availableForKinds: ['ROUTE_METRIC', 'EXCHANGE_MATCH', 'AGENT_STATE', 'DEPLOYMENT_STATE', 'LOG_PATTERN', 'JVM_METRIC'], mayBeNull: true }, { path: 'app.id', type: 'uuid', description: 'App UUID', sampleValue: '33333333-...', availableForKinds: ['ROUTE_METRIC', 'EXCHANGE_MATCH', 'AGENT_STATE', 'DEPLOYMENT_STATE', 'LOG_PATTERN', 'JVM_METRIC'], mayBeNull: true }, // ROUTE_METRIC + EXCHANGE_MATCH share route.* { path: 'route.id', type: 'string', description: 'Route ID', sampleValue: 'route-1', availableForKinds: ['ROUTE_METRIC', 'EXCHANGE_MATCH'] }, { path: 'route.uri', type: 'string', description: 'Route URI', sampleValue: 'direct:orders', availableForKinds: ['ROUTE_METRIC', 'EXCHANGE_MATCH'] }, // EXCHANGE_MATCH { path: 'exchange.id', type: 'string', description: 'Exchange ID', sampleValue: 'exch-ab12', availableForKinds: ['EXCHANGE_MATCH'] }, { path: 'exchange.status', type: 'string', description: 'Exchange status', sampleValue: 'FAILED', availableForKinds: ['EXCHANGE_MATCH'] }, // AGENT_STATE + JVM_METRIC share agent.id/name; AGENT_STATE adds agent.state { path: 'agent.id', type: 'string', description: 'Agent instance ID', sampleValue: 'prod-orders-0', availableForKinds: ['AGENT_STATE', 'JVM_METRIC'] }, { path: 'agent.name', type: 'string', description: 'Agent display name', sampleValue: 'orders-0', availableForKinds: ['AGENT_STATE', 'JVM_METRIC'] }, { path: 'agent.state', type: 'string', description: 'Agent state', sampleValue: 'DEAD', availableForKinds: ['AGENT_STATE'] }, // DEPLOYMENT_STATE { path: 'deployment.id', type: 'uuid', description: 'Deployment UUID', sampleValue: '44444444-...', availableForKinds: ['DEPLOYMENT_STATE'] }, { path: 'deployment.status', type: 'string', description: 'Deployment status', sampleValue: 'FAILED', availableForKinds: ['DEPLOYMENT_STATE'] }, // LOG_PATTERN โ€” leaf names match NotificationContextBuilder (log.pattern + log.matchCount) { path: 'log.pattern', type: 'string', description: 'Matched log pattern', sampleValue: 'TimeoutException', availableForKinds: ['LOG_PATTERN'] }, { path: 'log.matchCount', type: 'number', description: 'Matches in window', sampleValue: '7', availableForKinds: ['LOG_PATTERN'] }, // JVM_METRIC { path: 'metric.name', type: 'string', description: 'Metric name', sampleValue: 'heap_used_percent', availableForKinds: ['JVM_METRIC'] }, { path: 'metric.value', type: 'number', description: 'Metric value', sampleValue: '92.1', availableForKinds: ['JVM_METRIC'] }, ]; /** Filter variables to those available for the given condition kind. * If kind is undefined (e.g. connection URL editor), returns only "always" vars + app.*. */ export function availableVariables( kind: ConditionKind | undefined, opts: { reducedContext?: boolean } = {}, ): AlertVariable[] { if (opts.reducedContext) { return ALERT_VARIABLES.filter((v) => v.path.startsWith('env.')); } if (!kind) { return ALERT_VARIABLES.filter( (v) => v.availableForKinds === 'always', ); } return ALERT_VARIABLES.filter( (v) => v.availableForKinds === 'always' || v.availableForKinds.includes(kind), ); } /** Parse a Mustache template and return the set of `{{path}}` references it contains. * Ignores `{{#section}}` / `{{/section}}` / `{{!comment}}` โ€” plain variable refs only. */ export function extractReferences(template: string): string[] { const out: string[] = []; const re = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)\s*\}\}/g; let m; while ((m = re.exec(template)) !== null) out.push(m[1]); return out; } /** Find references in a template that are not in the allowed-variable set. */ export function unknownReferences( template: string, allowed: readonly AlertVariable[], ): string[] { const allowedSet = new Set(allowed.map((v) => v.path)); return extractReferences(template).filter((r) => !allowedSet.has(r)); }