Skip to content

Commit e71bf03

Browse files
committed
Merge branch 'master' into feat/span-first-2
2 parents 7e33320 + 3373da2 commit e71bf03

File tree

8 files changed

+278
-162
lines changed

8 files changed

+278
-162
lines changed

CHANGELOG.md

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,69 @@
22

33
## 2.52.0a2
44

5-
### New Features ✨
5+
This is an alpha release for internal testing of span streaming.
66

7-
#### Span Streaming
7+
## 2.51.0a1
88

9-
- feat(span-streaming): Add spans to telemetry pipeline, add span name and attributes (3) by @sentrivana in [#5399](https://github.com/getsentry/sentry-python/pull/5399)
10-
- feat(span-streaming): Add span batcher (2) by @sentrivana in [#5398](https://github.com/getsentry/sentry-python/pull/5398)
9+
This is an alpha release for internal testing of span streaming.
1110

12-
### Internal Changes 🔧
11+
## 2.52.0
1312

14-
- test(mcp): Simulate stdio transport with memory streams by @alexander-alderman-webb in [#5329](https://github.com/getsentry/sentry-python/pull/5329)
13+
### New Features ✨
1514

16-
### Other
15+
#### Other
1716

18-
- [do not merge] feat: Span streaming & new span API by @sentrivana in [#5317](https://github.com/getsentry/sentry-python/pull/5317)
17+
- feat(integration): add `gen_ai.conversation.id` if available by @constantinius in [#5307](https://github.com/getsentry/sentry-python/pull/5307)
1918

20-
## 2.52.0a1
19+
### Bug Fixes 🐛
2120

22-
### New Features ✨
21+
#### Google Genai
2322

24-
#### Openai
23+
- fix(google-genai): Token reporting by @alexander-alderman-webb in [#5404](https://github.com/getsentry/sentry-python/pull/5404)
24+
- fix(google-genai): deactivate google genai when langchain is used by @shellmayr in [#5389](https://github.com/getsentry/sentry-python/pull/5389)
2525

26-
- feat(openai): Set system instruction attribute for Responses API by @alexander-alderman-webb in [#5376](https://github.com/getsentry/sentry-python/pull/5376)
27-
- feat(openai): Set system instruction attribute for Completions API by @alexander-alderman-webb in [#5358](https://github.com/getsentry/sentry-python/pull/5358)
26+
#### Mcp
2827

29-
#### Other
28+
- fix(mcp): Nest MCP spans under HTTP transactions by @alexander-alderman-webb in [#5292](https://github.com/getsentry/sentry-python/pull/5292)
29+
- fix(mcp): Handle all awaitable return types by @alexander-alderman-webb in [#5415](https://github.com/getsentry/sentry-python/pull/5415)
3030

31-
- feat(ai): Add original input length meta attribute by @alexander-alderman-webb in [#5375](https://github.com/getsentry/sentry-python/pull/5375)
32-
- feat(openai-agents): Set system instruction attribute on `gen_ai.chat` spans by @alexander-alderman-webb in [#5370](https://github.com/getsentry/sentry-python/pull/5370)
33-
- feat(span-streaming): Add experimental `trace_lifecycle` switch (1) by @sentrivana in [#5397](https://github.com/getsentry/sentry-python/pull/5397)
34-
- feat(transport): Report 413 responses for oversized envelopes by @alexander-alderman-webb in [#5380](https://github.com/getsentry/sentry-python/pull/5380)
35-
36-
### Bug Fixes 🐛
31+
#### Other
3732

38-
- fix(ai): Keep single content input message by @alexander-alderman-webb in [#5345](https://github.com/getsentry/sentry-python/pull/5345)
33+
- fix(anthropic): Token reporting by @alexander-alderman-webb in [#5403](https://github.com/getsentry/sentry-python/pull/5403)
3934
- fix(arq): handle settings_cls passed as keyword argument by @nc9 in [#5393](https://github.com/getsentry/sentry-python/pull/5393)
4035
- fix(dramatiq): cleanup isolated scope and transaction when message is skipped by @frankie567 in [#5346](https://github.com/getsentry/sentry-python/pull/5346)
41-
- fix(google-genai): deactivate google genai when langchain is used by @shellmayr in [#5389](https://github.com/getsentry/sentry-python/pull/5389)
36+
- fix(openai): Token reporting by @alexander-alderman-webb in [#5406](https://github.com/getsentry/sentry-python/pull/5406)
37+
- fix(openai-agents): Inject propagation headers for `HostedMCPTool` when streaming by @alexander-alderman-webb in [#5405](https://github.com/getsentry/sentry-python/pull/5405)
38+
- fix: Fix list attribute type by @sentrivana in [#5417](https://github.com/getsentry/sentry-python/pull/5417)
39+
- fix: Adapt to new packaging in toxgen by @sentrivana in [#5382](https://github.com/getsentry/sentry-python/pull/5382)
4240

4341
### Internal Changes 🔧
4442

45-
- ci: migration to the new codecov action by @MathurAditya724 in [#5392](https://github.com/getsentry/sentry-python/pull/5392)
46-
- ref: Replace `set_data_normalized()` with `Span.set_data()` for system instructions by @alexander-alderman-webb in [#5374](https://github.com/getsentry/sentry-python/pull/5374)
43+
#### Fastmcp
4744

48-
### Other
45+
- test(fastmcp): Wrap prompt in `Message` by @alexander-alderman-webb in [#5411](https://github.com/getsentry/sentry-python/pull/5411)
46+
- test(fastmcp): Remove `test_fastmcp_without_request_context()` by @alexander-alderman-webb in [#5412](https://github.com/getsentry/sentry-python/pull/5412)
47+
- test(fastmcp): Use `AsyncClient` for SSE by @alexander-alderman-webb in [#5400](https://github.com/getsentry/sentry-python/pull/5400)
48+
- test(fastmcp): Use `TestClient` for Streamable HTTP by @alexander-alderman-webb in [#5384](https://github.com/getsentry/sentry-python/pull/5384)
49+
- test(fastmcp): Simulate stdio transport with memory streams by @alexander-alderman-webb in [#5333](https://github.com/getsentry/sentry-python/pull/5333)
4950

50-
- [do not merge] feat: Span streaming & new span API by @sentrivana in [#5317](https://github.com/getsentry/sentry-python/pull/5317)
51-
- Update CHANGELOG.md by @alexander-alderman-webb in [8517eb0a](https://github.com/getsentry/sentry-python/commit/8517eb0a0750796ae73d0e4c020b0a71c7724d0a)
52-
- release: 2.51.0 by @alexander-alderman-webb in [93e89e4c](https://github.com/getsentry/sentry-python/commit/93e89e4c1b7e837c03dd62a81951e56634d4a9c0)
51+
#### Mcp
5352

54-
## 2.51.0a1
53+
- test(mcp): Use `AsyncClient` for SSE by @alexander-alderman-webb in [#5396](https://github.com/getsentry/sentry-python/pull/5396)
54+
- test(mcp): Use `TestClient` for Streamable HTTP by @alexander-alderman-webb in [#5383](https://github.com/getsentry/sentry-python/pull/5383)
55+
- test(mcp): Remove unused stdio helpers by @alexander-alderman-webb in [#5409](https://github.com/getsentry/sentry-python/pull/5409)
56+
- test(mcp): Simulate stdio transport with memory streams by @alexander-alderman-webb in [#5329](https://github.com/getsentry/sentry-python/pull/5329)
5557

56-
This is an alpha release for internal testing of span streaming.
58+
#### Other
59+
60+
- ci: Fix lint step by @sentrivana in [#5418](https://github.com/getsentry/sentry-python/pull/5418)
61+
- ci: 🤖 Update test matrix with new releases (02/02) by @github-actions in [#5413](https://github.com/getsentry/sentry-python/pull/5413)
62+
- ci: Update tox and pin packaging version for tox by @alexander-alderman-webb in [#5381](https://github.com/getsentry/sentry-python/pull/5381)
63+
- ci: migration to the new codecov action by @MathurAditya724 in [#5392](https://github.com/getsentry/sentry-python/pull/5392)
64+
65+
### Other
66+
67+
- Revert "feat(ai): Add original input length meta attribute (#5375)" by @alexander-alderman-webb in [#5419](https://github.com/getsentry/sentry-python/pull/5419)
5768

5869
## 2.51.0
5970

sentry_sdk/_span_batcher.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ def _to_transport_format(item: "StreamedSpan") -> "Any":
7070
res: "dict[str, Any]" = {
7171
"trace_id": item.trace_id,
7272
"span_id": item.span_id,
73-
"name": item.get_name(),
74-
"status": item.status,
73+
"name": item._name,
74+
"status": item._status,
7575
"is_segment": item.is_segment(),
7676
"start_timestamp": item.start_timestamp.timestamp(),
7777
}

sentry_sdk/integrations/mcp.py

Lines changed: 123 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
from sentry_sdk.integrations import Integration, DidNotEnable
1818
from sentry_sdk.utils import safe_serialize
1919
from sentry_sdk.scope import should_send_default_pii
20+
from sentry_sdk.integrations._wsgi_common import nullcontext
2021

2122
try:
2223
from mcp.server.lowlevel import Server # type: ignore[import-not-found]
2324
from mcp.server.lowlevel.server import request_ctx # type: ignore[import-not-found]
25+
from mcp.server.streamable_http import StreamableHTTPServerTransport # type: ignore[import-not-found]
2426
except ImportError:
2527
raise DidNotEnable("MCP SDK not installed")
2628

@@ -31,7 +33,9 @@
3133

3234

3335
if TYPE_CHECKING:
34-
from typing import Any, Callable, Optional
36+
from typing import Any, Callable, Optional, Tuple, ContextManager
37+
38+
from starlette.types import Receive, Scope, Send # type: ignore[import-not-found]
3539

3640

3741
class MCPIntegration(Integration):
@@ -54,11 +58,34 @@ def setup_once() -> None:
5458
Patches MCP server classes to instrument handler execution.
5559
"""
5660
_patch_lowlevel_server()
61+
_patch_handle_request()
5762

5863
if FastMCP is not None:
5964
_patch_fastmcp()
6065

6166

67+
def _get_active_http_scopes() -> (
68+
"Optional[Tuple[Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]]]"
69+
):
70+
try:
71+
ctx = request_ctx.get()
72+
except LookupError:
73+
return None
74+
75+
if (
76+
ctx is None
77+
or not hasattr(ctx, "request")
78+
or ctx.request is None
79+
or "state" not in ctx.request.scope
80+
):
81+
return None
82+
83+
return (
84+
ctx.request.scope["state"].get("sentry_sdk.isolation_scope"),
85+
ctx.request.scope["state"].get("sentry_sdk.current_scope"),
86+
)
87+
88+
6289
def _get_request_context_data() -> "tuple[Optional[str], Optional[str], str]":
6390
"""
6491
Extract request ID, session ID, and MCP transport type from the request context.
@@ -382,60 +409,85 @@ async def _handler_wrapper(
382409
result_data_key,
383410
) = _prepare_handler_data(handler_type, original_args, original_kwargs)
384411

385-
# Start span and execute
386-
with get_start_span_function()(
387-
op=OP.MCP_SERVER,
388-
name=span_name,
389-
origin=MCPIntegration.origin,
390-
) as span:
391-
# Get request ID, session ID, and transport from context
392-
request_id, session_id, mcp_transport = _get_request_context_data()
393-
394-
# Set input span data
395-
_set_span_input_data(
396-
span,
397-
handler_name,
398-
span_data_key,
399-
mcp_method_name,
400-
arguments,
401-
request_id,
402-
session_id,
403-
mcp_transport,
412+
scopes = _get_active_http_scopes()
413+
414+
isolation_scope_context: "ContextManager[Any]"
415+
current_scope_context: "ContextManager[Any]"
416+
417+
if scopes is None:
418+
isolation_scope_context = nullcontext()
419+
current_scope_context = nullcontext()
420+
else:
421+
isolation_scope, current_scope = scopes
422+
423+
isolation_scope_context = (
424+
nullcontext()
425+
if isolation_scope is None
426+
else sentry_sdk.scope.use_isolation_scope(isolation_scope)
427+
)
428+
current_scope_context = (
429+
nullcontext()
430+
if current_scope is None
431+
else sentry_sdk.scope.use_scope(current_scope)
404432
)
405433

406-
# For resources, extract and set protocol
407-
if handler_type == "resource":
408-
if original_args:
409-
uri = original_args[0]
410-
else:
411-
uri = original_kwargs.get("uri")
412-
413-
protocol = None
414-
if hasattr(uri, "scheme"):
415-
protocol = uri.scheme
416-
elif handler_name and "://" in handler_name:
417-
protocol = handler_name.split("://")[0]
418-
if protocol:
419-
span.set_data(SPANDATA.MCP_RESOURCE_PROTOCOL, protocol)
434+
# Get request ID, session ID, and transport from context
435+
request_id, session_id, mcp_transport = _get_request_context_data()
420436

421-
try:
422-
# Execute the async handler
423-
if self is not None:
424-
original_args = (self, *original_args)
425-
426-
result = func(*original_args, **original_kwargs)
427-
if force_await or inspect.isawaitable(result):
428-
result = await result
429-
430-
except Exception as e:
431-
# Set error flag for tools
432-
if handler_type == "tool":
433-
span.set_data(SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True)
434-
sentry_sdk.capture_exception(e)
435-
raise
436-
437-
_set_span_output_data(span, result, result_data_key, handler_type)
438-
return result
437+
# Start span and execute
438+
with isolation_scope_context:
439+
with current_scope_context:
440+
with get_start_span_function()(
441+
op=OP.MCP_SERVER,
442+
name=span_name,
443+
origin=MCPIntegration.origin,
444+
) as span:
445+
# Set input span data
446+
_set_span_input_data(
447+
span,
448+
handler_name,
449+
span_data_key,
450+
mcp_method_name,
451+
arguments,
452+
request_id,
453+
session_id,
454+
mcp_transport,
455+
)
456+
457+
# For resources, extract and set protocol
458+
if handler_type == "resource":
459+
if original_args:
460+
uri = original_args[0]
461+
else:
462+
uri = original_kwargs.get("uri")
463+
464+
protocol = None
465+
if hasattr(uri, "scheme"):
466+
protocol = uri.scheme
467+
elif handler_name and "://" in handler_name:
468+
protocol = handler_name.split("://")[0]
469+
if protocol:
470+
span.set_data(SPANDATA.MCP_RESOURCE_PROTOCOL, protocol)
471+
472+
try:
473+
# Execute the async handler
474+
if self is not None:
475+
original_args = (self, *original_args)
476+
477+
result = func(*original_args, **original_kwargs)
478+
if force_await or inspect.isawaitable(result):
479+
result = await result
480+
481+
except Exception as e:
482+
# Set error flag for tools
483+
if handler_type == "tool":
484+
span.set_data(SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True)
485+
sentry_sdk.capture_exception(e)
486+
raise
487+
488+
_set_span_output_data(span, result, result_data_key, handler_type)
489+
490+
return result
439491

440492

441493
def _create_instrumented_decorator(
@@ -521,6 +573,25 @@ def patched_read_resource(
521573
Server.read_resource = patched_read_resource
522574

523575

576+
def _patch_handle_request() -> None:
577+
original_handle_request = StreamableHTTPServerTransport.handle_request
578+
579+
@wraps(original_handle_request)
580+
async def patched_handle_request(
581+
self: "StreamableHTTPServerTransport",
582+
scope: "Scope",
583+
receive: "Receive",
584+
send: "Send",
585+
) -> None:
586+
scope.setdefault("state", {})["sentry_sdk.isolation_scope"] = (
587+
sentry_sdk.get_isolation_scope()
588+
)
589+
scope["state"]["sentry_sdk.current_scope"] = sentry_sdk.get_current_scope()
590+
await original_handle_request(self, scope, receive, send)
591+
592+
StreamableHTTPServerTransport.handle_request = patched_handle_request
593+
594+
524595
def _patch_fastmcp() -> None:
525596
"""
526597
Patches the standalone fastmcp package's FastMCP class.

0 commit comments

Comments
 (0)