Skip to content

Commit e83b34f

Browse files
.
2 parents ec7cd14 + 6a76cc5 commit e83b34f

File tree

15 files changed

+512
-76
lines changed

15 files changed

+512
-76
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/client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
get_before_send_log,
2828
get_before_send_metric,
2929
has_logs_enabled,
30+
has_metrics_enabled,
3031
)
3132
from sentry_sdk.serializer import serialize
3233
from sentry_sdk.tracing import trace
@@ -374,7 +375,9 @@ def _capture_envelope(envelope):
374375

375376
self.log_batcher = LogBatcher(capture_func=_capture_envelope)
376377

377-
self.metrics_batcher = MetricsBatcher(capture_func=_capture_envelope)
378+
self.metrics_batcher = None
379+
if has_metrics_enabled(self.options):
380+
self.metrics_batcher = MetricsBatcher(capture_func=_capture_envelope)
378381

379382
max_request_body_size = ("always", "never", "small", "medium")
380383
if self.options["max_request_body_size"] not in max_request_body_size:
@@ -975,7 +978,7 @@ def _capture_log(self, log):
975978

976979
def _capture_metric(self, metric):
977980
# type: (Optional[Metric]) -> None
978-
if metric is None:
981+
if not has_metrics_enabled(self.options) or metric is None:
979982
return
980983

981984
current_scope = sentry_sdk.get_current_scope()

sentry_sdk/consts.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,12 @@ class SPANDATA:
706706
Example: 6379
707707
"""
708708

709+
NETWORK_TRANSPORT = "network.transport"
710+
"""
711+
The transport protocol used for the network connection.
712+
Example: "tcp", "udp", "unix"
713+
"""
714+
709715
PROFILER_ID = "profiler_id"
710716
"""
711717
Label identifying the profiler id that the span occurred in. This should be a string.
@@ -824,7 +830,7 @@ class SPANDATA:
824830
MCP_TRANSPORT = "mcp.transport"
825831
"""
826832
The transport method used for MCP communication.
827-
Example: "pipe" (stdio), "tcp" (HTTP/WebSocket/SSE)
833+
Example: "http", "sse", "stdio"
828834
"""
829835

830836
MCP_SESSION_ID = "mcp.session.id"
@@ -1005,6 +1011,7 @@ def __init__(
10051011
enable_logs=False, # type: bool
10061012
before_send_log=None, # type: Optional[Callable[[Log, Hint], Optional[Log]]]
10071013
trace_ignore_status_codes=frozenset(), # type: AbstractSet[int]
1014+
enable_metrics=True, # type: bool
10081015
before_send_metric=None, # type: Optional[Callable[[Metric, Hint], Optional[Metric]]]
10091016
):
10101017
# type: (...) -> None

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:

sentry_sdk/integrations/mcp.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,34 +56,44 @@ def setup_once():
5656
def _get_request_context_data():
5757
# type: () -> tuple[Optional[str], Optional[str], str]
5858
"""
59-
Extract request ID, session ID, and transport type from the MCP request context.
59+
Extract request ID, session ID, and MCP transport type from the request context.
6060
6161
Returns:
62-
Tuple of (request_id, session_id, transport).
62+
Tuple of (request_id, session_id, mcp_transport).
6363
- request_id: May be None if not available
6464
- session_id: May be None if not available
65-
- transport: "tcp" for HTTP-based, "pipe" for stdio
65+
- mcp_transport: "http", "sse", "stdio"
6666
"""
6767
request_id = None # type: Optional[str]
6868
session_id = None # type: Optional[str]
69-
transport = "pipe" # type: str
69+
mcp_transport = "stdio" # type: str
7070

7171
try:
7272
ctx = request_ctx.get()
7373

7474
if ctx is not None:
7575
request_id = ctx.request_id
7676
if hasattr(ctx, "request") and ctx.request is not None:
77-
transport = "tcp"
7877
request = ctx.request
79-
if hasattr(request, "headers"):
78+
# Detect transport type by checking request characteristics
79+
if hasattr(request, "query_params") and request.query_params.get(
80+
"session_id"
81+
):
82+
# SSE transport uses query parameter
83+
mcp_transport = "sse"
84+
session_id = request.query_params.get("session_id")
85+
elif hasattr(request, "headers") and request.headers.get(
86+
"mcp-session-id"
87+
):
88+
# StreamableHTTP transport uses header
89+
mcp_transport = "http"
8090
session_id = request.headers.get("mcp-session-id")
8191

8292
except LookupError:
83-
# No request context available - default to pipe
93+
# No request context available - default to stdio
8494
pass
8595

86-
return request_id, session_id, transport
96+
return request_id, session_id, mcp_transport
8797

8898

8999
def _get_span_config(handler_type, item_name):
@@ -120,16 +130,20 @@ def _set_span_input_data(
120130
arguments,
121131
request_id,
122132
session_id,
123-
transport,
133+
mcp_transport,
124134
):
125135
# type: (Any, str, str, str, dict[str, Any], Optional[str], Optional[str], str) -> None
126136
"""Set input span data for MCP handlers."""
137+
127138
# Set handler identifier
128139
span.set_data(span_data_key, handler_name)
129140
span.set_data(SPANDATA.MCP_METHOD_NAME, mcp_method_name)
130141

131-
# Set transport type
132-
span.set_data(SPANDATA.MCP_TRANSPORT, transport)
142+
# Set transport/MCP transport type
143+
span.set_data(
144+
SPANDATA.NETWORK_TRANSPORT, "pipe" if mcp_transport == "stdio" else "tcp"
145+
)
146+
span.set_data(SPANDATA.MCP_TRANSPORT, mcp_transport)
133147

134148
# Set request_id if provided
135149
if request_id:
@@ -331,7 +345,7 @@ async def _async_handler_wrapper(handler_type, func, original_args):
331345
origin=MCPIntegration.origin,
332346
) as span:
333347
# Get request ID, session ID, and transport from context
334-
request_id, session_id, transport = _get_request_context_data()
348+
request_id, session_id, mcp_transport = _get_request_context_data()
335349

336350
# Set input span data
337351
_set_span_input_data(
@@ -342,7 +356,7 @@ async def _async_handler_wrapper(handler_type, func, original_args):
342356
arguments,
343357
request_id,
344358
session_id,
345-
transport,
359+
mcp_transport,
346360
)
347361

348362
# For resources, extract and set protocol
@@ -396,7 +410,7 @@ def _sync_handler_wrapper(handler_type, func, original_args):
396410
origin=MCPIntegration.origin,
397411
) as span:
398412
# Get request ID, session ID, and transport from context
399-
request_id, session_id, transport = _get_request_context_data()
413+
request_id, session_id, mcp_transport = _get_request_context_data()
400414

401415
# Set input span data
402416
_set_span_input_data(
@@ -407,7 +421,7 @@ def _sync_handler_wrapper(handler_type, func, original_args):
407421
arguments,
408422
request_id,
409423
session_id,
410-
transport,
424+
mcp_transport,
411425
)
412426

413427
# For resources, extract and set protocol

0 commit comments

Comments
 (0)