Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions sentry_sdk/integrations/launchdarkly.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
14 changes: 14 additions & 0 deletions sentry_sdk/integrations/openfeature.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,25 @@ 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.default_value
)
8 changes: 8 additions & 0 deletions sentry_sdk/integrations/statsig.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
8 changes: 8 additions & 0 deletions sentry_sdk/integrations/unleash.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
42 changes: 42 additions & 0 deletions tests/integrations/launchdarkly/test_launchdarkly.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -202,3 +204,43 @@ 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] why are we uninstalling the integration here? My guess is this would reduce the possibility of interactions between tests, but I this is my first time seeing this pattern in our tests so just want to check

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how I found the test suite so I'm following the pattern. I'm not sure why its necessary or what the implications are if its removed. I could look into this more.

Copy link
Member

@szokeasaurusrex szokeasaurusrex Apr 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, perhaps I just haven't touched this part of the codebase – if that's the pattern you can disregard my comment. Maybe @antonpirker or @sentrivana would know why this is needed.

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)
client.variation("other", Context.create("my-org", "organization"), False)

(event,) = events
assert event["spans"][0]["data"] == ApproxDict(
{"flag.hello": True, "flag.other": False}
)
26 changes: 26 additions & 0 deletions tests/integrations/openfeature/test_openfeature.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -151,3 +153,27 @@ 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)
client.get_boolean_value("world", default_value=False)

(event,) = events
assert event["spans"][0]["data"] == ApproxDict(
{"flag.hello": True, "flag.world": False}
)
20 changes: 20 additions & 0 deletions tests/integrations/statsig/test_statsig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -181,3 +183,21 @@ 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}):
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, "flag.world": False}
)
20 changes: 20 additions & 0 deletions tests/integrations/unleash/test_unleash.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -164,3 +166,21 @@ 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")
client.is_enabled("other")

(event,) = events
assert event["spans"][0]["data"] == ApproxDict(
{"flag.hello": True, "flag.other": False}
)
Loading