From f42bc4bd58ed1b41a67bd6a58a513329735d7e5b Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 May 2026 12:22:02 +0200 Subject: [PATCH 1/5] feat: Support feature flags in span first --- sentry_sdk/feature_flags.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/feature_flags.py b/sentry_sdk/feature_flags.py index de0cefbcad..c700c1c2fc 100644 --- a/sentry_sdk/feature_flags.py +++ b/sentry_sdk/feature_flags.py @@ -1,6 +1,7 @@ import copy import sentry_sdk from sentry_sdk._lru_cache import LRUCache +from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import Span from threading import Lock @@ -62,5 +63,8 @@ def add_feature_flag(flag: str, result: bool) -> None: flags.set(flag, result) span = sentry_sdk.get_current_span() - if span and isinstance(span, Span): - span.set_flag(f"flag.evaluation.{flag}", result) + if span: + if isinstance(span, Span): + span.set_flag(f"flag.evaluation.{flag}", result) + elif isinstance(span, StreamedSpan): + span.set_attribute(f"flag.evaluation.{flag}", result) From c08f6432338424c65e76d94169b2d9af8c3bef59 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 May 2026 12:40:34 +0200 Subject: [PATCH 2/5] fixes and unleash --- sentry_sdk/feature_flags.py | 17 ++++++--- tests/integrations/unleash/test_unleash.py | 44 +++++++++++++++++----- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/sentry_sdk/feature_flags.py b/sentry_sdk/feature_flags.py index c700c1c2fc..2bcded8f91 100644 --- a/sentry_sdk/feature_flags.py +++ b/sentry_sdk/feature_flags.py @@ -1,8 +1,8 @@ import copy import sentry_sdk from sentry_sdk._lru_cache import LRUCache -from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import Span +from sentry_sdk.tracing_utils import has_span_streaming_enabled from threading import Lock from typing import TYPE_CHECKING, Any @@ -59,12 +59,17 @@ def add_feature_flag(flag: str, result: bool) -> None: Records a flag and its value to be sent on subsequent error events. We recommend you do this on flag evaluations. Flags are buffered per Sentry scope. """ + client = sentry_sdk.get_client() + flags = sentry_sdk.get_isolation_scope().flags flags.set(flag, result) - span = sentry_sdk.get_current_span() - if span: - if isinstance(span, Span): - span.set_flag(f"flag.evaluation.{flag}", result) - elif isinstance(span, StreamedSpan): + if has_span_streaming_enabled(client.options): + span = sentry_sdk.traces._get_current_streamed_span() + if span and isinstance(span, sentry_sdk.traces.StreamedSpan): span.set_attribute(f"flag.evaluation.{flag}", result) + + else: + span = sentry_sdk.get_current_span() + if span and isinstance(span, Span): + span.set_flag(f"flag.evaluation.{flag}", result) diff --git a/tests/integrations/unleash/test_unleash.py b/tests/integrations/unleash/test_unleash.py index 98a6188181..1753d78626 100644 --- a/tests/integrations/unleash/test_unleash.py +++ b/tests/integrations/unleash/test_unleash.py @@ -168,19 +168,45 @@ def test_wrapper_attributes(sentry_init, uninstall_integration): assert client.is_enabled.__qualname__ == original_is_enabled.__qualname__ -def test_unleash_span_integration(sentry_init, capture_events, uninstall_integration): +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) +def test_unleash_span_integration( + sentry_init, capture_events, capture_items, uninstall_integration, span_streaming +): uninstall_integration(UnleashIntegration.identifier) with mock_unleash_client(): - sentry_init(traces_sample_rate=1.0, integrations=[UnleashIntegration()]) - events = capture_events() + sentry_init( + traces_sample_rate=1.0, + integrations=[UnleashIntegration()], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) + client = UnleashClient() # type: ignore[arg-type] - with start_transaction(name="hi"): - with start_span(op="foo", name="bar"): + + if span_streaming: + items = capture_items("span") + with sentry_sdk.traces.start_span(name="bar"): client.is_enabled("hello") client.is_enabled("other") - (event,) = events - assert event["spans"][0]["data"] == ApproxDict( - {"flag.evaluation.hello": True, "flag.evaluation.other": False} - ) + sentry_sdk.flush() + + assert len(items) == 1 + span = items[0].payload + assert span["attributes"]["flag.evaluation.hello"] is True + assert span["attributes"]["flag.evaluation.other"] is False + + else: + events = capture_events() + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + client.is_enabled("hello") + client.is_enabled("other") + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict( + {"flag.evaluation.hello": True, "flag.evaluation.other": False} + ) From 3104263ae5589ed7f723d51cba49c6eb6bd49d47 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 May 2026 12:45:49 +0200 Subject: [PATCH 3/5] statsig --- tests/integrations/statsig/test_statsig.py | 43 +++++++++++++++++----- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/tests/integrations/statsig/test_statsig.py b/tests/integrations/statsig/test_statsig.py index 5eb2cf39f3..4d091ddc30 100644 --- a/tests/integrations/statsig/test_statsig.py +++ b/tests/integrations/statsig/test_statsig.py @@ -185,19 +185,44 @@ def test_wrapper_attributes(sentry_init, uninstall_integration): statsig.check_gate = original_check_gate -def test_statsig_span_integration(sentry_init, capture_events, uninstall_integration): +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) +def test_statsig_span_integration( + sentry_init, capture_events, capture_items, uninstall_integration, span_streaming +): uninstall_integration(StatsigIntegration.identifier) with mock_statsig({"hello": True}): - sentry_init(traces_sample_rate=1.0, integrations=[StatsigIntegration()]) - events = capture_events() + sentry_init( + traces_sample_rate=1.0, + integrations=[StatsigIntegration()], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) user = StatsigUser(user_id="user-id") - with start_transaction(name="hi"): - with start_span(op="foo", name="bar"): + + if span_streaming: + items = capture_items("span") + with sentry_sdk.traces.start_span(name="hi"): statsig.check_gate(user, "hello") statsig.check_gate(user, "world") - (event,) = events - assert event["spans"][0]["data"] == ApproxDict( - {"flag.evaluation.hello": True, "flag.evaluation.world": False} - ) + sentry_sdk.flush() + + assert len(items) == 1 + span = items[0].payload + assert span["attributes"]["flag.evaluation.hello"] is True + assert span["attributes"]["flag.evaluation.world"] is False + + else: + events = capture_events() + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + statsig.check_gate(user, "hello") + statsig.check_gate(user, "world") + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict( + {"flag.evaluation.hello": True, "flag.evaluation.world": False} + ) From ef4ecd1ebf004f0edd71921354fb23f9260b116f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 May 2026 12:51:10 +0200 Subject: [PATCH 4/5] launchdarkly --- .../launchdarkly/test_launchdarkly.py | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index e588b596d3..80ff3d408d 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -216,8 +216,17 @@ def test_launchdarkly_integration_did_not_enable(monkeypatch): "use_global_client", (False, True), ) +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) def test_launchdarkly_span_integration( - sentry_init, use_global_client, capture_events, uninstall_integration + sentry_init, + use_global_client, + capture_events, + capture_items, + uninstall_integration, + span_streaming, ): td = TestData.data_source() td.update(td.flag("hello").variation_for_all(True)) @@ -229,23 +238,47 @@ def test_launchdarkly_span_integration( uninstall_integration(LaunchDarklyIntegration.identifier) if use_global_client: ldclient.set_config(config) - sentry_init(traces_sample_rate=1.0, integrations=[LaunchDarklyIntegration()]) + sentry_init( + traces_sample_rate=1.0, + integrations=[LaunchDarklyIntegration()], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) client = ldclient.get() else: client = LDClient(config=config) sentry_init( traces_sample_rate=1.0, integrations=[LaunchDarklyIntegration(ld_client=client)], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, ) - events = capture_events() + if span_streaming: + items = capture_items("span") - with start_transaction(name="hi"): - with start_span(op="foo", name="bar"): + with sentry_sdk.traces.start_span(name="bar"): client.variation("hello", Context.create("my-org", "organization"), False) client.variation("other", Context.create("my-org", "organization"), False) - (event,) = events - assert event["spans"][0]["data"] == ApproxDict( - {"flag.evaluation.hello": True, "flag.evaluation.other": False} - ) + sentry_sdk.flush() + + assert len(items) == 1 + span = items[0].payload + assert span["attributes"]["flag.evaluation.hello"] is True + assert span["attributes"]["flag.evaluation.other"] is False + + else: + events = capture_events() + + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + client.variation( + "hello", Context.create("my-org", "organization"), False + ) + client.variation( + "other", Context.create("my-org", "organization"), False + ) + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict( + {"flag.evaluation.hello": True, "flag.evaluation.other": False} + ) From bb282575406ae4cec585c9368b1b982070785fbc Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 7 May 2026 12:53:24 +0200 Subject: [PATCH 5/5] openfeature --- .../openfeature/test_openfeature.py | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/tests/integrations/openfeature/test_openfeature.py b/tests/integrations/openfeature/test_openfeature.py index 46acc61ae7..845c6c371f 100644 --- a/tests/integrations/openfeature/test_openfeature.py +++ b/tests/integrations/openfeature/test_openfeature.py @@ -155,25 +155,51 @@ async def runner(): } +@pytest.mark.parametrize( + "span_streaming", + [True, False], +) def test_openfeature_span_integration( - sentry_init, capture_events, uninstall_integration + sentry_init, + capture_events, + capture_items, + uninstall_integration, + span_streaming, ): uninstall_integration(OpenFeatureIntegration.identifier) - sentry_init(traces_sample_rate=1.0, integrations=[OpenFeatureIntegration()]) + sentry_init( + traces_sample_rate=1.0, + integrations=[OpenFeatureIntegration()], + _experiments={"trace_lifecycle": "stream" if span_streaming else "static"}, + ) api.set_provider( InMemoryProvider({"hello": InMemoryFlag("on", {"on": True, "off": False})}) ) client = api.get_client() - events = capture_events() - - with start_transaction(name="hi"): - with start_span(op="foo", name="bar"): + if span_streaming: + items = capture_items("span") + with sentry_sdk.traces.start_span(name="bar"): client.get_boolean_value("hello", default_value=False) client.get_boolean_value("world", default_value=False) - (event,) = events - assert event["spans"][0]["data"] == ApproxDict( - {"flag.evaluation.hello": True, "flag.evaluation.world": False} - ) + sentry_sdk.flush() + + assert len(items) == 1 + span = items[0].payload + assert span["attributes"]["flag.evaluation.hello"] is True + assert span["attributes"]["flag.evaluation.world"] is False + + else: + events = capture_events() + + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + client.get_boolean_value("hello", default_value=False) + client.get_boolean_value("world", default_value=False) + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict( + {"flag.evaluation.hello": True, "flag.evaluation.world": False} + )