Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/test-integrations-misc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ jobs:
run: |
set -x # print commands that are executed
./scripts/runtox.sh "py${{ matrix.python-version }}-typer"
- name: Test integration_deactivation
run: |
set -x # print commands that are executed
./scripts/runtox.sh "py${{ matrix.python-version }}-integration_deactivation"
- name: Generate coverage XML (Python 3.6)
if: ${{ !cancelled() && matrix.python-version == '3.6' }}
run: |
Expand Down
14 changes: 7 additions & 7 deletions scripts/populate_tox/package_dependencies.jsonl

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions scripts/populate_tox/populate_tox.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"aws_lambda",
"cloud_resource_context",
"common",
"integration_deactivation",
"gcp",
"gevent",
"opentelemetry",
Expand Down
32 changes: 16 additions & 16 deletions scripts/populate_tox/releases.jsonl

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions scripts/populate_tox/tox.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ envlist =
# === Gevent ===
{py3.6,py3.8,py3.10,py3.11,py3.12}-gevent

# === Integration Deactivation ===
{py3.9,py3.10,py3.11,py3.12,py3.13,py3.14}-integration_deactivation

# === Integrations ===

# Asgi
Expand Down Expand Up @@ -88,6 +91,11 @@ deps =
{py3.10,py3.11}-gevent: zope.event<5.0.0
{py3.10,py3.11}-gevent: zope.interface<8.0

# === Integration Deactivation ===
integration_deactivation: openai
integration_deactivation: anthropic
integration_deactivation: langchain

# === Integrations ===

# Asgi
Expand Down Expand Up @@ -144,6 +152,7 @@ setenv =
# TESTPATH definitions for test suites not managed by toxgen
common: TESTPATH=tests
gevent: TESTPATH=tests
integration_deactivation: TESTPATH=tests/test_ai_integration_deactivation.py
asgi: TESTPATH=tests/integrations/asgi
aws_lambda: TESTPATH=tests/integrations/aws_lambda
cloud_resource_context: TESTPATH=tests/integrations/cloud_resource_context
Expand Down
1 change: 1 addition & 0 deletions scripts/split_tox_gh_actions/split_tox_gh_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
"pure_eval",
"trytond",
"typer",
"integration_deactivation",
],
}

Expand Down
37 changes: 37 additions & 0 deletions sentry_sdk/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ def iter_default_integrations(with_auto_enabling_integrations):
}


_INTEGRATION_DEACTIVATES = {
"langchain": {"openai", "anthropic"},
}


def setup_integrations(
integrations, # type: Sequence[Integration]
with_defaults=True, # type: bool
Expand All @@ -187,13 +192,24 @@ def setup_integrations(

`disabled_integrations` takes precedence over `with_defaults` and
`with_auto_enabling_integrations`.

Some integrations are designed to automatically deactivate other integrations
in order to avoid conflicts and prevent duplicate telemetry from being collected.
For example, enabling the `langchain` integration will auto-deactivate both the
`openai` and `anthropic` integrations.

Users can override this behavior by:
- Explicitly providing an integration in the `integrations=[]` list, or
- Disabling the higher-level integration via the `disabled_integrations` option.
"""
integrations = dict(
(integration.identifier, integration) for integration in integrations or ()
)

logger.debug("Setting up integrations (with default = %s)", with_defaults)

user_provided_integrations = set(integrations.keys())

# Integrations that will not be enabled
disabled_integrations = [
integration if isinstance(integration, type) else type(integration)
Expand All @@ -212,6 +228,27 @@ def setup_integrations(
integrations[instance.identifier] = instance
used_as_default_integration.add(instance.identifier)

disabled_integration_identifiers = {
integration.identifier for integration in disabled_integrations
}

for integration, targets_to_deactivate in _INTEGRATION_DEACTIVATES.items():
if (
integration in integrations
and integration not in disabled_integration_identifiers
):
for target in targets_to_deactivate:
if target not in user_provided_integrations:
for cls in iter_default_integrations(True):
if cls.identifier == target:
if cls not in disabled_integrations:
disabled_integrations.append(cls)
logger.debug(
"Auto-deactivating %s integration because %s integration is active",
target,
integration,
)

for identifier, integration in integrations.items():
with _installer_lock:
if identifier not in _processed_integrations:
Expand Down
222 changes: 222 additions & 0 deletions tests/test_ai_integration_deactivation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import pytest

from sentry_sdk import get_client
from sentry_sdk.integrations import _INTEGRATION_DEACTIVATES


try:
from sentry_sdk.integrations.langchain import LangchainIntegration

has_langchain = True
except Exception:
has_langchain = False

try:
from sentry_sdk.integrations.openai import OpenAIIntegration

has_openai = True
except Exception:
has_openai = False

try:
from sentry_sdk.integrations.anthropic import AnthropicIntegration

has_anthropic = True
except Exception:
has_anthropic = False


pytestmark = pytest.mark.skipif(
not (has_langchain and has_openai and has_anthropic),
reason="Requires langchain, openai, and anthropic packages to be installed",
)


def test_integration_deactivates_map_exists():
assert "langchain" in _INTEGRATION_DEACTIVATES
assert "openai" in _INTEGRATION_DEACTIVATES["langchain"]
assert "anthropic" in _INTEGRATION_DEACTIVATES["langchain"]


def test_langchain_auto_deactivates_openai_and_anthropic(
sentry_init, reset_integrations
):
sentry_init(
default_integrations=False,
auto_enabling_integrations=True,
)

client = get_client()
integration_types = {
type(integration) for integration in client.integrations.values()
}

if LangchainIntegration in integration_types:
assert OpenAIIntegration not in integration_types
assert AnthropicIntegration not in integration_types


def test_user_can_override_with_explicit_openai(sentry_init, reset_integrations):
sentry_init(
default_integrations=False,
auto_enabling_integrations=True,
integrations=[OpenAIIntegration()],
)

client = get_client()
integration_types = {
type(integration) for integration in client.integrations.values()
}

assert OpenAIIntegration in integration_types


def test_user_can_override_with_explicit_anthropic(sentry_init, reset_integrations):
sentry_init(
default_integrations=False,
auto_enabling_integrations=True,
integrations=[AnthropicIntegration()],
)

client = get_client()
integration_types = {
type(integration) for integration in client.integrations.values()
}

assert AnthropicIntegration in integration_types


def test_user_can_override_with_both_explicit_integrations(
sentry_init, reset_integrations
):
sentry_init(
default_integrations=False,
auto_enabling_integrations=True,
integrations=[OpenAIIntegration(), AnthropicIntegration()],
)

client = get_client()
integration_types = {
type(integration) for integration in client.integrations.values()
}

assert OpenAIIntegration in integration_types
assert AnthropicIntegration in integration_types


def test_disabling_langchain_allows_openai_and_anthropic(
sentry_init, reset_integrations
):
sentry_init(
default_integrations=False,
auto_enabling_integrations=True,
disabled_integrations=[LangchainIntegration],
)

client = get_client()
integration_types = {
type(integration) for integration in client.integrations.values()
}

assert LangchainIntegration not in integration_types


def test_explicit_langchain_still_deactivates_others(sentry_init, reset_integrations):
sentry_init(
default_integrations=False,
auto_enabling_integrations=False,
integrations=[LangchainIntegration()],
)

client = get_client()
integration_types = {
type(integration) for integration in client.integrations.values()
}

if LangchainIntegration in integration_types:
assert OpenAIIntegration not in integration_types
assert AnthropicIntegration not in integration_types


def test_langchain_and_openai_both_explicit_both_active(
sentry_init, reset_integrations
):
sentry_init(
default_integrations=False,
auto_enabling_integrations=False,
integrations=[LangchainIntegration(), OpenAIIntegration()],
)

client = get_client()
integration_types = {
type(integration) for integration in client.integrations.values()
}

assert LangchainIntegration in integration_types
assert OpenAIIntegration in integration_types


def test_no_langchain_means_openai_and_anthropic_can_auto_enable(
sentry_init, reset_integrations, monkeypatch
):
import sys
import sentry_sdk.integrations

old_iter = sentry_sdk.integrations.iter_default_integrations

def filtered_iter(with_auto_enabling):
for cls in old_iter(with_auto_enabling):
if cls.identifier != "langchain":
yield cls

monkeypatch.setattr(
sentry_sdk.integrations, "iter_default_integrations", filtered_iter
)

sentry_init(
default_integrations=False,
auto_enabling_integrations=True,
)

client = get_client()
integration_types = {
type(integration) for integration in client.integrations.values()
}

assert LangchainIntegration not in integration_types


def test_deactivation_with_default_integrations_enabled(
sentry_init, reset_integrations
):
sentry_init(
default_integrations=True,
auto_enabling_integrations=True,
)

client = get_client()
integration_types = {
type(integration) for integration in client.integrations.values()
}

if LangchainIntegration in integration_types:
assert OpenAIIntegration not in integration_types
assert AnthropicIntegration not in integration_types


def test_only_auto_enabling_integrations_without_defaults(
sentry_init, reset_integrations
):
sentry_init(
default_integrations=False,
auto_enabling_integrations=True,
)

client = get_client()
integration_types = {
type(integration) for integration in client.integrations.values()
}

if LangchainIntegration in integration_types:
assert OpenAIIntegration not in integration_types
assert AnthropicIntegration not in integration_types
Loading