Skip to content
1 change: 1 addition & 0 deletions sentry_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"capture_message",
"configure_scope",
"continue_trace",
"enable_integration",
"flush",
"get_baggage",
"get_client",
Expand Down
34 changes: 34 additions & 0 deletions sentry_sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,6 +58,7 @@ def overload(x: "T") -> "T":
"capture_message",
"configure_scope",
"continue_trace",
"enable_integration",
"flush",
"get_baggage",
"get_client",
Expand Down Expand Up @@ -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

This comment was marked as outdated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how. The actual logic is wrapped in a lock.


client.integrations[integration.identifier] = integration
1 change: 1 addition & 0 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}}
Expand Down
26 changes: 26 additions & 0 deletions sentry_sdk/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -279,6 +280,31 @@ def setup_integrations(
return integrations


def _enable_integration(integration: "Integration") -> "Optional[Integration]":
with _installer_lock:
client = sentry_sdk.get_client()

Comment on lines +283 to +286
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: If an integration fails to initialize, enable_integration can be called later, causing its non-idempotent setup_once method to run a second time.
Severity: HIGH | Confidence: High

🔍 Detailed Analysis

If an integration fails to initialize during sentry_init by raising DidNotEnable, its identifier is added to _processed_integrations but not _installed_integrations. A subsequent manual call to enable_integration() for the same integration will bypass the initial check (which verifies against _installed_integrations) and call setup_once() a second time. Since many integrations like Threading and Stdlib perform non-idempotent patching in setup_once(), this double-execution can lead to double-patching of methods, causing unpredictable behavior and performance degradation.

💡 Suggested Fix

In _enable_integration(), add a check to see if the integration identifier is already in _processed_integrations. If it is, avoid calling setup_once() again, as this indicates a setup attempt has already occurred, regardless of whether it succeeded or failed.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: sentry_sdk/integrations/__init__.py#L283-L286

Potential issue: If an integration fails to initialize during `sentry_init` by raising
`DidNotEnable`, its identifier is added to `_processed_integrations` but not
`_installed_integrations`. A subsequent manual call to `enable_integration()` for the
same integration will bypass the initial check (which verifies against
`_installed_integrations`) and call `setup_once()` a second time. Since many
integrations like `Threading` and `Stdlib` perform non-idempotent patching in
`setup_once()`, this double-execution can lead to double-patching of methods, causing
unpredictable behavior and performance degradation.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 8341613

if (
integration.identifier in _installed_integrations
and integration.identifier in client.integrations
):
return client.integrations[integration.identifier]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setup_once() can be called multiple times when conditions mismatch

Medium Severity

The guard condition in _enable_integration requires both _installed_integrations and client.integrations to contain the integration identifier to skip setup. However, setup_once() has a "called once, ever" contract per its docstring. If an integration was globally installed but not registered with the current client (e.g., after SDK re-initialization or due to a race condition where client.integrations is updated outside the lock), setup_once() gets called again. This could cause double-patching of libraries or duplicate hooks. The check for calling setup_once() needs to consider _installed_integrations alone.

Additional Locations (1)

Fix in Cursor Fix in Web


logger.debug(
"Setting up previously not enabled integration %s", integration.identifier
)
_processed_integrations.add(integration.identifier)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed enable_integration blocks subsequent sentry_init setup

Medium Severity

The _processed_integrations.add() call is placed before the try block, meaning the integration is marked as processed regardless of whether setup succeeds or fails. In contrast, setup_integrations adds to _processed_integrations after the try-except block. If enable_integration fails (with DidNotEnable or any other exception), the integration is still in _processed_integrations. A subsequent sentry_init(integrations=[...]) call would then silently skip this integration because setup_integrations checks identifier not in _processed_integrations, preventing the user from retrying via SDK re-initialization.

Fix in Cursor Fix in Web

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, ...]]",
Expand Down
116 changes: 116 additions & 0 deletions tests/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading