Skip to content

Commit e910302

Browse files
feat(integrations): add ability to auto-deactivate lower-level integrations based on map (#5052)
- Add a map that contains integrations that should be deactivated when others are active to prevent duplicate spans, etc. - Allow positive and negative overrides by users - Add the existing langchain deactivations for anthropic and openai - Simplifies setup for users, because they don't have to add the lower level integrations to the disabled integrations array in the init --------- Co-authored-by: Alexander Alderman Webb <[email protected]>
1 parent 7264a9f commit e910302

File tree

9 files changed

+312
-29
lines changed

9 files changed

+312
-29
lines changed

.github/workflows/test-integrations-misc.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ jobs:
7474
run: |
7575
set -x # print commands that are executed
7676
./scripts/runtox.sh "py${{ matrix.python-version }}-typer"
77+
- name: Test integration_deactivation
78+
run: |
79+
set -x # print commands that are executed
80+
./scripts/runtox.sh "py${{ matrix.python-version }}-integration_deactivation"
7781
- name: Generate coverage XML (Python 3.6)
7882
if: ${{ !cancelled() && matrix.python-version == '3.6' }}
7983
run: |

scripts/populate_tox/package_dependencies.jsonl

Lines changed: 8 additions & 8 deletions
Large diffs are not rendered by default.

scripts/populate_tox/populate_tox.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"aws_lambda",
6565
"cloud_resource_context",
6666
"common",
67+
"integration_deactivation",
6768
"gcp",
6869
"gevent",
6970
"opentelemetry",

scripts/populate_tox/releases.jsonl

Lines changed: 9 additions & 9 deletions
Large diffs are not rendered by default.

scripts/populate_tox/tox.jinja

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ envlist =
2323
# === Gevent ===
2424
{py3.6,py3.8,py3.10,py3.11,py3.12}-gevent
2525

26+
# === Integration Deactivation ===
27+
{py3.9,py3.10,py3.11,py3.12,py3.13,py3.14}-integration_deactivation
28+
2629
# === Integrations ===
2730

2831
# Asgi
@@ -88,6 +91,11 @@ deps =
8891
{py3.10,py3.11}-gevent: zope.event<5.0.0
8992
{py3.10,py3.11}-gevent: zope.interface<8.0
9093
94+
# === Integration Deactivation ===
95+
integration_deactivation: openai
96+
integration_deactivation: anthropic
97+
integration_deactivation: langchain
98+
9199
# === Integrations ===
92100
93101
# Asgi
@@ -144,6 +152,7 @@ setenv =
144152
# TESTPATH definitions for test suites not managed by toxgen
145153
common: TESTPATH=tests
146154
gevent: TESTPATH=tests
155+
integration_deactivation: TESTPATH=tests/test_ai_integration_deactivation.py
147156
asgi: TESTPATH=tests/integrations/asgi
148157
aws_lambda: TESTPATH=tests/integrations/aws_lambda
149158
cloud_resource_context: TESTPATH=tests/integrations/cloud_resource_context

scripts/split_tox_gh_actions/split_tox_gh_actions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
"pure_eval",
155155
"trytond",
156156
"typer",
157+
"integration_deactivation",
157158
],
158159
}
159160

sentry_sdk/integrations/__init__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ def iter_default_integrations(with_auto_enabling_integrations):
171171
}
172172

173173

174+
_INTEGRATION_DEACTIVATES = {
175+
"langchain": {"openai", "anthropic"},
176+
}
177+
178+
174179
def setup_integrations(
175180
integrations, # type: Sequence[Integration]
176181
with_defaults=True, # type: bool
@@ -187,13 +192,24 @@ def setup_integrations(
187192
188193
`disabled_integrations` takes precedence over `with_defaults` and
189194
`with_auto_enabling_integrations`.
195+
196+
Some integrations are designed to automatically deactivate other integrations
197+
in order to avoid conflicts and prevent duplicate telemetry from being collected.
198+
For example, enabling the `langchain` integration will auto-deactivate both the
199+
`openai` and `anthropic` integrations.
200+
201+
Users can override this behavior by:
202+
- Explicitly providing an integration in the `integrations=[]` list, or
203+
- Disabling the higher-level integration via the `disabled_integrations` option.
190204
"""
191205
integrations = dict(
192206
(integration.identifier, integration) for integration in integrations or ()
193207
)
194208

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

211+
user_provided_integrations = set(integrations.keys())
212+
197213
# Integrations that will not be enabled
198214
disabled_integrations = [
199215
integration if isinstance(integration, type) else type(integration)
@@ -212,6 +228,27 @@ def setup_integrations(
212228
integrations[instance.identifier] = instance
213229
used_as_default_integration.add(instance.identifier)
214230

231+
disabled_integration_identifiers = {
232+
integration.identifier for integration in disabled_integrations
233+
}
234+
235+
for integration, targets_to_deactivate in _INTEGRATION_DEACTIVATES.items():
236+
if (
237+
integration in integrations
238+
and integration not in disabled_integration_identifiers
239+
):
240+
for target in targets_to_deactivate:
241+
if target not in user_provided_integrations:
242+
for cls in iter_default_integrations(True):
243+
if cls.identifier == target:
244+
if cls not in disabled_integrations:
245+
disabled_integrations.append(cls)
246+
logger.debug(
247+
"Auto-deactivating %s integration because %s integration is active",
248+
target,
249+
integration,
250+
)
251+
215252
for identifier, integration in integrations.items():
216253
with _installer_lock:
217254
if identifier not in _processed_integrations:
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import pytest
2+
3+
from sentry_sdk import get_client
4+
from sentry_sdk.integrations import _INTEGRATION_DEACTIVATES
5+
6+
7+
try:
8+
from sentry_sdk.integrations.langchain import LangchainIntegration
9+
10+
has_langchain = True
11+
except Exception:
12+
has_langchain = False
13+
14+
try:
15+
from sentry_sdk.integrations.openai import OpenAIIntegration
16+
17+
has_openai = True
18+
except Exception:
19+
has_openai = False
20+
21+
try:
22+
from sentry_sdk.integrations.anthropic import AnthropicIntegration
23+
24+
has_anthropic = True
25+
except Exception:
26+
has_anthropic = False
27+
28+
29+
pytestmark = pytest.mark.skipif(
30+
not (has_langchain and has_openai and has_anthropic),
31+
reason="Requires langchain, openai, and anthropic packages to be installed",
32+
)
33+
34+
35+
def test_integration_deactivates_map_exists():
36+
assert "langchain" in _INTEGRATION_DEACTIVATES
37+
assert "openai" in _INTEGRATION_DEACTIVATES["langchain"]
38+
assert "anthropic" in _INTEGRATION_DEACTIVATES["langchain"]
39+
40+
41+
def test_langchain_auto_deactivates_openai_and_anthropic(
42+
sentry_init, reset_integrations
43+
):
44+
sentry_init(
45+
default_integrations=False,
46+
auto_enabling_integrations=True,
47+
)
48+
49+
client = get_client()
50+
integration_types = {
51+
type(integration) for integration in client.integrations.values()
52+
}
53+
54+
if LangchainIntegration in integration_types:
55+
assert OpenAIIntegration not in integration_types
56+
assert AnthropicIntegration not in integration_types
57+
58+
59+
def test_user_can_override_with_explicit_openai(sentry_init, reset_integrations):
60+
sentry_init(
61+
default_integrations=False,
62+
auto_enabling_integrations=True,
63+
integrations=[OpenAIIntegration()],
64+
)
65+
66+
client = get_client()
67+
integration_types = {
68+
type(integration) for integration in client.integrations.values()
69+
}
70+
71+
assert OpenAIIntegration in integration_types
72+
73+
74+
def test_user_can_override_with_explicit_anthropic(sentry_init, reset_integrations):
75+
sentry_init(
76+
default_integrations=False,
77+
auto_enabling_integrations=True,
78+
integrations=[AnthropicIntegration()],
79+
)
80+
81+
client = get_client()
82+
integration_types = {
83+
type(integration) for integration in client.integrations.values()
84+
}
85+
86+
assert AnthropicIntegration in integration_types
87+
88+
89+
def test_user_can_override_with_both_explicit_integrations(
90+
sentry_init, reset_integrations
91+
):
92+
sentry_init(
93+
default_integrations=False,
94+
auto_enabling_integrations=True,
95+
integrations=[OpenAIIntegration(), AnthropicIntegration()],
96+
)
97+
98+
client = get_client()
99+
integration_types = {
100+
type(integration) for integration in client.integrations.values()
101+
}
102+
103+
assert OpenAIIntegration in integration_types
104+
assert AnthropicIntegration in integration_types
105+
106+
107+
def test_disabling_langchain_allows_openai_and_anthropic(
108+
sentry_init, reset_integrations
109+
):
110+
sentry_init(
111+
default_integrations=False,
112+
auto_enabling_integrations=True,
113+
disabled_integrations=[LangchainIntegration],
114+
)
115+
116+
client = get_client()
117+
integration_types = {
118+
type(integration) for integration in client.integrations.values()
119+
}
120+
121+
assert LangchainIntegration not in integration_types
122+
123+
124+
def test_explicit_langchain_still_deactivates_others(sentry_init, reset_integrations):
125+
sentry_init(
126+
default_integrations=False,
127+
auto_enabling_integrations=False,
128+
integrations=[LangchainIntegration()],
129+
)
130+
131+
client = get_client()
132+
integration_types = {
133+
type(integration) for integration in client.integrations.values()
134+
}
135+
136+
if LangchainIntegration in integration_types:
137+
assert OpenAIIntegration not in integration_types
138+
assert AnthropicIntegration not in integration_types
139+
140+
141+
def test_langchain_and_openai_both_explicit_both_active(
142+
sentry_init, reset_integrations
143+
):
144+
sentry_init(
145+
default_integrations=False,
146+
auto_enabling_integrations=False,
147+
integrations=[LangchainIntegration(), OpenAIIntegration()],
148+
)
149+
150+
client = get_client()
151+
integration_types = {
152+
type(integration) for integration in client.integrations.values()
153+
}
154+
155+
assert LangchainIntegration in integration_types
156+
assert OpenAIIntegration in integration_types
157+
158+
159+
def test_no_langchain_means_openai_and_anthropic_can_auto_enable(
160+
sentry_init, reset_integrations, monkeypatch
161+
):
162+
import sys
163+
import sentry_sdk.integrations
164+
165+
old_iter = sentry_sdk.integrations.iter_default_integrations
166+
167+
def filtered_iter(with_auto_enabling):
168+
for cls in old_iter(with_auto_enabling):
169+
if cls.identifier != "langchain":
170+
yield cls
171+
172+
monkeypatch.setattr(
173+
sentry_sdk.integrations, "iter_default_integrations", filtered_iter
174+
)
175+
176+
sentry_init(
177+
default_integrations=False,
178+
auto_enabling_integrations=True,
179+
)
180+
181+
client = get_client()
182+
integration_types = {
183+
type(integration) for integration in client.integrations.values()
184+
}
185+
186+
assert LangchainIntegration not in integration_types
187+
188+
189+
def test_deactivation_with_default_integrations_enabled(
190+
sentry_init, reset_integrations
191+
):
192+
sentry_init(
193+
default_integrations=True,
194+
auto_enabling_integrations=True,
195+
)
196+
197+
client = get_client()
198+
integration_types = {
199+
type(integration) for integration in client.integrations.values()
200+
}
201+
202+
if LangchainIntegration in integration_types:
203+
assert OpenAIIntegration not in integration_types
204+
assert AnthropicIntegration not in integration_types
205+
206+
207+
def test_only_auto_enabling_integrations_without_defaults(
208+
sentry_init, reset_integrations
209+
):
210+
sentry_init(
211+
default_integrations=False,
212+
auto_enabling_integrations=True,
213+
)
214+
215+
client = get_client()
216+
integration_types = {
217+
type(integration) for integration in client.integrations.values()
218+
}
219+
220+
if LangchainIntegration in integration_types:
221+
assert OpenAIIntegration not in integration_types
222+
assert AnthropicIntegration not in integration_types

0 commit comments

Comments
 (0)