Skip to content

Commit b9c3221

Browse files
committed
Merge branch 'master' into py-2322-port-event-processor-data-to-span-first-rme79
2 parents 5fafecf + 9c1d475 commit b9c3221

27 files changed

Lines changed: 1201 additions & 656 deletions

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
strategy:
3333
fail-fast: false
3434
matrix:
35-
python-version: ["3.7","3.8","3.9","3.10","3.12","3.13","3.14","3.14t"]
35+
python-version: ["3.7","3.8","3.10","3.12","3.13","3.14","3.14t"]
3636
# python3.6 reached EOL and is no longer being supported on
3737
# new versions of hosted runners on Github Actions
3838
# ubuntu-20.04 is the last version that supported python3.6

scripts/populate_tox/package_dependencies.jsonl

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

scripts/populate_tox/releases.jsonl

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

sentry_sdk/integrations/asgi.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"""
66

77
import sys
8-
import asyncio
98
import inspect
109
from copy import deepcopy
1110
from functools import partial
@@ -69,6 +68,13 @@
6968
TRANSACTION_STYLE_VALUES = ("endpoint", "url")
7069

7170

71+
# Vendored: https://github.com/Kludex/uvicorn/blob/b224045f5900b7f766743bcb16ba9fc3adea2606/uvicorn/_compat.py#L10-L13
72+
if sys.version_info >= (3, 14):
73+
from inspect import iscoroutinefunction
74+
else:
75+
from asyncio import iscoroutinefunction
76+
77+
7278
def _capture_exception(exc: "Any", mechanism_type: str = "asgi") -> None:
7379
event, hint = event_from_exception(
7480
exc,
@@ -87,10 +93,10 @@ def _looks_like_asgi3(app: "Any") -> bool:
8793
if inspect.isclass(app):
8894
return hasattr(app, "__await__")
8995
elif inspect.isfunction(app):
90-
return asyncio.iscoroutinefunction(app)
96+
return iscoroutinefunction(app)
9197
else:
9298
call = getattr(app, "__call__", None) # noqa
93-
return asyncio.iscoroutinefunction(call)
99+
return iscoroutinefunction(call)
94100

95101

96102
class SentryAsgiMiddleware:

sentry_sdk/integrations/celery/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
)
1515
from sentry_sdk.integrations.celery.utils import _now_seconds_since_epoch
1616
from sentry_sdk.integrations.logging import ignore_logger
17+
from sentry_sdk.scope import should_send_default_pii
1718
from sentry_sdk.traces import StreamedSpan
1819
from sentry_sdk.tracing import BAGGAGE_HEADER_NAME, Span, TransactionSource
1920
from sentry_sdk.tracing_utils import Baggage, has_span_streaming_enabled
2021
from sentry_sdk.utils import (
22+
SENSITIVE_DATA_SUBSTITUTE,
2123
capture_internal_exceptions,
2224
event_from_exception,
2325
reraise,
@@ -143,8 +145,12 @@ def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]":
143145
extra = event.setdefault("extra", {})
144146
extra["celery-job"] = {
145147
"task_name": task.name,
146-
"args": args,
147-
"kwargs": kwargs,
148+
"args": (
149+
args if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE
150+
),
151+
"kwargs": (
152+
kwargs if should_send_default_pii() else SENSITIVE_DATA_SUBSTITUTE
153+
),
148154
}
149155

150156
if "exc_info" in hint:

sentry_sdk/integrations/fastapi.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import asyncio
1+
import sys
22
from copy import deepcopy
33
from functools import wraps
44

@@ -34,6 +34,13 @@
3434
_DEFAULT_TRANSACTION_NAME = "generic FastAPI request"
3535

3636

37+
# Vendored: https://github.com/Kludex/starlette/blob/0a29b5ccdcbd1285c75c4fdb5d62ae1d244a21b0/starlette/_utils.py#L11-L17
38+
if sys.version_info >= (3, 13): # pragma: no cover
39+
from inspect import iscoroutinefunction
40+
else:
41+
from asyncio import iscoroutinefunction
42+
43+
3744
class FastApiIntegration(StarletteIntegration):
3845
identifier = "fastapi"
3946

@@ -76,7 +83,7 @@ def _sentry_get_request_handler(*args: "Any", **kwargs: "Any") -> "Any":
7683
if (
7784
dependant
7885
and dependant.call is not None
79-
and not asyncio.iscoroutinefunction(dependant.call)
86+
and not iscoroutinefunction(dependant.call)
8087
):
8188
old_call = dependant.call
8289

sentry_sdk/integrations/graphene.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ def graphql_span(
143143

144144
_graphql_span = sentry_sdk.start_span(op=op, name=operation_name)
145145

146-
_graphql_span.set_data("graphql.document", source)
146+
if should_send_default_pii():
147+
_graphql_span.set_data("graphql.document", source)
147148
_graphql_span.set_data("graphql.operation.name", operation_name)
148149
_graphql_span.set_data("graphql.operation.type", operation_type)
149150

sentry_sdk/integrations/grpc/aio/server.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def __init__(
2525
self: "ServerInterceptor",
2626
find_name: "Callable[[ServicerContext], str] | None" = None,
2727
) -> None:
28-
self._find_method_name = find_name or self._find_name
28+
self._custom_find_name = find_name
2929

3030
super().__init__()
3131

@@ -34,17 +34,21 @@ async def intercept_service(
3434
continuation: "Callable[[HandlerCallDetails], Awaitable[RpcMethodHandler]]",
3535
handler_call_details: "HandlerCallDetails",
3636
) -> "Optional[Awaitable[RpcMethodHandler]]":
37-
self._handler_call_details = handler_call_details
3837
handler = await continuation(handler_call_details)
3938
if handler is None:
4039
return None
4140

41+
method_name = handler_call_details.method
42+
custom_find_name = self._custom_find_name
43+
4244
if not handler.request_streaming and not handler.response_streaming:
4345
handler_factory = grpc.unary_unary_rpc_method_handler
4446

4547
async def wrapped(request: "Any", context: "ServicerContext") -> "Any":
4648
with sentry_sdk.isolation_scope():
47-
name = self._find_method_name(context)
49+
name = (
50+
custom_find_name(context) if custom_find_name else method_name
51+
)
4852
if not name:
4953
return await handler(request, context)
5054

@@ -96,6 +100,3 @@ async def wrapped(request: "Any", context: "ServicerContext") -> "Any": # type:
96100
request_deserializer=handler.request_deserializer,
97101
response_serializer=handler.response_serializer,
98102
)
99-
100-
def _find_name(self, context: "ServicerContext") -> str:
101-
return self._handler_call_details.method

sentry_sdk/integrations/mcp.py

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
from typing import TYPE_CHECKING
1313

1414
import sentry_sdk
15-
from sentry_sdk.ai.utils import get_start_span_function
15+
from sentry_sdk.ai.utils import _set_span_data_attribute, get_start_span_function
1616
from sentry_sdk.consts import OP, SPANDATA
1717
from sentry_sdk.integrations import Integration, DidNotEnable
18+
from sentry_sdk.traces import StreamedSpan
19+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
1820
from sentry_sdk.utils import safe_serialize
1921
from sentry_sdk.scope import should_send_default_pii
2022
from sentry_sdk.integrations._wsgi_common import nullcontext
@@ -33,8 +35,10 @@
3335

3436

3537
if TYPE_CHECKING:
36-
from typing import Any, Callable, Optional, Tuple, ContextManager
38+
from typing import Any, Callable, Optional, Tuple, Union, ContextManager
3739

40+
from sentry_sdk.tracing import Span
41+
from sentry_sdk.traces import StreamedSpan
3842
from starlette.types import Receive, Scope, Send # type: ignore[import-not-found]
3943

4044

@@ -156,7 +160,7 @@ def _get_span_config(
156160

157161

158162
def _set_span_input_data(
159-
span: "Any",
163+
span: "Union[StreamedSpan, Span]",
160164
handler_name: str,
161165
span_data_key: str,
162166
mcp_method_name: str,
@@ -168,26 +172,28 @@ def _set_span_input_data(
168172
"""Set input span data for MCP handlers."""
169173

170174
# Set handler identifier
171-
span.set_data(span_data_key, handler_name)
172-
span.set_data(SPANDATA.MCP_METHOD_NAME, mcp_method_name)
175+
_set_span_data_attribute(span, span_data_key, handler_name)
176+
_set_span_data_attribute(span, SPANDATA.MCP_METHOD_NAME, mcp_method_name)
173177

174178
# Set transport/MCP transport type
175-
span.set_data(
176-
SPANDATA.NETWORK_TRANSPORT, "pipe" if mcp_transport == "stdio" else "tcp"
179+
_set_span_data_attribute(
180+
span,
181+
SPANDATA.NETWORK_TRANSPORT,
182+
"pipe" if mcp_transport == "stdio" else "tcp",
177183
)
178-
span.set_data(SPANDATA.MCP_TRANSPORT, mcp_transport)
184+
_set_span_data_attribute(span, SPANDATA.MCP_TRANSPORT, mcp_transport)
179185

180186
# Set request_id if provided
181187
if request_id:
182-
span.set_data(SPANDATA.MCP_REQUEST_ID, request_id)
188+
_set_span_data_attribute(span, SPANDATA.MCP_REQUEST_ID, request_id)
183189

184190
# Set session_id if provided
185191
if session_id:
186-
span.set_data(SPANDATA.MCP_SESSION_ID, session_id)
192+
_set_span_data_attribute(span, SPANDATA.MCP_SESSION_ID, session_id)
187193

188194
# Set request arguments (excluding common request context objects)
189195
for k, v in arguments.items():
190-
span.set_data(f"mcp.request.argument.{k}", safe_serialize(v))
196+
_set_span_data_attribute(span, f"mcp.request.argument.{k}", safe_serialize(v))
191197

192198

193199
def _extract_tool_result_content(result: "Any") -> "Any":
@@ -231,7 +237,10 @@ def _extract_tool_result_content(result: "Any") -> "Any":
231237

232238

233239
def _set_span_output_data(
234-
span: "Any", result: "Any", result_data_key: "Optional[str]", handler_type: str
240+
span: "Union[StreamedSpan, Span]",
241+
result: "Any",
242+
result_data_key: "Optional[str]",
243+
handler_type: str,
235244
) -> None:
236245
"""Set output span data for MCP handlers."""
237246
if result is None:
@@ -248,11 +257,17 @@ def _set_span_output_data(
248257
# For tools, extract the meaningful content
249258
if handler_type == "tool":
250259
extracted = _extract_tool_result_content(result)
251-
if extracted is not None and should_include_data:
252-
span.set_data(result_data_key, safe_serialize(extracted))
260+
if (
261+
extracted is not None
262+
and should_include_data
263+
and result_data_key is not None
264+
):
265+
_set_span_data_attribute(span, result_data_key, safe_serialize(extracted))
253266
# Set content count if result is a dict
254267
if isinstance(extracted, dict):
255-
span.set_data(SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted))
268+
_set_span_data_attribute(
269+
span, SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted)
270+
)
256271
elif handler_type == "prompt":
257272
# For prompts, count messages and set role/content only for single-message prompts
258273
try:
@@ -270,7 +285,9 @@ def _set_span_output_data(
270285

271286
# Always set message count if we found messages
272287
if message_count > 0:
273-
span.set_data(SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count)
288+
_set_span_data_attribute(
289+
span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count
290+
)
274291

275292
# Only set role and content for single-message prompts if PII is allowed
276293
if message_count == 1 and should_include_data and messages:
@@ -283,7 +300,9 @@ def _set_span_output_data(
283300
role = first_message["role"]
284301

285302
if role:
286-
span.set_data(SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role)
303+
_set_span_data_attribute(
304+
span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role
305+
)
287306

288307
# Extract content text
289308
content_text = None
@@ -303,8 +322,8 @@ def _set_span_output_data(
303322
elif isinstance(msg_content, str):
304323
content_text = msg_content
305324

306-
if content_text:
307-
span.set_data(result_data_key, content_text)
325+
if content_text and result_data_key is not None:
326+
_set_span_data_attribute(span, result_data_key, content_text)
308327
except Exception:
309328
# Silently ignore if we can't extract message info
310329
pass
@@ -434,14 +453,28 @@ async def _handler_wrapper(
434453
# Get request ID, session ID, and transport from context
435454
request_id, session_id, mcp_transport = _get_request_context_data()
436455

456+
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
457+
437458
# Start span and execute
438459
with isolation_scope_context:
439460
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:
461+
span_mgr: "Union[Span, StreamedSpan]"
462+
if span_streaming:
463+
span_mgr = sentry_sdk.traces.start_span(
464+
name=span_name,
465+
attributes={
466+
"sentry.op": OP.MCP_SERVER,
467+
"sentry.origin": MCPIntegration.origin,
468+
},
469+
)
470+
else:
471+
span_mgr = get_start_span_function()(
472+
op=OP.MCP_SERVER,
473+
name=span_name,
474+
origin=MCPIntegration.origin,
475+
)
476+
477+
with span_mgr as span:
445478
# Set input span data
446479
_set_span_input_data(
447480
span,
@@ -467,7 +500,9 @@ async def _handler_wrapper(
467500
elif handler_name and "://" in handler_name:
468501
protocol = handler_name.split("://")[0]
469502
if protocol:
470-
span.set_data(SPANDATA.MCP_RESOURCE_PROTOCOL, protocol)
503+
_set_span_data_attribute(
504+
span, SPANDATA.MCP_RESOURCE_PROTOCOL, protocol
505+
)
471506

472507
try:
473508
# Execute the async handler
@@ -481,7 +516,9 @@ async def _handler_wrapper(
481516
except Exception as e:
482517
# Set error flag for tools
483518
if handler_type == "tool":
484-
span.set_data(SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True)
519+
_set_span_data_attribute(
520+
span, SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True
521+
)
485522
sentry_sdk.capture_exception(e)
486523
raise
487524

sentry_sdk/integrations/quart.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import inspect
3+
import sys
34
from functools import wraps
45

56
import sentry_sdk
@@ -100,15 +101,19 @@ async def sentry_patched_asgi_app(
100101

101102

102103
def patch_scaffold_route() -> None:
104+
# Vendored: https://github.com/pallets/quart/blob/5817e983d0b586889337a596d674c0c246d68878/src/quart/app.py#L137-L140
105+
if sys.version_info >= (3, 12):
106+
iscoroutinefunction = inspect.iscoroutinefunction
107+
else:
108+
iscoroutinefunction = asyncio.iscoroutinefunction
109+
103110
old_route = Scaffold.route
104111

105112
def _sentry_route(*args: "Any", **kwargs: "Any") -> "Any":
106113
old_decorator = old_route(*args, **kwargs)
107114

108115
def decorator(old_func: "Any") -> "Any":
109-
if inspect.isfunction(old_func) and not asyncio.iscoroutinefunction(
110-
old_func
111-
):
116+
if inspect.isfunction(old_func) and not iscoroutinefunction(old_func):
112117

113118
@wraps(old_func)
114119
@ensure_integration_enabled(QuartIntegration, old_func)

0 commit comments

Comments
 (0)