Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
6 changes: 6 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.

Expand Down
4 changes: 3 additions & 1 deletion sentry_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
114 changes: 91 additions & 23 deletions tests/tracing/test_span_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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,
Expand Down
Loading