Skip to content
Open
Changes from all 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
74 changes: 74 additions & 0 deletions sentry_sdk/ai/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,87 @@ def normalize_message_roles(messages: "list[dict[str, Any]]") -> "list[dict[str,


def get_start_span_function() -> "Callable[..., Any]":
"""
Determine whether to create a span or transaction for AI operations.

Checks in priority order:
1. MCP request context has sentry-trace headers: create transaction continuing the trace
2. Transaction exists in current scope: create child span
3. Otherwise: create new transaction

This ensures MCP tool calls properly continue distributed traces while maintaining
correct span hierarchies for local operations.
"""
# Check for distributed trace headers in MCP request context for cross-service tracing
try:
from mcp.server.lowlevel.server import request_ctx # type: ignore[import-not-found]

ctx = request_ctx.get()
if ctx and hasattr(ctx, "request") and ctx.request is not None:
request = ctx.request
if hasattr(request, "headers"):
sentry_trace = request.headers.get("sentry-trace")
if sentry_trace:
return _create_transaction_from_mcp_headers
except (ImportError, LookupError):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AttributeError uncaught when headers attribute is None

In get_start_span_function(), the code uses hasattr(request, "headers") to check if the attribute exists, then immediately calls request.headers.get("sentry-trace"). If headers exists but is None, this raises an AttributeError that isn't caught by the except (ImportError, LookupError) handler. The similar code in _create_transaction_from_mcp_headers() catches all Exception, but this function would fail and propagate the error.

Fix in Cursor Fix in Web

# MCP not installed or no request context, fall through to normal logic
pass

# Normal logic: create span if transaction exists, otherwise create transaction
current_span = sentry_sdk.get_current_span()
transaction_exists = (
current_span is not None and current_span.containing_transaction is not None
)
return sentry_sdk.start_span if transaction_exists else sentry_sdk.start_transaction


def _create_transaction_from_mcp_headers(
op: "Optional[str]" = None,
name: "Optional[str]" = None,
origin: str = "manual",
**kwargs: "Any",
) -> "Any":
"""
Create transaction continuing distributed trace from MCP request headers.

Extracts sentry-trace and baggage headers from MCP request context and uses
continue_trace() to create a transaction inheriting trace_id and parent_span_id.
Ensures MCP tool executions join the distributed trace while remaining transactions
for MCP Insights dashboard compatibility.
"""
try:
from mcp.server.lowlevel.server import request_ctx # type: ignore[import-not-found]

ctx = request_ctx.get()
if ctx and hasattr(ctx, "request") and ctx.request is not None:
request = ctx.request
if hasattr(request, "headers"):
headers = {}

# Extract trace propagation headers
sentry_trace = request.headers.get("sentry-trace")
baggage = request.headers.get("baggage")

if sentry_trace:
headers["sentry-trace"] = sentry_trace
if baggage:
headers["baggage"] = baggage

# Use continue_trace to create transaction inheriting trace context
isolation_scope = sentry_sdk.get_isolation_scope()
transaction = isolation_scope.continue_trace(
environ_or_headers=headers, op=op, name=name, origin=origin
)

# Start transaction on scope so it becomes active
return sentry_sdk.start_transaction(transaction=transaction)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kwargs ignored in MCP headers success path

The **kwargs parameter is passed to start_transaction() in the fallback path (line 181) but completely ignored when the MCP headers path succeeds (line 176). This causes inconsistent behavior where additional keyword arguments are applied only when the primary code path fails, leading to silent differences in transaction configuration depending on whether MCP headers are present.

Additional Locations (1)

Fix in Cursor Fix in Web

except Exception as e:
logger.debug("Could not create transaction from MCP headers: %s", e)

# Fallback: create new transaction if headers unavailable
return sentry_sdk.start_transaction(op=op, name=name, origin=origin, **kwargs)


def _truncate_single_message_content_if_present(
message: "Dict[str, Any]", max_chars: int
) -> "Dict[str, Any]":
Expand Down