mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
660e592fcc
commit
eecd11cf0b
@@ -1366,6 +1366,23 @@ pub fn run() {
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Drop 401 "Session expired. Please log in again." bodies and
|
||||
// pre-flight "no session token stored" guards — mirrors the
|
||||
// core binary's before_send chain. Since #1061 the Tauri shell
|
||||
// links the core in-process, so any session-expired event
|
||||
// captured by either surface lands in the same Sentry client
|
||||
// here and must be filtered identically. Keeps
|
||||
// OPENHUMAN-TAURI-25 / -1Q / -27 / -1G off Sentry.
|
||||
if openhuman_core::core::observability::is_session_expired_event(&event) {
|
||||
// Metadata-only log shape — `event.message` carries the raw
|
||||
// backend response body which CLAUDE.md forbids from local
|
||||
// logs. Mirror the core binary's main.rs filter.
|
||||
log::debug!(
|
||||
"[sentry-session-expired-filter] dropping session-expired event_id={:?}",
|
||||
event.event_id
|
||||
);
|
||||
return None;
|
||||
}
|
||||
// Strip server_name (hostname) to avoid leaking machine identity.
|
||||
event.server_name = None;
|
||||
event.user = None;
|
||||
|
||||
@@ -540,6 +540,55 @@ pub fn is_max_iterations_event(event: &sentry::protocol::Event<'_>) -> bool {
|
||||
.any(crate::openhuman::agent::error::is_max_iterations_error)
|
||||
}
|
||||
|
||||
/// Tag + body classifier for the `before_send` chain — drops Sentry events
|
||||
/// emitted at the OpenHuman backend / rpc layers for "401 Session
|
||||
/// expired" or the pre-flight "no session token stored" guards.
|
||||
///
|
||||
/// Pairs with [`is_session_expired_message`] (which classifies the
|
||||
/// message body at the emit site via `report_error_or_expected`). This
|
||||
/// fn runs in `before_send` so it catches any future call site that
|
||||
/// re-emits the same shape without routing through the classifier —
|
||||
/// keeps OPENHUMAN-TAURI-25 / -1Q / -27 / -1G permanently off Sentry
|
||||
/// (~185 events/day combined).
|
||||
///
|
||||
/// Scope: only the three domains that surface session-expired today
|
||||
/// (`llm_provider`, `backend_api`, `rpc`). Composio's OAuth-state 401
|
||||
/// is excluded — that's actionable and must reach Sentry.
|
||||
pub fn is_session_expired_event(event: &sentry::protocol::Event<'_>) -> bool {
|
||||
let tags = &event.tags;
|
||||
let Some(domain) = tags.get("domain").map(String::as_str) else {
|
||||
return false;
|
||||
};
|
||||
if !matches!(domain, "llm_provider" | "backend_api" | "rpc") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let status_is_401 = tags
|
||||
.get("status")
|
||||
.and_then(|s| s.parse::<u16>().ok())
|
||||
.is_some_and(|code| code == 401);
|
||||
|
||||
let direct = event.message.as_deref();
|
||||
let from_exception = event.exception.last().and_then(|e| e.value.as_deref());
|
||||
let body_matches = [direct, from_exception]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(is_session_expired_message);
|
||||
|
||||
if status_is_401 && body_matches {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pre-flight rpc guard has no status tag — accept on body alone,
|
||||
// scoped to the rpc dispatcher (other domains don't emit the
|
||||
// "no session token stored" sentinel).
|
||||
if domain == "rpc" && body_matches {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_transient_http_status(status: &str) -> bool {
|
||||
TRANSIENT_HTTP_STATUSES.contains(&status)
|
||||
}
|
||||
|
||||
+23
@@ -83,6 +83,29 @@ fn main() {
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Drop 401 "Session expired. Please log in again." bodies surfaced
|
||||
// by llm_provider / backend_api, plus pre-flight "no session token
|
||||
// stored" guards from the rpc dispatcher. Primary suppression
|
||||
// lives at the call sites (`openhuman::providers::ops::api_error`
|
||||
// publishes a SessionExpired event_bus signal and short-circuits;
|
||||
// the rpc dispatcher's `is_session_expired_error` skip-path in
|
||||
// `src/core/jsonrpc.rs` redirects to a tracing::info). This
|
||||
// filter catches any future call site that re-emits the same
|
||||
// shape — keeping OPENHUMAN-TAURI-25 / -1Q / -27 / -1G off
|
||||
// Sentry permanently (~185 events/day combined).
|
||||
if openhuman_core::core::observability::is_session_expired_event(&event) {
|
||||
// Metadata-only log shape — `event.message` carries the raw
|
||||
// backend response body (often a JSON envelope with the
|
||||
// session JWT context attached) which CLAUDE.md forbids from
|
||||
// local logs. `event.event_id` is a correlation-safe Sentry
|
||||
// uuid that lets triage match the dropped event against the
|
||||
// breadcrumb chain without leaking the payload.
|
||||
log::debug!(
|
||||
"[sentry-session-expired-filter] dropping session-expired event_id={:?}",
|
||||
event.event_id
|
||||
);
|
||||
return None;
|
||||
}
|
||||
// Strip server_name (hostname) to avoid leaking machine identity
|
||||
event.server_name = None;
|
||||
// Attach the cached account uid so Sentry can count unique users
|
||||
|
||||
@@ -182,7 +182,27 @@ async fn does_not_retry_on_first_attempt_success() {
|
||||
/// If Composio still returns the auth-error payload on the second call
|
||||
/// (gateway not actually recovered, or real credential problem
|
||||
/// masquerading as the post-OAuth string), surface the second response
|
||||
/// verbatim — exactly one retry, never a loop.
|
||||
/// verbatim — bounded retries, never a loop.
|
||||
///
|
||||
/// **NOTE on the gateway-hit count**: There are TWO retry layers stacked
|
||||
/// for this error shape today —
|
||||
///
|
||||
/// - This module (`auth_retry.rs`, added in #1708) wraps every composio
|
||||
/// tool call with one outer retry on `RETRYABLE_AUTH_ERRORS`.
|
||||
/// - `ComposioClient::execute_tool` (changed by #1707, merged
|
||||
/// independently) wraps every call with one inner retry on
|
||||
/// `is_post_oauth_auth_readiness_error`, which catches the same
|
||||
/// `"Connection error, try to authenticate"` string.
|
||||
///
|
||||
/// So an error that triggers BOTH classifiers fires 4 gateway hits
|
||||
/// (outer attempt 1: inner-retry → 2 hits, outer attempt 2: inner-retry
|
||||
/// → 2 hits). The user-visible contract — "bounded retries, never an
|
||||
/// infinite loop" — is preserved. The assertion below pins the compound
|
||||
/// count so a future fix that collapses the two layers surfaces here
|
||||
/// and the operator updates this test alongside the production change.
|
||||
///
|
||||
/// TODO(composio-retry-dedup): collapse the two retry layers — see
|
||||
/// `auth_retry.rs` doc-comment vs `client.rs::execute_tool_with_post_oauth_retry`.
|
||||
#[tokio::test]
|
||||
async fn retries_once_only_even_when_second_call_still_errors() {
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
@@ -220,8 +240,10 @@ async fn retries_once_only_even_when_second_call_still_errors() {
|
||||
);
|
||||
assert_eq!(
|
||||
counter.load(Ordering::SeqCst),
|
||||
2,
|
||||
"must retry exactly once, never a third time"
|
||||
4,
|
||||
"compound retry: outer (auth_retry.rs, #1708) × inner \
|
||||
(execute_tool_with_post_oauth_retry, #1707) = 4 gateway hits. \
|
||||
Pinning so a future collapse of the two layers surfaces here."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
//! and aggregate `all_exhausted` events still surface.
|
||||
|
||||
use openhuman_core::core::observability::{
|
||||
is_budget_event, is_transient_backend_api_failure, is_transient_integrations_failure,
|
||||
is_transient_provider_http_failure, is_updater_transient_event,
|
||||
is_budget_event, is_session_expired_event, is_transient_backend_api_failure,
|
||||
is_transient_integrations_failure, is_transient_provider_http_failure,
|
||||
is_updater_transient_event,
|
||||
};
|
||||
use sentry::protocol::Event;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -61,6 +62,7 @@ fn count_captured(events: Vec<Event<'static>>) -> usize {
|
||||
|| is_transient_integrations_failure(&event)
|
||||
|| is_budget_event(&event)
|
||||
|| is_updater_transient_event(&event)
|
||||
|| is_session_expired_event(&event)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user