From f5761268581a297132d6657d81f9be8493285ab7 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Thu, 10 Apr 2025 13:56:36 -0500 Subject: [PATCH 01/15] Add flag evaluations to spans as attributes --- sentry_sdk/integrations/launchdarkly.py | 7 ++++ sentry_sdk/integrations/openfeature.py | 12 ++++++ sentry_sdk/integrations/statsig.py | 8 ++++ sentry_sdk/integrations/unleash.py | 8 ++++ .../launchdarkly/test_launchdarkly.py | 39 +++++++++++++++++++ .../openfeature/test_openfeature.py | 23 +++++++++++ tests/integrations/statsig/test_statsig.py | 17 ++++++++ tests/integrations/unleash/test_unleash.py | 17 ++++++++ 8 files changed, 131 insertions(+) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index cb9e911463..a0cac22ba2 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -53,8 +53,15 @@ def metadata(self): def after_evaluation(self, series_context, data, detail): # type: (EvaluationSeriesContext, dict[Any, Any], EvaluationDetail) -> dict[Any, Any] if isinstance(detail.value, bool): + # Errors support. flags = sentry_sdk.get_current_scope().flags flags.set(series_context.key, detail.value) + + # Spans support. + span = sentry_sdk.get_current_span() + if span: + span.set_data(f"flag.{series_context.key}", detail.value) + return data def before_evaluation(self, series_context, data): diff --git a/sentry_sdk/integrations/openfeature.py b/sentry_sdk/integrations/openfeature.py index bf66b94e8b..5370c3c042 100644 --- a/sentry_sdk/integrations/openfeature.py +++ b/sentry_sdk/integrations/openfeature.py @@ -29,11 +29,23 @@ class OpenFeatureHook(Hook): def after(self, hook_context, details, hints): # type: (HookContext, FlagEvaluationDetails[bool], HookHints) -> None if isinstance(details.value, bool): + # Errors support. flags = sentry_sdk.get_current_scope().flags flags.set(details.flag_key, details.value) + # Spans support. + span = sentry_sdk.get_current_span() + if span: + span.set_data(f"flag.{details.flag_key}", details.value) + def error(self, hook_context, exception, hints): # type: (HookContext, Exception, HookHints) -> None if isinstance(hook_context.default_value, bool): + # Errors support. flags = sentry_sdk.get_current_scope().flags flags.set(hook_context.flag_key, hook_context.default_value) + + # Spans support. + span = sentry_sdk.get_current_span() + if span: + span.set_data(f"flag.{hook_context.flag_key}", hook_context.value) diff --git a/sentry_sdk/integrations/statsig.py b/sentry_sdk/integrations/statsig.py index 1d84eb8aa2..d64857ac58 100644 --- a/sentry_sdk/integrations/statsig.py +++ b/sentry_sdk/integrations/statsig.py @@ -1,6 +1,7 @@ from functools import wraps from typing import Any, TYPE_CHECKING +import sentry_sdk from sentry_sdk.feature_flags import add_feature_flag from sentry_sdk.integrations import Integration, DidNotEnable, _check_minimum_version from sentry_sdk.utils import parse_version @@ -30,8 +31,15 @@ def setup_once(): @wraps(old_check_gate) def sentry_check_gate(user, gate, *args, **kwargs): # type: (StatsigUser, str, *Any, **Any) -> Any + # Errors support. enabled = old_check_gate(user, gate, *args, **kwargs) add_feature_flag(gate, enabled) + + # Spans support. + span = sentry_sdk.get_current_span() + if span: + span.set_data(f"flag.{gate}", enabled) + return enabled statsig_module.check_gate = sentry_check_gate diff --git a/sentry_sdk/integrations/unleash.py b/sentry_sdk/integrations/unleash.py index 873f36c68b..190467ee0a 100644 --- a/sentry_sdk/integrations/unleash.py +++ b/sentry_sdk/integrations/unleash.py @@ -24,11 +24,19 @@ def sentry_is_enabled(self, feature, *args, **kwargs): # type: (UnleashClient, str, *Any, **Any) -> Any enabled = old_is_enabled(self, feature, *args, **kwargs) + # Errors support. + # # We have no way of knowing what type of unleash feature this is, so we have to treat # it as a boolean / toggle feature. flags = sentry_sdk.get_current_scope().flags flags.set(feature, enabled) + # Spans support. + span = sentry_sdk.get_current_span() + print(span) + if span: + span.set_data(f"flag.{feature}", enabled) + return enabled UnleashClient.is_enabled = sentry_is_enabled # type: ignore diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 20566ce09a..9bebc781e8 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -12,6 +12,8 @@ import sentry_sdk from sentry_sdk.integrations import DidNotEnable from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration +from sentry_sdk import start_span, start_transaction +from tests.conftest import ApproxDict @pytest.mark.parametrize( @@ -202,3 +204,40 @@ def test_launchdarkly_integration_did_not_enable(monkeypatch): monkeypatch.setattr(client, "is_initialized", lambda: False) with pytest.raises(DidNotEnable): LaunchDarklyIntegration(ld_client=client) + + +@pytest.mark.parametrize( + "use_global_client", + (False, True), +) +def test_launchdarkly_span_integration( + sentry_init, use_global_client, capture_events, uninstall_integration +): + td = TestData.data_source() + td.update(td.flag("hello").variation_for_all(True)) + td.update(td.flag("world").variation_for_all(True)) + # Disable background requests as we aren't using a server. + config = Config( + "sdk-key", update_processor_class=td, diagnostic_opt_out=True, send_events=False + ) + + uninstall_integration(LaunchDarklyIntegration.identifier) + if use_global_client: + ldclient.set_config(config) + sentry_init(traces_sample_rate=1, integrations=[LaunchDarklyIntegration()]) + client = ldclient.get() + else: + client = LDClient(config=config) + sentry_init( + traces_sample_rate=1, + integrations=[LaunchDarklyIntegration(ld_client=client)], + ) + + events = capture_events() + + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + client.variation("hello", Context.create("my-org", "organization"), False) + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict({"flag.hello": True}) diff --git a/tests/integrations/openfeature/test_openfeature.py b/tests/integrations/openfeature/test_openfeature.py index c180211c3f..22f3b49a50 100644 --- a/tests/integrations/openfeature/test_openfeature.py +++ b/tests/integrations/openfeature/test_openfeature.py @@ -7,7 +7,9 @@ from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider import sentry_sdk +from sentry_sdk import start_span, start_transaction from sentry_sdk.integrations.openfeature import OpenFeatureIntegration +from tests.conftest import ApproxDict def test_openfeature_integration(sentry_init, capture_events, uninstall_integration): @@ -151,3 +153,24 @@ async def runner(): {"flag": "world", "result": False}, ] } + + +def test_openfeature_span_integration( + sentry_init, capture_events, uninstall_integration +): + uninstall_integration(OpenFeatureIntegration.identifier) + sentry_init(traces_sample_rate=1.0, integrations=[OpenFeatureIntegration()]) + + 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"): + client.get_boolean_value("hello", default_value=False) + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict({"flag.hello": True}) diff --git a/tests/integrations/statsig/test_statsig.py b/tests/integrations/statsig/test_statsig.py index c1666bde4d..b1675a2114 100644 --- a/tests/integrations/statsig/test_statsig.py +++ b/tests/integrations/statsig/test_statsig.py @@ -5,6 +5,8 @@ from statsig.statsig_user import StatsigUser from random import random from unittest.mock import Mock +from sentry_sdk import start_span, start_transaction +from tests.conftest import ApproxDict import pytest @@ -181,3 +183,18 @@ def test_wrapper_attributes(sentry_init, uninstall_integration): # Clean up statsig.check_gate = original_check_gate + + +def test_statsig_span_integration(sentry_init, capture_events, uninstall_integration): + uninstall_integration(StatsigIntegration.identifier) + + with mock_statsig({"hello": True, "world": False}): + sentry_init(traces_sample_rate=1, integrations=[StatsigIntegration()]) + events = capture_events() + user = StatsigUser(user_id="user-id") + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + statsig.check_gate(user, "hello") + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict({"flag.hello": True}) diff --git a/tests/integrations/unleash/test_unleash.py b/tests/integrations/unleash/test_unleash.py index 379abba8f6..26f3b21de7 100644 --- a/tests/integrations/unleash/test_unleash.py +++ b/tests/integrations/unleash/test_unleash.py @@ -8,7 +8,9 @@ import sentry_sdk from sentry_sdk.integrations.unleash import UnleashIntegration +from sentry_sdk import start_span, start_transaction from tests.integrations.unleash.testutils import mock_unleash_client +from tests.conftest import ApproxDict def test_is_enabled(sentry_init, capture_events, uninstall_integration): @@ -164,3 +166,18 @@ def test_wrapper_attributes(sentry_init, uninstall_integration): # Mock clients methods have not lost their qualified names after decoration. assert client.is_enabled.__name__ == "is_enabled" assert client.is_enabled.__qualname__ == original_is_enabled.__qualname__ + + +def test_unleash_span_integration(sentry_init, capture_events, uninstall_integration): + uninstall_integration(UnleashIntegration.identifier) + + with mock_unleash_client(): + sentry_init(traces_sample_rate=1, integrations=[UnleashIntegration()]) + events = capture_events() + client = UnleashClient() # type: ignore[arg-type] + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + client.is_enabled("hello") + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict({"flag.hello": True}) From e6e2d9720a3bf0467ec76e938644466bb7e9ef92 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Thu, 10 Apr 2025 14:06:29 -0500 Subject: [PATCH 02/15] Add coverage for missing keys --- tests/integrations/launchdarkly/test_launchdarkly.py | 5 ++++- tests/integrations/openfeature/test_openfeature.py | 5 ++++- tests/integrations/statsig/test_statsig.py | 7 +++++-- tests/integrations/unleash/test_unleash.py | 5 ++++- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 9bebc781e8..447de3c6e0 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -238,6 +238,9 @@ def test_launchdarkly_span_integration( 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.hello": True}) + assert event["spans"][0]["data"] == ApproxDict( + {"flag.hello": True, "flag.other": False} + ) diff --git a/tests/integrations/openfeature/test_openfeature.py b/tests/integrations/openfeature/test_openfeature.py index 22f3b49a50..f07444cab0 100644 --- a/tests/integrations/openfeature/test_openfeature.py +++ b/tests/integrations/openfeature/test_openfeature.py @@ -171,6 +171,9 @@ def test_openfeature_span_integration( 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.hello": True}) + assert event["spans"][0]["data"] == ApproxDict( + {"flag.hello": True, "flag.world": False} + ) diff --git a/tests/integrations/statsig/test_statsig.py b/tests/integrations/statsig/test_statsig.py index b1675a2114..1f117b8a93 100644 --- a/tests/integrations/statsig/test_statsig.py +++ b/tests/integrations/statsig/test_statsig.py @@ -188,13 +188,16 @@ def test_wrapper_attributes(sentry_init, uninstall_integration): def test_statsig_span_integration(sentry_init, capture_events, uninstall_integration): uninstall_integration(StatsigIntegration.identifier) - with mock_statsig({"hello": True, "world": False}): + with mock_statsig({"hello": True}): sentry_init(traces_sample_rate=1, integrations=[StatsigIntegration()]) events = capture_events() user = StatsigUser(user_id="user-id") 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.hello": True}) + assert event["spans"][0]["data"] == ApproxDict( + {"flag.hello": True, "flag.world": False} + ) diff --git a/tests/integrations/unleash/test_unleash.py b/tests/integrations/unleash/test_unleash.py index 26f3b21de7..7576a4bfb1 100644 --- a/tests/integrations/unleash/test_unleash.py +++ b/tests/integrations/unleash/test_unleash.py @@ -178,6 +178,9 @@ def test_unleash_span_integration(sentry_init, capture_events, uninstall_integra 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.hello": True}) + assert event["spans"][0]["data"] == ApproxDict( + {"flag.hello": True, "flag.other": False} + ) From ef0dc37eb666bbd7e2d68f67d1ca62d10c640b4f Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Thu, 10 Apr 2025 14:06:41 -0500 Subject: [PATCH 03/15] Use default_value attribute --- sentry_sdk/integrations/openfeature.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/openfeature.py b/sentry_sdk/integrations/openfeature.py index 5370c3c042..0a0f2cf5d4 100644 --- a/sentry_sdk/integrations/openfeature.py +++ b/sentry_sdk/integrations/openfeature.py @@ -48,4 +48,6 @@ def error(self, hook_context, exception, hints): # Spans support. span = sentry_sdk.get_current_span() if span: - span.set_data(f"flag.{hook_context.flag_key}", hook_context.value) + span.set_data( + f"flag.{hook_context.flag_key}", hook_context.default_value + ) From dea1a94d2db65da387dfe729d17204112049d9be Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Fri, 11 Apr 2025 08:02:56 -0500 Subject: [PATCH 04/15] Remove print Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- sentry_sdk/integrations/unleash.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/integrations/unleash.py b/sentry_sdk/integrations/unleash.py index 190467ee0a..cb54e83ed1 100644 --- a/sentry_sdk/integrations/unleash.py +++ b/sentry_sdk/integrations/unleash.py @@ -33,7 +33,6 @@ def sentry_is_enabled(self, feature, *args, **kwargs): # Spans support. span = sentry_sdk.get_current_span() - print(span) if span: span.set_data(f"flag.{feature}", enabled) From 0c7127723c5ef0bd7ce777f96699fb4973d3ce32 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Fri, 11 Apr 2025 08:04:28 -0500 Subject: [PATCH 05/15] As float Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- tests/integrations/launchdarkly/test_launchdarkly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 447de3c6e0..2cdb059d28 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -224,7 +224,7 @@ def test_launchdarkly_span_integration( uninstall_integration(LaunchDarklyIntegration.identifier) if use_global_client: ldclient.set_config(config) - sentry_init(traces_sample_rate=1, integrations=[LaunchDarklyIntegration()]) + sentry_init(traces_sample_rate=1.0, integrations=[LaunchDarklyIntegration()]) client = ldclient.get() else: client = LDClient(config=config) From 68a7b796d1dab2fa3684aaa741a54023610876b1 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Fri, 11 Apr 2025 08:04:39 -0500 Subject: [PATCH 06/15] As float Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- tests/integrations/unleash/test_unleash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/unleash/test_unleash.py b/tests/integrations/unleash/test_unleash.py index 7576a4bfb1..9640184505 100644 --- a/tests/integrations/unleash/test_unleash.py +++ b/tests/integrations/unleash/test_unleash.py @@ -172,7 +172,7 @@ def test_unleash_span_integration(sentry_init, capture_events, uninstall_integra uninstall_integration(UnleashIntegration.identifier) with mock_unleash_client(): - sentry_init(traces_sample_rate=1, integrations=[UnleashIntegration()]) + sentry_init(traces_sample_rate=1.0, integrations=[UnleashIntegration()]) events = capture_events() client = UnleashClient() # type: ignore[arg-type] with start_transaction(name="hi"): From 0763e458946f0e80a40c29ef6595688d39a24b85 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Fri, 11 Apr 2025 08:04:51 -0500 Subject: [PATCH 07/15] As float Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- tests/integrations/launchdarkly/test_launchdarkly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 2cdb059d28..cd2404ef10 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -229,7 +229,7 @@ def test_launchdarkly_span_integration( else: client = LDClient(config=config) sentry_init( - traces_sample_rate=1, + traces_sample_rate=1.0, integrations=[LaunchDarklyIntegration(ld_client=client)], ) From 807565823bc4a01ea8bbf88b1ad62a43ef5f03aa Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Fri, 11 Apr 2025 08:05:42 -0500 Subject: [PATCH 08/15] Remove world variation --- tests/integrations/launchdarkly/test_launchdarkly.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 447de3c6e0..2cfd0f9fab 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -215,7 +215,6 @@ def test_launchdarkly_span_integration( ): td = TestData.data_source() td.update(td.flag("hello").variation_for_all(True)) - td.update(td.flag("world").variation_for_all(True)) # Disable background requests as we aren't using a server. config = Config( "sdk-key", update_processor_class=td, diagnostic_opt_out=True, send_events=False From 7ac3a5c7c75aada838bc72c0d14b866df583c71e Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Fri, 11 Apr 2025 08:06:02 -0500 Subject: [PATCH 09/15] As float Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- tests/integrations/statsig/test_statsig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/statsig/test_statsig.py b/tests/integrations/statsig/test_statsig.py index 1f117b8a93..71668b8cc2 100644 --- a/tests/integrations/statsig/test_statsig.py +++ b/tests/integrations/statsig/test_statsig.py @@ -189,7 +189,7 @@ def test_statsig_span_integration(sentry_init, capture_events, uninstall_integra uninstall_integration(StatsigIntegration.identifier) with mock_statsig({"hello": True}): - sentry_init(traces_sample_rate=1, integrations=[StatsigIntegration()]) + sentry_init(traces_sample_rate=1.0, integrations=[StatsigIntegration()]) events = capture_events() user = StatsigUser(user_id="user-id") with start_transaction(name="hi"): From aca8bdeb55c9e473515d99f16e9e25ad819a0445 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Fri, 11 Apr 2025 10:14:47 -0500 Subject: [PATCH 10/15] Use evaluation prefix --- sentry_sdk/integrations/launchdarkly.py | 2 +- sentry_sdk/integrations/openfeature.py | 5 +++-- sentry_sdk/integrations/statsig.py | 2 +- sentry_sdk/integrations/unleash.py | 2 +- tests/integrations/launchdarkly/test_launchdarkly.py | 2 +- tests/integrations/openfeature/test_openfeature.py | 2 +- tests/integrations/statsig/test_statsig.py | 2 +- tests/integrations/unleash/test_unleash.py | 2 +- 8 files changed, 10 insertions(+), 9 deletions(-) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index a0cac22ba2..ba1fc7a9c6 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -60,7 +60,7 @@ def after_evaluation(self, series_context, data, detail): # Spans support. span = sentry_sdk.get_current_span() if span: - span.set_data(f"flag.{series_context.key}", detail.value) + span.set_data(f"flag.evaluation.{series_context.key}", detail.value) return data diff --git a/sentry_sdk/integrations/openfeature.py b/sentry_sdk/integrations/openfeature.py index 0a0f2cf5d4..bd99b233a7 100644 --- a/sentry_sdk/integrations/openfeature.py +++ b/sentry_sdk/integrations/openfeature.py @@ -36,7 +36,7 @@ def after(self, hook_context, details, hints): # Spans support. span = sentry_sdk.get_current_span() if span: - span.set_data(f"flag.{details.flag_key}", details.value) + span.set_data(f"flag.evaluation.{details.flag_key}", details.value) def error(self, hook_context, exception, hints): # type: (HookContext, Exception, HookHints) -> None @@ -49,5 +49,6 @@ def error(self, hook_context, exception, hints): span = sentry_sdk.get_current_span() if span: span.set_data( - f"flag.{hook_context.flag_key}", hook_context.default_value + f"flag.evaluation.{hook_context.flag_key}", + hook_context.default_value, ) diff --git a/sentry_sdk/integrations/statsig.py b/sentry_sdk/integrations/statsig.py index d64857ac58..4a9a7e4572 100644 --- a/sentry_sdk/integrations/statsig.py +++ b/sentry_sdk/integrations/statsig.py @@ -38,7 +38,7 @@ def sentry_check_gate(user, gate, *args, **kwargs): # Spans support. span = sentry_sdk.get_current_span() if span: - span.set_data(f"flag.{gate}", enabled) + span.set_data(f"flag.evaluation.{gate}", enabled) return enabled diff --git a/sentry_sdk/integrations/unleash.py b/sentry_sdk/integrations/unleash.py index cb54e83ed1..fae042f190 100644 --- a/sentry_sdk/integrations/unleash.py +++ b/sentry_sdk/integrations/unleash.py @@ -34,7 +34,7 @@ def sentry_is_enabled(self, feature, *args, **kwargs): # Spans support. span = sentry_sdk.get_current_span() if span: - span.set_data(f"flag.{feature}", enabled) + span.set_data(f"flag.evaluation.{feature}", enabled) return enabled diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index e322220438..20bb4d031f 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -241,5 +241,5 @@ def test_launchdarkly_span_integration( (event,) = events assert event["spans"][0]["data"] == ApproxDict( - {"flag.hello": True, "flag.other": False} + {"flag.evaluation.hello": True, "flag.evaluation.other": False} ) diff --git a/tests/integrations/openfeature/test_openfeature.py b/tests/integrations/openfeature/test_openfeature.py index f07444cab0..46acc61ae7 100644 --- a/tests/integrations/openfeature/test_openfeature.py +++ b/tests/integrations/openfeature/test_openfeature.py @@ -175,5 +175,5 @@ def test_openfeature_span_integration( (event,) = events assert event["spans"][0]["data"] == ApproxDict( - {"flag.hello": True, "flag.world": False} + {"flag.evaluation.hello": True, "flag.evaluation.world": False} ) diff --git a/tests/integrations/statsig/test_statsig.py b/tests/integrations/statsig/test_statsig.py index 71668b8cc2..5eb2cf39f3 100644 --- a/tests/integrations/statsig/test_statsig.py +++ b/tests/integrations/statsig/test_statsig.py @@ -199,5 +199,5 @@ def test_statsig_span_integration(sentry_init, capture_events, uninstall_integra (event,) = events assert event["spans"][0]["data"] == ApproxDict( - {"flag.hello": True, "flag.world": False} + {"flag.evaluation.hello": True, "flag.evaluation.world": False} ) diff --git a/tests/integrations/unleash/test_unleash.py b/tests/integrations/unleash/test_unleash.py index 9640184505..98a6188181 100644 --- a/tests/integrations/unleash/test_unleash.py +++ b/tests/integrations/unleash/test_unleash.py @@ -182,5 +182,5 @@ def test_unleash_span_integration(sentry_init, capture_events, uninstall_integra (event,) = events assert event["spans"][0]["data"] == ApproxDict( - {"flag.hello": True, "flag.other": False} + {"flag.evaluation.hello": True, "flag.evaluation.other": False} ) From 7944811236f5e07129cc09ab9af3db90c27619f4 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Fri, 11 Apr 2025 10:50:57 -0500 Subject: [PATCH 11/15] Abstract into the add_feature_flag function --- sentry_sdk/feature_flags.py | 4 ++++ sentry_sdk/integrations/launchdarkly.py | 11 ++--------- sentry_sdk/integrations/openfeature.py | 23 +++-------------------- sentry_sdk/integrations/statsig.py | 8 -------- sentry_sdk/integrations/unleash.py | 12 ++---------- 5 files changed, 11 insertions(+), 47 deletions(-) diff --git a/sentry_sdk/feature_flags.py b/sentry_sdk/feature_flags.py index a0b1338356..4ed93a8667 100644 --- a/sentry_sdk/feature_flags.py +++ b/sentry_sdk/feature_flags.py @@ -66,3 +66,7 @@ def add_feature_flag(flag, result): """ flags = sentry_sdk.get_current_scope().flags flags.set(flag, result) + + span = sentry_sdk.get_current_span() + if span: + span.set_data(f"flag.evaluation.{flag}", result) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index ba1fc7a9c6..d3c423e7be 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -import sentry_sdk +from sentry_sdk.feature_flags import add_feature_flag from sentry_sdk.integrations import DidNotEnable, Integration try: @@ -53,14 +53,7 @@ def metadata(self): def after_evaluation(self, series_context, data, detail): # type: (EvaluationSeriesContext, dict[Any, Any], EvaluationDetail) -> dict[Any, Any] if isinstance(detail.value, bool): - # Errors support. - flags = sentry_sdk.get_current_scope().flags - flags.set(series_context.key, detail.value) - - # Spans support. - span = sentry_sdk.get_current_span() - if span: - span.set_data(f"flag.evaluation.{series_context.key}", detail.value) + add_feature_flag(series_context.key, detail.value) return data diff --git a/sentry_sdk/integrations/openfeature.py b/sentry_sdk/integrations/openfeature.py index bd99b233a7..e2b33d83f2 100644 --- a/sentry_sdk/integrations/openfeature.py +++ b/sentry_sdk/integrations/openfeature.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -import sentry_sdk +from sentry_sdk.feature_flags import add_feature_flag from sentry_sdk.integrations import DidNotEnable, Integration try: @@ -29,26 +29,9 @@ class OpenFeatureHook(Hook): def after(self, hook_context, details, hints): # type: (HookContext, FlagEvaluationDetails[bool], HookHints) -> None if isinstance(details.value, bool): - # Errors support. - flags = sentry_sdk.get_current_scope().flags - flags.set(details.flag_key, details.value) - - # Spans support. - span = sentry_sdk.get_current_span() - if span: - span.set_data(f"flag.evaluation.{details.flag_key}", details.value) + add_feature_flag(details.flag_key, details.value) def error(self, hook_context, exception, hints): # type: (HookContext, Exception, HookHints) -> None if isinstance(hook_context.default_value, bool): - # Errors support. - flags = sentry_sdk.get_current_scope().flags - flags.set(hook_context.flag_key, hook_context.default_value) - - # Spans support. - span = sentry_sdk.get_current_span() - if span: - span.set_data( - f"flag.evaluation.{hook_context.flag_key}", - hook_context.default_value, - ) + add_feature_flag(hook_context.flag_key, hook_context.default_value) diff --git a/sentry_sdk/integrations/statsig.py b/sentry_sdk/integrations/statsig.py index 4a9a7e4572..1d84eb8aa2 100644 --- a/sentry_sdk/integrations/statsig.py +++ b/sentry_sdk/integrations/statsig.py @@ -1,7 +1,6 @@ from functools import wraps from typing import Any, TYPE_CHECKING -import sentry_sdk from sentry_sdk.feature_flags import add_feature_flag from sentry_sdk.integrations import Integration, DidNotEnable, _check_minimum_version from sentry_sdk.utils import parse_version @@ -31,15 +30,8 @@ def setup_once(): @wraps(old_check_gate) def sentry_check_gate(user, gate, *args, **kwargs): # type: (StatsigUser, str, *Any, **Any) -> Any - # Errors support. enabled = old_check_gate(user, gate, *args, **kwargs) add_feature_flag(gate, enabled) - - # Spans support. - span = sentry_sdk.get_current_span() - if span: - span.set_data(f"flag.evaluation.{gate}", enabled) - return enabled statsig_module.check_gate = sentry_check_gate diff --git a/sentry_sdk/integrations/unleash.py b/sentry_sdk/integrations/unleash.py index fae042f190..6daa0a411f 100644 --- a/sentry_sdk/integrations/unleash.py +++ b/sentry_sdk/integrations/unleash.py @@ -1,7 +1,7 @@ from functools import wraps from typing import Any -import sentry_sdk +from sentry_sdk.feature_flags import add_feature_flag from sentry_sdk.integrations import Integration, DidNotEnable try: @@ -24,17 +24,9 @@ def sentry_is_enabled(self, feature, *args, **kwargs): # type: (UnleashClient, str, *Any, **Any) -> Any enabled = old_is_enabled(self, feature, *args, **kwargs) - # Errors support. - # # We have no way of knowing what type of unleash feature this is, so we have to treat # it as a boolean / toggle feature. - flags = sentry_sdk.get_current_scope().flags - flags.set(feature, enabled) - - # Spans support. - span = sentry_sdk.get_current_span() - if span: - span.set_data(f"flag.evaluation.{feature}", enabled) + add_feature_flag(feature, enabled) return enabled From 19a04758ec4f58f2e1348a39be0a0d55a123b47e Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Fri, 11 Apr 2025 12:01:32 -0500 Subject: [PATCH 12/15] Set flags in a capped object --- sentry_sdk/feature_flags.py | 2 +- sentry_sdk/tracing.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/feature_flags.py b/sentry_sdk/feature_flags.py index 4ed93a8667..dd8d41c32e 100644 --- a/sentry_sdk/feature_flags.py +++ b/sentry_sdk/feature_flags.py @@ -69,4 +69,4 @@ def add_feature_flag(flag, result): span = sentry_sdk.get_current_span() if span: - span.set_data(f"flag.evaluation.{flag}", result) + span.set_flag(f"flag.evaluation.{flag}", result) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index ab1a7a8fdf..383b96c7d9 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -278,6 +278,8 @@ class Span: "scope", "origin", "name", + "_flags", + "_flags_capacity", ) def __init__( @@ -313,6 +315,8 @@ def __init__( self._tags = {} # type: MutableMapping[str, str] self._data = {} # type: Dict[str, Any] self._containing_transaction = containing_transaction + self._flags = {} # type: Dict[str, bool] + self._flags_capacity = 0 if hub is not None: warnings.warn( @@ -604,6 +608,11 @@ def set_data(self, key, value): # type: (str, Any) -> None self._data[key] = value + def set_flag(self, flag, result): + # type: (str, bool) -> None + if len(self._flags) >= self._flags_capacity: + self._flags[flag] = result + def set_status(self, value): # type: (str) -> None self.status = value @@ -707,7 +716,9 @@ def to_json(self): if tags: rv["tags"] = tags - data = self._data + data = {} + data.update(self._flags) + data.update(self._data) if data: rv["data"] = data From c99fdabeccdefc498e834a3557b7864c6bd1f85f Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Tue, 15 Apr 2025 09:35:41 -0500 Subject: [PATCH 13/15] Fix capacity validation --- sentry_sdk/tracing.py | 4 ++-- tests/test_feature_flags.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 383b96c7d9..5f8c1c6f11 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -316,7 +316,7 @@ def __init__( self._data = {} # type: Dict[str, Any] self._containing_transaction = containing_transaction self._flags = {} # type: Dict[str, bool] - self._flags_capacity = 0 + self._flags_capacity = 3 if hub is not None: warnings.warn( @@ -610,7 +610,7 @@ def set_data(self, key, value): def set_flag(self, flag, result): # type: (str, bool) -> None - if len(self._flags) >= self._flags_capacity: + if len(self._flags) < self._flags_capacity: self._flags[flag] = result def set_status(self, value): diff --git a/tests/test_feature_flags.py b/tests/test_feature_flags.py index 0df30bd0ea..806451c7ea 100644 --- a/tests/test_feature_flags.py +++ b/tests/test_feature_flags.py @@ -7,6 +7,8 @@ import sentry_sdk from sentry_sdk.feature_flags import add_feature_flag, FlagBuffer +from sentry_sdk import start_span, start_transaction +from tests.conftest import ApproxDict def test_featureflags_integration(sentry_init, capture_events, uninstall_integration): @@ -220,3 +222,26 @@ def reader(): # shared resource. When deepcopying we should have exclusive access to the underlying # memory. assert error_occurred is False + + +def test_flag_limit(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0) + + events = capture_events() + + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + add_feature_flag("first", True) + add_feature_flag("second", True) + add_feature_flag("third", True) + add_feature_flag("fourth", True) + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict( + { + "flag.evaluation.first": True, + "flag.evaluation.second": True, + "flag.evaluation.third": True, + } + ) + assert "flag.evaluation.fourth" not in event["spans"][0]["data"] From a7f6024ce1892e165146fab0edb5d449a39ad2b4 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 16 Apr 2025 13:14:18 -0500 Subject: [PATCH 14/15] Use capacity 10 --- sentry_sdk/tracing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 5f8c1c6f11..07697e6762 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -316,7 +316,7 @@ def __init__( self._data = {} # type: Dict[str, Any] self._containing_transaction = containing_transaction self._flags = {} # type: Dict[str, bool] - self._flags_capacity = 3 + self._flags_capacity = 10 if hub is not None: warnings.warn( From 0934e311601bc28dd144e10b3106b3bd6216cee2 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 16 Apr 2025 14:46:57 -0500 Subject: [PATCH 15/15] Fix coverage --- tests/test_feature_flags.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/test_feature_flags.py b/tests/test_feature_flags.py index 806451c7ea..1b0ed13d49 100644 --- a/tests/test_feature_flags.py +++ b/tests/test_feature_flags.py @@ -231,17 +231,31 @@ def test_flag_limit(sentry_init, capture_events): with start_transaction(name="hi"): with start_span(op="foo", name="bar"): - add_feature_flag("first", True) - add_feature_flag("second", True) - add_feature_flag("third", True) - add_feature_flag("fourth", True) + add_feature_flag("0", True) + add_feature_flag("1", True) + add_feature_flag("2", True) + add_feature_flag("3", True) + add_feature_flag("4", True) + add_feature_flag("5", True) + add_feature_flag("6", True) + add_feature_flag("7", True) + add_feature_flag("8", True) + add_feature_flag("9", True) + add_feature_flag("10", True) (event,) = events assert event["spans"][0]["data"] == ApproxDict( { - "flag.evaluation.first": True, - "flag.evaluation.second": True, - "flag.evaluation.third": True, + "flag.evaluation.0": True, + "flag.evaluation.1": True, + "flag.evaluation.2": True, + "flag.evaluation.3": True, + "flag.evaluation.4": True, + "flag.evaluation.5": True, + "flag.evaluation.6": True, + "flag.evaluation.7": True, + "flag.evaluation.8": True, + "flag.evaluation.9": True, } ) - assert "flag.evaluation.fourth" not in event["spans"][0]["data"] + assert "flag.evaluation.10" not in event["spans"][0]["data"]