diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index c10daad6ed..5f05f506d4 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -397,6 +397,12 @@ def _get_options(*args: "Optional[str]", **kwargs: "Any") -> "Dict[str, Any]": stacklevel=2, ) + if rv["before_send_span"] and not has_span_streaming_enabled(rv): + warnings.warn( + "The `before_send_span` parameter only works when `trace_lifecycle` is set to `stream`.", + stacklevel=2, + ) + return rv diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index dff264f9f8..951ea66783 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1336,6 +1336,7 @@ def __init__( trace_ignore_status_codes: "AbstractSet[int]" = frozenset(), enable_metrics: bool = True, before_send_metric: "Optional[Callable[[Metric, Hint], Optional[Metric]]]" = None, + before_send_span: "Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]]" = None, org_id: "Optional[str]" = None, strict_trace_continuation: bool = False, stream_gen_ai_spans: bool = True, @@ -1756,6 +1757,11 @@ def __init__( but you can provide it explicitly for self-hosted and Relay setups. This value is used for trace propagation and for features like `strict_trace_continuation`. + :param before_send_span: An optional function to modify spans before they're sent to Sentry. + Modifications to the span's attributes and name will be retained. Unlike ``before_send_log`` + and ``before_send_metric``, spans cannot be dropped by returning None. Only works when + ``trace_lifecycle="stream"`` is enabled. + :param stream_gen_ai_spans: When set, generative AI spans are sent in a new transport format to reduce downstream data loss. diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 5aa533dea0..a6ece4faf1 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -2122,7 +2122,9 @@ def get_before_send_span( if options is None: return None - return options["_experiments"].get("before_send_span") + return options.get("before_send_span") or options["_experiments"].get( + "before_send_span" + ) def format_attribute(val: "Any") -> "AttributeValue": diff --git a/tests/tracing/test_span_streaming.py b/tests/tracing/test_span_streaming.py index 74e4744066..cae8e181a0 100644 --- a/tests/tracing/test_span_streaming.py +++ b/tests/tracing/test_span_streaming.py @@ -235,10 +235,8 @@ def before_send_span(span, hint): sentry_init( traces_sample_rate=1.0, - _experiments={ - "before_send_span": before_send_span, - "trace_lifecycle": "stream", - }, + trace_lifecycle="stream", + before_send_span=before_send_span, ) items = capture_items("span") @@ -278,10 +276,8 @@ def before_send_span(span, hint): sentry_init( traces_sample_rate=1.0, - _experiments={ - "before_send_span": before_send_span, - "trace_lifecycle": "stream", - }, + trace_lifecycle="stream", + before_send_span=before_send_span, ) items = capture_items("span") @@ -307,10 +303,8 @@ def before_send_span(span, hint): sentry_init( traces_sample_rate=1.0, - _experiments={ - "before_send_span": before_send_span, - "trace_lifecycle": "stream", - }, + trace_lifecycle="stream", + before_send_span=before_send_span, ) items = capture_items("span") @@ -338,13 +332,11 @@ def before_send_span(span, hint): sentry_init( traces_sample_rate=1.0, - _experiments={ - "before_send_span": before_send_span, - "trace_lifecycle": "stream", - "ignore_spans": [ - "ignored", - ], - }, + trace_lifecycle="stream", + before_send_span=before_send_span, + ignore_spans=[ + "ignored", + ], ) items = capture_items("span") @@ -370,10 +362,8 @@ def before_send_span(span, hint): sentry_init( traces_sample_rate=1.0, - _experiments={ - "before_send_span": before_send_span, - "trace_lifecycle": "stream", - }, + trace_lifecycle="stream", + before_send_span=before_send_span, ) items = capture_items("span") @@ -394,6 +384,84 @@ def before_send_span(span, hint): assert "mutated" not in span["attributes"] +def test_before_send_span_set_in_experiments(sentry_init, capture_items): + def before_send_span(span, hint): + span["name"] = "from experiments" + return span + + sentry_init( + traces_sample_rate=1.0, + trace_lifecycle="stream", + _experiments={ + "before_send_span": before_send_span, + }, + ) + + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="span"): + ... + + sentry_sdk.get_client().flush() + spans = [item.payload for item in items] + + assert len(spans) == 1 + (span,) = spans + + assert span["name"] == "from experiments" + + +def test_before_send_span_top_level_takes_precedence_over_experiments( + sentry_init, capture_items +): + def top_level(span, hint): + span["name"] = "top-level" + return span + + def experimental(span, hint): + span["name"] = "experimental" + return span + + sentry_init( + traces_sample_rate=1.0, + trace_lifecycle="stream", + before_send_span=top_level, + _experiments={ + "before_send_span": experimental, + }, + ) + + items = capture_items("span") + + with sentry_sdk.traces.start_span(name="span"): + ... + + sentry_sdk.get_client().flush() + spans = [item.payload for item in items] + + assert len(spans) == 1 + (span,) = spans + + assert span["name"] == "top-level" + + +def test_before_send_span_warns_without_span_streaming(sentry_init): + import warnings + + def before_send_span(span, hint): + return span + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + sentry_init( + traces_sample_rate=1.0, + before_send_span=before_send_span, + ) + + (warning,) = [x for x in w if "before_send_span" in str(x.message)] + assert "trace_lifecycle" in str(warning.message) + + def test_span_attributes(sentry_init, capture_items): sentry_init( traces_sample_rate=1.0,