diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index e149418c38..04df103bb8 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -24,6 +24,7 @@ "capture_message", "configure_scope", "continue_trace", + "enable_integration", "flush", "get_baggage", "get_client", diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index c4e2229938..34c053aa9e 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -4,6 +4,7 @@ from sentry_sdk import tracing_utils, Client from sentry_sdk._init_implementation import init +from sentry_sdk.integrations import Integration, _enable_integration from sentry_sdk.consts import INSTRUMENTER from sentry_sdk.scope import Scope, _ScopeManager, new_scope, isolation_scope from sentry_sdk.tracing import NoOpSpan, Transaction, trace @@ -57,6 +58,7 @@ def overload(x: "T") -> "T": "capture_message", "configure_scope", "continue_trace", + "enable_integration", "flush", "get_baggage", "get_client", @@ -523,3 +525,35 @@ def update_current_span( if attributes is not None: current_span.update_data(attributes) + + +def enable_integration(integration: Integration) -> None: + """ + Enable an additional integration after sentry_sdk.init() has been called. + + This should be used sparingly, only in situations where, for whatever reason, + it's not feasible to enable an integration during init(). + + Most integrations rely on being enabled before any actual user code is + executed, and this function doesn't change that. enable_integration should + still be called as early as possible. + + One usecase for enable_integration is setting up the AsyncioIntegration only + once the event loop is running, but without having to set up the whole SDK + anew. + + Running this function with an integration that is already enabled will not + have any effect. + + :param integration: The integration to enable. + :type integration: sentry_sdk.integrations.Integration + """ + client = get_client() + if not client.is_active(): + return + + integration = _enable_integration(integration) + if integration is None: + return + + client.integrations[integration.identifier] = integration 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 5ab181df25..c159df9a5c 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,31 @@ def setup_integrations( return integrations +def _enable_integration(integration: "Integration") -> "Optional[Integration]": + with _installer_lock: + client = sentry_sdk.get_client() + + if ( + integration.identifier in _installed_integrations + and integration.identifier in client.integrations + ): + return client.integrations[integration.identifier] + + logger.debug( + "Setting up previously not enabled integration %s", integration.identifier + ) + _processed_integrations.add(integration.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", integration.identifier, e) + return None + else: + _installed_integrations.add(integration.identifier) + return integration + + def _check_minimum_version( integration: "type[Integration]", version: "Optional[tuple[int, ...]]", diff --git a/tests/test_basics.py b/tests/test_basics.py index da836462d8..73a85b4088 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -985,6 +985,122 @@ def test_multiple_setup_integrations_calls(): assert second_call_return == {NoOpIntegration.identifier: NoOpIntegration()} +def test_enable_integration(sentry_init): + sentry_init() + + client = get_client() + assert "gnu_backtrace" not in client.integrations + + # Using gnu backtrace here because to test this properly it should be a + # non-auto-enabling integration that's also unlikely to become auto-enabling + # in the future. AsyncioIntegration would be nicer but it doesn't work in 3.6 + from sentry_sdk.integrations.gnu_backtrace import GnuBacktraceIntegration + + sentry_sdk.enable_integration(GnuBacktraceIntegration()) + + client = get_client() + assert "gnu_backtrace" in client.integrations + + +def test_enable_enabled_integration(sentry_init): + from sentry_sdk.integrations.gnu_backtrace import GnuBacktraceIntegration + + integration = GnuBacktraceIntegration() + sentry_init(integrations=[integration]) + + assert "gnu_backtrace" in get_client().integrations + + # Second call should not raise or cause issues + sentry_sdk.enable_integration(GnuBacktraceIntegration()) + assert "gnu_backtrace" in get_client().integrations + assert get_client().integrations["gnu_backtrace"] == integration + + +def test_enable_integration_twice(sentry_init): + sentry_init() + + from sentry_sdk.integrations.gnu_backtrace import GnuBacktraceIntegration + + assert "gnu_backtrace" not in get_client().integrations + + integration = GnuBacktraceIntegration() + sentry_sdk.enable_integration(integration) + assert "gnu_backtrace" in get_client().integrations + + # Second call should not raise or cause issues + sentry_sdk.enable_integration(GnuBacktraceIntegration()) + assert "gnu_backtrace" in get_client().integrations + assert get_client().integrations["gnu_backtrace"] == integration + + +def test_enable_integration_did_not_enable(sentry_init): + sentry_init() + + class FailingIntegration(Integration): + identifier = "failing_test_integration" + + @staticmethod + def setup_once(): + raise DidNotEnable("This integration cannot be enabled") + + sentry_sdk.enable_integration(FailingIntegration()) + + assert "failing_test_integration" not in get_client().integrations + + +def test_enable_integration_setup_once_called(sentry_init): + sentry_init() + + setup_called = [] + + class TrackingIntegration(Integration): + identifier = "tracking_test_integration" + + @staticmethod + def setup_once(): + setup_called.append(True) + + def setup_once_with_options(self, options): + setup_called.append(True) + + assert len(setup_called) == 0 + sentry_sdk.enable_integration(TrackingIntegration()) + assert len(setup_called) == 2 + assert "tracking_test_integration" in get_client().integrations + + +def test_enable_integration_setup_once_not_called_twice(sentry_init): + sentry_init() + + setup_count = [] + + class CountingIntegration(Integration): + identifier = "counting_test_integration" + + @staticmethod + def setup_once(): + setup_count.append(1) + + sentry_sdk.enable_integration(CountingIntegration()) + assert len(setup_count) == 1 + + # Enable again - setup_once should NOT be called + sentry_sdk.enable_integration(CountingIntegration()) + assert len(setup_count) == 1 + + +def test_enable_integration_after_disabled(sentry_init): + from sentry_sdk.integrations.gnu_backtrace import GnuBacktraceIntegration + + sentry_init(disabled_integrations=[GnuBacktraceIntegration]) + + assert "gnu_backtrace" not in get_client().integrations + + sentry_sdk.enable_integration(GnuBacktraceIntegration()) + + assert "gnu_backtrace" in get_client().integrations + + class TracingTestClass: @staticmethod def static(arg):