From 0e18cfacdea1dbb293f38a2a3eca68d8a4f35352 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 13 May 2026 16:46:39 +0530 Subject: [PATCH 1/7] feat(providers/billing): add budget exhaustion matcher --- src/openhuman/providers/billing_error.rs | 60 ++++++++++++++++++++++++ src/openhuman/providers/mod.rs | 2 + 2 files changed, 62 insertions(+) create mode 100644 src/openhuman/providers/billing_error.rs diff --git a/src/openhuman/providers/billing_error.rs b/src/openhuman/providers/billing_error.rs new file mode 100644 index 0000000000..3966fb92ff --- /dev/null +++ b/src/openhuman/providers/billing_error.rs @@ -0,0 +1,60 @@ +/// Returns true if a 400 response body indicates the user is out of +/// budget / has insufficient balance / over their plan. These are +/// deterministic user-state errors — already surfaced in the UI as a +/// toast — and must not flow to Sentry as errors. +/// +/// Match is case-insensitive against any of the known phrases. Keep the +/// list deliberately tight: false positives demote real backend bugs. +pub fn is_budget_exhausted_message(body: &str) -> bool { + const PHRASES: &[&str] = &[ + "insufficient budget", + "budget exceeded", + "add credits", + "insufficient balance", + ]; + + let lower = body.to_ascii_lowercase(); + PHRASES.iter().any(|phrase| lower.contains(phrase)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn detects_known_budget_exhaustion_phrases() { + for body in [ + "Insufficient budget", + "Budget exceeded", + "Insufficient balance", + "Add credits to continue", + ] { + assert!( + is_budget_exhausted_message(body), + "{body:?} must be classified as budget-exhausted user-state" + ); + } + } + + #[test] + fn detection_is_case_insensitive() { + assert!(is_budget_exhausted_message("INSUFFICIENT BUDGET")); + assert!(is_budget_exhausted_message("budget EXCEEDED — ADD credits")); + assert!(is_budget_exhausted_message("Insufficient BALANCE")); + } + + #[test] + fn ignores_non_budget_messages() { + for body in [ + "Bad request: missing field", + "Invalid request: model not found", + "HTTP 400 Bad Request", + "", + ] { + assert!( + !is_budget_exhausted_message(body), + "{body:?} must not be classified as budget-exhausted" + ); + } + } +} diff --git a/src/openhuman/providers/mod.rs b/src/openhuman/providers/mod.rs index 7319b6920a..6d41a64c4d 100644 --- a/src/openhuman/providers/mod.rs +++ b/src/openhuman/providers/mod.rs @@ -1,3 +1,4 @@ +pub mod billing_error; pub mod compatible; pub mod openhuman_backend; pub mod ops; @@ -12,4 +13,5 @@ pub use traits::{ ProviderDelta, ToolCall, ToolResultMessage, UsageInfo, }; +pub use billing_error::is_budget_exhausted_message; pub use ops::*; From a9a04776506adb65922f998798bad8cddd815dc5 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 13 May 2026 16:50:28 +0530 Subject: [PATCH 2/7] fix(providers): demote budget-exhausted 400s from Sentry --- src/api/rest.rs | 15 ++- src/core/observability.rs | 20 ++++ .../agent/harness/session/runtime.rs | 9 +- src/openhuman/providers/compatible.rs | 45 +++++++- src/openhuman/providers/ops.rs | 105 +++++++----------- 5 files changed, 120 insertions(+), 74 deletions(-) diff --git a/src/api/rest.rs b/src/api/rest.rs index efab8d2961..ee34ab39ba 100644 --- a/src/api/rest.rs +++ b/src/api/rest.rs @@ -476,7 +476,20 @@ impl BackendOAuthClient { // implement retry/disable logic, so skip Sentry to avoid noise. let is_transient_infra = crate::core::observability::is_transient_http_status_code(status_code); - if is_transient_infra { + let is_budget_exhausted = status_code == 400 + && crate::openhuman::providers::is_budget_exhausted_message(&text); + if is_budget_exhausted { + tracing::info!( + method = method.as_str(), + path = url.path(), + status = status_code, + failure = "non_2xx", + kind = "budget", + "[backend_api] budget-exhausted 400 on {} {} — not reporting to Sentry", + method.as_str(), + url.path(), + ); + } else if is_transient_infra { tracing::warn!( domain = "backend_api", operation = "authed_json", diff --git a/src/core/observability.rs b/src/core/observability.rs index 723571988c..5bb8385460 100644 --- a/src/core/observability.rs +++ b/src/core/observability.rs @@ -61,6 +61,7 @@ pub enum ExpectedErrorKind { LocalAiBinaryMissing, BackendUserError, LocalAiCapabilityUnavailable, + BudgetExhausted, } pub fn expected_error_kind(message: &str) -> Option { @@ -86,6 +87,9 @@ pub fn expected_error_kind(message: &str) -> Option { if is_local_ai_capability_unavailable_message(&lower) { return Some(ExpectedErrorKind::LocalAiCapabilityUnavailable); } + if crate::openhuman::providers::is_budget_exhausted_message(message) { + return Some(ExpectedErrorKind::BudgetExhausted); + } None } @@ -321,6 +325,22 @@ fn report_expected_message(kind: ExpectedErrorKind, message: &str, domain: &str, "[observability] {domain}.{operation} skipped expected local-ai capability-unavailable error: {message}" ); } + ExpectedErrorKind::BudgetExhausted => { + // User-state condition: the backend reports the user is out of + // budget / credits / balance (HTTP 400 from the OpenHuman backend, + // surfaced by `providers::is_budget_exhausted_message`). The UI + // already surfaces this as an actionable toast — Sentry would + // turn each affected turn into noise (OPENHUMAN-TAURI-3M / -12 / + // -13). Demote to info so it still appears in breadcrumbs but + // never spawns a Sentry error event. + tracing::info!( + domain = domain, + operation = operation, + kind = "budget", + error = %message, + "[observability] {domain}.{operation} skipped expected budget-exhausted error: {message}" + ); + } } } diff --git a/src/openhuman/agent/harness/session/runtime.rs b/src/openhuman/agent/harness/session/runtime.rs index 82bc58ab7f..5727817c3c 100644 --- a/src/openhuman/agent/harness/session/runtime.rs +++ b/src/openhuman/agent/harness/session/runtime.rs @@ -510,10 +510,11 @@ impl Agent { // `log::info!` (OPENHUMAN-TAURI-99 / -98). // // Other agent errors go through `report_error_or_expected` - // so OPENHUMAN-TAURI-5Z and friends — upstream transient - // HTTP that bubbles up under `domain=agent` and escapes - // the `domain=llm_provider` filter — get demoted to a - // warn-level breadcrumb without losing genuine bugs. + // so OPENHUMAN-TAURI-5Z and the budget-noise cluster — + // upstream transient HTTP and backend budget-exhausted 400s + // that bubble up under `domain=agent` and escape the + // `domain=llm_provider` filter — get demoted to a + // warn/info-level breadcrumb without losing genuine bugs. // `Err` propagation, the `AgentError` domain event, and // downstream `recoverable=false` semantics are preserved. let is_max_iter = matches!( diff --git a/src/openhuman/providers/compatible.rs b/src/openhuman/providers/compatible.rs index 8936acc551..b9d7ff9ba2 100644 --- a/src/openhuman/providers/compatible.rs +++ b/src/openhuman/providers/compatible.rs @@ -400,7 +400,14 @@ impl OpenAiCompatibleProvider { let error = response.text().await?; let sanitized = super::sanitize_api_error(&error); let message = format!("{} Responses API error: {sanitized}", self.name); - if super::should_report_provider_http_failure(status) { + if super::is_budget_exhausted_http_400(status, &error) { + super::log_budget_exhausted_http_400( + "responses_api", + self.name.as_str(), + Some(model), + status, + ); + } else if super::should_report_provider_http_failure(status) { crate::core::observability::report_error( message.as_str(), "llm_provider", @@ -736,7 +743,14 @@ impl OpenAiCompatibleProvider { "{} streaming API error ({}): {}", self.name, status, sanitized ); - if super::should_report_provider_http_failure(status) { + if super::is_budget_exhausted_http_400(status, &body) { + super::log_budget_exhausted_http_400( + "streaming_chat", + self.name.as_str(), + Some(native_request.model.as_str()), + status, + ); + } else if super::should_report_provider_http_failure(status) { crate::core::observability::report_error( message.as_str(), "llm_provider", @@ -1190,7 +1204,14 @@ impl Provider for OpenAiCompatibleProvider { let status_str = status.as_u16().to_string(); let message = format!("{} API error ({status}): {sanitized}", self.name); - if super::should_report_provider_http_failure(status) { + if super::is_budget_exhausted_http_400(status, &error) { + super::log_budget_exhausted_http_400( + "chat_completions", + self.name.as_str(), + Some(model), + status, + ); + } else if super::should_report_provider_http_failure(status) { crate::core::observability::report_error( message.as_str(), "llm_provider", @@ -1574,7 +1595,14 @@ impl Provider for OpenAiCompatibleProvider { let status_str = status.as_u16().to_string(); let message = format!("{} API error ({status}): {sanitized}", self.name); - if super::should_report_provider_http_failure(status) { + if super::is_budget_exhausted_http_400(status, &error) { + super::log_budget_exhausted_http_400( + "native_chat", + self.name.as_str(), + Some(model), + status, + ); + } else if super::should_report_provider_http_failure(status) { crate::core::observability::report_error( message.as_str(), "llm_provider", @@ -1701,7 +1729,14 @@ impl Provider for OpenAiCompatibleProvider { }; let sanitized_error = super::sanitize_api_error(&raw_error); let message = format!("{}: {}", status, sanitized_error); - if super::should_report_provider_http_failure(status) { + if super::is_budget_exhausted_http_400(status, &raw_error) { + super::log_budget_exhausted_http_400( + "stream_chat", + provider_name.as_str(), + Some(model_owned.as_str()), + status, + ); + } else if super::should_report_provider_http_failure(status) { crate::core::observability::report_error( message.as_str(), "llm_provider", diff --git a/src/openhuman/providers/ops.rs b/src/openhuman/providers/ops.rs index ec62b44243..af6057df53 100644 --- a/src/openhuman/providers/ops.rs +++ b/src/openhuman/providers/ops.rs @@ -128,29 +128,28 @@ pub fn should_report_provider_http_failure(status: reqwest::StatusCode) -> bool !crate::core::observability::TRANSIENT_PROVIDER_HTTP_STATUSES.contains(&status.as_u16()) } -/// Whether a "Budget exceeded" error from `provider` at `status` should be -/// suppressed from Sentry. -/// -/// Suppression is scoped to `backend + 400` so that: -/// - Other providers (OpenAI, Anthropic, …) whose 400 bodies happen to mention -/// "Budget exceeded" still report. -/// - Backend 5xx errors that mention "Budget exceeded" still report (server bug, -/// not user-state). -/// - Only the exact user-actionable signal from the OpenHuman backend — which -/// the UI surfaces directly — is silenced. -pub(super) fn is_budget_exceeded_suppressed( +/// Whether a provider non-2xx response is a deterministic budget-exhausted +/// user-state error that should be demoted from Sentry to an info log. +pub(super) fn is_budget_exhausted_http_400(status: reqwest::StatusCode, body: &str) -> bool { + status == reqwest::StatusCode::BAD_REQUEST && super::is_budget_exhausted_message(body) +} + +pub(super) fn log_budget_exhausted_http_400( + operation: &str, provider: &str, + model: Option<&str>, status: reqwest::StatusCode, - sanitized_body: &str, -) -> bool { - provider == openhuman_backend::PROVIDER_LABEL - && matches!( - status, - reqwest::StatusCode::BAD_REQUEST | reqwest::StatusCode::PAYMENT_REQUIRED - ) - && sanitized_body - .to_ascii_lowercase() - .contains("budget exceeded") +) { + tracing::info!( + domain = "llm_provider", + operation = operation, + provider = provider, + model = model.unwrap_or(""), + status = status.as_u16(), + failure = "non_2xx", + kind = "budget", + "[llm_provider] {operation} budget-exhausted 400 — not reporting to Sentry" + ); } /// Build a sanitized provider error from a failed HTTP response. @@ -183,7 +182,7 @@ pub async fn api_error(provider: &str, response: reqwest::Response) -> anyhow::E let is_auth_failure = matches!(status.as_u16(), 401 | 403); let is_backend = provider == openhuman_backend::PROVIDER_LABEL; - let is_budget_exceeded_user_state = is_budget_exceeded_suppressed(provider, status, &sanitized); + let is_budget_exhausted_user_state = is_budget_exhausted_http_400(status, &body); if is_auth_failure && is_backend { tracing::warn!( @@ -204,14 +203,8 @@ pub async fn api_error(provider: &str, response: reqwest::Response) -> anyhow::E reason: sanitize_api_error(&message), }, ); - } else if is_budget_exceeded_user_state { - tracing::info!( - domain = "llm_provider", - operation = "api_error", - provider = provider, - status = status_str.as_str(), - "[llm_provider] budget-exceeded response suppressed from Sentry (user-actionable, not a bug)" - ); + } else if is_budget_exhausted_user_state { + log_budget_exhausted_http_400("api_error", provider, None, status); } else if should_report_provider_http_failure(status) { crate::core::observability::report_error( message.as_str(), @@ -547,76 +540,60 @@ mod tests { } } - // Confirm the Budget-exceeded suppression predicate is scoped correctly. + // Confirm the budget-exhausted suppression predicate is scoped correctly. // These tests exercise the real production function, not a duplicate. - mod budget_exceeded_suppression { + mod budget_exhausted_suppression { use super::*; - const BACKEND: &str = openhuman_backend::PROVIDER_LABEL; // "OpenHuman" - const OTHER: &str = "OpenAI"; - const BUDGET_BODY: &str = "Budget exceeded: you have no credits remaining"; + const BUDGET_BODY: &str = "Insufficient budget"; const UNRELATED_BODY: &str = "Invalid request: model not found"; #[test] - fn backend_400_budget_exceeded_is_suppressed() { - assert!(is_budget_exceeded_suppressed( - BACKEND, + fn budget_exhausted_400_is_suppressed() { + assert!(is_budget_exhausted_http_400( reqwest::StatusCode::BAD_REQUEST, BUDGET_BODY, )); } #[test] - fn other_provider_400_budget_exceeded_is_not_suppressed() { - // A third-party provider whose body happens to say "Budget exceeded" - // should still be reported to Sentry — only the backend gets special - // treatment. - assert!(!is_budget_exceeded_suppressed( - OTHER, + fn budget_exhausted_400_is_case_insensitive() { + assert!(is_budget_exhausted_http_400( reqwest::StatusCode::BAD_REQUEST, - BUDGET_BODY, + "budget exceeded — ADD credits to continue", )); } #[test] - fn backend_500_budget_exceeded_is_not_suppressed() { + fn budget_exhausted_500_is_not_suppressed() { // A 500 is a server bug, not expected user-state — keep reporting. - assert!(!is_budget_exceeded_suppressed( - BACKEND, + assert!(!is_budget_exhausted_http_400( reqwest::StatusCode::INTERNAL_SERVER_ERROR, BUDGET_BODY, )); } #[test] - fn backend_400_unrelated_body_is_not_suppressed() { - assert!(!is_budget_exceeded_suppressed( - BACKEND, + fn budget_exhausted_400_unrelated_body_is_not_suppressed() { + assert!(!is_budget_exhausted_http_400( reqwest::StatusCode::BAD_REQUEST, UNRELATED_BODY, )); } #[test] - fn backend_402_budget_exceeded_is_suppressed() { - // 402 Payment Required is a valid alternative status the backend may - // return for the same user-state (no credits); it should be suppressed - // just like 400. - assert!(is_budget_exceeded_suppressed( - BACKEND, + fn budget_exhausted_402_is_not_suppressed() { + assert!(!is_budget_exhausted_http_400( reqwest::StatusCode::PAYMENT_REQUIRED, BUDGET_BODY, )); } #[test] - fn backend_402_budget_exceeded_case_insensitive() { - // Body casing should not affect suppression — e.g. "budget exceeded" - // (all-lowercase) from a future backend change. - assert!(is_budget_exceeded_suppressed( - BACKEND, - reqwest::StatusCode::PAYMENT_REQUIRED, - "budget exceeded: no credits", + fn budget_exhausted_empty_body_is_not_suppressed() { + assert!(!is_budget_exhausted_http_400( + reqwest::StatusCode::BAD_REQUEST, + "", )); } } From 1a4b572ee041ba022231c91056051d508234e0b4 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 13 May 2026 16:51:34 +0530 Subject: [PATCH 3/7] chore(observability): classify budget events for before_send --- src/core/observability.rs | 89 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/src/core/observability.rs b/src/core/observability.rs index 5bb8385460..22928b7ee1 100644 --- a/src/core/observability.rs +++ b/src/core/observability.rs @@ -1,6 +1,6 @@ //! Centralised error reporting for the core, plus a Sentry -//! `before_send` filter that drops per-attempt transient-upstream -//! provider failures. +//! `before_send` filters that drop deterministic provider noise: +//! per-attempt transient-upstream failures and budget-exhausted user-state. //! //! Wraps `tracing::error!` (which the global subscriber forwards to Sentry via //! `sentry-tracing`) inside a `sentry::with_scope` so each captured event @@ -553,6 +553,47 @@ pub fn is_transient_message_failure(msg: &str) -> bool { || contains_transient_transport_phrase(&lower) } +/// Returns true when a Sentry event is a budget-exhausted 400 that should be +/// dropped from `before_send`. +/// +/// Match criteria (all required): +/// - tag `failure == "non_2xx"` +/// - tag `status == "400"` +/// - the event message or any exception value contains one of the tight +/// budget-exhaustion phrases +/// +/// Note: `domain` is intentionally not gated here as defense-in-depth over +/// the emit-site classifier — any non_2xx/400 event that carries the +/// budget-exhausted phrasing is dropped regardless of which domain produced +/// it, so a future re-emitter under a different tag still gets filtered. +pub fn is_budget_event(event: &sentry::protocol::Event<'_>) -> bool { + let tags = &event.tags; + if tags.get("failure").map(String::as_str) != Some("non_2xx") { + return false; + } + if tags.get("status").map(String::as_str) != Some("400") { + return false; + } + event_contains_budget_exhausted_message(event) +} + +fn event_contains_budget_exhausted_message(event: &sentry::protocol::Event<'_>) -> bool { + if event + .message + .as_deref() + .is_some_and(crate::openhuman::providers::is_budget_exhausted_message) + { + return true; + } + + event.exception.values.iter().any(|exception| { + exception + .value + .as_deref() + .is_some_and(crate::openhuman::providers::is_budget_exhausted_message) + }) +} + #[cfg(test)] mod tests { use super::*; @@ -1173,6 +1214,50 @@ mod tests { } } + #[test] + fn budget_filter_drops_budget_message_on_tagged_400() { + let event = event_with_tags_and_message( + &[("failure", "non_2xx"), ("status", "400")], + r#"OpenHuman API error (400 Bad Request): {"success":false,"error":"Insufficient budget"}"#, + ); + + assert!(is_budget_event(&event)); + } + + #[test] + fn budget_filter_drops_budget_exception_on_tagged_400() { + let mut event = event_with_tags(&[("failure", "non_2xx"), ("status", "400")]); + event.exception.values.push(sentry::protocol::Exception { + value: Some("Budget exceeded — add credits to continue".to_string()), + ..Default::default() + }); + + assert!(is_budget_event(&event)); + } + + #[test] + fn budget_filter_keeps_non_budget_400() { + let event = event_with_tags_and_message( + &[("failure", "non_2xx"), ("status", "400")], + "Bad request: missing field", + ); + + assert!(!is_budget_event(&event)); + } + + #[test] + fn budget_filter_requires_non_2xx_failure_and_400_status() { + let message = "Budget exceeded — add credits to continue"; + for tags in [ + vec![("failure", "transport"), ("status", "400")], + vec![("failure", "non_2xx"), ("status", "500")], + vec![("failure", "non_2xx")], + ] { + let event = event_with_tags_and_message(&tags, message); + assert!(!is_budget_event(&event)); + } + } + #[test] fn report_error_or_expected_does_not_panic() { report_error_or_expected( From 45945442d7517bd55c29f738b8de985ad5f232f8 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 13 May 2026 16:51:48 +0530 Subject: [PATCH 4/7] chore(main): drop budget events in before_send --- src/main.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main.rs b/src/main.rs index fe68b59d06..1dc8617e77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,13 @@ fn main() { if openhuman_core::core::observability::is_transient_provider_http_failure(&event) { return None; } + // Defense-in-depth for budget-exhausted 400s. Emit sites demote the + // known backend responses before they hit Sentry; this catches any + // future non_2xx/status=400 event that carries the same tight body + // phrases. + if openhuman_core::core::observability::is_budget_event(&event) { + return None; + } // Defense-in-depth: drop max-tool-iterations cap events that // slipped past the call-site filters in // `agent::harness::session::runtime::run_single`, From a3979721be1a2902b5a800627e3487422b6dd6dd Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 13 May 2026 16:52:09 +0530 Subject: [PATCH 5/7] chore(app/src-tauri): drop budget events in before_send --- app/src-tauri/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 8de76f7788..c98e7ec7b6 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -1314,6 +1314,13 @@ pub fn run() { ); return None; } + if openhuman_core::core::observability::is_budget_event(&event) { + log::debug!( + "[sentry-budget-filter] dropping budget-exhausted event: {:?}", + event.message.as_deref().unwrap_or("") + ); + return None; + } // Defense-in-depth: drop max-tool-iterations cap events that // slipped past the call-site filters in the core (see // `openhuman_core::core::observability::is_max_iterations_event` From 30e837c9b464e324c4e87ed1d05b25112899157e Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 13 May 2026 16:55:08 +0530 Subject: [PATCH 6/7] test(observability): smoke-test budget event filtering --- tests/observability_smoke.rs | 42 ++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/tests/observability_smoke.rs b/tests/observability_smoke.rs index f58c003c1b..b7a2c57188 100644 --- a/tests/observability_smoke.rs +++ b/tests/observability_smoke.rs @@ -1,5 +1,6 @@ //! Runtime smoke for the Sentry `before_send` filters that drop per-attempt -//! transient-upstream provider, backend_api, and integrations failures. +//! transient-upstream provider, backend_api, and integrations failures plus +//! budget-exhausted user-state 400s (OPENHUMAN-TAURI-3M / 12 / 13). //! //! Unit tests in `src/core/observability.rs` exercise the pure filter //! function. This integration test wires the actual `sentry::init` → @@ -8,7 +9,7 @@ //! and aggregate `all_exhausted` events still surface. use openhuman_core::core::observability::{ - is_transient_backend_api_failure, is_transient_integrations_failure, + is_budget_event, is_transient_backend_api_failure, is_transient_integrations_failure, is_transient_provider_http_failure, }; use sentry::protocol::Event; @@ -58,6 +59,7 @@ fn count_captured(events: Vec>) -> usize { if is_transient_provider_http_failure(&event) || is_transient_backend_api_failure(&event) || is_transient_integrations_failure(&event) + || is_budget_event(&event) { None } else { @@ -112,6 +114,42 @@ fn drops_integrations_transient_transport_timeout() { ); } +#[test] +fn drops_budget_exhausted_400() { + let event = event_with_tags_and_message( + &[ + ("domain", "llm_provider"), + ("failure", "non_2xx"), + ("status", "400"), + ], + r#"OpenHuman API error (400 Bad Request): {"success":false,"error":"Insufficient budget"}"#, + ); + + assert_eq!( + count_captured(vec![event]), + 0, + "budget-exhausted 400s must be filtered in before_send" + ); +} + +#[test] +fn keeps_non_budget_400() { + let event = event_with_tags_and_message( + &[ + ("domain", "llm_provider"), + ("failure", "non_2xx"), + ("status", "400"), + ], + "Bad request: missing field", + ); + + assert_eq!( + count_captured(vec![event]), + 1, + "non-budget 400s must still reach Sentry" + ); +} + #[test] fn drops_per_attempt_429_503_504_408_502() { // Each of these matches the tag shape `ops::api_error` sets when a From 99aad4445c15cca3cf71df5f1183d8800911abd6 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 13 May 2026 21:28:30 +0530 Subject: [PATCH 7/7] fix(observability): metadata-only logging in budget-filter before_send (#1633 CR) CodeRabbit major: the [sentry-budget-filter] log emitted event.message verbatim, which can carry upstream provider error text including tokens / pasted-through secrets. Per openhuman/CLAUDE.md "never log secrets or full PII". Switch to logging only the structured tag values (domain + status) that gate the drop decision; the message text is no longer needed for diagnostic. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/src-tauri/src/lib.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index c98e7ec7b6..2a276d5c3e 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -1315,9 +1315,15 @@ pub fn run() { return None; } if openhuman_core::core::observability::is_budget_event(&event) { + // Log only structured tag metadata — `event.message` can carry + // upstream provider error text including tokens / pasted-through + // secrets, and per `CLAUDE.md` "never log secrets or full PII". + // The (domain, status) pair is sufficient diagnostic since + // those are the tags `is_budget_event` gates on. log::debug!( - "[sentry-budget-filter] dropping budget-exhausted event: {:?}", - event.message.as_deref().unwrap_or("") + "[sentry-budget-filter] dropping budget-exhausted event (domain={:?}, status={:?})", + event.tags.get("domain"), + event.tags.get("status") ); return None; }