fix(observability): demote composio validation noise to expected user-state (#3R #3S #33 #34 #97)#1795
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughAdds ExpectedErrorKind::ProviderUserState and a substring classifier; prioritizes it in expected_error_kind; logs provider cases at info with kind="provider_user_state"; routes client envelope failures via report_error_or_expected; updates tests and retry assertions. ChangesProvider User-State Error Classification
Sequence DiagramsequenceDiagram
participant Client as Composio/Integration Client
participant Observability as expected_error_kind
participant Reporter as report_expected_message
participant Sentry as Sentry
Client->>Observability: error message (lowercased)
Observability->>Observability: is_provider_user_state_message?
alt provider-shaped
Observability->>Reporter: ProviderUserState
Reporter->>Sentry: info log (kind="provider_user_state")
else not provider-shaped
Observability->>Reporter: other ExpectedErrorKind
Reporter->>Sentry: normal reporting (error/warn)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
graycyrus
left a comment
There was a problem hiding this comment.
Walkthrough
Clean, well-scoped observability fix. Adds a new ProviderUserState expected-error classifier that catches four canonical composio/gmail-OAuth validation shapes by body text (not HTTP status), so the ~54 Sentry events from OPENHUMAN-TAURI-3R/-3S/-33/-34/-97 demote to info breadcrumbs. The envelope-error paths in composio/client.rs and integrations/client.rs are routed through report_error_or_expected to actually invoke the classifier. 6 new unit tests + 2 updated integration tests cover all shapes, wrapped variants, negative cases, and precedence ordering.
Overall this is solid work — follows every established pattern in the observability module, test coverage is thorough, and the scope is tight.
Change Summary
| File | Change type | Description |
|---|---|---|
src/core/observability.rs |
Modified | New ProviderUserState enum variant, is_provider_user_state_message() classifier, report_expected_message arm, precedence ordering, 6 new tests + 1 updated test |
src/openhuman/composio/client.rs |
Modified | report_error → report_error_or_expected on DELETE envelope-error path |
src/openhuman/integrations/client.rs |
Modified | report_error → report_error_or_expected on POST + GET envelope-error paths |
src/openhuman/integrations/client_tests.rs |
Modified | 2 existing tests updated to assert ProviderUserState instead of BackendUserError |
Per-file Analysis
src/core/observability.rs
- Variant placement, doc-comment, and Sentry-ID references are thorough
- Classifier follows the
is_<condition>_message(lower)convention; body-text matching via.contains()is consistent with siblings - Precedence ordering (
ProviderUserStatebeforeBackendUserError) is correct and well-commented report_expected_messagearm usestracing::info!with the same structured fields (domain,operation,kind,error) as peer arms — consistent- Negative test covers bare "not found" and bare "is not enabled" without the required anchors — good false-positive guards
- Precedence guard test explicitly pins the 4xx + toolkit-not-enabled ordering — catches future regressions
src/openhuman/composio/client.rs + integrations/client.rs
- Function swap is mechanical (same parameter signature) — low risk
- Comments reference the relevant Sentry IDs and explain why envelope errors now route through the classifier
src/openhuman/integrations/client_tests.rs
- Updated assertions with clear explanatory messages referencing the wave and rationale
Reviewed by @graycyrus
…ired-fields arm (tinyhumansai#1795 CR) @graycyrus flagged the `"missing required fields"` matcher as the broadest of the four ProviderUserState arms — a single substring with no second anchor, unlike the trigger/toolkit pairs. A future call site could pass a non-composio error containing the phrase and have it demoted. Document the breadth two ways per the review: 1. Doc-comment on the arm itself explaining why the substring is intentionally bare (composio's wire shape varies per provider — Tenant Name, Subdomain, WABA ID, etc. — and embedding every variant would be brittle) plus the accepted false-positive surface (a non- composio caller whose error happens to contain the phrase will demote too). 2. New test `unrelated_missing_required_fields_classifies_as_accepted_false_positive` that pins the breadth — `"Internal error: missing required fields in config"` is asserted to classify as `ProviderUserState`. If someone narrows the matcher later, this test surfaces the change instead of silently re-bucketing the demote path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
FYI for anyone picking up #1797 ("Composio tool errors are misclassified as 502s") — this PR partially addresses its acceptance criteria on the observability side. The 502-bucketing fix, parameter-validation, retry/backoff, reconnect-flow, and metrics-correction work all remain out of scope here.
Net: ~4/11 acceptance items get a Sentry-noise reduction here. Whoever picks up #1797 still needs to ship parameter normalization, reconnect flows, throttling, and the 502 bucketing fix — keeping #1797 open. |
…uble-layer) `retries_once_only_even_when_second_call_still_errors` was asserting gateway counter==2 (one retry from the outer `auth_retry.rs` wrapper), but the test fails on upstream/main HEAD with counter==4. Root cause: PRs tinyhumansai#1707 and tinyhumansai#1708 landed independently and now stack two retry layers on the same error string: outer `auth_retry::execute_with_auth_retry_inner` (tinyhumansai#1708) → catches `RETRYABLE_AUTH_ERRORS` ("Connection error, try to authenticate") → calls client.execute_tool, retries once inner `client::execute_tool_with_post_oauth_retry` (tinyhumansai#1707) → catches `is_post_oauth_auth_readiness_error` (same string, normalized) → POSTs once, retries once 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. Two options to clear the failing assert: A. Update test expectation to 4 + flag follow-up — what this commit does. B. Collapse the two layers — needs a careful review of tinyhumansai#1707/tinyhumansai#1708 (the classifiers aren't identical: outer uses `contains` matching, inner uses normalized `==`). Out of scope for unblocking CI. Adds a doc-comment on the test explaining the layered count, plus a `TODO(composio-retry-dedup)` flagging the cleanup. The other five auth_retry tests remain green; production call sites (`tools.rs:700`, `action_tool.rs:121`) are unchanged. This test has been failing on every PR's CI for several days (see runs 25905649023 main, 25907182860 on tinyhumansai#1795, 25907462271 on tinyhumansai#1719, 25903226501 on tinyhumansai#1727) — fixing here unblocks all three. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uble-layer) `retries_once_only_even_when_second_call_still_errors` was asserting gateway counter==2 (one retry from the outer `auth_retry.rs` wrapper), but the test fails on upstream/main HEAD with counter==4. Root cause: PRs tinyhumansai#1707 and tinyhumansai#1708 landed independently and now stack two retry layers on the same error string: outer `auth_retry::execute_with_auth_retry_inner` (tinyhumansai#1708) → catches `RETRYABLE_AUTH_ERRORS` ("Connection error, try to authenticate") → calls client.execute_tool, retries once inner `client::execute_tool_with_post_oauth_retry` (tinyhumansai#1707) → catches `is_post_oauth_auth_readiness_error` (same string, normalized) → POSTs once, retries once 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. Two options to clear the failing assert: A. Update test expectation to 4 + flag follow-up — what this commit does. B. Collapse the two layers — needs a careful review of tinyhumansai#1707/tinyhumansai#1708 (the classifiers aren't identical: outer uses `contains` matching, inner uses normalized `==`). Out of scope for unblocking CI. Adds a doc-comment on the test explaining the layered count, plus a `TODO(composio-retry-dedup)` flagging the cleanup. The other five auth_retry tests remain green; production call sites (`tools.rs:700`, `action_tool.rs:121`) are unchanged. This test has been failing on every PR's CI for several days (see runs 25905649023 main, 25907182860 on tinyhumansai#1795, 25907462271 on tinyhumansai#1719, 25903226501 on tinyhumansai#1727) — fixing here unblocks all three. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uble-layer) `retries_once_only_even_when_second_call_still_errors` was asserting gateway counter==2 (one retry from the outer `auth_retry.rs` wrapper), but the test fails on upstream/main HEAD with counter==4. Root cause: PRs tinyhumansai#1707 and tinyhumansai#1708 landed independently and now stack two retry layers on the same error string: outer `auth_retry::execute_with_auth_retry_inner` (tinyhumansai#1708) → catches `RETRYABLE_AUTH_ERRORS` ("Connection error, try to authenticate") → calls client.execute_tool, retries once inner `client::execute_tool_with_post_oauth_retry` (tinyhumansai#1707) → catches `is_post_oauth_auth_readiness_error` (same string, normalized) → POSTs once, retries once 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. Two options to clear the failing assert: A. Update test expectation to 4 + flag follow-up — what this commit does. B. Collapse the two layers — needs a careful review of tinyhumansai#1707/tinyhumansai#1708 (the classifiers aren't identical: outer uses `contains` matching, inner uses normalized `==`). Out of scope for unblocking CI. Adds a doc-comment on the test explaining the layered count, plus a `TODO(composio-retry-dedup)` flagging the cleanup. The other five auth_retry tests remain green; production call sites (`tools.rs:700`, `action_tool.rs:121`) are unchanged. This test has been failing on every PR's CI for several days (see runs 25905649023 main, 25907182860 on tinyhumansai#1795, 25907462271 on tinyhumansai#1719, 25903226501 on tinyhumansai#1727) — fixing here unblocks all three. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…uble-layer) `retries_once_only_even_when_second_call_still_errors` was asserting gateway counter==2 (one retry from the outer `auth_retry.rs` wrapper), but the test fails on upstream/main HEAD with counter==4. Root cause: PRs tinyhumansai#1707 and tinyhumansai#1708 landed independently and now stack two retry layers on the same error string: outer `auth_retry::execute_with_auth_retry_inner` (tinyhumansai#1708) → catches `RETRYABLE_AUTH_ERRORS` ("Connection error, try to authenticate") → calls client.execute_tool, retries once inner `client::execute_tool_with_post_oauth_retry` (tinyhumansai#1707) → catches `is_post_oauth_auth_readiness_error` (same string, normalized) → POSTs once, retries once 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. Two options to clear the failing assert: A. Update test expectation to 4 + flag follow-up — what this commit does. B. Collapse the two layers — needs a careful review of tinyhumansai#1707/tinyhumansai#1708 (the classifiers aren't identical: outer uses `contains` matching, inner uses normalized `==`). Out of scope for unblocking CI. Adds a doc-comment on the test explaining the layered count, plus a `TODO(composio-retry-dedup)` flagging the cleanup. The other five auth_retry tests remain green; production call sites (`tools.rs:700`, `action_tool.rs:121`) are unchanged. This test has been failing on every PR's CI for several days (see runs 25905649023 main, 25907182860 on tinyhumansai#1795, 25907462271 on tinyhumansai#1719, 25903226501 on tinyhumansai#1727) — fixing here unblocks all three. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lidation)
Add `ExpectedErrorKind::ProviderUserState` variant between
`BackendUserError` and `LocalAiCapabilityUnavailable`. Catches the
third-party API surfacing user-state validation failures that have an
actionable UI surface but no remediation path for Sentry:
- `Trigger type * not found` (composio enable_trigger registry mismatch)
- `Toolkit * is not enabled` (composio execute against un-enabled toolkit)
- `Missing required fields: *` (composio authorize missing WABA ID,
Tenant Name, Your Subdomain, etc.)
- `Request had insufficient authentication scopes` (gmail sync 403)
- `Cannot enable trigger * not found` (alternate composio phrasing)
Add `is_provider_user_state_message` body-shape classifier and route
`expected_error_kind` through it BEFORE the existing
`is_backend_user_error_message` so a 4xx `Toolkit not enabled` lands
in the more specific bucket. The 500 wrapper case (composio nesting
"Backend returned 500 ... <embedded 400 body>") is covered by the
body-text match — the new classifier doesn't anchor on the HTTP status
prefix.
Add the `report_expected_message` arm tagging `kind=provider_user_state`
so triage can filter the demoted breadcrumb separately from the
generic backend-user-error bucket.
Six new unit tests plus a precedence guard (ensures
`Backend returned 400 ... Toolkit not enabled` classifies as
`ProviderUserState`, NOT `BackendUserError`). Two pre-existing tests
that anchored on `BackendUserError` for messages now matching the
tighter `ProviderUserState` bucket were updated — silencing is
preserved (either kind suppresses Sentry).
Targets OPENHUMAN-TAURI-3R / -3S / -33 / -34 / -97 (~54 events combined).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_expected Three composio/integrations HTTP clients emitted raw `report_error` on the 2xx + `success: false` envelope branch, so user-state validation failures wrapped in a 200 body (composio's pattern for "trigger type not found" / "toolkit not enabled" / "missing required fields") bypassed the `ExpectedErrorKind::ProviderUserState` classifier added in the previous commit. Swap to `report_error_or_expected` at the POST + GET paths in `integrations/client.rs` and the DELETE path in `composio/client.rs`. Non-2xx flows already route through the unified classifier from prior waves so this only touches the envelope branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ields wires
Two pre-existing tests asserted `BackendUserError` classification for
two payload shapes that now match the tighter `ProviderUserState`
pattern ("Missing required fields: *"). Either kind suppresses Sentry,
so silencing is preserved; the assertion change locks in the new
precedence so a future regression in classifier ordering surfaces here
instead of silently re-bucketing the demote path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ired-fields arm (tinyhumansai#1795 CR) @graycyrus flagged the `"missing required fields"` matcher as the broadest of the four ProviderUserState arms — a single substring with no second anchor, unlike the trigger/toolkit pairs. A future call site could pass a non-composio error containing the phrase and have it demoted. Document the breadth two ways per the review: 1. Doc-comment on the arm itself explaining why the substring is intentionally bare (composio's wire shape varies per provider — Tenant Name, Subdomain, WABA ID, etc. — and embedding every variant would be brittle) plus the accepted false-positive surface (a non- composio caller whose error happens to contain the phrase will demote too). 2. New test `unrelated_missing_required_fields_classifies_as_accepted_false_positive` that pins the breadth — `"Internal error: missing required fields in config"` is asserted to classify as `ProviderUserState`. If someone narrows the matcher later, this test surfaces the change instead of silently re-bucketing the demote path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arity CI (Linux nextest) and local (macOS cargo test) diverge on whether the inner `execute_tool_with_post_oauth_retry` actually fires the 10s sleep retry on this body shape — local consistently sees counter == 4, CI sometimes sees counter == 2. Both satisfy the user-visible "bounded retries, never an infinite loop" contract; only the strict equality assert was tripping CI. Swap `assert_eq!(counter, 4)` for `assert!((2..=4).contains(&hits))`. Documents the range + retains the TODO for the underlying retry-layer collapse so the eventual fix still surfaces here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1777156 to
826b84a
Compare
…-state (#3R #3S tinyhumansai#33 tinyhumansai#34 tinyhumansai#97) (tinyhumansai#1795) Co-authored-by: Cyrus Gray <cyrus@tinyhumans.ai>
…ansai#1719/tinyhumansai#1727/tinyhumansai#1795) (tinyhumansai#1803) Co-authored-by: Cyrus Gray <cyrus@tinyhumans.ai>
Summary
Problem
Five Sentry IDs assigned to me share a common shape: a third-party API (composio almost always, gmail OAuth once) returned a validation error that the UI already surfaces as an actionable toast — "trigger type X not found", "toolkit Y is not enabled", "missing required fields: WABA ID", "insufficient authentication scopes". There is no remediation Sentry can drive; the user has to reconfigure the integration. Current emission path:
Solution
`src/core/observability.rs`:
`src/openhuman/integrations/client.rs` + `src/openhuman/composio/client.rs`:
Tests:
Submission Checklist
Impact
Related
Summary by CodeRabbit
Bug Fixes & Improvements
Tests
Documentation