Skip to content

Commit

Permalink
ref(flags): change LaunchDarkly and OpenFeature integrations to track…
Browse files Browse the repository at this point in the history
… a single client instance
  • Loading branch information
aliu39 committed Dec 23, 2024
1 parent bb85c26 commit b51d798
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 62 deletions.
24 changes: 11 additions & 13 deletions sentry_sdk/integrations/launchdarkly.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,37 @@
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.flag_utils import flag_error_processor

if TYPE_CHECKING:
from typing import Any, Optional

try:
import ldclient
from ldclient.hook import Hook, Metadata

if TYPE_CHECKING:
from ldclient import LDClient
from ldclient.hook import EvaluationSeriesContext
from ldclient.evaluation import EvaluationDetail

from typing import Any
except ImportError:
raise DidNotEnable("LaunchDarkly is not installed")


class LaunchDarklyIntegration(Integration):
identifier = "launchdarkly"
_ld_client = None # type: LDClient | None
_client = None # type: Optional[LDClient]

def __init__(self, ld_client=None):
# type: (LDClient | None) -> None
def __init__(self, client):
# type: (LDClient) -> None
"""
:param client: An initialized LDClient instance. If a client is not provided, this
integration will attempt to use the shared global instance.
:param client: An initialized LDClient instance.
"""
self.__class__._ld_client = ld_client
self.__class__._client = client

@staticmethod
def setup_once():
# type: () -> None
try:
client = LaunchDarklyIntegration._ld_client or ldclient.get()
except Exception as exc:
raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc))
client = LaunchDarklyIntegration._client
if not client:
raise DidNotEnable("Error getting LDClient instance")

# Register the flag collection hook with the LD client.
client.add_hook(LaunchDarklyHook())
Expand Down
21 changes: 17 additions & 4 deletions sentry_sdk/integrations/openfeature.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,42 @@
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.flag_utils import flag_error_processor

if TYPE_CHECKING:
from typing import Optional

try:
from openfeature import api
from openfeature.hook import Hook

if TYPE_CHECKING:
from openfeature.flag_evaluation import FlagEvaluationDetails
from openfeature.hook import HookContext, HookHints
from openfeature.client import OpenFeatureClient
except ImportError:
raise DidNotEnable("OpenFeature is not installed")


class OpenFeatureIntegration(Integration):
identifier = "openfeature"
_client = None # type: Optional[OpenFeatureClient]

def __init__(self, client):
# type: (OpenFeatureClient) -> None
self.__class__._client = client

@staticmethod
def setup_once():
# type: () -> None

client = OpenFeatureIntegration._client
if not client:
raise DidNotEnable("Error getting OpenFeatureClient instance")

# Register the hook within the openfeature client.
client.add_hooks(hooks=[OpenFeatureHook()])

scope = sentry_sdk.get_current_scope()
scope.add_error_processor(flag_error_processor)

# Register the hook within the global openfeature hooks list.
api.add_hooks(hooks=[OpenFeatureHook()])


class OpenFeatureHook(Hook):

Expand Down
28 changes: 3 additions & 25 deletions tests/integrations/launchdarkly/test_launchdarkly.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from ldclient.integrations.test_data import TestData

import sentry_sdk
from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration


Expand All @@ -27,11 +26,10 @@ def test_launchdarkly_integration(
uninstall_integration(LaunchDarklyIntegration.identifier)
if use_global_client:
ldclient.set_config(config)
sentry_init(integrations=[LaunchDarklyIntegration()])
client = ldclient.get()
else:
client = LDClient(config=config)
sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)])
sentry_init(integrations=[LaunchDarklyIntegration(client)])

# Set test values
td.update(td.flag("hello").variation_for_all(True))
Expand Down Expand Up @@ -63,7 +61,7 @@ def test_launchdarkly_integration_threaded(
context = Context.create("user1")

uninstall_integration(LaunchDarklyIntegration.identifier)
sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)])
sentry_init(integrations=[LaunchDarklyIntegration(client)])
events = capture_events()

def task(flag_key):
Expand Down Expand Up @@ -122,7 +120,7 @@ def test_launchdarkly_integration_asyncio(
context = Context.create("user1")

uninstall_integration(LaunchDarklyIntegration.identifier)
sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)])
sentry_init(integrations=[LaunchDarklyIntegration(client)])
events = capture_events()

async def task(flag_key):
Expand Down Expand Up @@ -166,23 +164,3 @@ async def runner():
{"flag": "world", "result": False},
]
}


def test_launchdarkly_integration_did_not_enable(sentry_init, uninstall_integration):
"""
Setup should fail when using global client and ldclient.set_config wasn't called.
We're accessing ldclient internals to set up this test, so it might break if launchdarkly's
implementation changes.
"""

ldclient._reset_client()
try:
ldclient.__lock.lock()
ldclient.__config = None
finally:
ldclient.__lock.unlock()

uninstall_integration(LaunchDarklyIntegration.identifier)
with pytest.raises(DidNotEnable):
sentry_init(integrations=[LaunchDarklyIntegration()])
40 changes: 20 additions & 20 deletions tests/integrations/openfeature/test_openfeature.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@


def test_openfeature_integration(sentry_init, capture_events, uninstall_integration):
uninstall_integration(OpenFeatureIntegration.identifier)
sentry_init(integrations=[OpenFeatureIntegration()])

flags = {
"hello": InMemoryFlag("on", {"on": True, "off": False}),
"world": InMemoryFlag("off", {"on": True, "off": False}),
}
api.set_provider(InMemoryProvider(flags))

client = api.get_client()

uninstall_integration(OpenFeatureIntegration.identifier)
sentry_init(integrations=[OpenFeatureIntegration(client)])

client.get_boolean_value("hello", default_value=False)
client.get_boolean_value("world", default_value=False)
client.get_boolean_value("other", default_value=True)
Expand All @@ -41,18 +41,18 @@ def test_openfeature_integration(sentry_init, capture_events, uninstall_integrat
def test_openfeature_integration_threaded(
sentry_init, capture_events, uninstall_integration
):
uninstall_integration(OpenFeatureIntegration.identifier)
sentry_init(integrations=[OpenFeatureIntegration()])
events = capture_events()

flags = {
"hello": InMemoryFlag("on", {"on": True, "off": False}),
"world": InMemoryFlag("off", {"on": True, "off": False}),
}
api.set_provider(InMemoryProvider(flags))
client = api.get_client()

uninstall_integration(OpenFeatureIntegration.identifier)
sentry_init(integrations=[OpenFeatureIntegration(client)])
events = capture_events()

# Capture an eval before we split isolation scopes.
client = api.get_client()
client.get_boolean_value("hello", default_value=False)

def task(flag):
Expand Down Expand Up @@ -101,10 +101,20 @@ def test_openfeature_integration_asyncio(

asyncio = pytest.importorskip("asyncio")

flags = {
"hello": InMemoryFlag("on", {"on": True, "off": False}),
"world": InMemoryFlag("off", {"on": True, "off": False}),
}
api.set_provider(InMemoryProvider(flags))
client = api.get_client()

uninstall_integration(OpenFeatureIntegration.identifier)
sentry_init(integrations=[OpenFeatureIntegration()])
sentry_init(integrations=[OpenFeatureIntegration(client)])
events = capture_events()

# Capture an eval before we split isolation scopes.
client.get_boolean_value("hello", default_value=False)

async def task(flag):
with sentry_sdk.isolation_scope():
client.get_boolean_value(flag, default_value=False)
Expand All @@ -115,16 +125,6 @@ async def task(flag):
async def runner():
return asyncio.gather(task("world"), task("other"))

flags = {
"hello": InMemoryFlag("on", {"on": True, "off": False}),
"world": InMemoryFlag("off", {"on": True, "off": False}),
}
api.set_provider(InMemoryProvider(flags))

# Capture an eval before we split isolation scopes.
client = api.get_client()
client.get_boolean_value("hello", default_value=False)

asyncio.run(runner())

# Capture error in original scope
Expand Down

0 comments on commit b51d798

Please sign in to comment.