From 7f73c9edcf87b95163437a7aff3a7ed828ec11d9 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 7 Jan 2025 13:38:12 +0100 Subject: [PATCH 1/8] Update test matrix for Sanic (#3904) Fixes the failing test suite. --- .github/workflows/test-integrations-web-2.yml | 2 +- tox.ini | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-integrations-web-2.yml b/.github/workflows/test-integrations-web-2.yml index f1fbec6c67..39c1eba535 100644 --- a/.github/workflows/test-integrations-web-2.yml +++ b/.github/workflows/test-integrations-web-2.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6","3.7","3.8","3.11","3.12","3.13"] + python-version: ["3.6","3.7","3.8","3.9","3.11","3.12","3.13"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 diff --git a/tox.ini b/tox.ini index 717ea62141..37273b2a35 100644 --- a/tox.ini +++ b/tox.ini @@ -247,9 +247,8 @@ envlist = # Sanic {py3.6,py3.7}-sanic-v{0.8} {py3.6,py3.8}-sanic-v{20} - {py3.7,py3.11}-sanic-v{22} - {py3.7,py3.11}-sanic-v{23} - {py3.8,py3.11,py3.12}-sanic-latest + {py3.8,py3.11,py3.12}-sanic-v{24.6} + {py3.9,py3.12,py3.13}-sanic-latest # Spark {py3.8,py3.10,py3.11}-spark-v{3.1,3.3,3.5} @@ -652,13 +651,12 @@ deps = # Sanic sanic: websockets<11.0 sanic: aiohttp - sanic-v{22,23}: sanic_testing + sanic-v{24.6}: sanic_testing sanic-latest: sanic_testing {py3.6}-sanic: aiocontextvars==0.2.1 sanic-v0.8: sanic~=0.8.0 sanic-v20: sanic~=20.0 - sanic-v22: sanic~=22.0 - sanic-v23: sanic~=23.0 + sanic-v24.6: sanic~=24.6.0 sanic-latest: sanic # Spark From 8fa6d3d814c76faf72098e4f4ba2d2207e87f5b9 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Tue, 7 Jan 2025 07:12:47 -0600 Subject: [PATCH 2/8] =?UTF-8?q?Revert=20"ref(flags):=20register=20LD=20hoo?= =?UTF-8?q?k=20in=20setup=20instead=20of=20init,=20and=20don't=20chec?= =?UTF-8?q?=E2=80=A6"=20(#3900)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mutating a class attribute on `__init__` violates encapsulation and will lead to strange errors. We need to rethink how we want to implement this before we merge any code. A simple reproduction of the issue: ```python >>> class X: ... y = 0 ... def __init__(self, z): ... self.__class__.y = z ... >>> a = X(1) >>> b = X(2) >>> X.y 2 >>> a.y 2 >>> b.y 2 ``` Reverts getsentry/sentry-python#3890 This reverts commit c3516db643af20396ea981393431646f1a3ef123. Co-authored-by: Anton Pirker --- sentry_sdk/integrations/launchdarkly.py | 14 ++++++------- .../launchdarkly/test_launchdarkly.py | 21 ++++++++++--------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index 066464cc22..a9eef9e1a9 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -20,7 +20,6 @@ class LaunchDarklyIntegration(Integration): identifier = "launchdarkly" - _ld_client = None # type: LDClient | None def __init__(self, ld_client=None): # type: (LDClient | None) -> None @@ -28,19 +27,20 @@ def __init__(self, ld_client=None): :param client: An initialized LDClient instance. If a client is not provided, this integration will attempt to use the shared global instance. """ - self.__class__._ld_client = ld_client - - @staticmethod - def setup_once(): - # type: () -> None try: - client = LaunchDarklyIntegration._ld_client or ldclient.get() + 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.") + # 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 9b2bbb6b86..20566ce09a 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -183,14 +183,10 @@ async def runner(): } -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. - """ - +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() @@ -198,6 +194,11 @@ def test_launchdarkly_integration_did_not_enable(sentry_init, uninstall_integrat finally: ldclient.__lock.unlock() - uninstall_integration(LaunchDarklyIntegration.identifier) with pytest.raises(DidNotEnable): - sentry_init(integrations=[LaunchDarklyIntegration()]) + 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 bf65ede42172dd9bc6718b69e3ea9a9dd417c93d Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 7 Jan 2025 05:27:08 -0800 Subject: [PATCH 3/8] ref(flags): Beter naming for featureflags module and identifier (#3902) Co-authored-by: Anton Pirker --- sentry_sdk/integrations/{featureflags.py => feature_flags.py} | 4 ++-- .../integrations/{featureflags => feature_flags}/__init__.py | 0 .../test_feature_flags.py} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename sentry_sdk/integrations/{featureflags.py => feature_flags.py} (91%) rename tests/integrations/{featureflags => feature_flags}/__init__.py (100%) rename tests/integrations/{featureflags/test_featureflags.py => feature_flags/test_feature_flags.py} (98%) diff --git a/sentry_sdk/integrations/featureflags.py b/sentry_sdk/integrations/feature_flags.py similarity index 91% rename from sentry_sdk/integrations/featureflags.py rename to sentry_sdk/integrations/feature_flags.py index 46947eec72..2aeabffbfa 100644 --- a/sentry_sdk/integrations/featureflags.py +++ b/sentry_sdk/integrations/feature_flags.py @@ -16,7 +16,7 @@ class FeatureFlagsIntegration(Integration): @example ``` import sentry_sdk - from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration, add_feature_flag + from sentry_sdk.integrations.feature_flags import FeatureFlagsIntegration, add_feature_flag sentry_sdk.init(dsn="my_dsn", integrations=[FeatureFlagsIntegration()]); @@ -25,7 +25,7 @@ class FeatureFlagsIntegration(Integration): ``` """ - identifier = "featureflags" + identifier = "feature_flags" @staticmethod def setup_once(): diff --git a/tests/integrations/featureflags/__init__.py b/tests/integrations/feature_flags/__init__.py similarity index 100% rename from tests/integrations/featureflags/__init__.py rename to tests/integrations/feature_flags/__init__.py diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/feature_flags/test_feature_flags.py similarity index 98% rename from tests/integrations/featureflags/test_featureflags.py rename to tests/integrations/feature_flags/test_feature_flags.py index 539e910607..ca6ac16949 100644 --- a/tests/integrations/featureflags/test_featureflags.py +++ b/tests/integrations/feature_flags/test_feature_flags.py @@ -4,7 +4,7 @@ import pytest import sentry_sdk -from sentry_sdk.integrations.featureflags import ( +from sentry_sdk.integrations.feature_flags import ( FeatureFlagsIntegration, add_feature_flag, ) From c6a89d64db965fe0ece6de10df38ab936af8f5e4 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 7 Jan 2025 06:17:03 -0800 Subject: [PATCH 4/8] feat(flags): add Unleash feature flagging integration (#3888) Adds an integration for tracking flag evaluations from [Unleash](https://www.getunleash.io/) customers. Implementation Unleash has no native support for evaluation hooks/listeners, unless the user opts in for each flag. Therefore we decided to patch the `is_enabled` and `get_variant` methods on the `UnleashClient` class. The methods are wrapped and the only side effect is writing to Sentry scope, so users shouldn't see any change in behavior. We patch one `UnleashClient` instance instead of the whole class. The reasons for this are described in - https://github.com/getsentry/sentry-python/pull/3895 It's also safer to not modify the unleash import. References - https://develop.sentry.dev/sdk/expected-features/#feature-flags - https://docs.getunleash.io/reference/sdks/python for methods we're patching/wrapping --------- Co-authored-by: Anton Pirker Co-authored-by: Colton Allen --- .github/workflows/test-integrations-misc.yml | 8 + requirements-linting.txt | 1 + .../split_tox_gh_actions.py | 1 + sentry_sdk/integrations/unleash.py | 55 ++++ setup.py | 1 + tests/conftest.py | 1 + tests/integrations/unleash/__init__.py | 3 + tests/integrations/unleash/test_unleash.py | 308 ++++++++++++++++++ tests/integrations/unleash/testutils.py | 77 +++++ tox.ini | 17 +- 10 files changed, 468 insertions(+), 4 deletions(-) create mode 100644 sentry_sdk/integrations/unleash.py create mode 100644 tests/integrations/unleash/__init__.py create mode 100644 tests/integrations/unleash/test_unleash.py create mode 100644 tests/integrations/unleash/testutils.py diff --git a/.github/workflows/test-integrations-misc.yml b/.github/workflows/test-integrations-misc.yml index 79b7ba020d..d524863423 100644 --- a/.github/workflows/test-integrations-misc.yml +++ b/.github/workflows/test-integrations-misc.yml @@ -79,6 +79,10 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh "py${{ matrix.python-version }}-typer-latest" + - name: Test unleash latest + run: | + set -x # print commands that are executed + ./scripts/runtox.sh "py${{ matrix.python-version }}-unleash-latest" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | @@ -163,6 +167,10 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-typer" + - name: Test unleash pinned + run: | + set -x # print commands that are executed + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-unleash" - name: Generate coverage XML (Python 3.6) if: ${{ !cancelled() && matrix.python-version == '3.6' }} run: | diff --git a/requirements-linting.txt b/requirements-linting.txt index c3f39ecd1f..4227acc26a 100644 --- a/requirements-linting.txt +++ b/requirements-linting.txt @@ -17,4 +17,5 @@ pre-commit # local linting httpcore openfeature-sdk launchdarkly-server-sdk +UnleashClient typer 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 1b53093c5e..743677daf4 100755 --- a/scripts/split_tox_gh_actions/split_tox_gh_actions.py +++ b/scripts/split_tox_gh_actions/split_tox_gh_actions.py @@ -133,6 +133,7 @@ "pure_eval", "trytond", "typer", + "unleash", ], } diff --git a/sentry_sdk/integrations/unleash.py b/sentry_sdk/integrations/unleash.py new file mode 100644 index 0000000000..33b0a4b9dc --- /dev/null +++ b/sentry_sdk/integrations/unleash.py @@ -0,0 +1,55 @@ +from functools import wraps +from typing import Any + +import sentry_sdk +from sentry_sdk.flag_utils import flag_error_processor +from sentry_sdk.integrations import Integration, DidNotEnable + +try: + from UnleashClient import UnleashClient +except ImportError: + raise DidNotEnable("UnleashClient is not installed") + + +class UnleashIntegration(Integration): + identifier = "unleash" + + @staticmethod + def setup_once(): + # type: () -> None + # Wrap and patch evaluation methods (instance methods) + old_is_enabled = UnleashClient.is_enabled + old_get_variant = UnleashClient.get_variant + + @wraps(old_is_enabled) + def sentry_is_enabled(self, feature, *args, **kwargs): + # type: (UnleashClient, str, *Any, **Any) -> Any + enabled = old_is_enabled(self, feature, *args, **kwargs) + + # 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) + + return enabled + + @wraps(old_get_variant) + def sentry_get_variant(self, feature, *args, **kwargs): + # type: (UnleashClient, str, *Any, **Any) -> Any + variant = old_get_variant(self, feature, *args, **kwargs) + enabled = variant.get("enabled", False) + + # Payloads are not always used as the feature's value for application logic. They + # may be used for metrics or debugging context instead. Therefore, we treat every + # variant as a boolean toggle, using the `enabled` field. + flags = sentry_sdk.get_current_scope().flags + flags.set(feature, enabled) + + return variant + + UnleashClient.is_enabled = sentry_is_enabled # type: ignore + UnleashClient.get_variant = sentry_get_variant # type: ignore + + # Error processor + scope = sentry_sdk.get_current_scope() + scope.add_error_processor(flag_error_processor) diff --git a/setup.py b/setup.py index da3adcab42..9e24d59d21 100644 --- a/setup.py +++ b/setup.py @@ -80,6 +80,7 @@ def get_file_text(file_name): "starlette": ["starlette>=0.19.1"], "starlite": ["starlite>=1.48"], "tornado": ["tornado>=6"], + "unleash": ["UnleashClient>=6.0.1"], }, entry_points={ "opentelemetry_propagator": [ diff --git a/tests/conftest.py b/tests/conftest.py index c0383d94b7..b5ab7aa804 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ import pytest import jsonschema + try: import gevent except ImportError: diff --git a/tests/integrations/unleash/__init__.py b/tests/integrations/unleash/__init__.py new file mode 100644 index 0000000000..33cff3e65a --- /dev/null +++ b/tests/integrations/unleash/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("UnleashClient") diff --git a/tests/integrations/unleash/test_unleash.py b/tests/integrations/unleash/test_unleash.py new file mode 100644 index 0000000000..9a7a3f57bd --- /dev/null +++ b/tests/integrations/unleash/test_unleash.py @@ -0,0 +1,308 @@ +import concurrent.futures as cf +import sys +from random import random +from unittest import mock +from UnleashClient import UnleashClient + +import pytest + +import sentry_sdk +from sentry_sdk.integrations.unleash import UnleashIntegration +from tests.integrations.unleash.testutils import mock_unleash_client + + +def test_is_enabled(sentry_init, capture_events, uninstall_integration): + uninstall_integration(UnleashIntegration.identifier) + + with mock_unleash_client(): + client = UnleashClient() + sentry_init(integrations=[UnleashIntegration()]) + client.is_enabled("hello") + client.is_enabled("world") + client.is_enabled("other") + + events = capture_events() + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 1 + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": False}, + {"flag": "other", "result": False}, + ] + } + + +def test_get_variant(sentry_init, capture_events, uninstall_integration): + uninstall_integration(UnleashIntegration.identifier) + + with mock_unleash_client(): + client = UnleashClient() + sentry_init(integrations=[UnleashIntegration()]) # type: ignore + client.get_variant("no_payload_feature") + client.get_variant("string_feature") + client.get_variant("json_feature") + client.get_variant("csv_feature") + client.get_variant("number_feature") + client.get_variant("unknown_feature") + + events = capture_events() + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 1 + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "no_payload_feature", "result": True}, + {"flag": "string_feature", "result": True}, + {"flag": "json_feature", "result": True}, + {"flag": "csv_feature", "result": True}, + {"flag": "number_feature", "result": True}, + {"flag": "unknown_feature", "result": False}, + ] + } + + +def test_is_enabled_threaded(sentry_init, capture_events, uninstall_integration): + uninstall_integration(UnleashIntegration.identifier) + + with mock_unleash_client(): + client = UnleashClient() + sentry_init(integrations=[UnleashIntegration()]) # type: ignore + events = capture_events() + + def task(flag_key): + # Creates a new isolation scope for the thread. + # This means the evaluations in each task are captured separately. + with sentry_sdk.isolation_scope(): + client.is_enabled(flag_key) + # use a tag to identify to identify events later on + sentry_sdk.set_tag("task_id", flag_key) + sentry_sdk.capture_exception(Exception("something wrong!")) + + # Capture an eval before we split isolation scopes. + client.is_enabled("hello") + + with cf.ThreadPoolExecutor(max_workers=2) as pool: + pool.map(task, ["world", "other"]) + + # Capture error in original scope + sentry_sdk.set_tag("task_id", "0") + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 3 + events.sort(key=lambda e: e["tags"]["task_id"]) + + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + ] + } + assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "other", "result": False}, + ] + } + assert events[2]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": False}, + ] + } + + +def test_get_variant_threaded(sentry_init, capture_events, uninstall_integration): + uninstall_integration(UnleashIntegration.identifier) + + with mock_unleash_client(): + client = UnleashClient() + sentry_init(integrations=[UnleashIntegration()]) # type: ignore + events = capture_events() + + def task(flag_key): + # Creates a new isolation scope for the thread. + # This means the evaluations in each task are captured separately. + with sentry_sdk.isolation_scope(): + client.get_variant(flag_key) + # use a tag to identify to identify events later on + sentry_sdk.set_tag("task_id", flag_key) + sentry_sdk.capture_exception(Exception("something wrong!")) + + # Capture an eval before we split isolation scopes. + client.get_variant("hello") + + with cf.ThreadPoolExecutor(max_workers=2) as pool: + pool.map(task, ["no_payload_feature", "other"]) + + # Capture error in original scope + sentry_sdk.set_tag("task_id", "0") + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 3 + events.sort(key=lambda e: e["tags"]["task_id"]) + + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + ] + } + assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "no_payload_feature", "result": True}, + ] + } + assert events[2]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "other", "result": False}, + ] + } + + +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +def test_is_enabled_asyncio(sentry_init, capture_events, uninstall_integration): + asyncio = pytest.importorskip("asyncio") + uninstall_integration(UnleashIntegration.identifier) + + with mock_unleash_client(): + client = UnleashClient() + sentry_init(integrations=[UnleashIntegration()]) # type: ignore + events = capture_events() + + async def task(flag_key): + with sentry_sdk.isolation_scope(): + client.is_enabled(flag_key) + # use a tag to identify to identify events later on + sentry_sdk.set_tag("task_id", flag_key) + sentry_sdk.capture_exception(Exception("something wrong!")) + + async def runner(): + return asyncio.gather(task("world"), task("other")) + + # Capture an eval before we split isolation scopes. + client.is_enabled("hello") + + asyncio.run(runner()) + + # Capture error in original scope + sentry_sdk.set_tag("task_id", "0") + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 3 + events.sort(key=lambda e: e["tags"]["task_id"]) + + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + ] + } + assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "other", "result": False}, + ] + } + assert events[2]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": False}, + ] + } + + +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +def test_get_variant_asyncio(sentry_init, capture_events, uninstall_integration): + asyncio = pytest.importorskip("asyncio") + + uninstall_integration(UnleashIntegration.identifier) + + with mock_unleash_client(): + client = UnleashClient() + sentry_init(integrations=[UnleashIntegration()]) # type: ignore + events = capture_events() + + async def task(flag_key): + with sentry_sdk.isolation_scope(): + client.get_variant(flag_key) + # use a tag to identify to identify events later on + sentry_sdk.set_tag("task_id", flag_key) + sentry_sdk.capture_exception(Exception("something wrong!")) + + async def runner(): + return asyncio.gather(task("no_payload_feature"), task("other")) + + # Capture an eval before we split isolation scopes. + client.get_variant("hello") + + asyncio.run(runner()) + + # Capture error in original scope + sentry_sdk.set_tag("task_id", "0") + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 3 + events.sort(key=lambda e: e["tags"]["task_id"]) + + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + ] + } + assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "no_payload_feature", "result": True}, + ] + } + assert events[2]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "other", "result": False}, + ] + } + + +def test_wraps_original(sentry_init, uninstall_integration): + with mock_unleash_client(): + client = UnleashClient() + + mock_is_enabled = mock.Mock(return_value=random() < 0.5) + mock_get_variant = mock.Mock(return_value={"enabled": random() < 0.5}) + client.is_enabled = mock_is_enabled + client.get_variant = mock_get_variant + + uninstall_integration(UnleashIntegration.identifier) + sentry_init(integrations=[UnleashIntegration()]) # type: ignore + + res = client.is_enabled("test-flag", "arg", kwarg=1) + assert res == mock_is_enabled.return_value + assert mock_is_enabled.call_args == ( + ("test-flag", "arg"), + {"kwarg": 1}, + ) + + res = client.get_variant("test-flag", "arg", kwarg=1) + assert res == mock_get_variant.return_value + assert mock_get_variant.call_args == ( + ("test-flag", "arg"), + {"kwarg": 1}, + ) + + +def test_wrapper_attributes(sentry_init, uninstall_integration): + with mock_unleash_client(): + client = UnleashClient() # <- Returns a MockUnleashClient + + original_is_enabled = client.is_enabled + original_get_variant = client.get_variant + + uninstall_integration(UnleashIntegration.identifier) + sentry_init(integrations=[UnleashIntegration()]) # type: ignore + + # 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__ + assert client.get_variant.__name__ == "get_variant" + assert client.get_variant.__qualname__ == original_get_variant.__qualname__ diff --git a/tests/integrations/unleash/testutils.py b/tests/integrations/unleash/testutils.py new file mode 100644 index 0000000000..c424b34c3a --- /dev/null +++ b/tests/integrations/unleash/testutils.py @@ -0,0 +1,77 @@ +from contextlib import contextmanager +from UnleashClient import UnleashClient + + +@contextmanager +def mock_unleash_client(): + """ + Temporarily replaces UnleashClient's methods with mock implementations + for testing. + + This context manager swaps out UnleashClient's __init__, is_enabled, + and get_variant methods with mock versions from MockUnleashClient. + Original methods are restored when exiting the context. + + After mocking the client class the integration can be initialized. + The methods on the mock client class are overridden by the + integration and flag tracking proceeds as expected. + + Example: + with mock_unleash_client(): + client = UnleashClient() # Uses mock implementation + sentry_init(integrations=[UnleashIntegration()]) + """ + old_init = UnleashClient.__init__ + old_is_enabled = UnleashClient.is_enabled + old_get_variant = UnleashClient.get_variant + + UnleashClient.__init__ = MockUnleashClient.__init__ + UnleashClient.is_enabled = MockUnleashClient.is_enabled + UnleashClient.get_variant = MockUnleashClient.get_variant + + yield + + UnleashClient.__init__ = old_init + UnleashClient.is_enabled = old_is_enabled + UnleashClient.get_variant = old_get_variant + + +class MockUnleashClient: + + def __init__(self, *a, **kw): + self.features = { + "hello": True, + "world": False, + } + + self.feature_to_variant = { + "string_feature": { + "name": "variant1", + "enabled": True, + "payload": {"type": "string", "value": "val1"}, + }, + "json_feature": { + "name": "variant1", + "enabled": True, + "payload": {"type": "json", "value": '{"key1": 0.53}'}, + }, + "number_feature": { + "name": "variant1", + "enabled": True, + "payload": {"type": "number", "value": "134.5"}, + }, + "csv_feature": { + "name": "variant1", + "enabled": True, + "payload": {"type": "csv", "value": "abc 123\ncsbq 94"}, + }, + "no_payload_feature": {"name": "variant1", "enabled": True}, + } + + self.disabled_variant = {"name": "disabled", "enabled": False} + + def is_enabled(self, feature, *a, **kw): + return self.features.get(feature, False) + + def get_variant(self, feature, *a, **kw): + return self.feature_to_variant.get(feature, self.disabled_variant) diff --git a/tox.ini b/tox.ini index 37273b2a35..95c09a573e 100644 --- a/tox.ini +++ b/tox.ini @@ -168,6 +168,10 @@ envlist = {py3.9,py3.11,py3.12}-langchain-latest {py3.9,py3.11,py3.12}-langchain-notiktoken + # LaunchDarkly + {py3.8,py3.12,py3.13}-launchdarkly-v9.8.0 + {py3.8,py3.12,py3.13}-launchdarkly-latest + # Litestar {py3.8,py3.11}-litestar-v{2.0} {py3.8,py3.11,py3.12}-litestar-v{2.6} @@ -189,10 +193,6 @@ 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 @@ -290,6 +290,10 @@ envlist = {py3.7,py3.12,py3.13}-typer-v{0.15} {py3.7,py3.12,py3.13}-typer-latest + # Unleash + {py3.8,py3.12,py3.13}-unleash-v6.0.1 + {py3.8,py3.12,py3.13}-unleash-latest + [testenv] deps = # if you change requirements-testing.txt and your change is not being reflected @@ -571,6 +575,10 @@ deps = launchdarkly-v9.8.0: launchdarkly-server-sdk~=9.8.0 launchdarkly-latest: launchdarkly-server-sdk + # Unleash + unleash-v6.0.1: UnleashClient~=6.0.1 + unleash-latest: UnleashClient + # OpenTelemetry (OTel) opentelemetry: opentelemetry-distro @@ -793,6 +801,7 @@ setenv = tornado: TESTPATH=tests/integrations/tornado trytond: TESTPATH=tests/integrations/trytond typer: TESTPATH=tests/integrations/typer + unleash: TESTPATH=tests/integrations/unleash socket: TESTPATH=tests/integrations/socket passenv = From 4432e26a45873080d4eaf20e769bc82f026851bb Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 9 Jan 2025 14:28:39 +0100 Subject: [PATCH 5/8] Small contribution docs update (#3909) --- Thank you for contributing to `sentry-python`! Please add tests to validate your changes, and lint your code using `tox -e linters`. Running the test suite on your PR might require maintainer approval. The AWS Lambda tests additionally require a maintainer to add a special label, and they will fail until this label is added. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f4839f8d7..085dbd6075 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -126,7 +126,7 @@ pytest -rs tests/integrations/flask/ # Replace "flask" with the specific integr ## Releasing a New Version -_(only relevant for Sentry employees)_ +_(only relevant for Python SDK core team)_ ### Prerequisites From e5558a6549630be1e7dd4f36e0bf0942d49287c3 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Thu, 9 Jan 2025 14:59:23 +0100 Subject: [PATCH 6/8] Cleanup start_transaction usages (#3884) --- sentry_sdk/integrations/aws_lambda.py | 2 +- sentry_sdk/integrations/celery/__init__.py | 2 +- sentry_sdk/integrations/gcp.py | 2 +- sentry_sdk/integrations/grpc/aio/server.py | 2 +- sentry_sdk/integrations/grpc/server.py | 2 +- sentry_sdk/integrations/tornado.py | 2 +- sentry_sdk/utils.py | 2 +- tests/integrations/aiohttp/test_aiohttp.py | 10 +++---- .../integrations/anthropic/test_anthropic.py | 18 ++++++------ tests/integrations/arq/test_arq.py | 6 ++-- tests/integrations/celery/test_celery.py | 28 +++++++++---------- .../celery/test_update_celery_task_headers.py | 4 +-- .../test_clickhouse_driver.py | 12 ++++---- tests/integrations/cohere/test_cohere.py | 12 ++++---- .../integrations/django/test_cache_module.py | 4 +-- .../integrations/django/test_db_query_data.py | 4 +-- .../huggingface_hub/test_huggingface_hub.py | 8 +++--- .../integrations/langchain/test_langchain.py | 8 +++--- tests/integrations/openai/test_openai.py | 26 ++++++++--------- .../redis/test_redis_cache_module.py | 10 +++---- .../test_redis_py_cluster_legacy.py | 8 +++--- tests/integrations/socket/test_socket.py | 8 +++--- tests/integrations/tornado/test_tornado.py | 6 ++-- tests/profiler/test_continuous_profiler.py | 10 +++---- tests/profiler/test_transaction_profiler.py | 10 +++---- tests/test_ai_monitoring.py | 8 +++--- tests/test_basics.py | 14 +++++----- tests/test_scope.py | 4 +-- tests/test_scrubber.py | 4 +-- 29 files changed, 118 insertions(+), 118 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index cd2b3cc417..8899cc53b2 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -160,7 +160,7 @@ def sentry_handler(aws_event, aws_context, *args, **kwargs): headers = {} with sentry_sdk.continue_trace(headers): - with sentry_sdk.start_transaction( + with sentry_sdk.start_span( op=OP.FUNCTION_AWS, name=aws_context.function_name, source=TRANSACTION_SOURCE_COMPONENT, diff --git a/sentry_sdk/integrations/celery/__init__.py b/sentry_sdk/integrations/celery/__init__.py index a943871335..43321ec89b 100644 --- a/sentry_sdk/integrations/celery/__init__.py +++ b/sentry_sdk/integrations/celery/__init__.py @@ -313,7 +313,7 @@ def _inner(*args, **kwargs): # something such as attribute access can fail. headers = args[3].get("headers") or {} with sentry_sdk.continue_trace(headers): - with sentry_sdk.start_transaction( + with sentry_sdk.start_span( op=OP.QUEUE_TASK_CELERY, name=task.name, source=TRANSACTION_SOURCE_TASK, diff --git a/sentry_sdk/integrations/gcp.py b/sentry_sdk/integrations/gcp.py index dd23ad1e0a..ec626ed699 100644 --- a/sentry_sdk/integrations/gcp.py +++ b/sentry_sdk/integrations/gcp.py @@ -87,7 +87,7 @@ def sentry_func(functionhandler, gcp_event, *args, **kwargs): headers = gcp_event.headers with sentry_sdk.continue_trace(headers): - with sentry_sdk.start_transaction( + with sentry_sdk.start_span( op=OP.FUNCTION_GCP, name=environ.get("FUNCTION_NAME", ""), source=TRANSACTION_SOURCE_COMPONENT, diff --git a/sentry_sdk/integrations/grpc/aio/server.py b/sentry_sdk/integrations/grpc/aio/server.py index 6d38e91363..4d54b0605c 100644 --- a/sentry_sdk/integrations/grpc/aio/server.py +++ b/sentry_sdk/integrations/grpc/aio/server.py @@ -45,7 +45,7 @@ async def wrapped(request, context): # What if the headers are empty? with sentry_sdk.continue_trace(dict(context.invocation_metadata())): - with sentry_sdk.start_transaction( + with sentry_sdk.start_span( op=OP.GRPC_SERVER, name=name, source=TRANSACTION_SOURCE_CUSTOM, diff --git a/sentry_sdk/integrations/grpc/server.py b/sentry_sdk/integrations/grpc/server.py index fb123c5ca4..d12b43b92b 100644 --- a/sentry_sdk/integrations/grpc/server.py +++ b/sentry_sdk/integrations/grpc/server.py @@ -39,7 +39,7 @@ def behavior(request, context): metadata = dict(context.invocation_metadata()) with sentry_sdk.continue_trace(metadata): - with sentry_sdk.start_transaction( + with sentry_sdk.start_span( op=OP.GRPC_SERVER, name=name, source=TRANSACTION_SOURCE_CUSTOM, diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index bb40fbf625..1af8950aa4 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -125,7 +125,7 @@ def _handle_request_impl(self): scope.add_event_processor(processor) with sentry_sdk.continue_trace(headers): - with sentry_sdk.start_transaction( + with sentry_sdk.start_span( op=OP.HTTP_SERVER, # Like with all other integrations, this is our # fallback transaction in case there is no route. diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 16dfd9c4fa..403ac84220 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1802,7 +1802,7 @@ def ensure_integration_enabled( ```python @ensure_integration_enabled(MyIntegration, my_function) def patch_my_function(): - with sentry_sdk.start_transaction(...): + with sentry_sdk.start_span(...): return my_function() ``` """ diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 4b491b152e..7960bc2dcc 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -15,7 +15,7 @@ HTTPUnavailableForLegalReasons, ) -from sentry_sdk import capture_message, start_transaction +from sentry_sdk import capture_message, start_span from sentry_sdk.integrations.aiohttp import AioHttpIntegration from tests.conftest import ApproxDict @@ -417,7 +417,7 @@ async def hello(request): # The aiohttp_client is instrumented so will generate the sentry-trace header and add request. # Get the sentry-trace header from the request so we can later compare with transaction events. client = await aiohttp_client(app) - with start_transaction(): + with start_span(name="request"): # Headers are only added to the span if there is an active transaction resp = await client.get("/") @@ -496,7 +496,7 @@ async def handler(request): raw_server = await aiohttp_raw_server(handler) - with start_transaction(): + with start_span(name="breadcrumb"): events = capture_events() client = await aiohttp_client(raw_server) @@ -538,7 +538,7 @@ async def handler(request): raw_server = await aiohttp_raw_server(handler) - with start_transaction( + with start_span( name="/interactions/other-dogs/new-dog", op="greeting.sniff", ) as transaction: @@ -573,7 +573,7 @@ async def handler(request): raw_server = await aiohttp_raw_server(handler) - with start_transaction( + with start_span( name="/interactions/other-dogs/new-dog", op="greeting.sniff", ) as transaction: diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index 4a7d7ed458..ece2cfe7a3 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -42,7 +42,7 @@ async def __call__(self, *args, **kwargs): except ImportError: from anthropic.types.content_block import ContentBlock as TextBlock -from sentry_sdk import start_transaction +from sentry_sdk import start_span from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations.anthropic import AnthropicIntegration @@ -90,7 +90,7 @@ def test_nonstreaming_create_message( } ] - with start_transaction(name="anthropic"): + with start_span(name="anthropic"): response = client.messages.create( max_tokens=1024, messages=messages, model="model" ) @@ -160,7 +160,7 @@ async def test_nonstreaming_create_message_async( } ] - with start_transaction(name="anthropic"): + with start_span(name="anthropic"): response = await client.messages.create( max_tokens=1024, messages=messages, model="model" ) @@ -263,7 +263,7 @@ def test_streaming_create_message( } ] - with start_transaction(name="anthropic"): + with start_span(name="anthropic"): message = client.messages.create( max_tokens=1024, messages=messages, model="model", stream=True ) @@ -368,7 +368,7 @@ async def test_streaming_create_message_async( } ] - with start_transaction(name="anthropic"): + with start_span(name="anthropic"): message = await client.messages.create( max_tokens=1024, messages=messages, model="model", stream=True ) @@ -500,7 +500,7 @@ def test_streaming_create_message_with_input_json_delta( } ] - with start_transaction(name="anthropic"): + with start_span(name="anthropic"): message = client.messages.create( max_tokens=1024, messages=messages, model="model", stream=True ) @@ -639,7 +639,7 @@ async def test_streaming_create_message_with_input_json_delta_async( } ] - with start_transaction(name="anthropic"): + with start_span(name="anthropic"): message = await client.messages.create( max_tokens=1024, messages=messages, model="model", stream=True ) @@ -736,7 +736,7 @@ def test_span_origin(sentry_init, capture_events): } ] - with start_transaction(name="anthropic"): + with start_span(name="anthropic"): client.messages.create(max_tokens=1024, messages=messages, model="model") (event,) = events @@ -763,7 +763,7 @@ async def test_span_origin_async(sentry_init, capture_events): } ] - with start_transaction(name="anthropic"): + with start_span(name="anthropic"): await client.messages.create(max_tokens=1024, messages=messages, model="model") (event,) = events diff --git a/tests/integrations/arq/test_arq.py b/tests/integrations/arq/test_arq.py index e74395e26c..0ebb97cd2b 100644 --- a/tests/integrations/arq/test_arq.py +++ b/tests/integrations/arq/test_arq.py @@ -1,7 +1,7 @@ import asyncio import pytest -from sentry_sdk import get_client, start_transaction +from sentry_sdk import get_client, start_span from sentry_sdk.integrations.arq import ArqIntegration import arq.worker @@ -292,7 +292,7 @@ async def dummy_job(_): events = capture_events() - with start_transaction() as transaction: + with start_span(name="test") as transaction: await pool.enqueue_job("dummy_job") (event,) = events @@ -343,7 +343,7 @@ async def dummy_job(_): events = capture_events() - with start_transaction(): + with start_span(name="job"): await pool.enqueue_job("dummy_job") (event,) = events diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index 1011429098..2f8de6968a 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -7,7 +7,7 @@ from celery.bin import worker import sentry_sdk -from sentry_sdk import start_transaction, get_current_span +from sentry_sdk import start_span, get_current_span from sentry_sdk.integrations.celery import ( CeleryIntegration, _wrap_task_run, @@ -126,7 +126,7 @@ def dummy_task(x, y): foo = 42 # noqa return x / y - with start_transaction(op="unit test transaction") as transaction: + with start_span(op="unit test transaction") as transaction: celery_invocation(dummy_task, 1, 2) _, expected_context = celery_invocation(dummy_task, 1, 0) @@ -195,7 +195,7 @@ def dummy_task(x, y): events = capture_events() - with start_transaction(name="submission") as transaction: + with start_span(name="submission") as transaction: celery_invocation(dummy_task, 1, 0 if task_fails else 1) if task_fails: @@ -275,11 +275,11 @@ def test_simple_no_propagation(capture_events, init_celery): def dummy_task(): 1 / 0 - with start_transaction() as transaction: + with start_span(name="task") as root_span: dummy_task.delay() (event,) = events - assert event["contexts"]["trace"]["trace_id"] != transaction.trace_id + assert event["contexts"]["trace"]["trace_id"] != root_span.trace_id assert event["transaction"] == "dummy_task" (exception,) = event["exception"]["values"] assert exception["type"] == "ZeroDivisionError" @@ -350,7 +350,7 @@ def dummy_task(self): runs.append(1) 1 / 0 - with start_transaction(name="submit_celery"): + with start_span(name="submit_celery"): # Curious: Cannot use delay() here or py2.7-celery-4.2 crashes res = dummy_task.apply_async() @@ -469,7 +469,7 @@ def __call__(self, *args, **kwargs): def dummy_task(x, y): return x / y - with start_transaction(): + with start_span(name="celery"): celery_invocation(dummy_task, 1, 0) assert not events @@ -510,7 +510,7 @@ def test_baggage_propagation(init_celery): def dummy_task(self, x, y): return _get_headers(self) - with start_transaction() as transaction: + with start_span(name="task") as root_span: result = dummy_task.apply_async( args=(1, 0), headers={"baggage": "custom=value"}, @@ -519,7 +519,7 @@ def dummy_task(self, x, y): assert sorted(result["baggage"].split(",")) == sorted( [ "sentry-release=abcdef", - "sentry-trace_id={}".format(transaction.trace_id), + "sentry-trace_id={}".format(root_span.trace_id), "sentry-environment=production", "sentry-sample_rate=1.0", "sentry-sampled=true", @@ -542,8 +542,8 @@ def dummy_task(self, message): trace_id = get_current_span().trace_id return trace_id - with start_transaction() as transaction: - transaction_trace_id = transaction.trace_id + with start_span(name="task") as root_span: + transaction_trace_id = root_span.trace_id # should propagate trace task_transaction_id = dummy_task.apply_async( @@ -710,7 +710,7 @@ def publish(*args, **kwargs): @celery.task() def task(): ... - with start_transaction(): + with start_span(name="task"): task.apply_async() (event,) = events @@ -773,7 +773,7 @@ def publish(*args, **kwargs): @celery.task() def task(): ... - with start_transaction(name="custom_transaction"): + with start_span(name="custom_transaction"): task.apply_async() (event,) = events @@ -799,7 +799,7 @@ def test_send_task_wrapped( events = capture_events() - with sentry_sdk.start_transaction(name="custom_transaction"): + with sentry_sdk.start_span(name="custom_transaction"): celery.send_task("very_creative_task_name", args=(1, 2), kwargs={"foo": "bar"}) (call,) = patched_send_task.call_args_list # We should have exactly one call diff --git a/tests/integrations/celery/test_update_celery_task_headers.py b/tests/integrations/celery/test_update_celery_task_headers.py index 705c00de58..709e49b54a 100644 --- a/tests/integrations/celery/test_update_celery_task_headers.py +++ b/tests/integrations/celery/test_update_celery_task_headers.py @@ -75,7 +75,7 @@ def test_span_with_transaction(sentry_init): headers = {} monitor_beat_tasks = False - with sentry_sdk.start_transaction(name="test_transaction") as transaction: + with sentry_sdk.start_span(name="test_transaction") as transaction: with sentry_sdk.start_span(op="test_span") as span: outgoing_headers = _update_celery_task_headers( headers, span, monitor_beat_tasks @@ -97,7 +97,7 @@ def test_span_with_transaction_custom_headers(sentry_init): "sentry-trace": SENTRY_TRACE_VALUE, } - with sentry_sdk.start_transaction(name="test_transaction") as transaction: + with sentry_sdk.start_span(name="test_transaction") as transaction: with sentry_sdk.start_span(op="test_span") as span: outgoing_headers = _update_celery_task_headers(headers, span, False) diff --git a/tests/integrations/clickhouse_driver/test_clickhouse_driver.py b/tests/integrations/clickhouse_driver/test_clickhouse_driver.py index 2c3d3c41a4..7eb0462231 100644 --- a/tests/integrations/clickhouse_driver/test_clickhouse_driver.py +++ b/tests/integrations/clickhouse_driver/test_clickhouse_driver.py @@ -8,7 +8,7 @@ import clickhouse_driver from clickhouse_driver import Client, connect -from sentry_sdk import start_transaction, capture_message +from sentry_sdk import start_span, capture_message from sentry_sdk.integrations.clickhouse_driver import ClickhouseDriverIntegration from tests.conftest import ApproxDict @@ -227,7 +227,7 @@ def test_clickhouse_client_spans( transaction_trace_id = None transaction_span_id = None - with start_transaction(name="test_clickhouse_transaction") as transaction: + with start_span(name="test_clickhouse_transaction") as transaction: transaction_trace_id = transaction.trace_id transaction_span_id = transaction.span_id @@ -360,7 +360,7 @@ def test_clickhouse_client_spans_with_pii(sentry_init, capture_events) -> None: transaction_trace_id = None transaction_span_id = None - with start_transaction(name="test_clickhouse_transaction") as transaction: + with start_span(name="test_clickhouse_transaction") as transaction: transaction_trace_id = transaction.trace_id transaction_span_id = transaction.span_id @@ -701,7 +701,7 @@ def test_clickhouse_dbapi_spans(sentry_init, capture_events, capture_envelopes) transaction_trace_id = None transaction_span_id = None - with start_transaction(name="test_clickhouse_transaction") as transaction: + with start_span(name="test_clickhouse_transaction") as transaction: transaction_trace_id = transaction.trace_id transaction_span_id = transaction.span_id @@ -835,7 +835,7 @@ def test_clickhouse_dbapi_spans_with_pii( transaction_trace_id = None transaction_span_id = None - with start_transaction(name="test_clickhouse_transaction") as transaction: + with start_span(name="test_clickhouse_transaction") as transaction: transaction_trace_id = transaction.trace_id transaction_span_id = transaction.span_id @@ -975,7 +975,7 @@ def test_span_origin(sentry_init, capture_events, capture_envelopes) -> None: events = capture_events() - with start_transaction(name="test_clickhouse_transaction"): + with start_span(name="test_clickhouse_transaction"): conn = connect("clickhouse://localhost") cursor = conn.cursor() cursor.execute("SELECT 1") diff --git a/tests/integrations/cohere/test_cohere.py b/tests/integrations/cohere/test_cohere.py index 672d71b6b3..ff41ceba11 100644 --- a/tests/integrations/cohere/test_cohere.py +++ b/tests/integrations/cohere/test_cohere.py @@ -4,7 +4,7 @@ import pytest from cohere import Client, ChatMessage -from sentry_sdk import start_transaction +from sentry_sdk import start_span from sentry_sdk.integrations.cohere import CohereIntegration from unittest import mock # python 3.3 and above @@ -41,7 +41,7 @@ def test_nonstreaming_chat( ) ) - with start_transaction(name="cohere tx"): + with start_span(name="cohere tx"): response = client.chat( model="some-model", chat_history=[ChatMessage(role="SYSTEM", message="some context")], @@ -110,7 +110,7 @@ def test_streaming_chat(sentry_init, capture_events, send_default_pii, include_p ) ) - with start_transaction(name="cohere tx"): + with start_span(name="cohere tx"): responses = list( client.chat_stream( model="some-model", @@ -186,7 +186,7 @@ def test_embed(sentry_init, capture_events, send_default_pii, include_prompts): ) ) - with start_transaction(name="cohere tx"): + with start_span(name="cohere tx"): response = client.embed(texts=["hello"], model="text-embedding-3-large") assert len(response.embeddings[0]) == 3 @@ -227,7 +227,7 @@ def test_span_origin_chat(sentry_init, capture_events): ) ) - with start_transaction(name="cohere tx"): + with start_span(name="cohere tx"): client.chat( model="some-model", chat_history=[ChatMessage(role="SYSTEM", message="some context")], @@ -265,7 +265,7 @@ def test_span_origin_embed(sentry_init, capture_events): ) ) - with start_transaction(name="cohere tx"): + with start_span(name="cohere tx"): client.embed(texts=["hello"], model="text-embedding-3-large") (event,) = events diff --git a/tests/integrations/django/test_cache_module.py b/tests/integrations/django/test_cache_module.py index 03e4925ab0..2d8cc3d5d6 100644 --- a/tests/integrations/django/test_cache_module.py +++ b/tests/integrations/django/test_cache_module.py @@ -530,7 +530,7 @@ def test_cache_spans_get_many( from django.core.cache import cache - with sentry_sdk.start_transaction(name="caches"): + with sentry_sdk.start_span(name="caches"): cache.get_many([f"S{id}", f"S{id+1}"]) cache.set(f"S{id}", "Sensitive1") cache.get_many([f"S{id}", f"S{id+1}"]) @@ -574,7 +574,7 @@ def test_cache_spans_set_many( from django.core.cache import cache - with sentry_sdk.start_transaction(name="caches"): + with sentry_sdk.start_span(name="caches"): cache.set_many({f"S{id}": "Sensitive1", f"S{id+1}": "Sensitive2"}) cache.get(f"S{id}") diff --git a/tests/integrations/django/test_db_query_data.py b/tests/integrations/django/test_db_query_data.py index ccbe6ee28a..82f1f339a6 100644 --- a/tests/integrations/django/test_db_query_data.py +++ b/tests/integrations/django/test_db_query_data.py @@ -16,7 +16,7 @@ from freezegun import freeze_time from werkzeug.test import Client -from sentry_sdk import start_transaction, start_span +from sentry_sdk import start_span from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.tracing_utils import record_sql_queries @@ -496,7 +496,7 @@ def test_db_span_origin_executemany(sentry_init, client, capture_events): if "postgres" not in connections: pytest.skip("postgres tests disabled") - with start_transaction(name="test_transaction"): + with start_span(name="test_transaction"): from django.db import connection, transaction cursor = connection.cursor() diff --git a/tests/integrations/huggingface_hub/test_huggingface_hub.py b/tests/integrations/huggingface_hub/test_huggingface_hub.py index f43159d80e..7e84d648ee 100644 --- a/tests/integrations/huggingface_hub/test_huggingface_hub.py +++ b/tests/integrations/huggingface_hub/test_huggingface_hub.py @@ -6,7 +6,7 @@ ) from huggingface_hub.errors import OverloadedError -from sentry_sdk import start_transaction +from sentry_sdk import start_span from sentry_sdk.integrations.huggingface_hub import HuggingfaceHubIntegration from unittest import mock # python 3.3 and above @@ -43,7 +43,7 @@ def test_nonstreaming_chat_completion( client.post = mock.Mock( return_value=b'[{"generated_text": "the model response"}]' ) - with start_transaction(name="huggingface_hub tx"): + with start_span(name="huggingface_hub tx"): response = client.text_generation( prompt="hello", details=details_arg, @@ -95,7 +95,7 @@ def test_streaming_chat_completion( }""", ] ) - with start_transaction(name="huggingface_hub tx"): + with start_span(name="huggingface_hub tx"): response = list( client.text_generation( prompt="hello", @@ -154,7 +154,7 @@ def test_span_origin(sentry_init, capture_events): }""", ] ) - with start_transaction(name="huggingface_hub tx"): + with start_span(name="huggingface_hub tx"): list( client.text_generation( prompt="hello", diff --git a/tests/integrations/langchain/test_langchain.py b/tests/integrations/langchain/test_langchain.py index b9e5705b88..2ac6679321 100644 --- a/tests/integrations/langchain/test_langchain.py +++ b/tests/integrations/langchain/test_langchain.py @@ -14,7 +14,7 @@ from langchain_core.messages import BaseMessage, AIMessageChunk from langchain_core.outputs import ChatGenerationChunk -from sentry_sdk import start_transaction +from sentry_sdk import start_span from sentry_sdk.integrations.langchain import LangchainIntegration from langchain.agents import tool, AgentExecutor, create_openai_tools_agent from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder @@ -163,7 +163,7 @@ def test_langchain_agent( agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True) - with start_transaction(): + with start_span(name="agent"): list(agent_executor.stream({"input": "How many letters in the word eudca"})) tx = events[0] @@ -237,7 +237,7 @@ def test_langchain_error(sentry_init, capture_events): agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True) - with start_transaction(), pytest.raises(Exception): + with start_span(name="agent"), pytest.raises(Exception): list(agent_executor.stream({"input": "How many letters in the word eudca"})) error = events[0] @@ -332,7 +332,7 @@ def test_span_origin(sentry_init, capture_events): agent_executor = AgentExecutor(agent=agent, tools=[get_word_length], verbose=True) - with start_transaction(): + with start_span(name="agent"): list(agent_executor.stream({"input": "How many letters in the word eudca"})) (event,) = events diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index 1d5ce7a2b1..0508d7d056 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -6,7 +6,7 @@ from openai.types.chat.chat_completion_chunk import ChoiceDelta, Choice as DeltaChoice from openai.types.create_embedding_response import Usage as EmbeddingTokenUsage -from sentry_sdk import start_transaction +from sentry_sdk import start_span from sentry_sdk.integrations.openai import ( OpenAIIntegration, _calculate_chat_completion_usage, @@ -67,7 +67,7 @@ def test_nonstreaming_chat_completion( client = OpenAI(api_key="z") client.chat.completions._post = mock.Mock(return_value=EXAMPLE_CHAT_COMPLETION) - with start_transaction(name="openai tx"): + with start_span(name="openai tx"): response = ( client.chat.completions.create( model="some-model", messages=[{"role": "system", "content": "hello"}] @@ -112,7 +112,7 @@ async def test_nonstreaming_chat_completion_async( client = AsyncOpenAI(api_key="z") client.chat.completions._post = AsyncMock(return_value=EXAMPLE_CHAT_COMPLETION) - with start_transaction(name="openai tx"): + with start_span(name="openai tx"): response = await client.chat.completions.create( model="some-model", messages=[{"role": "system", "content": "hello"}] ) @@ -204,7 +204,7 @@ def test_streaming_chat_completion( ] client.chat.completions._post = mock.Mock(return_value=returned_stream) - with start_transaction(name="openai tx"): + with start_span(name="openai tx"): response_stream = client.chat.completions.create( model="some-model", messages=[{"role": "system", "content": "hello"}] ) @@ -298,7 +298,7 @@ async def test_streaming_chat_completion_async( ) client.chat.completions._post = AsyncMock(return_value=returned_stream) - with start_transaction(name="openai tx"): + with start_span(name="openai tx"): response_stream = await client.chat.completions.create( model="some-model", messages=[{"role": "system", "content": "hello"}] ) @@ -393,7 +393,7 @@ def test_embeddings_create( ) client.embeddings._post = mock.Mock(return_value=returned_embedding) - with start_transaction(name="openai tx"): + with start_span(name="openai tx"): response = client.embeddings.create( input="hello", model="text-embedding-3-large" ) @@ -441,7 +441,7 @@ async def test_embeddings_create_async( ) client.embeddings._post = AsyncMock(return_value=returned_embedding) - with start_transaction(name="openai tx"): + with start_span(name="openai tx"): response = await client.embeddings.create( input="hello", model="text-embedding-3-large" ) @@ -528,7 +528,7 @@ def test_span_origin_nonstreaming_chat(sentry_init, capture_events): client = OpenAI(api_key="z") client.chat.completions._post = mock.Mock(return_value=EXAMPLE_CHAT_COMPLETION) - with start_transaction(name="openai tx"): + with start_span(name="openai tx"): client.chat.completions.create( model="some-model", messages=[{"role": "system", "content": "hello"}] ) @@ -550,7 +550,7 @@ async def test_span_origin_nonstreaming_chat_async(sentry_init, capture_events): client = AsyncOpenAI(api_key="z") client.chat.completions._post = AsyncMock(return_value=EXAMPLE_CHAT_COMPLETION) - with start_transaction(name="openai tx"): + with start_span(name="openai tx"): await client.chat.completions.create( model="some-model", messages=[{"role": "system", "content": "hello"}] ) @@ -607,7 +607,7 @@ def test_span_origin_streaming_chat(sentry_init, capture_events): ] client.chat.completions._post = mock.Mock(return_value=returned_stream) - with start_transaction(name="openai tx"): + with start_span(name="openai tx"): response_stream = client.chat.completions.create( model="some-model", messages=[{"role": "system", "content": "hello"}] ) @@ -671,7 +671,7 @@ async def test_span_origin_streaming_chat_async(sentry_init, capture_events): ) client.chat.completions._post = AsyncMock(return_value=returned_stream) - with start_transaction(name="openai tx"): + with start_span(name="openai tx"): response_stream = await client.chat.completions.create( model="some-model", messages=[{"role": "system", "content": "hello"}] ) @@ -706,7 +706,7 @@ def test_span_origin_embeddings(sentry_init, capture_events): ) client.embeddings._post = mock.Mock(return_value=returned_embedding) - with start_transaction(name="openai tx"): + with start_span(name="openai tx"): client.embeddings.create(input="hello", model="text-embedding-3-large") (event,) = events @@ -736,7 +736,7 @@ async def test_span_origin_embeddings_async(sentry_init, capture_events): ) client.embeddings._post = AsyncMock(return_value=returned_embedding) - with start_transaction(name="openai tx"): + with start_span(name="openai tx"): await client.embeddings.create(input="hello", model="text-embedding-3-large") (event,) = events diff --git a/tests/integrations/redis/test_redis_cache_module.py b/tests/integrations/redis/test_redis_cache_module.py index e02b1ec31a..b1c012e5ee 100644 --- a/tests/integrations/redis/test_redis_cache_module.py +++ b/tests/integrations/redis/test_redis_cache_module.py @@ -24,7 +24,7 @@ def test_no_cache_basic(sentry_init, capture_events, render_span_tree): events = capture_events() connection = FakeStrictRedis() - with sentry_sdk.start_transaction(): + with sentry_sdk.start_span(name="cache"): connection.get("mycachekey") (event,) = events @@ -49,7 +49,7 @@ def test_cache_basic(sentry_init, capture_events, render_span_tree): events = capture_events() connection = FakeStrictRedis() - with sentry_sdk.start_transaction(): + with sentry_sdk.start_span(name="cache"): connection.hget("mycachekey", "myfield") connection.get("mycachekey") connection.set("mycachekey1", "bla") @@ -87,7 +87,7 @@ def test_cache_keys(sentry_init, capture_events, render_span_tree): events = capture_events() connection = FakeStrictRedis() - with sentry_sdk.start_transaction(): + with sentry_sdk.start_span(name="cache"): connection.get("somethingelse") connection.get("blub") connection.get("blubkeything") @@ -120,7 +120,7 @@ def test_cache_data(sentry_init, capture_events): events = capture_events() connection = FakeStrictRedis(host="mycacheserver.io", port=6378) - with sentry_sdk.start_transaction(): + with sentry_sdk.start_span(name="cache"): connection.get("mycachekey") connection.set("mycachekey", "事实胜于雄辩") connection.get("mycachekey") @@ -203,7 +203,7 @@ def test_cache_prefixes(sentry_init, capture_events): events = capture_events() connection = FakeStrictRedis() - with sentry_sdk.start_transaction(): + with sentry_sdk.start_span(name="cache"): connection.mget("yes", "no") connection.mget("no", 1, "yes") connection.mget("no", "yes.1", "yes.2") diff --git a/tests/integrations/redis_py_cluster_legacy/test_redis_py_cluster_legacy.py b/tests/integrations/redis_py_cluster_legacy/test_redis_py_cluster_legacy.py index 5e0b724436..a530fec115 100644 --- a/tests/integrations/redis_py_cluster_legacy/test_redis_py_cluster_legacy.py +++ b/tests/integrations/redis_py_cluster_legacy/test_redis_py_cluster_legacy.py @@ -4,7 +4,7 @@ import rediscluster from sentry_sdk import capture_message -from sentry_sdk.api import start_transaction +from sentry_sdk.api import start_span from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations.redis import RedisIntegration from tests.conftest import ApproxDict @@ -84,7 +84,7 @@ def test_rediscluster_pipeline( events = capture_events() rc = rediscluster.RedisCluster(connection_pool=MOCK_CONNECTION_POOL) - with start_transaction(): + with start_span(name="redis"): pipeline = rc.pipeline() pipeline.get("foo") pipeline.set("bar", 1) @@ -120,7 +120,7 @@ def test_db_connection_attributes_client(sentry_init, capture_events, redisclust events = capture_events() rc = rediscluster_cls(connection_pool=MOCK_CONNECTION_POOL) - with start_transaction(): + with start_span(name="redis"): rc.get("foobar") (event,) = events @@ -147,7 +147,7 @@ def test_db_connection_attributes_pipeline( events = capture_events() rc = rediscluster.RedisCluster(connection_pool=MOCK_CONNECTION_POOL) - with start_transaction(): + with start_span(name="redis"): pipeline = rc.pipeline() pipeline.get("foo") pipeline.execute() diff --git a/tests/integrations/socket/test_socket.py b/tests/integrations/socket/test_socket.py index e629114b2b..500e9b5608 100644 --- a/tests/integrations/socket/test_socket.py +++ b/tests/integrations/socket/test_socket.py @@ -1,6 +1,6 @@ import socket -from sentry_sdk import start_transaction +from sentry_sdk import start_span from sentry_sdk.integrations.socket import SocketIntegration from tests.conftest import ApproxDict @@ -9,7 +9,7 @@ def test_getaddrinfo_trace(sentry_init, capture_events): sentry_init(integrations=[SocketIntegration()], traces_sample_rate=1.0) events = capture_events() - with start_transaction(): + with start_span(name="socket"): socket.getaddrinfo("example.com", 443) (event,) = events @@ -31,7 +31,7 @@ def test_create_connection_trace(sentry_init, capture_events): sentry_init(integrations=[SocketIntegration()], traces_sample_rate=1.0) events = capture_events() - with start_transaction(): + with start_span(name="socket"): socket.create_connection(("example.com", 443), timeout, None) (event,) = events @@ -65,7 +65,7 @@ def test_span_origin(sentry_init, capture_events): ) events = capture_events() - with start_transaction(name="foo"): + with start_span(name="foo"): socket.create_connection(("example.com", 443), 1, None) (event,) = events diff --git a/tests/integrations/tornado/test_tornado.py b/tests/integrations/tornado/test_tornado.py index 837da07434..e5dae3fcd9 100644 --- a/tests/integrations/tornado/test_tornado.py +++ b/tests/integrations/tornado/test_tornado.py @@ -4,7 +4,7 @@ import pytest import sentry_sdk -from sentry_sdk import start_transaction, capture_message +from sentry_sdk import start_span, capture_message from sentry_sdk.integrations.tornado import TornadoIntegration from tornado.web import RequestHandler, Application, HTTPError @@ -117,7 +117,7 @@ def test_transactions(tornado_testcase, sentry_init, capture_events, handler, co events = capture_events() client = tornado_testcase(Application([(r"/hi", handler)])) - with start_transaction(name="client") as span: + with start_span(name="client") as span: pass response = client.fetch( @@ -135,7 +135,7 @@ def test_transactions(tornado_testcase, sentry_init, capture_events, handler, co assert client_tx["transaction"] == "client" assert client_tx["transaction_info"] == { "source": "custom" - } # because this is just the start_transaction() above. + } # because this is just the start_span() above. if server_error is not None: assert server_error["exception"]["values"][0]["type"] == "ZeroDivisionError" diff --git a/tests/profiler/test_continuous_profiler.py b/tests/profiler/test_continuous_profiler.py index 1b96f27036..f56afe656e 100644 --- a/tests/profiler/test_continuous_profiler.py +++ b/tests/profiler/test_continuous_profiler.py @@ -198,7 +198,7 @@ def test_continuous_profiler_auto_start_and_manual_stop( thread = threading.current_thread() - with sentry_sdk.start_transaction(name="profiling"): + with sentry_sdk.start_span(name="profiling"): with sentry_sdk.start_span(op="op"): time.sleep(0.05) @@ -209,7 +209,7 @@ def test_continuous_profiler_auto_start_and_manual_stop( envelopes.clear() - with sentry_sdk.start_transaction(name="profiling"): + with sentry_sdk.start_span(name="profiling"): with sentry_sdk.start_span(op="op"): time.sleep(0.05) @@ -219,7 +219,7 @@ def test_continuous_profiler_auto_start_and_manual_stop( envelopes.clear() - with sentry_sdk.start_transaction(name="profiling"): + with sentry_sdk.start_span(name="profiling"): with sentry_sdk.start_span(op="op"): time.sleep(0.05) @@ -260,7 +260,7 @@ def test_continuous_profiler_manual_start_and_stop( envelopes.clear() - with sentry_sdk.start_transaction(name="profiling"): + with sentry_sdk.start_span(name="profiling"): with sentry_sdk.start_span(op="op"): time.sleep(0.05) @@ -270,7 +270,7 @@ def test_continuous_profiler_manual_start_and_stop( envelopes.clear() - with sentry_sdk.start_transaction(name="profiling"): + with sentry_sdk.start_span(name="profiling"): with sentry_sdk.start_span(op="op"): time.sleep(0.05) diff --git a/tests/profiler/test_transaction_profiler.py b/tests/profiler/test_transaction_profiler.py index a77942e788..7679831be3 100644 --- a/tests/profiler/test_transaction_profiler.py +++ b/tests/profiler/test_transaction_profiler.py @@ -9,7 +9,7 @@ import pytest -from sentry_sdk import start_transaction +from sentry_sdk import start_span from sentry_sdk.profiler.transaction_profiler import ( GeventScheduler, Profile, @@ -148,7 +148,7 @@ def test_profiles_sample_rate( with mock.patch( "sentry_sdk.profiler.transaction_profiler.random.random", return_value=0.5 ): - with start_transaction(name="profiling"): + with start_span(name="profiling"): pass items = defaultdict(list) @@ -219,7 +219,7 @@ def test_profiles_sampler( with mock.patch( "sentry_sdk.profiler.transaction_profiler.random.random", return_value=0.5 ): - with start_transaction(name="profiling"): + with start_span(name="profiling"): pass items = defaultdict(list) @@ -249,7 +249,7 @@ def test_minimum_unique_samples_required( envelopes = capture_envelopes() record_lost_event_calls = capture_record_lost_event_calls() - with start_transaction(name="profiling"): + with start_span(name="profiling"): pass items = defaultdict(list) @@ -277,7 +277,7 @@ def test_profile_captured( envelopes = capture_envelopes() - with start_transaction(name="profiling"): + with start_span(name="profiling"): time.sleep(0.05) items = defaultdict(list) diff --git a/tests/test_ai_monitoring.py b/tests/test_ai_monitoring.py index 5e7c7432fa..9ecd75fc84 100644 --- a/tests/test_ai_monitoring.py +++ b/tests/test_ai_monitoring.py @@ -16,7 +16,7 @@ def tool(**kwargs): def pipeline(): tool() - with sentry_sdk.start_transaction(): + with sentry_sdk.start_span(name="pipeline"): pipeline() transaction = events[0] @@ -43,7 +43,7 @@ def tool(**kwargs): def pipeline(): tool() - with sentry_sdk.start_transaction(): + with sentry_sdk.start_span(name="pipeline"): pipeline(sentry_tags={"user": "colin"}, sentry_data={"some_data": "value"}) transaction = events[0] @@ -74,7 +74,7 @@ async def async_tool(**kwargs): async def async_pipeline(): await async_tool() - with sentry_sdk.start_transaction(): + with sentry_sdk.start_span(name="async_pipeline"): await async_pipeline() transaction = events[0] @@ -102,7 +102,7 @@ async def async_tool(**kwargs): async def async_pipeline(): await async_tool() - with sentry_sdk.start_transaction(): + with sentry_sdk.start_span(name="async_pipeline"): await async_pipeline( sentry_tags={"user": "czyber"}, sentry_data={"some_data": "value"} ) diff --git a/tests/test_basics.py b/tests/test_basics.py index 3c05f9848a..cbf0177403 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -16,7 +16,7 @@ capture_event, capture_exception, capture_message, - start_transaction, + start_span, last_event_id, add_breadcrumb, isolation_scope, @@ -174,7 +174,7 @@ def before_send_transaction(event, hint): traces_sample_rate=1.0, ) events = capture_events() - transaction = start_transaction(name="foo") + transaction = start_span(name="foo") transaction.finish() (event,) = events @@ -191,7 +191,7 @@ def before_send_transaction_discard(event, hint): traces_sample_rate=1.0, ) events = capture_events() - transaction = start_transaction(name="foo") + transaction = start_span(name="foo") transaction.finish() assert len(events) == 0 @@ -592,7 +592,7 @@ def foo(event, hint): capture_message("dropped") - with start_transaction(name="dropped"): + with start_span(name="dropped"): pass assert len(events) == 0 @@ -697,7 +697,7 @@ def test_functions_to_trace(sentry_init, capture_events): events = capture_events() - with start_transaction(name="something"): + with start_span(name="something"): time.sleep(0) for word in ["World", "You"]: @@ -733,7 +733,7 @@ def test_functions_to_trace_with_class(sentry_init, capture_events): events = capture_events() - with start_transaction(name="something"): + with start_span(name="something"): wg = WorldGreeter("World") wg.greet() wg.greet("You") @@ -822,7 +822,7 @@ def test_last_event_id_transaction(sentry_init): assert last_event_id() is None - with start_transaction(name="test"): + with start_span(name="test"): pass assert last_event_id() is None, "Transaction should not set last_event_id" diff --git a/tests/test_scope.py b/tests/test_scope.py index e04fcb2e05..1ae1a2fd35 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -807,7 +807,7 @@ def test_nested_scopes_with_tags(sentry_init, capture_envelopes): with sentry_sdk.new_scope() as scope2: scope2.set_tag("current_scope2", 1) - with sentry_sdk.start_transaction(name="trx") as trx: + with sentry_sdk.start_span(name="trx") as trx: trx.set_tag("trx", 1) with sentry_sdk.start_span(op="span1") as span1: @@ -898,7 +898,7 @@ def test_last_event_id_transaction(sentry_init): assert Scope.last_event_id() is None - with sentry_sdk.start_transaction(name="test"): + with sentry_sdk.start_span(name="test"): pass assert Scope.last_event_id() is None, "Transaction should not set last_event_id" diff --git a/tests/test_scrubber.py b/tests/test_scrubber.py index 2c462153dd..8bef3bac10 100644 --- a/tests/test_scrubber.py +++ b/tests/test_scrubber.py @@ -1,7 +1,7 @@ import sys import logging -from sentry_sdk import capture_exception, capture_event, start_transaction, start_span +from sentry_sdk import capture_exception, capture_event, start_span from sentry_sdk.utils import event_from_exception from sentry_sdk.scrubber import EventScrubber from tests.conftest import ApproxDict @@ -145,7 +145,7 @@ def test_span_data_scrubbing(sentry_init, capture_events): sentry_init(traces_sample_rate=1.0) events = capture_events() - with start_transaction(name="hi"): + with start_span(name="hi"): with start_span(op="foo", name="bar") as span: span.set_data("password", "secret") span.set_data("datafoo", "databar") From be5327356fdae8efc77a9faa9a2ffb0773e80665 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 9 Jan 2025 15:26:50 +0100 Subject: [PATCH 7/8] Centralize minimum version checking (#3910) For [populating tox automatically](https://github.com/getsentry/sentry-python/issues/3808), we need to store min versions of frameworks/libraries in a programmatically accessible place. The obvious place for this would be in each integration; however, since integrations can't be imported unless the respective framework is installed, this couldn't be used from the script (unless we'd always install all requirements of all integrations prior to running it, which takes a non trivial amount of time). So instead I've opted for a central place within `sentry_sdk/integrations/__init__.py`. Note: the min versions probably need updating. Not sure when this was last done, but some of them look quite ancient and we probably don't support them because we'd already dropped the last Python version they'd be able to run on. --- sentry_sdk/integrations/__init__.py | 42 +++++++++++++++++++- sentry_sdk/integrations/aiohttp.py | 8 +--- sentry_sdk/integrations/anthropic.py | 9 +---- sentry_sdk/integrations/ariadne.py | 9 +---- sentry_sdk/integrations/arq.py | 8 +--- sentry_sdk/integrations/asyncpg.py | 12 +++--- sentry_sdk/integrations/boto3.py | 12 +----- sentry_sdk/integrations/bottle.py | 8 +--- sentry_sdk/integrations/celery/__init__.py | 5 +-- sentry_sdk/integrations/clickhouse_driver.py | 7 ++-- sentry_sdk/integrations/django/__init__.py | 6 +-- sentry_sdk/integrations/falcon.py | 9 +---- sentry_sdk/integrations/flask.py | 9 +---- sentry_sdk/integrations/gql.py | 11 ++--- sentry_sdk/integrations/graphene.py | 9 +---- sentry_sdk/integrations/ray.py | 9 +---- sentry_sdk/integrations/rq.py | 10 +---- sentry_sdk/integrations/sanic.py | 12 ++---- sentry_sdk/integrations/sqlalchemy.py | 12 +----- sentry_sdk/integrations/strawberry.py | 11 +---- sentry_sdk/integrations/tornado.py | 5 +-- 21 files changed, 87 insertions(+), 136 deletions(-) diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 12336a939b..683382bb9a 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -111,7 +111,6 @@ def iter_default_integrations(with_auto_enabling_integrations): "sentry_sdk.integrations.tornado.TornadoIntegration", ] - iter_default_integrations = _generate_default_integrations_iterator( integrations=_DEFAULT_INTEGRATIONS, auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS, @@ -120,6 +119,30 @@ def iter_default_integrations(with_auto_enabling_integrations): del _generate_default_integrations_iterator +_MIN_VERSIONS = { + "aiohttp": (3, 4), + "anthropic": (0, 16), + "ariadne": (0, 20), + "arq": (0, 23), + "asyncpg": (0, 23), + "boto3": (1, 12), # this is actually the botocore version + "bottle": (0, 12), + "celery": (4, 4, 7), + "clickhouse_driver": (0, 2, 0), + "django": (1, 8), + "falcon": (1, 4), + "flask": (0, 10), + "gql": (3, 4, 1), + "graphene": (3, 3), + "ray": (2, 7, 0), + "rq": (0, 6), + "sanic": (0, 8), + "sqlalchemy": (1, 2), + "strawberry": (0, 209, 5), + "tornado": (6, 0), +} + + def setup_integrations( integrations, with_defaults=True, @@ -195,6 +218,23 @@ def setup_integrations( return integrations +def _check_minimum_version(integration, version, package=None): + # type: (type[Integration], Optional[tuple[int, ...]], Optional[str]) -> None + package = package or integration.identifier + + if version is None: + raise DidNotEnable(f"Unparsable {package} version.") + + min_version = _MIN_VERSIONS.get(integration.identifier) + if min_version is None: + return + + if version < min_version: + raise DidNotEnable( + f"Integration only supports {package} {'.'.join(map(str, min_version))} or newer." + ) + + class DidNotEnable(Exception): # noqa: N818 """ The integration could not be enabled due to a trivial user error like diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index d0226bc156..47c1272ae1 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -7,6 +7,7 @@ from sentry_sdk.consts import OP, SPANSTATUS, SPANDATA from sentry_sdk.integrations import ( _DEFAULT_FAILED_REQUEST_STATUS_CODES, + _check_minimum_version, Integration, DidNotEnable, ) @@ -91,12 +92,7 @@ def setup_once(): # type: () -> None version = parse_version(AIOHTTP_VERSION) - - if version is None: - raise DidNotEnable("Unparsable AIOHTTP version: {}".format(AIOHTTP_VERSION)) - - if version < (3, 4): - raise DidNotEnable("AIOHTTP 3.4 or newer required.") + _check_minimum_version(AioHttpIntegration, version) if not HAS_REAL_CONTEXTVARS: # We better have contextvars or we're going to leak state between diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 87e69a3113..f06d8a14db 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -4,7 +4,7 @@ import sentry_sdk from sentry_sdk.ai.monitoring import record_token_usage from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import ( capture_internal_exceptions, @@ -37,12 +37,7 @@ def __init__(self, include_prompts=True): def setup_once(): # type: () -> None version = package_version("anthropic") - - if version is None: - raise DidNotEnable("Unparsable anthropic version.") - - if version < (0, 16): - raise DidNotEnable("anthropic 0.16 or newer required.") + _check_minimum_version(AnthropicIntegration, version) Messages.create = _wrap_message_create(Messages.create) AsyncMessages.create = _wrap_message_create_async(AsyncMessages.create) diff --git a/sentry_sdk/integrations/ariadne.py b/sentry_sdk/integrations/ariadne.py index 70a3424a48..0336140441 100644 --- a/sentry_sdk/integrations/ariadne.py +++ b/sentry_sdk/integrations/ariadne.py @@ -2,7 +2,7 @@ import sentry_sdk from sentry_sdk import get_client, capture_event -from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.integrations._wsgi_common import request_body_within_bounds from sentry_sdk.scope import should_send_default_pii @@ -36,12 +36,7 @@ class AriadneIntegration(Integration): def setup_once(): # type: () -> None version = package_version("ariadne") - - if version is None: - raise DidNotEnable("Unparsable ariadne version.") - - if version < (0, 20): - raise DidNotEnable("ariadne 0.20 or newer required.") + _check_minimum_version(AriadneIntegration, version) ignore_logger("ariadne") diff --git a/sentry_sdk/integrations/arq.py b/sentry_sdk/integrations/arq.py index d61499139b..a2cce8e0ff 100644 --- a/sentry_sdk/integrations/arq.py +++ b/sentry_sdk/integrations/arq.py @@ -2,7 +2,7 @@ import sentry_sdk from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.scope import should_send_default_pii from sentry_sdk.tracing import Transaction, TRANSACTION_SOURCE_TASK @@ -55,11 +55,7 @@ def setup_once(): except (TypeError, ValueError): version = None - if version is None: - raise DidNotEnable("Unparsable arq version: {}".format(ARQ_VERSION)) - - if version < (0, 23): - raise DidNotEnable("arq 0.23 or newer required.") + _check_minimum_version(ArqIntegration, version) patch_enqueue_job() patch_run_job() diff --git a/sentry_sdk/integrations/asyncpg.py b/sentry_sdk/integrations/asyncpg.py index b05d5615ba..b6b53f4668 100644 --- a/sentry_sdk/integrations/asyncpg.py +++ b/sentry_sdk/integrations/asyncpg.py @@ -4,7 +4,7 @@ import sentry_sdk from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable from sentry_sdk.tracing import Span from sentry_sdk.tracing_utils import add_query_source, record_sql_queries from sentry_sdk.utils import ( @@ -20,12 +20,6 @@ except ImportError: raise DidNotEnable("asyncpg not installed.") -# asyncpg.__version__ is a string containing the semantic version in the form of ".." -asyncpg_version = parse_version(asyncpg.__version__) - -if asyncpg_version is not None and asyncpg_version < (0, 23, 0): - raise DidNotEnable("asyncpg >= 0.23.0 required") - class AsyncPGIntegration(Integration): identifier = "asyncpg" @@ -37,6 +31,10 @@ def __init__(self, *, record_params: bool = False): @staticmethod def setup_once() -> None: + # asyncpg.__version__ is a string containing the semantic version in the form of ".." + asyncpg_version = parse_version(asyncpg.__version__) + _check_minimum_version(AsyncPGIntegration, asyncpg_version) + asyncpg.Connection.execute = _wrap_execute( asyncpg.Connection.execute, ) diff --git a/sentry_sdk/integrations/boto3.py b/sentry_sdk/integrations/boto3.py index c8da56fb14..0207341f1b 100644 --- a/sentry_sdk/integrations/boto3.py +++ b/sentry_sdk/integrations/boto3.py @@ -2,7 +2,7 @@ import sentry_sdk from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable from sentry_sdk.tracing import Span from sentry_sdk.utils import ( capture_internal_exceptions, @@ -35,16 +35,8 @@ class Boto3Integration(Integration): @staticmethod def setup_once(): # type: () -> None - version = parse_version(BOTOCORE_VERSION) - - if version is None: - raise DidNotEnable( - "Unparsable botocore version: {}".format(BOTOCORE_VERSION) - ) - - if version < (1, 12): - raise DidNotEnable("Botocore 1.12 or newer is required.") + _check_minimum_version(Boto3Integration, version, "botocore") orig_init = BaseClient.__init__ diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py index a2d6b51033..148b86852e 100644 --- a/sentry_sdk/integrations/bottle.py +++ b/sentry_sdk/integrations/bottle.py @@ -13,6 +13,7 @@ Integration, DidNotEnable, _DEFAULT_FAILED_REQUEST_STATUS_CODES, + _check_minimum_version, ) from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.integrations._wsgi_common import RequestExtractor @@ -72,12 +73,7 @@ def __init__( def setup_once(): # type: () -> None version = parse_version(BOTTLE_VERSION) - - if version is None: - raise DidNotEnable("Unparsable Bottle version: {}".format(BOTTLE_VERSION)) - - if version < (0, 12): - raise DidNotEnable("Bottle 0.12 or newer required.") + _check_minimum_version(BottleIntegration, version) old_app = Bottle.__call__ diff --git a/sentry_sdk/integrations/celery/__init__.py b/sentry_sdk/integrations/celery/__init__.py index 9a984de8c3..dc48aac0e6 100644 --- a/sentry_sdk/integrations/celery/__init__.py +++ b/sentry_sdk/integrations/celery/__init__.py @@ -6,7 +6,7 @@ from sentry_sdk import isolation_scope from sentry_sdk.api import continue_trace from sentry_sdk.consts import OP, SPANSTATUS, SPANDATA -from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable from sentry_sdk.integrations.celery.beat import ( _patch_beat_apply_entry, _patch_redbeat_maybe_due, @@ -79,8 +79,7 @@ def __init__( @staticmethod def setup_once(): # type: () -> None - if CELERY_VERSION < (4, 4, 7): - raise DidNotEnable("Celery 4.4.7 or newer required.") + _check_minimum_version(CeleryIntegration, CELERY_VERSION) _patch_build_tracer() _patch_task_apply_async() diff --git a/sentry_sdk/integrations/clickhouse_driver.py b/sentry_sdk/integrations/clickhouse_driver.py index daf4c2257c..2561bfad04 100644 --- a/sentry_sdk/integrations/clickhouse_driver.py +++ b/sentry_sdk/integrations/clickhouse_driver.py @@ -1,6 +1,6 @@ import sentry_sdk from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable from sentry_sdk.tracing import Span from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled @@ -34,9 +34,6 @@ def __getitem__(self, _): except ImportError: raise DidNotEnable("clickhouse-driver not installed.") -if clickhouse_driver.VERSION < (0, 2, 0): - raise DidNotEnable("clickhouse-driver >= 0.2.0 required") - class ClickhouseDriverIntegration(Integration): identifier = "clickhouse_driver" @@ -44,6 +41,8 @@ class ClickhouseDriverIntegration(Integration): @staticmethod def setup_once() -> None: + _check_minimum_version(ClickhouseDriverIntegration, clickhouse_driver.VERSION) + # Every query is done using the Connection's `send_query` function clickhouse_driver.connection.Connection.send_query = _wrap_start( clickhouse_driver.connection.Connection.send_query diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index e68f0cacef..54bc25675d 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -22,7 +22,7 @@ transaction_from_function, walk_exception_chain, ) -from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.integrations._wsgi_common import ( @@ -154,9 +154,7 @@ def __init__( @staticmethod def setup_once(): # type: () -> None - - if DJANGO_VERSION < (1, 8): - raise DidNotEnable("Django 1.8 or newer is required.") + _check_minimum_version(DjangoIntegration, DJANGO_VERSION) install_sql_hook() # Patch in our custom middleware. diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py index ce771d16e7..ddedcb10de 100644 --- a/sentry_sdk/integrations/falcon.py +++ b/sentry_sdk/integrations/falcon.py @@ -1,5 +1,5 @@ import sentry_sdk -from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable from sentry_sdk.integrations._wsgi_common import RequestExtractor from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.tracing import SOURCE_FOR_STYLE @@ -135,12 +135,7 @@ def setup_once(): # type: () -> None version = parse_version(FALCON_VERSION) - - if version is None: - raise DidNotEnable("Unparsable Falcon version: {}".format(FALCON_VERSION)) - - if version < (1, 4): - raise DidNotEnable("Falcon 1.4 or newer required.") + _check_minimum_version(FalconIntegration, version) _patch_wsgi_app() _patch_handle_exception() diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index 128301ddb4..45b4f0b2b1 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -1,5 +1,5 @@ import sentry_sdk -from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.integrations._wsgi_common import ( DEFAULT_HTTP_METHODS_TO_CAPTURE, RequestExtractor, @@ -73,12 +73,7 @@ def __init__( def setup_once(): # type: () -> None version = package_version("flask") - - if version is None: - raise DidNotEnable("Unparsable Flask version.") - - if version < (0, 10): - raise DidNotEnable("Flask 0.10 or newer is required.") + _check_minimum_version(FlaskIntegration, version) before_render_template.connect(_add_sentry_trace) request_started.connect(_request_started) diff --git a/sentry_sdk/integrations/gql.py b/sentry_sdk/integrations/gql.py index 5074442986..d5341d2cf6 100644 --- a/sentry_sdk/integrations/gql.py +++ b/sentry_sdk/integrations/gql.py @@ -5,7 +5,7 @@ parse_version, ) -from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii try: @@ -24,8 +24,6 @@ EventDataType = Dict[str, Union[str, Tuple[VariableDefinitionNode, ...]]] -MIN_GQL_VERSION = (3, 4, 1) - class GQLIntegration(Integration): identifier = "gql" @@ -34,11 +32,8 @@ class GQLIntegration(Integration): def setup_once(): # type: () -> None gql_version = parse_version(gql.__version__) - if gql_version is None or gql_version < MIN_GQL_VERSION: - raise DidNotEnable( - "GQLIntegration is only supported for GQL versions %s and above." - % ".".join(str(num) for num in MIN_GQL_VERSION) - ) + _check_minimum_version(GQLIntegration, gql_version) + _patch_execute() diff --git a/sentry_sdk/integrations/graphene.py b/sentry_sdk/integrations/graphene.py index 03731dcaaa..198aea50d2 100644 --- a/sentry_sdk/integrations/graphene.py +++ b/sentry_sdk/integrations/graphene.py @@ -2,7 +2,7 @@ import sentry_sdk from sentry_sdk.consts import OP -from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import ( capture_internal_exceptions, @@ -34,12 +34,7 @@ class GrapheneIntegration(Integration): def setup_once(): # type: () -> None version = package_version("graphene") - - if version is None: - raise DidNotEnable("Unparsable graphene version.") - - if version < (3, 3): - raise DidNotEnable("graphene 3.3 or newer required.") + _check_minimum_version(GrapheneIntegration, version) _patch_graphql() diff --git a/sentry_sdk/integrations/ray.py b/sentry_sdk/integrations/ray.py index 2f5086ed92..24a28c307f 100644 --- a/sentry_sdk/integrations/ray.py +++ b/sentry_sdk/integrations/ray.py @@ -3,7 +3,7 @@ import sentry_sdk from sentry_sdk.consts import OP, SPANSTATUS -from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.tracing import TRANSACTION_SOURCE_TASK from sentry_sdk.utils import ( event_from_exception, @@ -136,11 +136,6 @@ class RayIntegration(Integration): def setup_once(): # type: () -> None version = package_version("ray") - - if version is None: - raise DidNotEnable("Unparsable ray version: {}".format(version)) - - if version < (2, 7, 0): - raise DidNotEnable("Ray 2.7.0 or newer required") + _check_minimum_version(RayIntegration, version) _patch_ray_remote() diff --git a/sentry_sdk/integrations/rq.py b/sentry_sdk/integrations/rq.py index 462f3ad30a..d4fca6a33b 100644 --- a/sentry_sdk/integrations/rq.py +++ b/sentry_sdk/integrations/rq.py @@ -3,7 +3,7 @@ import sentry_sdk from sentry_sdk.consts import OP from sentry_sdk.api import continue_trace -from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.tracing import TRANSACTION_SOURCE_TASK from sentry_sdk.utils import ( @@ -41,14 +41,8 @@ class RqIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - version = parse_version(RQ_VERSION) - - if version is None: - raise DidNotEnable("Unparsable RQ version: {}".format(RQ_VERSION)) - - if version < (0, 6): - raise DidNotEnable("RQ 0.6 or newer is required.") + _check_minimum_version(RqIntegration, version) old_perform_job = Worker.perform_job diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 26e29cb78c..dfcc299d42 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -6,7 +6,7 @@ import sentry_sdk from sentry_sdk import continue_trace from sentry_sdk.consts import OP -from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.tracing import TRANSACTION_SOURCE_COMPONENT, TRANSACTION_SOURCE_URL @@ -73,14 +73,8 @@ def __init__(self, unsampled_statuses=frozenset({404})): @staticmethod def setup_once(): # type: () -> None - SanicIntegration.version = parse_version(SANIC_VERSION) - - if SanicIntegration.version is None: - raise DidNotEnable("Unparsable Sanic version: {}".format(SANIC_VERSION)) - - if SanicIntegration.version < (0, 8): - raise DidNotEnable("Sanic 0.8 or newer required.") + _check_minimum_version(SanicIntegration, SanicIntegration.version) if not HAS_REAL_CONTEXTVARS: # We better have contextvars or we're going to leak state between @@ -102,7 +96,7 @@ def setup_once(): # https://github.com/huge-success/sanic/issues/1332 ignore_logger("root") - if SanicIntegration.version < (21, 9): + if SanicIntegration.version is not None and SanicIntegration.version < (21, 9): _setup_legacy_sanic() return diff --git a/sentry_sdk/integrations/sqlalchemy.py b/sentry_sdk/integrations/sqlalchemy.py index 0a54108e75..068d373053 100644 --- a/sentry_sdk/integrations/sqlalchemy.py +++ b/sentry_sdk/integrations/sqlalchemy.py @@ -1,5 +1,5 @@ from sentry_sdk.consts import SPANSTATUS, SPANDATA -from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable from sentry_sdk.tracing_utils import add_query_source, record_sql_queries from sentry_sdk.utils import ( capture_internal_exceptions, @@ -31,16 +31,8 @@ class SqlalchemyIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - version = parse_version(SQLALCHEMY_VERSION) - - if version is None: - raise DidNotEnable( - "Unparsable SQLAlchemy version: {}".format(SQLALCHEMY_VERSION) - ) - - if version < (1, 2): - raise DidNotEnable("SQLAlchemy 1.2 or newer required.") + _check_minimum_version(SqlalchemyIntegration, version) listen(Engine, "before_cursor_execute", _before_cursor_execute) listen(Engine, "after_cursor_execute", _after_cursor_execute) diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index 58860a633b..d27e0eaf1c 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -4,7 +4,7 @@ import sentry_sdk from sentry_sdk.consts import OP -from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.scope import should_send_default_pii from sentry_sdk.tracing import TRANSACTION_SOURCE_COMPONENT @@ -75,14 +75,7 @@ def __init__(self, async_execution=None): def setup_once(): # type: () -> None version = package_version("strawberry-graphql") - - if version is None: - raise DidNotEnable( - "Unparsable strawberry-graphql version: {}".format(version) - ) - - if version < (0, 209, 5): - raise DidNotEnable("strawberry-graphql 0.209.5 or newer required.") + _check_minimum_version(StrawberryIntegration, version, "strawberry-graphql") _patch_schema_init() _patch_execute() diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index f1bd196261..b9e465c7c7 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -18,7 +18,7 @@ capture_internal_exceptions, transaction_from_function, ) -from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations import _check_minimum_version, Integration, DidNotEnable from sentry_sdk.integrations._wsgi_common import ( RequestExtractor, _filter_headers, @@ -52,8 +52,7 @@ class TornadoIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - if TORNADO_VERSION < (6, 0): - raise DidNotEnable("Tornado 6.0+ required") + _check_minimum_version(TornadoIntegration, TORNADO_VERSION) if not HAS_REAL_CONTEXTVARS: # Tornado is async. We better have contextvars or we're going to leak From fa241c3425e446878f173407fd7358f38d8bd529 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 9 Jan 2025 18:07:32 +0100 Subject: [PATCH 8/8] Treat potel-base as release branch in CI (#3912) ...and remove `sentry-sdk-2.0` from the CI yamls. --- .github/workflows/ci.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/enforce-license-compliance.yml | 2 +- .github/workflows/test-integrations-ai.yml | 2 +- .github/workflows/test-integrations-aws.yml | 2 +- .github/workflows/test-integrations-cloud.yml | 2 +- .github/workflows/test-integrations-common.yml | 2 +- .github/workflows/test-integrations-dbs.yml | 2 +- .github/workflows/test-integrations-graphql.yml | 2 +- .github/workflows/test-integrations-misc.yml | 2 +- .github/workflows/test-integrations-network.yml | 2 +- .github/workflows/test-integrations-tasks.yml | 2 +- .github/workflows/test-integrations-web-1.yml | 2 +- .github/workflows/test-integrations-web-2.yml | 2 +- scripts/split_tox_gh_actions/templates/base.jinja | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ef6604e39..e8931e229e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base pull_request: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e362d1e620..d824757ee9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -15,7 +15,7 @@ on: push: branches: - master - - sentry-sdk-2.0 + - potel-base pull_request: schedule: - cron: '18 18 * * 3' diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index ef79ed112b..5517e5347f 100644 --- a/.github/workflows/enforce-license-compliance.yml +++ b/.github/workflows/enforce-license-compliance.yml @@ -6,7 +6,7 @@ on: - master - main - release/* - - sentry-sdk-2.0 + - potel-base pull_request: # Cancel in progress workflows on pull_requests. diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml index 2fd6995a5f..6e06e6067c 100644 --- a/.github/workflows/test-integrations-ai.yml +++ b/.github/workflows/test-integrations-ai.yml @@ -8,7 +8,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base pull_request: # Cancel in progress workflows on pull_requests. # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value diff --git a/.github/workflows/test-integrations-aws.yml b/.github/workflows/test-integrations-aws.yml index f83e3379f6..eae488776a 100644 --- a/.github/workflows/test-integrations-aws.yml +++ b/.github/workflows/test-integrations-aws.yml @@ -8,7 +8,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base # XXX: We are using `pull_request_target` instead of `pull_request` because we want # this to run on forks with access to the secrets necessary to run the test suite. # Prefer to use `pull_request` when possible. diff --git a/.github/workflows/test-integrations-cloud.yml b/.github/workflows/test-integrations-cloud.yml index 9e34dc6b2b..af089caede 100644 --- a/.github/workflows/test-integrations-cloud.yml +++ b/.github/workflows/test-integrations-cloud.yml @@ -8,7 +8,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base pull_request: # Cancel in progress workflows on pull_requests. # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value diff --git a/.github/workflows/test-integrations-common.yml b/.github/workflows/test-integrations-common.yml index f1806597af..d9e08bbeb8 100644 --- a/.github/workflows/test-integrations-common.yml +++ b/.github/workflows/test-integrations-common.yml @@ -8,7 +8,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base pull_request: # Cancel in progress workflows on pull_requests. # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value diff --git a/.github/workflows/test-integrations-dbs.yml b/.github/workflows/test-integrations-dbs.yml index d9bea0611b..f612b8fb14 100644 --- a/.github/workflows/test-integrations-dbs.yml +++ b/.github/workflows/test-integrations-dbs.yml @@ -8,7 +8,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base pull_request: # Cancel in progress workflows on pull_requests. # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value diff --git a/.github/workflows/test-integrations-graphql.yml b/.github/workflows/test-integrations-graphql.yml index 7138204e16..d239b2ed6c 100644 --- a/.github/workflows/test-integrations-graphql.yml +++ b/.github/workflows/test-integrations-graphql.yml @@ -8,7 +8,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base pull_request: # Cancel in progress workflows on pull_requests. # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value diff --git a/.github/workflows/test-integrations-misc.yml b/.github/workflows/test-integrations-misc.yml index d524863423..5747448442 100644 --- a/.github/workflows/test-integrations-misc.yml +++ b/.github/workflows/test-integrations-misc.yml @@ -8,7 +8,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base pull_request: # Cancel in progress workflows on pull_requests. # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value diff --git a/.github/workflows/test-integrations-network.yml b/.github/workflows/test-integrations-network.yml index 1b9ee3c529..ab1c5b0658 100644 --- a/.github/workflows/test-integrations-network.yml +++ b/.github/workflows/test-integrations-network.yml @@ -8,7 +8,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base pull_request: # Cancel in progress workflows on pull_requests. # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value diff --git a/.github/workflows/test-integrations-tasks.yml b/.github/workflows/test-integrations-tasks.yml index 0f97146d6d..8ecc7ab598 100644 --- a/.github/workflows/test-integrations-tasks.yml +++ b/.github/workflows/test-integrations-tasks.yml @@ -8,7 +8,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base pull_request: # Cancel in progress workflows on pull_requests. # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value diff --git a/.github/workflows/test-integrations-web-1.yml b/.github/workflows/test-integrations-web-1.yml index 53206f764f..2dc5f361de 100644 --- a/.github/workflows/test-integrations-web-1.yml +++ b/.github/workflows/test-integrations-web-1.yml @@ -8,7 +8,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base pull_request: # Cancel in progress workflows on pull_requests. # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value diff --git a/.github/workflows/test-integrations-web-2.yml b/.github/workflows/test-integrations-web-2.yml index 39c1eba535..2b3204ae80 100644 --- a/.github/workflows/test-integrations-web-2.yml +++ b/.github/workflows/test-integrations-web-2.yml @@ -8,7 +8,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base pull_request: # Cancel in progress workflows on pull_requests. # https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value diff --git a/scripts/split_tox_gh_actions/templates/base.jinja b/scripts/split_tox_gh_actions/templates/base.jinja index 16dbc04a76..e69b6f9134 100644 --- a/scripts/split_tox_gh_actions/templates/base.jinja +++ b/scripts/split_tox_gh_actions/templates/base.jinja @@ -11,7 +11,7 @@ on: branches: - master - release/** - - sentry-sdk-2.0 + - potel-base {% if needs_github_secrets %} # XXX: We are using `pull_request_target` instead of `pull_request` because we want