Skip to content

Commit f576126

Browse files
committed
Add flag evaluations to spans as attributes
1 parent 6000f87 commit f576126

File tree

8 files changed

+131
-0
lines changed

8 files changed

+131
-0
lines changed

sentry_sdk/integrations/launchdarkly.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,15 @@ def metadata(self):
5353
def after_evaluation(self, series_context, data, detail):
5454
# type: (EvaluationSeriesContext, dict[Any, Any], EvaluationDetail) -> dict[Any, Any]
5555
if isinstance(detail.value, bool):
56+
# Errors support.
5657
flags = sentry_sdk.get_current_scope().flags
5758
flags.set(series_context.key, detail.value)
59+
60+
# Spans support.
61+
span = sentry_sdk.get_current_span()
62+
if span:
63+
span.set_data(f"flag.{series_context.key}", detail.value)
64+
5865
return data
5966

6067
def before_evaluation(self, series_context, data):

sentry_sdk/integrations/openfeature.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,23 @@ class OpenFeatureHook(Hook):
2929
def after(self, hook_context, details, hints):
3030
# type: (HookContext, FlagEvaluationDetails[bool], HookHints) -> None
3131
if isinstance(details.value, bool):
32+
# Errors support.
3233
flags = sentry_sdk.get_current_scope().flags
3334
flags.set(details.flag_key, details.value)
3435

36+
# Spans support.
37+
span = sentry_sdk.get_current_span()
38+
if span:
39+
span.set_data(f"flag.{details.flag_key}", details.value)
40+
3541
def error(self, hook_context, exception, hints):
3642
# type: (HookContext, Exception, HookHints) -> None
3743
if isinstance(hook_context.default_value, bool):
44+
# Errors support.
3845
flags = sentry_sdk.get_current_scope().flags
3946
flags.set(hook_context.flag_key, hook_context.default_value)
47+
48+
# Spans support.
49+
span = sentry_sdk.get_current_span()
50+
if span:
51+
span.set_data(f"flag.{hook_context.flag_key}", hook_context.value)

sentry_sdk/integrations/statsig.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from functools import wraps
22
from typing import Any, TYPE_CHECKING
33

4+
import sentry_sdk
45
from sentry_sdk.feature_flags import add_feature_flag
56
from sentry_sdk.integrations import Integration, DidNotEnable, _check_minimum_version
67
from sentry_sdk.utils import parse_version
@@ -30,8 +31,15 @@ def setup_once():
3031
@wraps(old_check_gate)
3132
def sentry_check_gate(user, gate, *args, **kwargs):
3233
# type: (StatsigUser, str, *Any, **Any) -> Any
34+
# Errors support.
3335
enabled = old_check_gate(user, gate, *args, **kwargs)
3436
add_feature_flag(gate, enabled)
37+
38+
# Spans support.
39+
span = sentry_sdk.get_current_span()
40+
if span:
41+
span.set_data(f"flag.{gate}", enabled)
42+
3543
return enabled
3644

3745
statsig_module.check_gate = sentry_check_gate

sentry_sdk/integrations/unleash.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,19 @@ def sentry_is_enabled(self, feature, *args, **kwargs):
2424
# type: (UnleashClient, str, *Any, **Any) -> Any
2525
enabled = old_is_enabled(self, feature, *args, **kwargs)
2626

27+
# Errors support.
28+
#
2729
# We have no way of knowing what type of unleash feature this is, so we have to treat
2830
# it as a boolean / toggle feature.
2931
flags = sentry_sdk.get_current_scope().flags
3032
flags.set(feature, enabled)
3133

34+
# Spans support.
35+
span = sentry_sdk.get_current_span()
36+
print(span)
37+
if span:
38+
span.set_data(f"flag.{feature}", enabled)
39+
3240
return enabled
3341

3442
UnleashClient.is_enabled = sentry_is_enabled # type: ignore

tests/integrations/launchdarkly/test_launchdarkly.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import sentry_sdk
1313
from sentry_sdk.integrations import DidNotEnable
1414
from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration
15+
from sentry_sdk import start_span, start_transaction
16+
from tests.conftest import ApproxDict
1517

1618

1719
@pytest.mark.parametrize(
@@ -202,3 +204,40 @@ def test_launchdarkly_integration_did_not_enable(monkeypatch):
202204
monkeypatch.setattr(client, "is_initialized", lambda: False)
203205
with pytest.raises(DidNotEnable):
204206
LaunchDarklyIntegration(ld_client=client)
207+
208+
209+
@pytest.mark.parametrize(
210+
"use_global_client",
211+
(False, True),
212+
)
213+
def test_launchdarkly_span_integration(
214+
sentry_init, use_global_client, capture_events, uninstall_integration
215+
):
216+
td = TestData.data_source()
217+
td.update(td.flag("hello").variation_for_all(True))
218+
td.update(td.flag("world").variation_for_all(True))
219+
# Disable background requests as we aren't using a server.
220+
config = Config(
221+
"sdk-key", update_processor_class=td, diagnostic_opt_out=True, send_events=False
222+
)
223+
224+
uninstall_integration(LaunchDarklyIntegration.identifier)
225+
if use_global_client:
226+
ldclient.set_config(config)
227+
sentry_init(traces_sample_rate=1, integrations=[LaunchDarklyIntegration()])
228+
client = ldclient.get()
229+
else:
230+
client = LDClient(config=config)
231+
sentry_init(
232+
traces_sample_rate=1,
233+
integrations=[LaunchDarklyIntegration(ld_client=client)],
234+
)
235+
236+
events = capture_events()
237+
238+
with start_transaction(name="hi"):
239+
with start_span(op="foo", name="bar"):
240+
client.variation("hello", Context.create("my-org", "organization"), False)
241+
242+
(event,) = events
243+
assert event["spans"][0]["data"] == ApproxDict({"flag.hello": True})

tests/integrations/openfeature/test_openfeature.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
88

99
import sentry_sdk
10+
from sentry_sdk import start_span, start_transaction
1011
from sentry_sdk.integrations.openfeature import OpenFeatureIntegration
12+
from tests.conftest import ApproxDict
1113

1214

1315
def test_openfeature_integration(sentry_init, capture_events, uninstall_integration):
@@ -151,3 +153,24 @@ async def runner():
151153
{"flag": "world", "result": False},
152154
]
153155
}
156+
157+
158+
def test_openfeature_span_integration(
159+
sentry_init, capture_events, uninstall_integration
160+
):
161+
uninstall_integration(OpenFeatureIntegration.identifier)
162+
sentry_init(traces_sample_rate=1.0, integrations=[OpenFeatureIntegration()])
163+
164+
api.set_provider(
165+
InMemoryProvider({"hello": InMemoryFlag("on", {"on": True, "off": False})})
166+
)
167+
client = api.get_client()
168+
169+
events = capture_events()
170+
171+
with start_transaction(name="hi"):
172+
with start_span(op="foo", name="bar"):
173+
client.get_boolean_value("hello", default_value=False)
174+
175+
(event,) = events
176+
assert event["spans"][0]["data"] == ApproxDict({"flag.hello": True})

tests/integrations/statsig/test_statsig.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from statsig.statsig_user import StatsigUser
66
from random import random
77
from unittest.mock import Mock
8+
from sentry_sdk import start_span, start_transaction
9+
from tests.conftest import ApproxDict
810

911
import pytest
1012

@@ -181,3 +183,18 @@ def test_wrapper_attributes(sentry_init, uninstall_integration):
181183

182184
# Clean up
183185
statsig.check_gate = original_check_gate
186+
187+
188+
def test_statsig_span_integration(sentry_init, capture_events, uninstall_integration):
189+
uninstall_integration(StatsigIntegration.identifier)
190+
191+
with mock_statsig({"hello": True, "world": False}):
192+
sentry_init(traces_sample_rate=1, integrations=[StatsigIntegration()])
193+
events = capture_events()
194+
user = StatsigUser(user_id="user-id")
195+
with start_transaction(name="hi"):
196+
with start_span(op="foo", name="bar"):
197+
statsig.check_gate(user, "hello")
198+
199+
(event,) = events
200+
assert event["spans"][0]["data"] == ApproxDict({"flag.hello": True})

tests/integrations/unleash/test_unleash.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
import sentry_sdk
1010
from sentry_sdk.integrations.unleash import UnleashIntegration
11+
from sentry_sdk import start_span, start_transaction
1112
from tests.integrations.unleash.testutils import mock_unleash_client
13+
from tests.conftest import ApproxDict
1214

1315

1416
def test_is_enabled(sentry_init, capture_events, uninstall_integration):
@@ -164,3 +166,18 @@ def test_wrapper_attributes(sentry_init, uninstall_integration):
164166
# Mock clients methods have not lost their qualified names after decoration.
165167
assert client.is_enabled.__name__ == "is_enabled"
166168
assert client.is_enabled.__qualname__ == original_is_enabled.__qualname__
169+
170+
171+
def test_unleash_span_integration(sentry_init, capture_events, uninstall_integration):
172+
uninstall_integration(UnleashIntegration.identifier)
173+
174+
with mock_unleash_client():
175+
sentry_init(traces_sample_rate=1, integrations=[UnleashIntegration()])
176+
events = capture_events()
177+
client = UnleashClient() # type: ignore[arg-type]
178+
with start_transaction(name="hi"):
179+
with start_span(op="foo", name="bar"):
180+
client.is_enabled("hello")
181+
182+
(event,) = events
183+
assert event["spans"][0]["data"] == ApproxDict({"flag.hello": True})

0 commit comments

Comments
 (0)