feat(span-first): Support before_send_span#6239
3 issues
find-bugs: Found 3 issues (2 medium, 1 low)
Medium
`before_send_span` callback invoked without exception guard, crashing user code on error - `sentry_sdk/client.py:55`
The before_send_span callback is called at _capture_telemetry without wrapping it in capture_internal_exceptions(), so any exception raised by the user's callback propagates all the way to span.end() / the with start_span(): exit, breaking user code. Both before_send (line 873) and before_send_transaction (line 899) are wrapped with with capture_internal_exceptions():; apply the same guard here.
Also found at:
sentry_sdk/utils.py:2121
`before_send_span` silently drops all attributes when callback returns dict with `name` but no `attributes` key - `sentry_sdk/client.py:1211-1216`
When before_send_span returns a dict containing "name" but no "attributes" key, telemetry._attributes is unconditionally cleared and rebuilt from the (missing) key — effectively dropping all span attributes, including sentry-internal ones like sentry.segment.id and sentry.segment.name that are set in _end() just before the span is captured.
Low
`before_send_span` return type annotation declares `Optional[SpanJSON]` but `None` is not a valid drop signal - `sentry_sdk/consts.py:88-90`
In sentry_sdk/consts.py the _experiments typed option declares before_send_span as Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]], and get_before_send_span in sentry_sdk/utils.py mirrors this Optional[SpanJSON] return type. However, by design (and per the PR description), before_send_span cannot drop a span. In client.py::_capture_telemetry, when the callback returns None, the code falls through the isinstance(serialized, dict) and 'name' in serialized check, logs "[Tracing] Invalid return value from before_send_span. Keeping original span." at debug level, and re-serializes the original span. This contradicts the type signature, which suggests None is a legitimate return (as it is for before_send, before_send_log, and before_send_metric). Users typing against the public type may reasonably write return None to attempt to drop a span and instead get a silent debug-log warning while the span is still sent. Recommend tightening the type to Callable[[SpanJSON, Hint], SpanJSON] (non-optional return), and updating get_before_send_span accordingly.
⏱ 14m 55s · 6.6M in / 188.5k out · $7.01
Annotations
Check warning on line 55 in sentry_sdk/client.py
sentry-warden / warden: find-bugs
`before_send_span` callback invoked without exception guard, crashing user code on error
The `before_send_span` callback is called at `_capture_telemetry` without wrapping it in `capture_internal_exceptions()`, so any exception raised by the user's callback propagates all the way to `span.end()` / the `with start_span():` exit, breaking user code. Both `before_send` (line 873) and `before_send_transaction` (line 899) are wrapped with `with capture_internal_exceptions():`; apply the same guard here.
Check warning on line 2121 in sentry_sdk/utils.py
sentry-warden / warden: find-bugs
[284-92E] `before_send_span` callback invoked without exception guard, crashing user code on error (additional location)
The `before_send_span` callback is called at `_capture_telemetry` without wrapping it in `capture_internal_exceptions()`, so any exception raised by the user's callback propagates all the way to `span.end()` / the `with start_span():` exit, breaking user code. Both `before_send` (line 873) and `before_send_transaction` (line 899) are wrapped with `with capture_internal_exceptions():`; apply the same guard here.
Check warning on line 1216 in sentry_sdk/client.py
sentry-warden / warden: find-bugs
`before_send_span` silently drops all attributes when callback returns dict with `name` but no `attributes` key
When `before_send_span` returns a dict containing `"name"` but no `"attributes"` key, `telemetry._attributes` is unconditionally cleared and rebuilt from the (missing) key — effectively dropping all span attributes, including sentry-internal ones like `sentry.segment.id` and `sentry.segment.name` that are set in `_end()` just before the span is captured.