From 42d7b15db3f820bb1fafabb655f48b81a295ca48 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 8 Jan 2026 17:46:27 +0100 Subject: [PATCH 01/10] feat(asyncio): Add on-demand way to enable AsyncioIntegration --- sentry_sdk/integrations/__init__.py | 19 ++++ sentry_sdk/integrations/asyncio.py | 39 ++++++- tests/integrations/asyncio/test_asyncio.py | 116 ++++++++++++++++++++- 3 files changed, 172 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 5ab181df25..36411a6f0c 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -2,6 +2,7 @@ from threading import Lock from typing import TYPE_CHECKING +import sentry_sdk from sentry_sdk.utils import logger if TYPE_CHECKING: @@ -279,6 +280,24 @@ def setup_integrations( return integrations +def _enable_integration(integration: "Integration") -> "Optional[Integration]": + identifier = integration.identifier + client = sentry_sdk.get_client() + + with _installer_lock: + logger.debug("Setting up integration %s", identifier) + try: + type(integration).setup_once() + integration.setup_once_with_options(client.options) + except DidNotEnable as e: + logger.debug("Did not enable integration %s: %s", identifier, e) + else: + _installed_integrations.add(identifier) + return integration + + _processed_integrations.add(identifier) + + def _check_minimum_version( integration: "type[Integration]", version: "Optional[tuple[int, ...]]", diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py index 39c7e3f879..dfc7cde754 100644 --- a/sentry_sdk/integrations/asyncio.py +++ b/sentry_sdk/integrations/asyncio.py @@ -3,7 +3,7 @@ import sentry_sdk from sentry_sdk.consts import OP -from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations import Integration, DidNotEnable, _enable_integration from sentry_sdk.utils import event_from_exception, logger, reraise try: @@ -138,3 +138,40 @@ class AsyncioIntegration(Integration): @staticmethod def setup_once() -> None: patch_asyncio() + + +def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None: + """ + Enable AsyncioIntegration with the provided options. + + The options need to correspond to the options currently accepted by the + AsyncioIntegration() constructor. + + This is useful in scenarios where Sentry needs to be initialized before + an event loop is set up, but you still want to instrument asyncio once there + is an event loop. In that case, you can sentry_sdk.init() early on without + the AsyncioIntegration and then, once the event loop has been set up, execute + + ```python + from sentry_sdk.integrations.asyncio import enable_asyncio_integration + + async def async_entrypoint(): + enable_asyncio_integration() + ``` + + If AsyncioIntegration is already enabled (e.g. because it was provided in + sentry_sdk.init(integrations=[...])), this function will re-enable it. + + If AsyncioIntegration was provided in + sentry_sdk.init(disabled_integrations=[...]), this function will ignore that + and enable it. + """ + client = sentry_sdk.get_client() + if not client.is_active(): + return + + integration = _enable_integration(AsyncioIntegration(*args, **kwargs)) + if integration is None: + return + + client.integrations[integration.identifier] = integration diff --git a/tests/integrations/asyncio/test_asyncio.py b/tests/integrations/asyncio/test_asyncio.py index 11b60fb0e1..912d83dac3 100644 --- a/tests/integrations/asyncio/test_asyncio.py +++ b/tests/integrations/asyncio/test_asyncio.py @@ -7,7 +7,11 @@ import sentry_sdk from sentry_sdk.consts import OP -from sentry_sdk.integrations.asyncio import AsyncioIntegration, patch_asyncio +from sentry_sdk.integrations.asyncio import ( + AsyncioIntegration, + patch_asyncio, + enable_asyncio_integration, +) try: from contextvars import Context, ContextVar @@ -386,3 +390,113 @@ async def test_span_origin( assert event["contexts"]["trace"]["origin"] == "manual" assert event["spans"][0]["origin"] == "auto.function.asyncio" + + +@minimum_python_38 +@pytest.mark.asyncio +async def test_delayed_enable_integration(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0) + + assert "asyncio" not in sentry_sdk.get_client().integrations + + events = capture_events() + + with sentry_sdk.start_transaction(name="test"): + await asyncio.create_task(foo()) + + assert len(events) == 1 + (transaction,) = events + assert not transaction["spans"] + + enable_asyncio_integration() + + events = capture_events() + + assert "asyncio" in sentry_sdk.get_client().integrations + + with sentry_sdk.start_transaction(name="test"): + await asyncio.create_task(foo()) + + assert len(events) == 1 + (transaction,) = events + assert transaction["spans"] + assert transaction["spans"][0]["origin"] == "auto.function.asyncio" + + +@minimum_python_38 +@pytest.mark.asyncio +async def test_delayed_enable_integration_with_options(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0) + + assert "asyncio" not in sentry_sdk.get_client().integrations + + events = capture_events() + + with sentry_sdk.start_transaction(name="test"): + await asyncio.create_task(foo()) + + assert len(events) == 1 + (transaction,) = events + assert not transaction["spans"] + + mock_init = MagicMock(return_value=None) + mock_setup_once = MagicMock() + with patch( + "sentry_sdk.integrations.asyncio.AsyncioIntegration.__init__", mock_init + ): + with patch( + "sentry_sdk.integrations.asyncio.AsyncioIntegration.setup_once", + mock_setup_once, + ): + enable_asyncio_integration("arg", kwarg="kwarg") + + assert "asyncio" in sentry_sdk.get_client().integrations + mock_init.assert_called_once_with("arg", kwarg="kwarg") + mock_setup_once.assert_called_once() + + +@minimum_python_38 +@pytest.mark.asyncio +async def test_delayed_enable_enabled_integration(sentry_init): + sentry_init(integrations=[AsyncioIntegration()], traces_sample_rate=1.0) + + assert "asyncio" in sentry_sdk.get_client().integrations + + original_integration = sentry_sdk.get_client().integrations["asyncio"] + enable_asyncio_integration() + + assert "asyncio" in sentry_sdk.get_client().integrations + + # The new asyncio integration should override the old one + assert sentry_sdk.get_client().integrations["asyncio"] is not original_integration + + +@minimum_python_38 +@pytest.mark.asyncio +async def test_delayed_enable_integration_after_disabling(sentry_init, capture_events): + sentry_init(disabled_integrations=[AsyncioIntegration()], traces_sample_rate=1.0) + + assert "asyncio" not in sentry_sdk.get_client().integrations + + events = capture_events() + + with sentry_sdk.start_transaction(name="test"): + await asyncio.create_task(foo()) + + assert len(events) == 1 + (transaction,) = events + assert not transaction["spans"] + + enable_asyncio_integration() + + events = capture_events() + + assert "asyncio" in sentry_sdk.get_client().integrations + + with sentry_sdk.start_transaction(name="test"): + await asyncio.create_task(foo()) + + assert len(events) == 1 + (transaction,) = events + assert transaction["spans"] + assert transaction["spans"][0]["origin"] == "auto.function.asyncio" From c1dd56aa35e0e754086c24c769262b46832e5906 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 8 Jan 2026 17:51:05 +0100 Subject: [PATCH 02/10] mypy --- sentry_sdk/client.py | 1 + sentry_sdk/integrations/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 259196d1c6..e3821f48ca 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -188,6 +188,7 @@ def __init__(self, options: "Optional[Dict[str, Any]]" = None) -> None: self.monitor: "Optional[Monitor]" = None self.log_batcher: "Optional[LogBatcher]" = None self.metrics_batcher: "Optional[MetricsBatcher]" = None + self.integrations: "dict[str, Integration]" = {} def __getstate__(self, *args: "Any", **kwargs: "Any") -> "Any": return {"options": {}} diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 36411a6f0c..1314a75838 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -286,17 +286,17 @@ def _enable_integration(integration: "Integration") -> "Optional[Integration]": with _installer_lock: logger.debug("Setting up integration %s", identifier) + _processed_integrations.add(identifier) try: type(integration).setup_once() integration.setup_once_with_options(client.options) except DidNotEnable as e: logger.debug("Did not enable integration %s: %s", identifier, e) + return None else: _installed_integrations.add(identifier) return integration - _processed_integrations.add(identifier) - def _check_minimum_version( integration: "type[Integration]", From e6651e4f8f80aa7e2a145d2fb3ffd0001990da53 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 8 Jan 2026 18:03:48 +0100 Subject: [PATCH 03/10] . --- sentry_sdk/integrations/__init__.py | 4 ++++ sentry_sdk/integrations/asyncio.py | 7 +++---- tests/integrations/asyncio/test_asyncio.py | 8 ++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 1314a75838..8c85d5a193 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -285,6 +285,10 @@ def _enable_integration(integration: "Integration") -> "Optional[Integration]": client = sentry_sdk.get_client() with _installer_lock: + if identifier in client.integrations: + logger.debug("Integration already enabled: %s", identifier) + return None + logger.debug("Setting up integration %s", identifier) _processed_integrations.add(identifier) try: diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py index dfc7cde754..0cada3571d 100644 --- a/sentry_sdk/integrations/asyncio.py +++ b/sentry_sdk/integrations/asyncio.py @@ -144,9 +144,6 @@ def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None: """ Enable AsyncioIntegration with the provided options. - The options need to correspond to the options currently accepted by the - AsyncioIntegration() constructor. - This is useful in scenarios where Sentry needs to be initialized before an event loop is set up, but you still want to instrument asyncio once there is an event loop. In that case, you can sentry_sdk.init() early on without @@ -159,8 +156,10 @@ async def async_entrypoint(): enable_asyncio_integration() ``` + Any arguments provided will be passed to AsyncioIntegration() as-is. + If AsyncioIntegration is already enabled (e.g. because it was provided in - sentry_sdk.init(integrations=[...])), this function will re-enable it. + sentry_sdk.init(integrations=[...])), this function won't have any effect. If AsyncioIntegration was provided in sentry_sdk.init(disabled_integrations=[...]), this function will ignore that diff --git a/tests/integrations/asyncio/test_asyncio.py b/tests/integrations/asyncio/test_asyncio.py index 912d83dac3..55758c14bb 100644 --- a/tests/integrations/asyncio/test_asyncio.py +++ b/tests/integrations/asyncio/test_asyncio.py @@ -458,17 +458,17 @@ async def test_delayed_enable_integration_with_options(sentry_init, capture_even @minimum_python_38 @pytest.mark.asyncio async def test_delayed_enable_enabled_integration(sentry_init): - sentry_init(integrations=[AsyncioIntegration()], traces_sample_rate=1.0) + integration = AsyncioIntegration() + sentry_init(integrations=[integration], traces_sample_rate=1.0) assert "asyncio" in sentry_sdk.get_client().integrations - original_integration = sentry_sdk.get_client().integrations["asyncio"] enable_asyncio_integration() assert "asyncio" in sentry_sdk.get_client().integrations - # The new asyncio integration should override the old one - assert sentry_sdk.get_client().integrations["asyncio"] is not original_integration + # The new asyncio integration should not override the old one + assert sentry_sdk.get_client().integrations["asyncio"] == integration @minimum_python_38 From 0042efafa67f961aedfe7c79de52f29b35f42a18 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 8 Jan 2026 18:07:44 +0100 Subject: [PATCH 04/10] wording --- sentry_sdk/integrations/asyncio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py index 0cada3571d..43cc30cec8 100644 --- a/sentry_sdk/integrations/asyncio.py +++ b/sentry_sdk/integrations/asyncio.py @@ -156,14 +156,14 @@ async def async_entrypoint(): enable_asyncio_integration() ``` - Any arguments provided will be passed to AsyncioIntegration() as-is. + Any arguments provided will be passed to AsyncioIntegration() as is. If AsyncioIntegration is already enabled (e.g. because it was provided in sentry_sdk.init(integrations=[...])), this function won't have any effect. If AsyncioIntegration was provided in sentry_sdk.init(disabled_integrations=[...]), this function will ignore that - and enable it. + and the integration will be enabled. """ client = sentry_sdk.get_client() if not client.is_active(): From dd5b71e37de3342e7e2a9b47b33328a5d45f69c5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 8 Jan 2026 18:08:37 +0100 Subject: [PATCH 05/10] simplify test --- tests/integrations/asyncio/test_asyncio.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/integrations/asyncio/test_asyncio.py b/tests/integrations/asyncio/test_asyncio.py index 55758c14bb..5be675402f 100644 --- a/tests/integrations/asyncio/test_asyncio.py +++ b/tests/integrations/asyncio/test_asyncio.py @@ -430,15 +430,6 @@ async def test_delayed_enable_integration_with_options(sentry_init, capture_even assert "asyncio" not in sentry_sdk.get_client().integrations - events = capture_events() - - with sentry_sdk.start_transaction(name="test"): - await asyncio.create_task(foo()) - - assert len(events) == 1 - (transaction,) = events - assert not transaction["spans"] - mock_init = MagicMock(return_value=None) mock_setup_once = MagicMock() with patch( From c447df06c0f86baf309438ed2b0179276eb7eab7 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 12 Jan 2026 12:25:27 +0100 Subject: [PATCH 06/10] move logic to _enable_integration --- sentry_sdk/integrations/__init__.py | 17 ++++++++++------- sentry_sdk/integrations/asyncio.py | 10 +--------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 8c85d5a193..e691ea5dee 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -280,14 +280,17 @@ def setup_integrations( return integrations -def _enable_integration(integration: "Integration") -> "Optional[Integration]": +def _enable_integration(integration: "Integration") -> None: identifier = integration.identifier - client = sentry_sdk.get_client() with _installer_lock: + client = sentry_sdk.get_client() + if not client.is_active(): + return + if identifier in client.integrations: logger.debug("Integration already enabled: %s", identifier) - return None + return logger.debug("Setting up integration %s", identifier) _processed_integrations.add(identifier) @@ -296,10 +299,10 @@ def _enable_integration(integration: "Integration") -> "Optional[Integration]": integration.setup_once_with_options(client.options) except DidNotEnable as e: logger.debug("Did not enable integration %s: %s", identifier, e) - return None - else: - _installed_integrations.add(identifier) - return integration + return + + _installed_integrations.add(identifier) + client.integrations[integration.identifier] = integration def _check_minimum_version( diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py index 43cc30cec8..7beec2407e 100644 --- a/sentry_sdk/integrations/asyncio.py +++ b/sentry_sdk/integrations/asyncio.py @@ -165,12 +165,4 @@ async def async_entrypoint(): sentry_sdk.init(disabled_integrations=[...]), this function will ignore that and the integration will be enabled. """ - client = sentry_sdk.get_client() - if not client.is_active(): - return - - integration = _enable_integration(AsyncioIntegration(*args, **kwargs)) - if integration is None: - return - - client.integrations[integration.identifier] = integration + _enable_integration(AsyncioIntegration(*args, **kwargs)) From 36ef8cb084b6108e9752c38887dc4277192cd399 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 12 Jan 2026 13:47:58 +0100 Subject: [PATCH 07/10] Take _installed_integrations into account, sometimes --- sentry_sdk/integrations/__init__.py | 23 ++++++++++++++--------- sentry_sdk/integrations/asyncio.py | 8 +++++--- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index e691ea5dee..c30c831b06 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -292,16 +292,21 @@ def _enable_integration(integration: "Integration") -> None: logger.debug("Integration already enabled: %s", identifier) return - logger.debug("Setting up integration %s", identifier) - _processed_integrations.add(identifier) - try: - type(integration).setup_once() - integration.setup_once_with_options(client.options) - except DidNotEnable as e: - logger.debug("Did not enable integration %s: %s", identifier, e) - return + if identifier not in _installed_integrations or identifier == "asyncio": + # Asyncio is special because it patches the currently running event + # loop. _installed_integrations, on the other hand, prevents + # re-patching on the process level. + logger.debug("Setting up integration %s", identifier) + _processed_integrations.add(identifier) + try: + type(integration).setup_once() + integration.setup_once_with_options(client.options) + except DidNotEnable as e: + logger.debug("Did not enable integration %s: %s", identifier, e) + return + + _installed_integrations.add(identifier) - _installed_integrations.add(identifier) client.integrations[integration.identifier] = integration diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py index 7beec2407e..86556fbdc1 100644 --- a/sentry_sdk/integrations/asyncio.py +++ b/sentry_sdk/integrations/asyncio.py @@ -147,7 +147,8 @@ def enable_asyncio_integration(*args: "Any", **kwargs: "Any") -> None: This is useful in scenarios where Sentry needs to be initialized before an event loop is set up, but you still want to instrument asyncio once there is an event loop. In that case, you can sentry_sdk.init() early on without - the AsyncioIntegration and then, once the event loop has been set up, execute + the AsyncioIntegration and then, once the event loop has been set up, + execute: ```python from sentry_sdk.integrations.asyncio import enable_asyncio_integration @@ -158,8 +159,9 @@ async def async_entrypoint(): Any arguments provided will be passed to AsyncioIntegration() as is. - If AsyncioIntegration is already enabled (e.g. because it was provided in - sentry_sdk.init(integrations=[...])), this function won't have any effect. + If AsyncioIntegration is already enabled on the current client (e.g. because + it was provided in sentry_sdk.init(integrations=[...])), this function won't + have any effect. If AsyncioIntegration was provided in sentry_sdk.init(disabled_integrations=[...]), this function will ignore that From 0193dea360bee9fc6fe9b4eca2bd6841bf98363c Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 12 Jan 2026 17:36:35 +0100 Subject: [PATCH 08/10] Check if event loop is unpatched --- sentry_sdk/integrations/__init__.py | 30 ---------------------- sentry_sdk/integrations/asyncio.py | 16 ++++++++++-- tests/integrations/asyncio/test_asyncio.py | 18 ++++++++++--- 3 files changed, 29 insertions(+), 35 deletions(-) diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index c30c831b06..021c4a2068 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -280,36 +280,6 @@ def setup_integrations( return integrations -def _enable_integration(integration: "Integration") -> None: - identifier = integration.identifier - - with _installer_lock: - client = sentry_sdk.get_client() - if not client.is_active(): - return - - if identifier in client.integrations: - logger.debug("Integration already enabled: %s", identifier) - return - - if identifier not in _installed_integrations or identifier == "asyncio": - # Asyncio is special because it patches the currently running event - # loop. _installed_integrations, on the other hand, prevents - # re-patching on the process level. - logger.debug("Setting up integration %s", identifier) - _processed_integrations.add(identifier) - try: - type(integration).setup_once() - integration.setup_once_with_options(client.options) - except DidNotEnable as e: - logger.debug("Did not enable integration %s: %s", identifier, e) - return - - _installed_integrations.add(identifier) - - client.integrations[integration.identifier] = integration - - def _check_minimum_version( integration: "type[Integration]", version: "Optional[tuple[int, ...]]", diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py index 86556fbdc1..b43c80d805 100644 --- a/sentry_sdk/integrations/asyncio.py +++ b/sentry_sdk/integrations/asyncio.py @@ -3,7 +3,7 @@ import sentry_sdk from sentry_sdk.consts import OP -from sentry_sdk.integrations import Integration, DidNotEnable, _enable_integration +from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.utils import event_from_exception, logger, reraise try: @@ -47,6 +47,10 @@ def patch_asyncio() -> None: loop = asyncio.get_running_loop() orig_task_factory = loop.get_task_factory() + # Check if already patched + if getattr(orig_task_factory, "_is_sentry_task_factory", False): + return + def _sentry_task_factory( loop: "asyncio.AbstractEventLoop", coro: "Coroutine[Any, Any, Any]", @@ -102,6 +106,7 @@ async def _task_with_sentry_span_creation() -> "Any": return task + _sentry_task_factory._is_sentry_task_factory = True # type: ignore loop.set_task_factory(_sentry_task_factory) # type: ignore except RuntimeError: @@ -167,4 +172,11 @@ async def async_entrypoint(): sentry_sdk.init(disabled_integrations=[...]), this function will ignore that and the integration will be enabled. """ - _enable_integration(AsyncioIntegration(*args, **kwargs)) + client = sentry_sdk.get_client() + if not client.is_active(): + return + + logger.debug("Setting up integration asyncio") + integration = AsyncioIntegration(*args, **kwargs) + integration.setup_once() + client.integrations["asyncio"] = integration diff --git a/tests/integrations/asyncio/test_asyncio.py b/tests/integrations/asyncio/test_asyncio.py index 5be675402f..58072b0071 100644 --- a/tests/integrations/asyncio/test_asyncio.py +++ b/tests/integrations/asyncio/test_asyncio.py @@ -233,6 +233,7 @@ def test_patch_asyncio(mock_get_running_loop): Test that the patch_asyncio function will patch the task factory. """ mock_loop = mock_get_running_loop.return_value + mock_loop.get_task_factory.return_value._is_sentry_task_factory = False patch_asyncio() @@ -282,6 +283,7 @@ def test_sentry_task_factory_with_factory(mock_get_running_loop): # The original task factory will be mocked out here, let's retrieve the value for later orig_task_factory = mock_loop.get_task_factory.return_value + orig_task_factory._is_sentry_task_factory = False # Retieve sentry task factory (since it is an inner function within patch_asyncio) sentry_task_factory = get_sentry_task_factory(mock_get_running_loop) @@ -344,6 +346,7 @@ def test_sentry_task_factory_context_with_factory(mock_get_running_loop): # The original task factory will be mocked out here, let's retrieve the value for later orig_task_factory = mock_loop.get_task_factory.return_value + orig_task_factory._is_sentry_task_factory = False # Retieve sentry task factory (since it is an inner function within patch_asyncio) sentry_task_factory = get_sentry_task_factory(mock_get_running_loop) @@ -448,18 +451,27 @@ async def test_delayed_enable_integration_with_options(sentry_init, capture_even @minimum_python_38 @pytest.mark.asyncio -async def test_delayed_enable_enabled_integration(sentry_init): +async def test_delayed_enable_enabled_integration(sentry_init, uninstall_integration): + # Ensure asyncio integration is not already installed from previous tests + uninstall_integration("asyncio") + integration = AsyncioIntegration() sentry_init(integrations=[integration], traces_sample_rate=1.0) assert "asyncio" in sentry_sdk.get_client().integrations + # Get the task factory after initial setup - it should be Sentry's + loop = asyncio.get_running_loop() + task_factory_before = loop.get_task_factory() + assert getattr(task_factory_before, "_is_sentry_task_factory", False) is True + enable_asyncio_integration() assert "asyncio" in sentry_sdk.get_client().integrations - # The new asyncio integration should not override the old one - assert sentry_sdk.get_client().integrations["asyncio"] == integration + # The task factory should be the same (loop not re-patched) + task_factory_after = loop.get_task_factory() + assert task_factory_before is task_factory_after @minimum_python_38 From 8903c06ebbfc0346c7b0b0c99ae9e8287fecb114 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 12 Jan 2026 17:53:25 +0100 Subject: [PATCH 09/10] writedown --- sentry_sdk/integrations/__init__.py | 1 - sentry_sdk/integrations/asyncio.py | 11 ++++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 021c4a2068..5ab181df25 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -2,7 +2,6 @@ from threading import Lock from typing import TYPE_CHECKING -import sentry_sdk from sentry_sdk.utils import logger if TYPE_CHECKING: diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py index b43c80d805..d589f8020e 100644 --- a/sentry_sdk/integrations/asyncio.py +++ b/sentry_sdk/integrations/asyncio.py @@ -164,9 +164,8 @@ async def async_entrypoint(): Any arguments provided will be passed to AsyncioIntegration() as is. - If AsyncioIntegration is already enabled on the current client (e.g. because - it was provided in sentry_sdk.init(integrations=[...])), this function won't - have any effect. + If AsyncioIntegration has already patched the current event loop, this + function won't have any effect. If AsyncioIntegration was provided in sentry_sdk.init(disabled_integrations=[...]), this function will ignore that @@ -176,6 +175,12 @@ async def async_entrypoint(): if not client.is_active(): return + # This function purposefully bypasses the integration machinery in + # integrations/__init__.py. _installed_integrations/_processed_integrations + # is used to prevent double patching the same module, but in the case of + # the AsyncioIntegration, we don't monkeypatch the standard library directly, + # we patch the currently running event loop, and we keep the record of doing + # that on the loop itself. logger.debug("Setting up integration asyncio") integration = AsyncioIntegration(*args, **kwargs) integration.setup_once() From 26802c77d125e32f8fa48775fc109bcbc90bba79 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 12 Jan 2026 17:57:16 +0100 Subject: [PATCH 10/10] . --- sentry_sdk/integrations/asyncio.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/asyncio.py b/sentry_sdk/integrations/asyncio.py index d589f8020e..afaca73d33 100644 --- a/sentry_sdk/integrations/asyncio.py +++ b/sentry_sdk/integrations/asyncio.py @@ -182,6 +182,9 @@ async def async_entrypoint(): # we patch the currently running event loop, and we keep the record of doing # that on the loop itself. logger.debug("Setting up integration asyncio") + integration = AsyncioIntegration(*args, **kwargs) integration.setup_once() - client.integrations["asyncio"] = integration + + if "asyncio" not in client.integrations: + client.integrations["asyncio"] = integration