diff --git a/crates/lib/pager/src/client/pager_client.rs b/crates/lib/pager/src/client/pager_client.rs index 052af7337b..42e6605817 100644 --- a/crates/lib/pager/src/client/pager_client.rs +++ b/crates/lib/pager/src/client/pager_client.rs @@ -116,10 +116,14 @@ impl PagerClient { labels.push(("http_method".to_string(), method.clone())); } - if let Some(path) = ¬ification.endpoint_path { + if let Some(path) = ¬ification.http_path { labels.push(("http_path".to_string(), path.clone())); } + if let Some(status_code) = notification.http_status_code { + labels.push(("http_status_code".to_string(), status_code.to_string())); + } + let labels = if labels.is_empty() { None } else { Some(labels) }; // Enrich the description with HTTP context and hostname if present. @@ -131,10 +135,14 @@ impl PagerClient { parts.push(format!("HTTP Method: {}", method)); } - if let Some(path) = ¬ification.endpoint_path { + if let Some(path) = ¬ification.http_path { parts.push(format!("HTTP Path: {}", path)); } + if let Some(status_code) = notification.http_status_code { + parts.push(format!("HTTP Status Code: {}", status_code)); + } + if let Some(h) = &self.hostname { parts.push(format!("Hostname: {}", h)); } diff --git a/crates/lib/pager/src/notification/generate_deduplication_key.rs b/crates/lib/pager/src/notification/generate_deduplication_key.rs index 1f2f962202..1cf425f9f8 100644 --- a/crates/lib/pager/src/notification/generate_deduplication_key.rs +++ b/crates/lib/pager/src/notification/generate_deduplication_key.rs @@ -23,11 +23,15 @@ pub(crate) fn generate_deduplication_key(details: &NotificationDetails) -> Strin hasher.update(details.summary.as_bytes()); hasher.update(if details.is_from_error { b"1" } else { b"0" }); - if let (Some(method), Some(path)) = (&details.http_method, &details.endpoint_path) { + if let (Some(method), Some(path)) = (&details.http_method, &details.http_path) { hasher.update(method.as_bytes()); hasher.update(path.as_bytes()); } + if let Some(status_code) = details.http_status_code { + hasher.update(status_code.to_le_bytes()); + } + hasher.update(hours_since_epoch(details.event_time).to_le_bytes()); let result = hasher.finalize(); @@ -43,7 +47,7 @@ mod tests { summary: &str, is_from_error: bool, http_method: Option<&str>, - endpoint_path: Option<&str>, + http_path: Option<&str>, event_time: DateTime, ) -> NotificationDetails { NotificationDetails { @@ -51,7 +55,8 @@ mod tests { description: None, event_time, http_method: http_method.map(|s| s.to_string()), - endpoint_path: endpoint_path.map(|s| s.to_string()), + http_path: http_path.map(|s| s.to_string()), + http_status_code: None, is_from_error, } } @@ -188,6 +193,70 @@ mod tests { } } + mod http_status_code_tests { + use super::*; + + fn make_details_with_status( + summary: &str, + is_from_error: bool, + http_method: Option<&str>, + http_path: Option<&str>, + http_status_code: Option, + event_time: DateTime, + ) -> NotificationDetails { + NotificationDetails { + summary: summary.to_string(), + description: None, + event_time, + http_method: http_method.map(|s| s.to_string()), + http_path: http_path.map(|s| s.to_string()), + http_status_code, + is_from_error, + } + } + + #[test] + fn different_status_code_different_key() { + let time = Utc.with_ymd_and_hms(2026, 3, 30, 14, 0, 0).unwrap(); + + let a = make_details_with_status("db error", true, Some("POST"), Some("/v1/jobs"), Some(500), time); + let b = make_details_with_status("db error", true, Some("POST"), Some("/v1/jobs"), Some(503), time); + + assert_ne!(generate_deduplication_key(&a), generate_deduplication_key(&b)); + } + + #[test] + fn status_code_vs_none_different_key() { + let time = Utc.with_ymd_and_hms(2026, 3, 30, 14, 0, 0).unwrap(); + + let a = make_details_with_status("db error", true, Some("POST"), Some("/v1/jobs"), Some(500), time); + let b = make_details_with_status("db error", true, Some("POST"), Some("/v1/jobs"), None, time); + + assert_ne!(generate_deduplication_key(&a), generate_deduplication_key(&b)); + } + + #[test] + fn same_status_code_same_key() { + let t1 = Utc.with_ymd_and_hms(2026, 3, 30, 14, 0, 0).unwrap(); + let t2 = Utc.with_ymd_and_hms(2026, 3, 30, 14, 30, 0).unwrap(); + + let a = make_details_with_status("timeout", true, Some("GET"), Some("/v1/users"), Some(504), t1); + let b = make_details_with_status("timeout", true, Some("GET"), Some("/v1/users"), Some(504), t2); + + assert_eq!(generate_deduplication_key(&a), generate_deduplication_key(&b)); + } + + #[test] + fn status_code_without_http_fields_still_differentiates() { + let time = Utc.with_ymd_and_hms(2026, 3, 30, 14, 0, 0).unwrap(); + + let a = make_details_with_status("error", true, None, None, Some(500), time); + let b = make_details_with_status("error", true, None, None, Some(502), time); + + assert_ne!(generate_deduplication_key(&a), generate_deduplication_key(&b)); + } + } + mod other_behaviors { use super::*; #[test] diff --git a/crates/lib/pager/src/notification/notification_details.rs b/crates/lib/pager/src/notification/notification_details.rs index 06b8282648..db81b39323 100644 --- a/crates/lib/pager/src/notification/notification_details.rs +++ b/crates/lib/pager/src/notification/notification_details.rs @@ -18,8 +18,11 @@ pub struct NotificationDetails { /// HTTP method associated with the event, if any. pub http_method: Option, - /// Endpoint path associated with the event, if any. - pub endpoint_path: Option, + /// HTTP endpoint path associated with the event, if any. + pub http_path: Option, + + /// HTTP status code associated with the event, if any. + pub http_status_code: Option, /// Whether this notification originated from an error. pub is_from_error: bool, @@ -37,7 +40,8 @@ impl NotificationDetails { description: Some(description), event_time: Utc::now(), http_method: None, - endpoint_path: None, + http_path: None, + http_status_code: None, is_from_error: false, } } @@ -49,7 +53,8 @@ impl NotificationDetails { description: None, event_time: Utc::now(), http_method: None, - endpoint_path: None, + http_path: None, + http_status_code: None, is_from_error: false, } } @@ -108,7 +113,8 @@ impl NotificationDetails { description: Some(description), event_time, http_method: None, - endpoint_path: None, + http_path: None, + http_status_code: None, is_from_error: true, } } diff --git a/crates/lib/pager/src/notification/notification_details_builder.rs b/crates/lib/pager/src/notification/notification_details_builder.rs index 0e22a6c3bb..e8c83dc1a9 100644 --- a/crates/lib/pager/src/notification/notification_details_builder.rs +++ b/crates/lib/pager/src/notification/notification_details_builder.rs @@ -8,7 +8,8 @@ pub struct NotificationDetailsBuilder { description: Option, event_time: DateTime, http_method: Option, - endpoint_path: Option, + http_path: Option, + http_status_code: Option, is_from_error: bool, } @@ -20,7 +21,8 @@ impl NotificationDetailsBuilder { description: None, event_time: Utc::now(), http_method: None, - endpoint_path: None, + http_path: None, + http_status_code: None, is_from_error: false, } } @@ -37,7 +39,8 @@ impl NotificationDetailsBuilder { description: details.description, event_time: details.event_time, http_method: None, - endpoint_path: None, + http_path: None, + http_status_code: None, is_from_error: true, } } @@ -52,8 +55,13 @@ impl NotificationDetailsBuilder { self } - pub fn set_endpoint_path(mut self, endpoint_path: Option) -> Self { - self.endpoint_path = endpoint_path; + pub fn set_http_path(mut self, http_path: Option) -> Self { + self.http_path = http_path; + self + } + + pub fn set_http_status_code(mut self, http_status_code: Option) -> Self { + self.http_status_code = http_status_code; self } @@ -63,7 +71,8 @@ impl NotificationDetailsBuilder { description: self.description, event_time: self.event_time, http_method: self.http_method, - endpoint_path: self.endpoint_path, + http_path: self.http_path, + http_status_code: self.http_status_code, is_from_error: self.is_from_error, } }