From c25b802d61a6caf07aa4f001836124b8631f2aa3 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Mon, 21 Oct 2024 20:50:21 -0700 Subject: [PATCH 01/29] Initial src and dependency code --- requirements-linting.txt | 1 + .../split-tox-gh-actions.py | 1 + sentry_sdk/integrations/launchdarkly.py | 72 +++++++++++++++++++ setup.py | 1 + tests/integrations/launchdarkly/__init__.py | 3 + tox.ini | 7 ++ 6 files changed, 85 insertions(+) create mode 100644 sentry_sdk/integrations/launchdarkly.py create mode 100644 tests/integrations/launchdarkly/__init__.py diff --git a/requirements-linting.txt b/requirements-linting.txt index 4411a204db..c9d4bd7f5c 100644 --- a/requirements-linting.txt +++ b/requirements-linting.txt @@ -16,3 +16,4 @@ pep8-naming pre-commit # local linting httpcore openfeature-sdk +launchdarkly-server-sdk diff --git a/scripts/split-tox-gh-actions/split-tox-gh-actions.py b/scripts/split-tox-gh-actions/split-tox-gh-actions.py index fb22f97958..803d7ad52c 100755 --- a/scripts/split-tox-gh-actions/split-tox-gh-actions.py +++ b/scripts/split-tox-gh-actions/split-tox-gh-actions.py @@ -125,6 +125,7 @@ "tornado", ], "Miscellaneous": [ + "ldclient", "loguru", "openfeature", "opentelemetry", diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py new file mode 100644 index 0000000000..001023bc35 --- /dev/null +++ b/sentry_sdk/integrations/launchdarkly.py @@ -0,0 +1,72 @@ +from typing import TYPE_CHECKING +import sentry_sdk + +from sentry_sdk.integrations import DidNotEnable, Integration + +try: + import ldclient + from ldclient.hook import Hook + + if TYPE_CHECKING: + from ldclient import LDClient + from ldclient.hook import EvaluationSeriesContext, Metadata + from ldclient.evaluation import EvaluationDetail + + from sentry_sdk._types import Event, ExcInfo + from typing import Any, Optional +except ImportError: + raise DidNotEnable("LaunchDarkly is not installed") + + +class LaunchDarklyIntegration(Integration): + identifier = "launchdarkly" + + @staticmethod + def get_ldclient(): + # type: () -> LDClient + try: + client = ldclient.get() + except Exception as exc: + sentry_sdk.capture_exception(exc) + raise DidNotEnable( + "Failed to find LaunchDarkly client instance. " + str(exc) + ) + + if client and client.is_initialized(): + return client + raise DidNotEnable("LaunchDarkly client is not initialized") + + @staticmethod + def setup_once(): + # type: () -> None + def error_processor(event, _exc_info): + # type: (Event, ExcInfo) -> Optional[Event] + scope = sentry_sdk.get_current_scope() + event["contexts"]["flags"] = {"values": scope.flags.get()} + return event + + scope = sentry_sdk.get_current_scope() + scope.add_error_processor(error_processor) + + # Register the hook with the global launchdarkly client. + client = LaunchDarklyIntegration.get_ldclient() + client.add_hook(LaunchDarklyHook()) + + +class LaunchDarklyHook(Hook): + + @property + def metadata(self): + # type: () -> Metadata + return Metadata(name="sentry-on-error-hook") + + def after_evaluation(self, series_context, data, detail): + # type: (EvaluationSeriesContext, dict[Any, Any], EvaluationDetail) -> dict[Any, Any] + if isinstance(detail.value, bool): + flags = sentry_sdk.get_current_scope().flags + flags.set(series_context.key, detail.value) + return data + + def before_evaluation(self, _series_context, data): + # type: (EvaluationSeriesContext, dict[Any, Any]) -> dict[Any, Any] + return data # No-op. diff --git a/setup.py b/setup.py index 7b0d11a051..b121ee07df 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ def get_file_text(file_name): "huey": ["huey>=2"], "huggingface_hub": ["huggingface_hub>=0.22"], "langchain": ["langchain>=0.0.210"], + "ldclient": ["launchdarkly-server-sdk>=9.8.0"], "litestar": ["litestar>=2.0.0"], "loguru": ["loguru>=0.5"], "openai": ["openai>=1.0.0", "tiktoken>=0.3.0"], diff --git a/tests/integrations/launchdarkly/__init__.py b/tests/integrations/launchdarkly/__init__.py new file mode 100644 index 0000000000..06e09884c8 --- /dev/null +++ b/tests/integrations/launchdarkly/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("ldclient") diff --git a/tox.ini b/tox.ini index 437360a9d6..370f819973 100644 --- a/tox.ini +++ b/tox.ini @@ -167,6 +167,9 @@ envlist = {py3.9,py3.11,py3.12}-langchain-latest {py3.9,py3.11,py3.12}-langchain-notiktoken + # LaunchDarkly + {py3.8,py3.9,py3.10,py3.11,py3.12}-launchdarkly-latest + # Litestar # litestar 2.0.0 is the earliest version that supports Python < 3.12 {py3.8,py3.11}-litestar-v{2.0} @@ -520,6 +523,9 @@ deps = langchain-notiktoken: langchain-openai langchain-notiktoken: openai>=1.6.1 + # LaunchDarkly + launchdarkly-latest: launchdarkly-server-sdk~=9.8.0 + # Litestar litestar: pytest-asyncio litestar: python-multipart @@ -727,6 +733,7 @@ setenv = huey: TESTPATH=tests/integrations/huey huggingface_hub: TESTPATH=tests/integrations/huggingface_hub langchain: TESTPATH=tests/integrations/langchain + launchdarkly: TESTPATH=tests/integrations/launchdarkly litestar: TESTPATH=tests/integrations/litestar loguru: TESTPATH=tests/integrations/loguru openai: TESTPATH=tests/integrations/openai From e60e1a4b6539f5be3d5fd05f58ada46b87700231 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Mon, 21 Oct 2024 20:57:12 -0700 Subject: [PATCH 02/29] Move get_ldclient to a top-level helper --- sentry_sdk/integrations/launchdarkly.py | 30 ++++++++++++------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index 001023bc35..5b966e8472 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -18,24 +18,22 @@ raise DidNotEnable("LaunchDarkly is not installed") +def _get_ldclient(): + # type: () -> LDClient + try: + client = ldclient.get() + except Exception as exc: + sentry_sdk.capture_exception(exc) + raise DidNotEnable("Failed to find LaunchDarkly client instance. " + str(exc)) + + if client and client.is_initialized(): + return client + raise DidNotEnable("LaunchDarkly client is not initialized") + + class LaunchDarklyIntegration(Integration): identifier = "launchdarkly" - @staticmethod - def get_ldclient(): - # type: () -> LDClient - try: - client = ldclient.get() - except Exception as exc: - sentry_sdk.capture_exception(exc) - raise DidNotEnable( - "Failed to find LaunchDarkly client instance. " + str(exc) - ) - - if client and client.is_initialized(): - return client - raise DidNotEnable("LaunchDarkly client is not initialized") - @staticmethod def setup_once(): # type: () -> None @@ -49,7 +47,7 @@ def error_processor(event, _exc_info): scope.add_error_processor(error_processor) # Register the hook with the global launchdarkly client. - client = LaunchDarklyIntegration.get_ldclient() + client = _get_ldclient() client.add_hook(LaunchDarklyHook()) From 597004b5324cc5301befd229a5a90ab2e3333393 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:03:45 -0700 Subject: [PATCH 03/29] Add to requirements-testing --- requirements-testing.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-testing.txt b/requirements-testing.txt index dfbd821845..fe9b205b19 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -14,3 +14,4 @@ socksio httpcore[http2] setuptools Brotli +launchdarkly-server-sdk From 75a34421b3f2ec1f268cfc4e9d6428a4fa09231e Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:29:24 -0700 Subject: [PATCH 04/29] Split up static version from latest --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 370f819973..c6de274e1d 100644 --- a/tox.ini +++ b/tox.ini @@ -168,6 +168,7 @@ envlist = {py3.9,py3.11,py3.12}-langchain-notiktoken # LaunchDarkly + {py3.8,py3.9,py3.10,py3.11,py3.12}-launchdarkly-v9.8 {py3.8,py3.9,py3.10,py3.11,py3.12}-launchdarkly-latest # Litestar @@ -524,7 +525,8 @@ deps = langchain-notiktoken: openai>=1.6.1 # LaunchDarkly - launchdarkly-latest: launchdarkly-server-sdk~=9.8.0 + launchdarkly-v9.8: launchdarkly-server-sdk~=9.8.0 + launchdarkly-latest: launchdarkly-server-sdk # Litestar litestar: pytest-asyncio From a2e33834dbfb6fbaf698cbb32255b8a2834b8a72 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 22 Oct 2024 21:26:22 -0700 Subject: [PATCH 05/29] Fix import --- sentry_sdk/integrations/launchdarkly.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index 5b966e8472..f6fdd56b5b 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -5,11 +5,11 @@ try: import ldclient - from ldclient.hook import Hook + from ldclient.hook import Hook, Metadata if TYPE_CHECKING: from ldclient import LDClient - from ldclient.hook import EvaluationSeriesContext, Metadata + from ldclient.hook import EvaluationSeriesContext from ldclient.evaluation import EvaluationDetail from sentry_sdk._types import Event, ExcInfo @@ -24,7 +24,7 @@ def _get_ldclient(): client = ldclient.get() except Exception as exc: sentry_sdk.capture_exception(exc) - raise DidNotEnable("Failed to find LaunchDarkly client instance. " + str(exc)) + raise DidNotEnable("Error getting LaunchDarkly client. " + str(exc)) if client and client.is_initialized(): return client From 5165ffba47933667d16caaab931173febbdf2ad4 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 22 Oct 2024 22:48:36 -0700 Subject: [PATCH 06/29] Pass in client to Integration initializer and basic unit test --- sentry_sdk/integrations/launchdarkly.py | 29 ++++++++--------- .../launchdarkly/test_launchdarkly.py | 32 +++++++++++++++++++ 2 files changed, 45 insertions(+), 16 deletions(-) create mode 100644 tests/integrations/launchdarkly/test_launchdarkly.py diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index f6fdd56b5b..5699e0edfc 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -18,21 +18,22 @@ raise DidNotEnable("LaunchDarkly is not installed") -def _get_ldclient(): - # type: () -> LDClient - try: - client = ldclient.get() - except Exception as exc: - sentry_sdk.capture_exception(exc) - raise DidNotEnable("Error getting LaunchDarkly client. " + str(exc)) +class LaunchDarklyIntegration(Integration): + identifier = "launchdarkly" - if client and client.is_initialized(): - return client - raise DidNotEnable("LaunchDarkly client is not initialized") + def __init__(self, client=None): + # type: (LDClient | None) -> None + if client is None: + try: + client = ldclient.get() # global singleton + except Exception as exc: + raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) + if not client.is_initialized(): + raise DidNotEnable("LaunchDarkly client is not initialized") -class LaunchDarklyIntegration(Integration): - identifier = "launchdarkly" + # Register the flag collection hook with the given client. + client.add_hook(LaunchDarklyHook()) @staticmethod def setup_once(): @@ -46,10 +47,6 @@ def error_processor(event, _exc_info): scope = sentry_sdk.get_current_scope() scope.add_error_processor(error_processor) - # Register the hook with the global launchdarkly client. - client = _get_ldclient() - client.add_hook(LaunchDarklyHook()) - class LaunchDarklyHook(Hook): diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py new file mode 100644 index 0000000000..ecdc22cb6d --- /dev/null +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -0,0 +1,32 @@ +import sentry_sdk + +from ldclient import LDClient +from ldclient.config import Config +from ldclient.context import Context +from ldclient.integrations.test_data import TestData + +from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration + +# Ref: https://docs.launchdarkly.com/sdk/features/test-data-sources#python + + +def test_launchdarkly_integration(sentry_init): + td = TestData.data_source() + client = LDClient(config=Config("sdk-key", update_processor_class=td)) + sentry_init(integrations=[LaunchDarklyIntegration(client=client)]) + + # Set test values + td.update(td.flag("hello").variation_for_all(True)) + td.update(td.flag("world").variation_for_all(True)) + td.update(td.flag("goodbye").variation_for_all(False)) + + # Evaluate + client.variation("hello", Context.create("my-org", "organization"), False) + client.variation("world", Context.create("user1", "user"), False) + client.variation("goodbye", Context.create("user2", "user"), False) + + assert sentry_sdk.get_current_scope().flags.get() == [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": True}, + {"flag": "goodbye", "result": False}, + ] From c9daf171958d88bd5f1c25d50c2368446574a7a3 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 22 Oct 2024 23:16:30 -0700 Subject: [PATCH 07/29] Add threaded, asyncio, and global ldclient tests --- .../launchdarkly/test_launchdarkly.py | 77 +++++++++++++++++-- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index ecdc22cb6d..bb105b403f 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -1,4 +1,10 @@ +import asyncio +import concurrent.futures as cf + +import ldclient + import sentry_sdk +import pytest from ldclient import LDClient from ldclient.config import Config @@ -8,25 +14,84 @@ from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration # Ref: https://docs.launchdarkly.com/sdk/features/test-data-sources#python +# https://launchdarkly-python-sdk.readthedocs.io/en/latest/api-testing.html#ldclient.integrations.test_data.TestData -def test_launchdarkly_integration(sentry_init): +@pytest.mark.parametrize( + "use_global_client", + (False, True), +) +def test_launchdarkly_integration(sentry_init, use_global_client): td = TestData.data_source() - client = LDClient(config=Config("sdk-key", update_processor_class=td)) - sentry_init(integrations=[LaunchDarklyIntegration(client=client)]) + config = Config("sdk-key", update_processor_class=td) + if use_global_client: + ldclient.set_config(config) + sentry_init(integrations=[LaunchDarklyIntegration()]) + client = ldclient.get() + else: + client = LDClient(config=config) + sentry_init(integrations=[LaunchDarklyIntegration(client=client)]) # Set test values td.update(td.flag("hello").variation_for_all(True)) td.update(td.flag("world").variation_for_all(True)) - td.update(td.flag("goodbye").variation_for_all(False)) # Evaluate client.variation("hello", Context.create("my-org", "organization"), False) client.variation("world", Context.create("user1", "user"), False) - client.variation("goodbye", Context.create("user2", "user"), False) + client.variation("other", Context.create("user2", "user"), False) assert sentry_sdk.get_current_scope().flags.get() == [ {"flag": "hello", "result": True}, {"flag": "world", "result": True}, - {"flag": "goodbye", "result": False}, + {"flag": "other", "result": False}, ] + + +def test_launchdarkly_integration_threaded(sentry_init): + td = TestData.data_source() + client = LDClient(config=Config("sdk-key", update_processor_class=td)) + sentry_init(integrations=[LaunchDarklyIntegration(client=client)]) + context = Context.create("user1") + + def task(flag_key): + # Create a new isolation scope for the thread. This means the evaluations in each task are captured separately. + with sentry_sdk.isolation_scope(): + client.variation(flag_key, context, False) + return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()] + + td.update(td.flag("hello").variation_for_all(True)) + td.update(td.flag("world").variation_for_all(False)) + client.variation( + "hello", context, False + ) # Captured before splitting isolation scopes. + + with cf.ThreadPoolExecutor(max_workers=2) as pool: + results = list(pool.map(task, ["world", "other"])) + + assert results[0] == ["hello", "world"] + assert results[1] == ["hello", "other"] + + +def test_launchdarkly_integration_asyncio(sentry_init): + """Assert concurrently evaluated flags do not pollute one another.""" + td = TestData.data_source() + client = LDClient(config=Config("sdk-key", update_processor_class=td)) + sentry_init(integrations=[LaunchDarklyIntegration(client=client)]) + context = Context.create("user1") + + async def task(flag_key): + with sentry_sdk.isolation_scope(): + client.variation(flag_key, context, False) + return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()] + + async def runner(): + return asyncio.gather(task("world"), task("other")) + + td.update(td.flag("hello").variation_for_all(True)) + td.update(td.flag("world").variation_for_all(False)) + client.variation("hello", context, False) + + results = asyncio.run(runner()).result() + assert results[0] == ["hello", "world"] + assert results[1] == ["hello", "other"] From d7ae9f57beb2ed60783b0ad361c7926104956f18 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:29:22 -0700 Subject: [PATCH 08/29] Change metadata, test not enabled cases --- sentry_sdk/integrations/launchdarkly.py | 8 ++++--- .../launchdarkly/test_launchdarkly.py | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index 5699e0edfc..71b2b99116 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -25,12 +25,14 @@ def __init__(self, client=None): # type: (LDClient | None) -> None if client is None: try: - client = ldclient.get() # global singleton + client = ( + ldclient.get() + ) # global singleton. Fails if set_config hasn't been called. except Exception as exc: raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) if not client.is_initialized(): - raise DidNotEnable("LaunchDarkly client is not initialized") + raise DidNotEnable("LaunchDarkly client is not initialized.") # Register the flag collection hook with the given client. client.add_hook(LaunchDarklyHook()) @@ -53,7 +55,7 @@ class LaunchDarklyHook(Hook): @property def metadata(self): # type: () -> Metadata - return Metadata(name="sentry-on-error-hook") + return Metadata(name="sentry-feature-flag-recorder") def after_evaluation(self, series_context, data, detail): # type: (EvaluationSeriesContext, dict[Any, Any], EvaluationDetail) -> dict[Any, Any] diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index bb105b403f..88db5c489d 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -11,6 +11,7 @@ from ldclient.context import Context from ldclient.integrations.test_data import TestData +from sentry_sdk.integrations import DidNotEnable from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration # Ref: https://docs.launchdarkly.com/sdk/features/test-data-sources#python @@ -95,3 +96,23 @@ async def runner(): results = asyncio.run(runner()).result() assert results[0] == ["hello", "world"] assert results[1] == ["hello", "other"] + + +def test_launchdarkly_integration_did_not_enable(monkeypatch): + # Client is not passed in and set_config wasn't called. + # Bad practice to access internals like this. TODO: can skip this test, or remove this case entirely (force user to pass in a client instance). + ldclient._reset_client() + try: + ldclient.__lock.lock() + ldclient.__config = None + finally: + ldclient.__lock.unlock() + + with pytest.raises(DidNotEnable): + LaunchDarklyIntegration() + + # Client not initialized. + client = LDClient(config=Config("sdk-key")) + monkeypatch.setattr(client, "is_initialized", lambda: False) + with pytest.raises(DidNotEnable): + LaunchDarklyIntegration(client=client) From 7740f4385fd115acdf4c9dbc2a91bf0bba126843 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:33:43 -0700 Subject: [PATCH 09/29] Add versioned tests to workflows --- .github/workflows/test-integrations-miscellaneous.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test-integrations-miscellaneous.yml b/.github/workflows/test-integrations-miscellaneous.yml index 11ca9e03ae..dfc839bc7e 100644 --- a/.github/workflows/test-integrations-miscellaneous.yml +++ b/.github/workflows/test-integrations-miscellaneous.yml @@ -45,6 +45,10 @@ jobs: - name: Erase coverage run: | coverage erase + - name: Test launchdarkly latest + run: | + set -x # print commands that are executed + ./scripts/runtox.sh "py${{ matrix.python-version }}-launchdarkly-latest" - name: Test loguru latest run: | set -x # print commands that are executed @@ -121,6 +125,10 @@ jobs: - name: Erase coverage run: | coverage erase + - name: Test launchdarkly pinned + run: | + set -x # print commands that are executed + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-launchdarkly" - name: Test loguru pinned run: | set -x # print commands that are executed From 0309b82ee50efbf0bcf4addd13d0589c8c9a3075 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:35:05 -0700 Subject: [PATCH 10/29] Rm doc references --- tests/integrations/launchdarkly/test_launchdarkly.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 88db5c489d..9e97c85440 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -14,9 +14,6 @@ from sentry_sdk.integrations import DidNotEnable from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration -# Ref: https://docs.launchdarkly.com/sdk/features/test-data-sources#python -# https://launchdarkly-python-sdk.readthedocs.io/en/latest/api-testing.html#ldclient.integrations.test_data.TestData - @pytest.mark.parametrize( "use_global_client", From cec37dc80ab58c80591a047a99cd929e2103e713 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 23 Oct 2024 11:17:21 -0700 Subject: [PATCH 11/29] Fix split-tox-gh-actions GROUPS --- scripts/split-tox-gh-actions/split-tox-gh-actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/split-tox-gh-actions/split-tox-gh-actions.py b/scripts/split-tox-gh-actions/split-tox-gh-actions.py index 803d7ad52c..c0bf2a7a09 100755 --- a/scripts/split-tox-gh-actions/split-tox-gh-actions.py +++ b/scripts/split-tox-gh-actions/split-tox-gh-actions.py @@ -125,7 +125,7 @@ "tornado", ], "Miscellaneous": [ - "ldclient", + "launchdarkly", "loguru", "openfeature", "opentelemetry", From 22d1024cc9ce532f9844fd2faa5bec40204142fa Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:46:27 -0700 Subject: [PATCH 12/29] Add doc references --- sentry_sdk/integrations/launchdarkly.py | 10 +++++++--- tests/integrations/launchdarkly/test_launchdarkly.py | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index 71b2b99116..485960dbda 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -23,11 +23,15 @@ class LaunchDarklyIntegration(Integration): def __init__(self, client=None): # type: (LDClient | None) -> None + """ + @param client An initialized LDClient instance. If a client is not provided, this integration will attempt to + use the shared global instance. This will fail if ldclient.set_config() hasn't been called. + + Docs reference: https://docs.launchdarkly.com/sdk/server-side/python + """ if client is None: try: - client = ( - ldclient.get() - ) # global singleton. Fails if set_config hasn't been called. + client = ldclient.get() # global singleton. except Exception as exc: raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 9e97c85440..b04fa1d11c 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -14,6 +14,8 @@ from sentry_sdk.integrations import DidNotEnable from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration +# Docs reference: https://launchdarkly-python-sdk.readthedocs.io/en/latest/api-testing.html#ldclient.integrations.test_data.TestData + @pytest.mark.parametrize( "use_global_client", From d9775b8dbf8a93a291648223a3c48a8054205214 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:11:54 -0700 Subject: [PATCH 13/29] Formatting from pr comments. Max line length=100 --- sentry_sdk/integrations/launchdarkly.py | 6 ++---- .../integrations/launchdarkly/test_launchdarkly.py | 13 ++++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index 485960dbda..12a80f87c6 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -24,10 +24,8 @@ class LaunchDarklyIntegration(Integration): def __init__(self, client=None): # type: (LDClient | None) -> None """ - @param client An initialized LDClient instance. If a client is not provided, this integration will attempt to - use the shared global instance. This will fail if ldclient.set_config() hasn't been called. - - Docs reference: https://docs.launchdarkly.com/sdk/server-side/python + :param client: An initialized LDClient instance. If a client is not provided, this + integration will attempt to use the shared global instance. """ if client is None: try: diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index b04fa1d11c..5d62b88ebe 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -14,8 +14,6 @@ from sentry_sdk.integrations import DidNotEnable from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration -# Docs reference: https://launchdarkly-python-sdk.readthedocs.io/en/latest/api-testing.html#ldclient.integrations.test_data.TestData - @pytest.mark.parametrize( "use_global_client", @@ -55,16 +53,16 @@ def test_launchdarkly_integration_threaded(sentry_init): context = Context.create("user1") def task(flag_key): - # Create a new isolation scope for the thread. This means the evaluations in each task are captured separately. + # Creates a new isolation scope for the thread. + # This means the evaluations in each task are captured separately. with sentry_sdk.isolation_scope(): client.variation(flag_key, context, False) return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()] td.update(td.flag("hello").variation_for_all(True)) td.update(td.flag("world").variation_for_all(False)) - client.variation( - "hello", context, False - ) # Captured before splitting isolation scopes. + # Capture an eval before we split isolation scopes. + client.variation("hello", context, False) with cf.ThreadPoolExecutor(max_workers=2) as pool: results = list(pool.map(task, ["world", "other"])) @@ -99,7 +97,8 @@ async def runner(): def test_launchdarkly_integration_did_not_enable(monkeypatch): # Client is not passed in and set_config wasn't called. - # Bad practice to access internals like this. TODO: can skip this test, or remove this case entirely (force user to pass in a client instance). + # TODO: Bad practice to access internals like this. We can skip this test, or remove this + # case entirely (force user to pass in a client instance). ldclient._reset_client() try: ldclient.__lock.lock() From 91eb3526e6763ed3799374293b88d797f6fd1e85 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 25 Oct 2024 13:17:03 -0700 Subject: [PATCH 14/29] Move hook registration to setup_once --- sentry_sdk/integrations/launchdarkly.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index 12a80f87c6..5098bc610d 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -35,9 +35,7 @@ def __init__(self, client=None): if not client.is_initialized(): raise DidNotEnable("LaunchDarkly client is not initialized.") - - # Register the flag collection hook with the given client. - client.add_hook(LaunchDarklyHook()) + self.ld_client = client @staticmethod def setup_once(): @@ -51,6 +49,12 @@ def error_processor(event, _exc_info): scope = sentry_sdk.get_current_scope() scope.add_error_processor(error_processor) + # Register the flag collection hook with the LD client. + ld_client = ( + sentry_sdk.get_client().get_integration(LaunchDarklyIntegration).ld_client + ) + ld_client.add_hook(LaunchDarklyHook()) + class LaunchDarklyHook(Hook): From a9d5099411b5ecffc427e2e6259bbed7651a790f Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 10:34:05 -0500 Subject: [PATCH 15/29] Fix typing and extract error_processor to common module --- sentry_sdk/flag_utils.py | 11 +++++++- sentry_sdk/integrations/launchdarkly.py | 34 ++++++++++--------------- sentry_sdk/integrations/openfeature.py | 11 ++------ 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/sentry_sdk/flag_utils.py b/sentry_sdk/flag_utils.py index 3e18e60e07..2b345a7f0b 100644 --- a/sentry_sdk/flag_utils.py +++ b/sentry_sdk/flag_utils.py @@ -1,10 +1,12 @@ from copy import copy from typing import TYPE_CHECKING +import sentry_sdk from sentry_sdk._lru_cache import LRUCache if TYPE_CHECKING: - from typing import TypedDict + from typing import TypedDict, Optional + from sentry_sdk._types import Event, ExcInfo FlagData = TypedDict("FlagData", {"flag": str, "result": bool}) @@ -36,3 +38,10 @@ def get(self): def set(self, flag, result): # type: (str, bool) -> None self.buffer.set(flag, result) + + +def flag_error_processor(event, exc_info): + # type: (Event, ExcInfo) -> Optional[Event] + scope = sentry_sdk.get_current_scope() + event["contexts"]["flags"] = {"values": scope.flags.get()} + return event diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index 5098bc610d..9ac24dc5d1 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -2,6 +2,7 @@ import sentry_sdk from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.flag_utils import flag_error_processor try: import ldclient @@ -12,8 +13,7 @@ from ldclient.hook import EvaluationSeriesContext from ldclient.evaluation import EvaluationDetail - from sentry_sdk._types import Event, ExcInfo - from typing import Any, Optional + from typing import Any except ImportError: raise DidNotEnable("LaunchDarkly is not installed") @@ -21,39 +21,31 @@ class LaunchDarklyIntegration(Integration): identifier = "launchdarkly" - def __init__(self, client=None): + def __init__(self, ld_client=None): # type: (LDClient | None) -> None """ :param client: An initialized LDClient instance. If a client is not provided, this integration will attempt to use the shared global instance. """ - if client is None: - try: - client = ldclient.get() # global singleton. - except Exception as exc: - raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) + try: + client = ld_client or ldclient.get() + except Exception as exc: + raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) if not client.is_initialized(): raise DidNotEnable("LaunchDarkly client is not initialized.") - self.ld_client = client + + self.client = client @staticmethod def setup_once(): # type: () -> None - def error_processor(event, _exc_info): - # type: (Event, ExcInfo) -> Optional[Event] - scope = sentry_sdk.get_current_scope() - event["contexts"]["flags"] = {"values": scope.flags.get()} - return event - scope = sentry_sdk.get_current_scope() - scope.add_error_processor(error_processor) + scope.add_error_processor(flag_error_processor) # Register the flag collection hook with the LD client. - ld_client = ( - sentry_sdk.get_client().get_integration(LaunchDarklyIntegration).ld_client - ) - ld_client.add_hook(LaunchDarklyHook()) + client = sentry_sdk.get_client().get_integration(LaunchDarklyIntegration).client + client.add_hook(LaunchDarklyHook()) class LaunchDarklyHook(Hook): @@ -70,6 +62,6 @@ def after_evaluation(self, series_context, data, detail): flags.set(series_context.key, detail.value) return data - def before_evaluation(self, _series_context, data): + def before_evaluation(self, series_context, data): # type: (EvaluationSeriesContext, dict[Any, Any]) -> dict[Any, Any] return data # No-op. diff --git a/sentry_sdk/integrations/openfeature.py b/sentry_sdk/integrations/openfeature.py index 8b3b6ed9a9..18f968a703 100644 --- a/sentry_sdk/integrations/openfeature.py +++ b/sentry_sdk/integrations/openfeature.py @@ -2,6 +2,7 @@ import sentry_sdk from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.flag_utils import flag_error_processor try: from openfeature import api @@ -10,8 +11,6 @@ if TYPE_CHECKING: from openfeature.flag_evaluation import FlagEvaluationDetails from openfeature.hook import HookContext, HookHints - from sentry_sdk._types import Event, ExcInfo - from typing import Optional except ImportError: raise DidNotEnable("OpenFeature is not installed") @@ -22,14 +21,8 @@ class OpenFeatureIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - def error_processor(event, exc_info): - # type: (Event, ExcInfo) -> Optional[Event] - scope = sentry_sdk.get_current_scope() - event["contexts"]["flags"] = {"values": scope.flags.get()} - return event - scope = sentry_sdk.get_current_scope() - scope.add_error_processor(error_processor) + scope.add_error_processor(flag_error_processor) # Register the hook within the global openfeature hooks list. api.add_hooks(hooks=[OpenFeatureHook()]) From 50d2dae082e28601f16b0e8201fc71d13c90e26f Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 10:37:32 -0500 Subject: [PATCH 16/29] Raise if the integration was not enabled before setup_once is called --- sentry_sdk/integrations/launchdarkly.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index 9ac24dc5d1..408d8bad2c 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -40,12 +40,14 @@ def __init__(self, ld_client=None): @staticmethod def setup_once(): # type: () -> None + integration = sentry_sdk.get_client().get_integration(LaunchDarklyIntegration) + if integration is None: + raise DidNotEnable("LaunchDarkly client is not initialized.") + scope = sentry_sdk.get_current_scope() scope.add_error_processor(flag_error_processor) - # Register the flag collection hook with the LD client. - client = sentry_sdk.get_client().get_integration(LaunchDarklyIntegration).client - client.add_hook(LaunchDarklyHook()) + integration.client.add_hook(LaunchDarklyHook()) class LaunchDarklyHook(Hook): From 44aebf3067c371e0518aa983f5cf4582e34a4ee1 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 11:09:07 -0500 Subject: [PATCH 17/29] Rename parameter --- tests/integrations/launchdarkly/test_launchdarkly.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 5d62b88ebe..acbe764104 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -28,7 +28,7 @@ def test_launchdarkly_integration(sentry_init, use_global_client): client = ldclient.get() else: client = LDClient(config=config) - sentry_init(integrations=[LaunchDarklyIntegration(client=client)]) + sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)]) # Set test values td.update(td.flag("hello").variation_for_all(True)) @@ -49,7 +49,7 @@ def test_launchdarkly_integration(sentry_init, use_global_client): def test_launchdarkly_integration_threaded(sentry_init): td = TestData.data_source() client = LDClient(config=Config("sdk-key", update_processor_class=td)) - sentry_init(integrations=[LaunchDarklyIntegration(client=client)]) + sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)]) context = Context.create("user1") def task(flag_key): @@ -75,7 +75,7 @@ def test_launchdarkly_integration_asyncio(sentry_init): """Assert concurrently evaluated flags do not pollute one another.""" td = TestData.data_source() client = LDClient(config=Config("sdk-key", update_processor_class=td)) - sentry_init(integrations=[LaunchDarklyIntegration(client=client)]) + sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)]) context = Context.create("user1") async def task(flag_key): @@ -113,4 +113,4 @@ def test_launchdarkly_integration_did_not_enable(monkeypatch): client = LDClient(config=Config("sdk-key")) monkeypatch.setattr(client, "is_initialized", lambda: False) with pytest.raises(DidNotEnable): - LaunchDarklyIntegration(client=client) + LaunchDarklyIntegration(ld_client=client) From 144e064abb7832caad128037b36cb0473a4bb64c Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 11:33:41 -0500 Subject: [PATCH 18/29] Move hook registration to the init method --- sentry_sdk/integrations/launchdarkly.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index 408d8bad2c..9e00e12ede 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -35,19 +35,14 @@ def __init__(self, ld_client=None): if not client.is_initialized(): raise DidNotEnable("LaunchDarkly client is not initialized.") - self.client = client + # Register the flag collection hook with the LD client. + client.add_hook(LaunchDarklyHook()) @staticmethod def setup_once(): # type: () -> None - integration = sentry_sdk.get_client().get_integration(LaunchDarklyIntegration) - if integration is None: - raise DidNotEnable("LaunchDarkly client is not initialized.") - scope = sentry_sdk.get_current_scope() scope.add_error_processor(flag_error_processor) - # Register the flag collection hook with the LD client. - integration.client.add_hook(LaunchDarklyHook()) class LaunchDarklyHook(Hook): From 13434c3ae667d8b475b63e65eb0063a268a72bec Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 11:36:42 -0500 Subject: [PATCH 19/29] Update tox to use 3.8 or greater --- tox.ini | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tox.ini b/tox.ini index a7deedd618..57f28832d8 100644 --- a/tox.ini +++ b/tox.ini @@ -192,6 +192,10 @@ envlist = {py3.8,py3.12,py3.13}-openfeature-v0.7 {py3.8,py3.12,py3.13}-openfeature-latest + # LaunchDarkly + {py3.8,py3.12,py3.13}-launchdarkly-v9.8.0 + {py3.8,py3.12,py3.13}-launchdarkly-latest + # OpenTelemetry (OTel) {py3.7,py3.9,py3.12,py3.13}-opentelemetry @@ -555,6 +559,10 @@ deps = openfeature-v0.7: openfeature-sdk~=0.7.1 openfeature-latest: openfeature-sdk + # LaunchDarkly + launchdarkly-v9.8.0: launchdarkly-server-sdk~=9.8.0 + launchdarkly-latest: launchdarkly-server-sdk + # OpenTelemetry (OTel) opentelemetry: opentelemetry-distro From 77d4055101de38ff3f9405b031bd90129f366b82 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 11:48:50 -0500 Subject: [PATCH 20/29] Fix name --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 57f28832d8..575ffee431 100644 --- a/tox.ini +++ b/tox.ini @@ -168,7 +168,7 @@ envlist = {py3.9,py3.11,py3.12}-langchain-notiktoken # LaunchDarkly - {py3.8,py3.9,py3.10,py3.11,py3.12}-launchdarkly-v9.8 + {py3.8,py3.9,py3.10,py3.11,py3.12}-launchdarkly-v9.8.0 {py3.8,py3.9,py3.10,py3.11,py3.12}-launchdarkly-latest # Litestar From 8a1a20eed4c4c6856f6142d632ed2de6d4e6911f Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 11:50:53 -0500 Subject: [PATCH 21/29] Remove duplicate definition --- tox.ini | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index 575ffee431..d3f7b64355 100644 --- a/tox.ini +++ b/tox.ini @@ -167,10 +167,6 @@ envlist = {py3.9,py3.11,py3.12}-langchain-latest {py3.9,py3.11,py3.12}-langchain-notiktoken - # LaunchDarkly - {py3.8,py3.9,py3.10,py3.11,py3.12}-launchdarkly-v9.8.0 - {py3.8,py3.9,py3.10,py3.11,py3.12}-launchdarkly-latest - # Litestar # litestar 2.0.0 is the earliest version that supports Python < 3.12 {py3.8,py3.11}-litestar-v{2.0} @@ -193,8 +189,8 @@ envlist = {py3.8,py3.12,py3.13}-openfeature-latest # LaunchDarkly - {py3.8,py3.12,py3.13}-launchdarkly-v9.8.0 - {py3.8,py3.12,py3.13}-launchdarkly-latest + {py3.8,py3.9,py3.11,py3.12,py3.12,py3.13}-launchdarkly + {py3.8,py3.9,py3.11,py3.12,py3.12,py3.13}-launchdarkly-latest # OpenTelemetry (OTel) {py3.7,py3.9,py3.12,py3.13}-opentelemetry @@ -560,7 +556,7 @@ deps = openfeature-latest: openfeature-sdk # LaunchDarkly - launchdarkly-v9.8.0: launchdarkly-server-sdk~=9.8.0 + launchdarkly: launchdarkly-server-sdk~=9.8.0 launchdarkly-latest: launchdarkly-server-sdk # OpenTelemetry (OTel) From 2dab8c3a96833064a7b3e11798e9621212c2a0c5 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 12:16:55 -0500 Subject: [PATCH 22/29] Remove another dupe and change naming --- tox.ini | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index d3f7b64355..b98207f74e 100644 --- a/tox.ini +++ b/tox.ini @@ -189,7 +189,7 @@ envlist = {py3.8,py3.12,py3.13}-openfeature-latest # LaunchDarkly - {py3.8,py3.9,py3.11,py3.12,py3.12,py3.13}-launchdarkly + {py3.8,py3.9,py3.11,py3.12,py3.12,py3.13}-launchdarkly-v9.8.0 {py3.8,py3.9,py3.11,py3.12,py3.12,py3.13}-launchdarkly-latest # OpenTelemetry (OTel) @@ -525,10 +525,6 @@ deps = langchain-notiktoken: langchain-openai langchain-notiktoken: openai>=1.6.1 - # LaunchDarkly - launchdarkly-v9.8: launchdarkly-server-sdk~=9.8.0 - launchdarkly-latest: launchdarkly-server-sdk - # Litestar litestar: pytest-asyncio litestar: python-multipart @@ -556,7 +552,7 @@ deps = openfeature-latest: openfeature-sdk # LaunchDarkly - launchdarkly: launchdarkly-server-sdk~=9.8.0 + launchdarkly-v9.8.0: launchdarkly-server-sdk~=9.8.0 launchdarkly-latest: launchdarkly-server-sdk # OpenTelemetry (OTel) From c97e102237452f16cf0f818326e750e62a5e4ec5 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 12:29:20 -0500 Subject: [PATCH 23/29] Restrict versions --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index b98207f74e..cd4dca2481 100644 --- a/tox.ini +++ b/tox.ini @@ -189,8 +189,8 @@ envlist = {py3.8,py3.12,py3.13}-openfeature-latest # LaunchDarkly - {py3.8,py3.9,py3.11,py3.12,py3.12,py3.13}-launchdarkly-v9.8.0 - {py3.8,py3.9,py3.11,py3.12,py3.12,py3.13}-launchdarkly-latest + {py3.8,py3.12,py3.13}-launchdarkly-v9.8.0 + {py3.8,py3.12,py3.13}-launchdarkly-latest # OpenTelemetry (OTel) {py3.7,py3.9,py3.12,py3.13}-opentelemetry From ead840f326560b0ca5eb35bd1563b331d62ef00f Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 13:59:31 -0500 Subject: [PATCH 24/29] Remove integration init --- sentry_sdk/integrations/launchdarkly.py | 15 ++---- .../launchdarkly/test_launchdarkly.py | 48 ++++--------------- 2 files changed, 12 insertions(+), 51 deletions(-) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index 9e00e12ede..1399b7b4ed 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -9,7 +9,6 @@ from ldclient.hook import Hook, Metadata if TYPE_CHECKING: - from ldclient import LDClient from ldclient.hook import EvaluationSeriesContext from ldclient.evaluation import EvaluationDetail @@ -21,14 +20,11 @@ class LaunchDarklyIntegration(Integration): identifier = "launchdarkly" - def __init__(self, ld_client=None): - # type: (LDClient | None) -> None - """ - :param client: An initialized LDClient instance. If a client is not provided, this - integration will attempt to use the shared global instance. - """ + @staticmethod + def setup_once(): + # type: () -> None try: - client = ld_client or ldclient.get() + client = ldclient.get() except Exception as exc: raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) @@ -38,9 +34,6 @@ def __init__(self, ld_client=None): # Register the flag collection hook with the LD client. client.add_hook(LaunchDarklyHook()) - @staticmethod - def setup_once(): - # type: () -> None scope = sentry_sdk.get_current_scope() scope.add_error_processor(flag_error_processor) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index acbe764104..8051c1ad26 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -4,31 +4,20 @@ import ldclient import sentry_sdk -import pytest -from ldclient import LDClient from ldclient.config import Config from ldclient.context import Context from ldclient.integrations.test_data import TestData -from sentry_sdk.integrations import DidNotEnable from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration -@pytest.mark.parametrize( - "use_global_client", - (False, True), -) -def test_launchdarkly_integration(sentry_init, use_global_client): +def test_launchdarkly_integration(sentry_init): td = TestData.data_source() config = Config("sdk-key", update_processor_class=td) - 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)]) + ldclient.set_config(config) + client = ldclient.get() + sentry_init(integrations=[LaunchDarklyIntegration()]) # Set test values td.update(td.flag("hello").variation_for_all(True)) @@ -48,8 +37,8 @@ def test_launchdarkly_integration(sentry_init, use_global_client): def test_launchdarkly_integration_threaded(sentry_init): td = TestData.data_source() - client = LDClient(config=Config("sdk-key", update_processor_class=td)) - sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)]) + client = ldclient.get() + sentry_init(integrations=[LaunchDarklyIntegration()]) context = Context.create("user1") def task(flag_key): @@ -74,8 +63,8 @@ def task(flag_key): def test_launchdarkly_integration_asyncio(sentry_init): """Assert concurrently evaluated flags do not pollute one another.""" td = TestData.data_source() - client = LDClient(config=Config("sdk-key", update_processor_class=td)) - sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)]) + client = ldclient.get() + sentry_init(integrations=[LaunchDarklyIntegration()]) context = Context.create("user1") async def task(flag_key): @@ -93,24 +82,3 @@ async def runner(): results = asyncio.run(runner()).result() assert results[0] == ["hello", "world"] assert results[1] == ["hello", "other"] - - -def test_launchdarkly_integration_did_not_enable(monkeypatch): - # Client is not passed in and set_config wasn't called. - # TODO: Bad practice to access internals like this. We can skip this test, or remove this - # case entirely (force user to pass in a client instance). - ldclient._reset_client() - try: - ldclient.__lock.lock() - ldclient.__config = None - finally: - ldclient.__lock.unlock() - - with pytest.raises(DidNotEnable): - LaunchDarklyIntegration() - - # Client not initialized. - client = LDClient(config=Config("sdk-key")) - monkeypatch.setattr(client, "is_initialized", lambda: False) - with pytest.raises(DidNotEnable): - LaunchDarklyIntegration(ld_client=client) From bb678c237003dd5efecd607166fb07ef27039b56 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 14:04:08 -0500 Subject: [PATCH 25/29] Rename extras_require for launchdarkly --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9e2deb4fed..e5e0c8eaa4 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,7 @@ def get_file_text(file_name): "huey": ["huey>=2"], "huggingface_hub": ["huggingface_hub>=0.22"], "langchain": ["langchain>=0.0.210"], - "ldclient": ["launchdarkly-server-sdk>=9.8.0"], + "launchdarkly": ["launchdarkly-server-sdk>=9.8.0"], "litestar": ["litestar>=2.0.0"], "loguru": ["loguru>=0.5"], "openai": ["openai>=1.0.0", "tiktoken>=0.3.0"], From 08289c294ad8ab6e88d5762cbb4b66ae3a0334c0 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 14:34:32 -0500 Subject: [PATCH 26/29] Try resetting the client --- tests/integrations/launchdarkly/test_launchdarkly.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 8051c1ad26..331ce1f46f 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -34,6 +34,8 @@ def test_launchdarkly_integration(sentry_init): {"flag": "other", "result": False}, ] + ldclient._reset_client() + def test_launchdarkly_integration_threaded(sentry_init): td = TestData.data_source() @@ -59,6 +61,8 @@ def task(flag_key): assert results[0] == ["hello", "world"] assert results[1] == ["hello", "other"] + ldclient._reset_client() + def test_launchdarkly_integration_asyncio(sentry_init): """Assert concurrently evaluated flags do not pollute one another.""" @@ -82,3 +86,5 @@ async def runner(): results = asyncio.run(runner()).result() assert results[0] == ["hello", "world"] assert results[1] == ["hello", "other"] + + ldclient._reset_client() From a3d90bd6567d8f59c818d98be4c6467b91cee7d8 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 15:06:45 -0500 Subject: [PATCH 27/29] Remove launchdarkly from testing requirements --- requirements-testing.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index fe9b205b19..dfbd821845 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -14,4 +14,3 @@ socksio httpcore[http2] setuptools Brotli -launchdarkly-server-sdk From 711fe5546bea3067837d593fbe9ec9ac89284c2f Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 15:16:22 -0500 Subject: [PATCH 28/29] Revert "Remove integration init" This reverts commit ead840f326560b0ca5eb35bd1563b331d62ef00f. --- sentry_sdk/integrations/launchdarkly.py | 15 ++++-- .../launchdarkly/test_launchdarkly.py | 46 +++++++++++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index 1399b7b4ed..9e00e12ede 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -9,6 +9,7 @@ from ldclient.hook import Hook, Metadata if TYPE_CHECKING: + from ldclient import LDClient from ldclient.hook import EvaluationSeriesContext from ldclient.evaluation import EvaluationDetail @@ -20,11 +21,14 @@ class LaunchDarklyIntegration(Integration): identifier = "launchdarkly" - @staticmethod - def setup_once(): - # type: () -> None + def __init__(self, ld_client=None): + # type: (LDClient | None) -> None + """ + :param client: An initialized LDClient instance. If a client is not provided, this + integration will attempt to use the shared global instance. + """ try: - client = ldclient.get() + client = ld_client or ldclient.get() except Exception as exc: raise DidNotEnable("Error getting LaunchDarkly client. " + repr(exc)) @@ -34,6 +38,9 @@ def setup_once(): # Register the flag collection hook with the LD client. client.add_hook(LaunchDarklyHook()) + @staticmethod + def setup_once(): + # type: () -> None scope = sentry_sdk.get_current_scope() scope.add_error_processor(flag_error_processor) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 331ce1f46f..c058a89525 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -4,20 +4,31 @@ import ldclient import sentry_sdk +import pytest +from ldclient import LDClient from ldclient.config import Config from ldclient.context import Context from ldclient.integrations.test_data import TestData +from sentry_sdk.integrations import DidNotEnable from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration -def test_launchdarkly_integration(sentry_init): +@pytest.mark.parametrize( + "use_global_client", + (False, True), +) +def test_launchdarkly_integration(sentry_init, use_global_client): td = TestData.data_source() config = Config("sdk-key", update_processor_class=td) - ldclient.set_config(config) - client = ldclient.get() - sentry_init(integrations=[LaunchDarklyIntegration()]) + 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)]) # Set test values td.update(td.flag("hello").variation_for_all(True)) @@ -39,8 +50,8 @@ def test_launchdarkly_integration(sentry_init): def test_launchdarkly_integration_threaded(sentry_init): td = TestData.data_source() - client = ldclient.get() - sentry_init(integrations=[LaunchDarklyIntegration()]) + client = LDClient(config=Config("sdk-key", update_processor_class=td)) + sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)]) context = Context.create("user1") def task(flag_key): @@ -67,8 +78,8 @@ def task(flag_key): def test_launchdarkly_integration_asyncio(sentry_init): """Assert concurrently evaluated flags do not pollute one another.""" td = TestData.data_source() - client = ldclient.get() - sentry_init(integrations=[LaunchDarklyIntegration()]) + client = LDClient(config=Config("sdk-key", update_processor_class=td)) + sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)]) context = Context.create("user1") async def task(flag_key): @@ -87,4 +98,23 @@ async def runner(): assert results[0] == ["hello", "world"] assert results[1] == ["hello", "other"] + +def test_launchdarkly_integration_did_not_enable(monkeypatch): + # Client is not passed in and set_config wasn't called. + # TODO: Bad practice to access internals like this. We can skip this test, or remove this + # case entirely (force user to pass in a client instance). ldclient._reset_client() + try: + ldclient.__lock.lock() + ldclient.__config = None + finally: + ldclient.__lock.unlock() + + with pytest.raises(DidNotEnable): + LaunchDarklyIntegration() + + # Client not initialized. + client = LDClient(config=Config("sdk-key")) + monkeypatch.setattr(client, "is_initialized", lambda: False) + with pytest.raises(DidNotEnable): + LaunchDarklyIntegration(ld_client=client) From 5218c7aa02caff8c1769f81e2a8864dbb4f4a6a3 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Wed, 30 Oct 2024 15:18:58 -0500 Subject: [PATCH 29/29] Remove client reset --- tests/integrations/launchdarkly/test_launchdarkly.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index c058a89525..acbe764104 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -45,8 +45,6 @@ def test_launchdarkly_integration(sentry_init, use_global_client): {"flag": "other", "result": False}, ] - ldclient._reset_client() - def test_launchdarkly_integration_threaded(sentry_init): td = TestData.data_source() @@ -72,8 +70,6 @@ def task(flag_key): assert results[0] == ["hello", "world"] assert results[1] == ["hello", "other"] - ldclient._reset_client() - def test_launchdarkly_integration_asyncio(sentry_init): """Assert concurrently evaluated flags do not pollute one another."""