Skip to content

Commit 106fe71

Browse files
authored
feat: Support feature flags in span first (#6234)
### Description Set [`flag.evaluation.{key}`](https://getsentry.github.io/sentry-conventions/attributes/flag/#flag-evaluation-key) as span attribute in span streaming mode. Add a span streaming test variant to affected integrations: - Unleash - Statsig - Launchdarkly - OpenFeature #### Issues - Closes https://linear.app/getsentry/issue/PY-2141/support-feature-flags-in-span-first - Closes #5676 - Closes https://linear.app/getsentry/issue/PY-2372/migrate-unleash-to-span-first - Closes #6070 - Closes https://linear.app/getsentry/issue/PY-2364/migrate-statsig-to-span-first - Closes #6062 - Closes https://linear.app/getsentry/issue/PY-2335/migrate-launchdarkly-to-span-first - Closes #6033 - Closes https://linear.app/getsentry/issue/PY-2344/migrate-openfeature-to-span-first - Closes #6042 #### Reminders - Please add tests to validate your changes, and lint your code using `tox -e linters`. - Add GH Issue ID _&_ Linear ID (if applicable) - PR title should use [conventional commit](https://develop.sentry.dev/engineering-practices/commit-messages/#type) style (`feat:`, `fix:`, `ref:`, `meta:`) - For external contributors: [CONTRIBUTING.md](https://github.com/getsentry/sentry-python/blob/master/CONTRIBUTING.md), [Sentry SDK development docs](https://develop.sentry.dev/sdk/), [Discord community](https://discord.gg/Ww9hbqr)
1 parent 1690236 commit 106fe71

5 files changed

Lines changed: 159 additions & 40 deletions

File tree

sentry_sdk/feature_flags.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import sentry_sdk
33
from sentry_sdk._lru_cache import LRUCache
44
from sentry_sdk.tracing import Span
5+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
56
from threading import Lock
67

78
from typing import TYPE_CHECKING, Any
@@ -58,9 +59,17 @@ def add_feature_flag(flag: str, result: bool) -> None:
5859
Records a flag and its value to be sent on subsequent error events.
5960
We recommend you do this on flag evaluations. Flags are buffered per Sentry scope.
6061
"""
62+
client = sentry_sdk.get_client()
63+
6164
flags = sentry_sdk.get_isolation_scope().flags
6265
flags.set(flag, result)
6366

64-
span = sentry_sdk.get_current_span()
65-
if span and isinstance(span, Span):
66-
span.set_flag(f"flag.evaluation.{flag}", result)
67+
if has_span_streaming_enabled(client.options):
68+
span = sentry_sdk.traces._get_current_streamed_span()
69+
if span and isinstance(span, sentry_sdk.traces.StreamedSpan):
70+
span.set_attribute(f"flag.evaluation.{flag}", result)
71+
72+
else:
73+
span = sentry_sdk.get_current_span()
74+
if span and isinstance(span, Span):
75+
span.set_flag(f"flag.evaluation.{flag}", result)

tests/integrations/launchdarkly/test_launchdarkly.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,17 @@ def test_launchdarkly_integration_did_not_enable(monkeypatch):
216216
"use_global_client",
217217
(False, True),
218218
)
219+
@pytest.mark.parametrize(
220+
"span_streaming",
221+
[True, False],
222+
)
219223
def test_launchdarkly_span_integration(
220-
sentry_init, use_global_client, capture_events, uninstall_integration
224+
sentry_init,
225+
use_global_client,
226+
capture_events,
227+
capture_items,
228+
uninstall_integration,
229+
span_streaming,
221230
):
222231
td = TestData.data_source()
223232
td.update(td.flag("hello").variation_for_all(True))
@@ -229,23 +238,47 @@ def test_launchdarkly_span_integration(
229238
uninstall_integration(LaunchDarklyIntegration.identifier)
230239
if use_global_client:
231240
ldclient.set_config(config)
232-
sentry_init(traces_sample_rate=1.0, integrations=[LaunchDarklyIntegration()])
241+
sentry_init(
242+
traces_sample_rate=1.0,
243+
integrations=[LaunchDarklyIntegration()],
244+
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
245+
)
233246
client = ldclient.get()
234247
else:
235248
client = LDClient(config=config)
236249
sentry_init(
237250
traces_sample_rate=1.0,
238251
integrations=[LaunchDarklyIntegration(ld_client=client)],
252+
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
239253
)
240254

241-
events = capture_events()
255+
if span_streaming:
256+
items = capture_items("span")
242257

243-
with start_transaction(name="hi"):
244-
with start_span(op="foo", name="bar"):
258+
with sentry_sdk.traces.start_span(name="bar"):
245259
client.variation("hello", Context.create("my-org", "organization"), False)
246260
client.variation("other", Context.create("my-org", "organization"), False)
247261

248-
(event,) = events
249-
assert event["spans"][0]["data"] == ApproxDict(
250-
{"flag.evaluation.hello": True, "flag.evaluation.other": False}
251-
)
262+
sentry_sdk.flush()
263+
264+
assert len(items) == 1
265+
span = items[0].payload
266+
assert span["attributes"]["flag.evaluation.hello"] is True
267+
assert span["attributes"]["flag.evaluation.other"] is False
268+
269+
else:
270+
events = capture_events()
271+
272+
with start_transaction(name="hi"):
273+
with start_span(op="foo", name="bar"):
274+
client.variation(
275+
"hello", Context.create("my-org", "organization"), False
276+
)
277+
client.variation(
278+
"other", Context.create("my-org", "organization"), False
279+
)
280+
281+
(event,) = events
282+
assert event["spans"][0]["data"] == ApproxDict(
283+
{"flag.evaluation.hello": True, "flag.evaluation.other": False}
284+
)

tests/integrations/openfeature/test_openfeature.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -155,25 +155,51 @@ async def runner():
155155
}
156156

157157

158+
@pytest.mark.parametrize(
159+
"span_streaming",
160+
[True, False],
161+
)
158162
def test_openfeature_span_integration(
159-
sentry_init, capture_events, uninstall_integration
163+
sentry_init,
164+
capture_events,
165+
capture_items,
166+
uninstall_integration,
167+
span_streaming,
160168
):
161169
uninstall_integration(OpenFeatureIntegration.identifier)
162-
sentry_init(traces_sample_rate=1.0, integrations=[OpenFeatureIntegration()])
170+
sentry_init(
171+
traces_sample_rate=1.0,
172+
integrations=[OpenFeatureIntegration()],
173+
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
174+
)
163175

164176
api.set_provider(
165177
InMemoryProvider({"hello": InMemoryFlag("on", {"on": True, "off": False})})
166178
)
167179
client = api.get_client()
168180

169-
events = capture_events()
170-
171-
with start_transaction(name="hi"):
172-
with start_span(op="foo", name="bar"):
181+
if span_streaming:
182+
items = capture_items("span")
183+
with sentry_sdk.traces.start_span(name="bar"):
173184
client.get_boolean_value("hello", default_value=False)
174185
client.get_boolean_value("world", default_value=False)
175186

176-
(event,) = events
177-
assert event["spans"][0]["data"] == ApproxDict(
178-
{"flag.evaluation.hello": True, "flag.evaluation.world": False}
179-
)
187+
sentry_sdk.flush()
188+
189+
assert len(items) == 1
190+
span = items[0].payload
191+
assert span["attributes"]["flag.evaluation.hello"] is True
192+
assert span["attributes"]["flag.evaluation.world"] is False
193+
194+
else:
195+
events = capture_events()
196+
197+
with start_transaction(name="hi"):
198+
with start_span(op="foo", name="bar"):
199+
client.get_boolean_value("hello", default_value=False)
200+
client.get_boolean_value("world", default_value=False)
201+
202+
(event,) = events
203+
assert event["spans"][0]["data"] == ApproxDict(
204+
{"flag.evaluation.hello": True, "flag.evaluation.world": False}
205+
)

tests/integrations/statsig/test_statsig.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,19 +185,44 @@ def test_wrapper_attributes(sentry_init, uninstall_integration):
185185
statsig.check_gate = original_check_gate
186186

187187

188-
def test_statsig_span_integration(sentry_init, capture_events, uninstall_integration):
188+
@pytest.mark.parametrize(
189+
"span_streaming",
190+
[True, False],
191+
)
192+
def test_statsig_span_integration(
193+
sentry_init, capture_events, capture_items, uninstall_integration, span_streaming
194+
):
189195
uninstall_integration(StatsigIntegration.identifier)
190196

191197
with mock_statsig({"hello": True}):
192-
sentry_init(traces_sample_rate=1.0, integrations=[StatsigIntegration()])
193-
events = capture_events()
198+
sentry_init(
199+
traces_sample_rate=1.0,
200+
integrations=[StatsigIntegration()],
201+
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
202+
)
194203
user = StatsigUser(user_id="user-id")
195-
with start_transaction(name="hi"):
196-
with start_span(op="foo", name="bar"):
204+
205+
if span_streaming:
206+
items = capture_items("span")
207+
with sentry_sdk.traces.start_span(name="hi"):
197208
statsig.check_gate(user, "hello")
198209
statsig.check_gate(user, "world")
199210

200-
(event,) = events
201-
assert event["spans"][0]["data"] == ApproxDict(
202-
{"flag.evaluation.hello": True, "flag.evaluation.world": False}
203-
)
211+
sentry_sdk.flush()
212+
213+
assert len(items) == 1
214+
span = items[0].payload
215+
assert span["attributes"]["flag.evaluation.hello"] is True
216+
assert span["attributes"]["flag.evaluation.world"] is False
217+
218+
else:
219+
events = capture_events()
220+
with start_transaction(name="hi"):
221+
with start_span(op="foo", name="bar"):
222+
statsig.check_gate(user, "hello")
223+
statsig.check_gate(user, "world")
224+
225+
(event,) = events
226+
assert event["spans"][0]["data"] == ApproxDict(
227+
{"flag.evaluation.hello": True, "flag.evaluation.world": False}
228+
)

tests/integrations/unleash/test_unleash.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,19 +168,45 @@ def test_wrapper_attributes(sentry_init, uninstall_integration):
168168
assert client.is_enabled.__qualname__ == original_is_enabled.__qualname__
169169

170170

171-
def test_unleash_span_integration(sentry_init, capture_events, uninstall_integration):
171+
@pytest.mark.parametrize(
172+
"span_streaming",
173+
[True, False],
174+
)
175+
def test_unleash_span_integration(
176+
sentry_init, capture_events, capture_items, uninstall_integration, span_streaming
177+
):
172178
uninstall_integration(UnleashIntegration.identifier)
173179

174180
with mock_unleash_client():
175-
sentry_init(traces_sample_rate=1.0, integrations=[UnleashIntegration()])
176-
events = capture_events()
181+
sentry_init(
182+
traces_sample_rate=1.0,
183+
integrations=[UnleashIntegration()],
184+
_experiments={"trace_lifecycle": "stream" if span_streaming else "static"},
185+
)
186+
177187
client = UnleashClient() # type: ignore[arg-type]
178-
with start_transaction(name="hi"):
179-
with start_span(op="foo", name="bar"):
188+
189+
if span_streaming:
190+
items = capture_items("span")
191+
with sentry_sdk.traces.start_span(name="bar"):
180192
client.is_enabled("hello")
181193
client.is_enabled("other")
182194

183-
(event,) = events
184-
assert event["spans"][0]["data"] == ApproxDict(
185-
{"flag.evaluation.hello": True, "flag.evaluation.other": False}
186-
)
195+
sentry_sdk.flush()
196+
197+
assert len(items) == 1
198+
span = items[0].payload
199+
assert span["attributes"]["flag.evaluation.hello"] is True
200+
assert span["attributes"]["flag.evaluation.other"] is False
201+
202+
else:
203+
events = capture_events()
204+
with start_transaction(name="hi"):
205+
with start_span(op="foo", name="bar"):
206+
client.is_enabled("hello")
207+
client.is_enabled("other")
208+
209+
(event,) = events
210+
assert event["spans"][0]["data"] == ApproxDict(
211+
{"flag.evaluation.hello": True, "flag.evaluation.other": False}
212+
)

0 commit comments

Comments
 (0)