Skip to content

Commit 0ebe64f

Browse files
oxoxDevclaude
andcommitted
fix(observability): scope before_send filter to llm_provider domain
Per CodeRabbit review on tinyhumansai#1529 (Major, observability.rs:81-89): the before_send filter matched on `failure=non_2xx` + `status` alone, so an unrelated subsystem emitting those tags would have its event silently dropped. Add a `domain == "llm_provider"` precondition so only provider-originated events qualify for the transient filter. All current emit sites at `providers/ops.rs:218-227` already pass `"llm_provider"` as the domain argument to `report_error`, which the helper writes as a tag — so behavior is preserved for the target events; the guard only narrows what *future* call sites can match. Tests updated to include the domain tag in the existing fixtures, plus two new regression cases: events with no domain tag, and events from a different domain (`scheduler`) reusing the same `failure`/`status` shape. Both must surface to Sentry rather than being dropped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1388d79 commit 0ebe64f

2 files changed

Lines changed: 69 additions & 10 deletions

File tree

‎src/core/observability.rs‎

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,17 @@ pub fn report_error<E: Display + ?Sized>(
9090
/// site that emits a `tracing::error!` with the same shape but bypasses
9191
/// the classifier.
9292
///
93-
/// Match criteria:
93+
/// Match criteria (all required):
94+
/// - tag `domain == "llm_provider"` — pins the filter to provider-originated
95+
/// events so an unrelated subsystem emitting `failure=non_2xx`/`status=503`
96+
/// for its own reasons doesn't get silently dropped
9497
/// - tag `failure == "non_2xx"` (the marker set by `ops::api_error`)
95-
/// - tag `status` matches a known transient status (see
96-
/// [`TRANSIENT_PROVIDER_HTTP_STATUSES`])
98+
/// - tag `status` parses to one of [`TRANSIENT_PROVIDER_HTTP_STATUSES`]
9799
pub fn is_transient_provider_http_failure(event: &sentry::protocol::Event<'_>) -> bool {
98100
let tags = &event.tags;
101+
if tags.get("domain").map(String::as_str) != Some("llm_provider") {
102+
return false;
103+
}
99104
if tags.get("failure").map(String::as_str) != Some("non_2xx") {
100105
return false;
101106
}
@@ -157,7 +162,11 @@ mod tests {
157162
#[test]
158163
fn transient_filter_drops_429_408_502_503_504() {
159164
for status in ["429", "408", "502", "503", "504"] {
160-
let event = event_with_tags(&[("failure", "non_2xx"), ("status", status)]);
165+
let event = event_with_tags(&[
166+
("domain", "llm_provider"),
167+
("failure", "non_2xx"),
168+
("status", status),
169+
]);
161170
assert!(
162171
is_transient_provider_http_failure(&event),
163172
"status {status} must be classified as transient and filtered"
@@ -168,7 +177,11 @@ mod tests {
168177
#[test]
169178
fn transient_filter_keeps_permanent_failures() {
170179
for status in ["400", "401", "403", "404", "500"] {
171-
let event = event_with_tags(&[("failure", "non_2xx"), ("status", status)]);
180+
let event = event_with_tags(&[
181+
("domain", "llm_provider"),
182+
("failure", "non_2xx"),
183+
("status", status),
184+
]);
172185
assert!(
173186
!is_transient_provider_http_failure(&event),
174187
"status {status} must NOT be filtered — it's actionable"
@@ -178,7 +191,11 @@ mod tests {
178191

179192
#[test]
180193
fn transient_filter_keeps_aggregate_all_exhausted() {
181-
let event = event_with_tags(&[("failure", "all_exhausted"), ("status", "503")]);
194+
let event = event_with_tags(&[
195+
("domain", "llm_provider"),
196+
("failure", "all_exhausted"),
197+
("status", "503"),
198+
]);
182199
assert!(
183200
!is_transient_provider_http_failure(&event),
184201
"aggregate all_exhausted events must surface (they are the cascade signal)"
@@ -187,10 +204,39 @@ mod tests {
187204

188205
#[test]
189206
fn transient_filter_keeps_events_with_no_status_tag() {
190-
let event = event_with_tags(&[("failure", "non_2xx")]);
207+
let event = event_with_tags(&[("domain", "llm_provider"), ("failure", "non_2xx")]);
191208
assert!(
192209
!is_transient_provider_http_failure(&event),
193210
"missing status tag must not be silently dropped"
194211
);
195212
}
213+
214+
// Regression guard for CodeRabbit review on #1529: the filter must scope
215+
// to provider events only. Other subsystems emit `failure=non_2xx` (e.g.
216+
// `providers/compatible.rs` uses the same marker for OAI-compatible
217+
// error paths, but every site goes through `report_error(..,
218+
// "llm_provider", ..)` so the domain tag is consistent), but the broader
219+
// point is: any future caller that re-uses the same tag set for a
220+
// different domain must NOT be silently dropped by this filter.
221+
#[test]
222+
fn transient_filter_keeps_events_with_no_domain_tag() {
223+
let event = event_with_tags(&[("failure", "non_2xx"), ("status", "503")]);
224+
assert!(
225+
!is_transient_provider_http_failure(&event),
226+
"missing domain tag means the event isn't provider-originated — must surface"
227+
);
228+
}
229+
230+
#[test]
231+
fn transient_filter_keeps_events_from_other_domains() {
232+
let event = event_with_tags(&[
233+
("domain", "scheduler"),
234+
("failure", "non_2xx"),
235+
("status", "503"),
236+
]);
237+
assert!(
238+
!is_transient_provider_http_failure(&event),
239+
"non-provider domain must surface even if failure/status tags collide"
240+
);
241+
}
196242
}

‎tests/observability_smoke.rs‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,13 @@ fn drops_per_attempt_429_503_504_408_502() {
6464
// before_send, none should leak through to the transport.
6565
let events = ["429", "503", "504", "408", "502"]
6666
.into_iter()
67-
.map(|status| event_with_tags(&[("failure", "non_2xx"), ("status", status)]))
67+
.map(|status| {
68+
event_with_tags(&[
69+
("domain", "llm_provider"),
70+
("failure", "non_2xx"),
71+
("status", status),
72+
])
73+
})
6874
.collect();
6975
assert_eq!(
7076
count_captured(events),
@@ -79,7 +85,13 @@ fn keeps_permanent_failures() {
7985
// they must reach Sentry exactly as before.
8086
let events = ["400", "401", "403", "404", "500"]
8187
.into_iter()
82-
.map(|status| event_with_tags(&[("failure", "non_2xx"), ("status", status)]))
88+
.map(|status| {
89+
event_with_tags(&[
90+
("domain", "llm_provider"),
91+
("failure", "non_2xx"),
92+
("status", status),
93+
])
94+
})
8395
.collect();
8496
assert_eq!(
8597
count_captured(events),
@@ -95,6 +107,7 @@ fn keeps_aggregate_all_exhausted_event() {
95107
// tried. That's the cascade signal we want — only the per-attempt
96108
// noise gets dropped.
97109
let event = event_with_tags(&[
110+
("domain", "llm_provider"),
98111
("failure", "all_exhausted"),
99112
("model", "claude-haiku-4-5-20251001"),
100113
("attempts", "12"),
@@ -111,7 +124,7 @@ fn keeps_event_missing_status_tag() {
111124
// Belt-and-suspenders: an event with `failure=non_2xx` but no `status`
112125
// tag (e.g. a future call site forgets to attach one) must NOT be
113126
// silently dropped — we'd rather see it and fix the tag emission.
114-
let event = event_with_tags(&[("failure", "non_2xx")]);
127+
let event = event_with_tags(&[("domain", "llm_provider"), ("failure", "non_2xx")]);
115128
assert_eq!(
116129
count_captured(vec![event]),
117130
1,

0 commit comments

Comments
 (0)