From 430218fc1cbb88ee04e5757f243dfd19966f33f7 Mon Sep 17 00:00:00 2001 From: Loakesh Indiran Date: Tue, 3 Nov 2026 23:28:33 +0530 Subject: [PATCH] fix(templates): set the resolved event on alert recoveries build_notification_context and the alert sample never set event, so a recovered alert rendered {notification_event} as firing next to alert_status=resolved. Both now follow resolved. Also moves the saveCustomDestination comment back above its function. --- src/core/src/alerts/alert.rs | 31 +++++++++++++++++++ src/core/src/alerts/notifications/examples.rs | 14 +++++++++ web/src/components/alerts/AddDestination.vue | 10 +++--- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/core/src/alerts/alert.rs b/src/core/src/alerts/alert.rs index e54c168cb3..7d1c15ddd6 100644 --- a/src/core/src/alerts/alert.rs +++ b/src/core/src/alerts/alert.rs @@ -4232,6 +4232,11 @@ async fn build_notification_context( crate::alerts::notifications::STATUS_FIRING } .to_string(), + event: if resolved { + config::meta::alerts::notification_kind::NotificationEvent::Resolved + } else { + config::meta::alerts::notification_kind::NotificationEvent::Firing + }, episode_id, recovery_reason: String::new(), alert_priority: alert.priority.map(|p| p.to_string()).unwrap_or_default(), @@ -9338,4 +9343,30 @@ mod rule_notification_tests { assert!(!url.contains("sql_mode")); assert!(!url.contains("query=")); } + + #[tokio::test] + async fn recovered_alert_context_carries_the_resolved_event() { + use config::meta::alerts::notification_kind::NotificationEvent; + + let mut alert = Alert::default(); + alert.org_id = "org".into(); + alert.stream_name = "default".into(); + for (resolved, want) in [ + (false, NotificationEvent::Firing), + (true, NotificationEvent::Resolved), + ] { + let options = ProcessTemplateOptions { + rows_end_time: 0, + start_time: None, + evaluation_timestamp: 0, + is_email: false, + level: None, + actual_value: None, + episode_id: None, + resolved, + }; + let ctx = build_notification_context("org", &alert, &[], &[], options, None).await; + assert_eq!(ctx.event, want); + } + } } diff --git a/src/core/src/alerts/notifications/examples.rs b/src/core/src/alerts/notifications/examples.rs index ae9195d78f..1138ab1915 100644 --- a/src/core/src/alerts/notifications/examples.rs +++ b/src/core/src/alerts/notifications/examples.rs @@ -198,6 +198,7 @@ pub fn example_context(kind: NotificationKind, resolved: bool) -> NotificationCo }; let mut ctx = super::preview::synthetic_context(Some(level)); ctx.kind = kind; + ctx.event = event_of(resolved); ctx } } @@ -625,6 +626,19 @@ mod tests { } /// Regenerate `render/testdata/examples/`; hand-check every file before committing it. + #[test] + fn alert_example_event_follows_resolved() { + let kind = NotificationKind::Alert; + assert_eq!( + example_context(kind, false).event, + NotificationEvent::Firing + ); + assert_eq!( + example_context(kind, true).event, + NotificationEvent::Resolved + ); + } + #[test] #[ignore] fn capture_examples() { diff --git a/web/src/components/alerts/AddDestination.vue b/web/src/components/alerts/AddDestination.vue index da1b3963a8..069686c52e 100644 --- a/web/src/components/alerts/AddDestination.vue +++ b/web/src/components/alerts/AddDestination.vue @@ -1476,11 +1476,6 @@ function headersFromRows(rows: { key: string; value: string }[]): Headers { return headers; } -// The custom/pipeline save path (dispatched from saveDestination for non-prebuilt -// types). OForm calls the submit only once the schema (incl. the type-keyed -// superRefine) passes — the schema, not a manual guard, gates the save. `value` -// is the validated payload source of truth; the payload is built with explicit -// keys so no schema-only field (e.g. `credentials`) leaks into the request. /** The stored metadata.payload_format as the select's value; anything else is automatic. */ function storedPayloadFormat( metadata: Record | null, @@ -1505,6 +1500,11 @@ function withPayloadFormat( return metadata; } +// The custom/pipeline save path (dispatched from saveDestination for non-prebuilt +// types). OForm calls the submit only once the schema (incl. the type-keyed +// superRefine) passes — the schema, not a manual guard, gates the save. `value` +// is the validated payload source of truth; the payload is built with explicit +// keys so no schema-only field (e.g. `credentials`) leaks into the request. function saveCustomDestination(value: AddDestinationForm) { const dismiss = toast({ variant: "loading",