Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(flags): allow OpenFeature integration to track a single client #3895

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
22 changes: 19 additions & 3 deletions sentry_sdk/integrations/openfeature.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,45 @@
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=None):
# type: (Optional[OpenFeatureClient]) -> None
self.__class__._client = client

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

client = OpenFeatureIntegration._client
if client:
# Register the hook within the openfeature client.
client.add_hooks(hooks=[OpenFeatureHook()])
print("added hook to", client)
else:
# Register the hook within the global openfeature hooks list.
api.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
63 changes: 45 additions & 18 deletions tests/integrations/launchdarkly/test_launchdarkly.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,32 @@
from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration


@pytest.fixture
def reset_launchdarkly(uninstall_integration):
yield

uninstall_integration(LaunchDarklyIntegration.identifier)

# Resets global client and config only. We're using ldclient internals here, so this might
# break if their implementation changes.
ldclient._reset_client()
try:
ldclient.__lock.lock()
ldclient.__config = None
finally:
ldclient.__lock.unlock()


@pytest.mark.parametrize(
"use_global_client",
(False, True),
)
def test_launchdarkly_integration(
sentry_init, use_global_client, capture_events, uninstall_integration
sentry_init, use_global_client, capture_events, reset_launchdarkly
):
td = TestData.data_source()
config = Config("sdk-key", update_processor_class=td)

uninstall_integration(LaunchDarklyIntegration.identifier)
if use_global_client:
ldclient.set_config(config)
sentry_init(integrations=[LaunchDarklyIntegration()])
Expand Down Expand Up @@ -56,13 +71,12 @@ def test_launchdarkly_integration(


def test_launchdarkly_integration_threaded(
sentry_init, capture_events, uninstall_integration
sentry_init, capture_events, reset_launchdarkly
):
td = TestData.data_source()
client = LDClient(config=Config("sdk-key", update_processor_class=td))
context = Context.create("user1")

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

Expand Down Expand Up @@ -111,7 +125,7 @@ def task(flag_key):

@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
def test_launchdarkly_integration_asyncio(
sentry_init, capture_events, uninstall_integration
sentry_init, capture_events, reset_launchdarkly
):
"""Assert concurrently evaluated flags do not pollute one another."""

Expand All @@ -121,7 +135,6 @@ def test_launchdarkly_integration_asyncio(
client = LDClient(config=Config("sdk-key", update_processor_class=td))
context = Context.create("user1")

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

Expand Down Expand Up @@ -168,21 +181,35 @@ async def runner():
}


def test_launchdarkly_integration_did_not_enable(sentry_init, uninstall_integration):
def test_launchdarkly_integration_client_isolation(
sentry_init, capture_events, reset_launchdarkly
):
"""
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.
If the integration is tracking a single client, evaluations from other clients should not be
captured.
"""
td = TestData.data_source()
td.update(td.flag("hello").variation_for_all(True))
td.update(td.flag("world").variation_for_all(True))
client = LDClient(config=Config("sdk-key", update_processor_class=td))
sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)])

ldclient._reset_client()
try:
ldclient.__lock.lock()
ldclient.__config = None
finally:
ldclient.__lock.unlock()
# For isolation you must use a new Config object, but data source can be the same.
other_client = LDClient(Config("sdk-key", update_processor_class=td))
other_client.variation("hello", Context.create("my-org", "organization"), False)
other_client.variation("world", Context.create("user1", "user"), False)
other_client.variation("other", Context.create("user2", "user"), False)

uninstall_integration(LaunchDarklyIntegration.identifier)
events = capture_events()
sentry_sdk.capture_exception(Exception("something wrong!"))

assert len(events) == 1
assert events[0]["contexts"]["flags"] == {"values": []}


def test_launchdarkly_integration_did_not_enable(sentry_init, reset_launchdarkly):
"""
Setup should fail when using global client and ldclient.set_config wasn't called.
"""
with pytest.raises(DidNotEnable):
sentry_init(integrations=[LaunchDarklyIntegration()])
89 changes: 66 additions & 23 deletions tests/integrations/openfeature/test_openfeature.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import concurrent.futures as cf
import sys

import pytest

from openfeature import api
Expand All @@ -10,17 +9,35 @@
from sentry_sdk.integrations.openfeature import OpenFeatureIntegration


def test_openfeature_integration(sentry_init, capture_events, uninstall_integration):
@pytest.fixture
def reset_openfeature(uninstall_integration):
yield

# Teardown
uninstall_integration(OpenFeatureIntegration.identifier)
sentry_init(integrations=[OpenFeatureIntegration()])
api.clear_hooks()
api.shutdown() # provider clean up


@pytest.mark.parametrize(
"use_global_client",
(False, True),
)
def test_openfeature_integration(
sentry_init, use_global_client, capture_events, reset_openfeature
):
flags = {
"hello": InMemoryFlag("on", {"on": True, "off": False}),
"world": InMemoryFlag("off", {"on": True, "off": False}),
}
api.set_provider(InMemoryProvider(flags))

client = api.get_client()

if use_global_client:
sentry_init(integrations=[OpenFeatureIntegration()])
else:
sentry_init(integrations=[OpenFeatureIntegration(client=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 @@ -39,20 +56,19 @@ def test_openfeature_integration(sentry_init, capture_events, uninstall_integrat


def test_openfeature_integration_threaded(
sentry_init, capture_events, uninstall_integration
sentry_init, capture_events, reset_openfeature
):
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()

sentry_init(integrations=[OpenFeatureIntegration(client=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 @@ -95,16 +111,25 @@ def task(flag):

@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
def test_openfeature_integration_asyncio(
sentry_init, capture_events, uninstall_integration
sentry_init, capture_events, reset_openfeature
):
"""Assert concurrently evaluated flags do not pollute one another."""

asyncio = pytest.importorskip("asyncio")

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()

sentry_init(integrations=[OpenFeatureIntegration(client=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 +140,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 All @@ -151,3 +166,31 @@ async def runner():
{"flag": "world", "result": False},
]
}


def test_openfeature_integration_client_isolation(
sentry_init, capture_events, reset_openfeature
):
"""
If the integration is tracking a single client, evaluations from other clients should not be
captured.
"""
flags = {
"hello": InMemoryFlag("on", {"on": True, "off": False}),
"world": InMemoryFlag("off", {"on": True, "off": False}),
}
api.set_provider(InMemoryProvider(flags))
client = api.get_client()
sentry_init(integrations=[OpenFeatureIntegration(client=client)])

other_client = api.get_client()
other_client.get_boolean_value("hello", default_value=False)
other_client.get_boolean_value("world", default_value=False)
other_client.get_boolean_value("other", default_value=True)

events = capture_events()
sentry_sdk.set_tag("apple", "0")
sentry_sdk.capture_exception(Exception("something wrong!"))

assert len(events) == 1
assert events[0]["contexts"]["flags"] == {"values": []}
Loading