Skip to content

Commit e0150a4

Browse files
merge master
2 parents 14d342d + 1fea08e commit e0150a4

7 files changed

Lines changed: 610 additions & 34 deletions

File tree

sentry_sdk/integrations/mcp.py

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@
2020
from sentry_sdk.scope import should_send_default_pii
2121
from sentry_sdk.traces import StreamedSpan
2222
from sentry_sdk.tracing_utils import has_span_streaming_enabled
23-
from sentry_sdk.utils import nullcontext, package_version, safe_serialize
23+
from sentry_sdk.utils import (
24+
has_data_collection_enabled,
25+
nullcontext,
26+
package_version,
27+
safe_serialize,
28+
)
2429

2530
try:
2631
from mcp.server.lowlevel import Server
@@ -322,13 +327,21 @@ async def _tool_handler_wrapper(
322327
original_kwargs: Original keyword arguments passed to the handler
323328
self: Optional instance for bound methods
324329
"""
330+
client = sentry_sdk.get_client()
331+
325332
if original_kwargs is None:
326333
original_kwargs = {}
327334

328335
handler_name, arguments = _extract_handler_data_from_args(
329336
"tool", original_args, original_kwargs
330337
)
331338

339+
if has_data_collection_enabled(client.options):
340+
if not client.options["data_collection"]["gen_ai"]["inputs"]:
341+
# Arguments can contain sensitive data and shouldn't be added to the span
342+
# if the user has opted out via this config.
343+
arguments = {}
344+
332345
ctx = None
333346
try:
334347
ctx = request_ctx.get()
@@ -388,14 +401,17 @@ async def _tool_handler_wrapper(
388401
return result
389402

390403
# Get integration to check PII settings
391-
integration = sentry_sdk.get_client().get_integration(MCPIntegration)
404+
integration = client.get_integration(MCPIntegration)
392405
if integration is None:
393406
return result
394407

395408
# Check if we should include sensitive data
396-
should_include_data = (
397-
should_send_default_pii() and integration.include_prompts
398-
)
409+
should_include_data = False
410+
if has_data_collection_enabled(client.options):
411+
if client.options["data_collection"]["gen_ai"]["outputs"]:
412+
should_include_data = True
413+
elif should_send_default_pii() and integration.include_prompts:
414+
should_include_data = True
399415

400416
extracted = _extract_tool_result_content(result)
401417
if extracted is not None and should_include_data:
@@ -422,15 +438,22 @@ async def _instrument_v2_tool_call(
422438
if ctx.params is None or ctx.params.get("name") is None:
423439
return await call_next(ctx)
424440

441+
client = sentry_sdk.get_client()
425442
handler_name = ctx.params["name"]
426443
arguments = ctx.params.get("arguments")
427444
if arguments is None:
428445
arguments = {}
429446

447+
if has_data_collection_enabled(client.options):
448+
if not client.options["data_collection"]["gen_ai"]["inputs"]:
449+
# Arguments can contain sensitive data and shouldn't be added to the span
450+
# if the user has opted out via this config.
451+
arguments = {}
452+
430453
# Get request ID, session ID, and transport from context
431454
request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx)
432455

433-
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
456+
span_streaming = has_span_streaming_enabled(client.options)
434457

435458
# Start span and execute
436459
with _active_http_scopes(ctx=ctx):
@@ -474,22 +497,25 @@ async def _instrument_v2_tool_call(
474497
return result
475498

476499
# Get integration to check PII settings
477-
integration = sentry_sdk.get_client().get_integration(MCPIntegration)
500+
integration = client.get_integration(MCPIntegration)
478501
if integration is None:
479502
return result
480503

481504
# Check if we should include sensitive data
482-
should_include_data = (
483-
should_send_default_pii() and integration.include_prompts
484-
)
505+
should_include_result_data = False
506+
if has_data_collection_enabled(client.options):
507+
if client.options["data_collection"]["gen_ai"]["outputs"]:
508+
should_include_result_data = True
509+
elif should_send_default_pii() and integration.include_prompts:
510+
should_include_result_data = True
485511

486512
result_content = result
487513
if "structuredContent" in result:
488514
result_content = result["structuredContent"]
489515
elif isinstance(result.get("content"), list):
490516
result_content = _extract_text_from_content_blocks(result["content"])
491517

492-
if result_content is not None and should_include_data:
518+
if result_content is not None and should_include_result_data:
493519
_set_span_data_attribute(
494520
span,
495521
SPANDATA.MCP_TOOL_RESULT_CONTENT,
@@ -526,10 +552,17 @@ async def _prompt_handler_wrapper(
526552
if original_kwargs is None:
527553
original_kwargs = {}
528554

555+
client = sentry_sdk.get_client()
529556
handler_name, arguments = _extract_handler_data_from_args(
530557
"prompt", original_args, original_kwargs
531558
)
532559

560+
if has_data_collection_enabled(client.options):
561+
if not client.options["data_collection"]["gen_ai"]["inputs"]:
562+
# Arguments can contain sensitive data and shouldn't be added to the span
563+
# if the user has opted out via this config.
564+
arguments = {}
565+
533566
ctx = None
534567
try:
535568
ctx = request_ctx.get()
@@ -539,7 +572,7 @@ async def _prompt_handler_wrapper(
539572
# Get request ID, session ID, and transport from context
540573
request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx)
541574

542-
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
575+
span_streaming = has_span_streaming_enabled(client.options)
543576

544577
# Start span and execute
545578
with _active_http_scopes(ctx=ctx):
@@ -589,14 +622,17 @@ async def _prompt_handler_wrapper(
589622
return result
590623

591624
# Get integration to check PII settings
592-
integration = sentry_sdk.get_client().get_integration(MCPIntegration)
625+
integration = client.get_integration(MCPIntegration)
593626
if integration is None:
594627
return result
595628

596629
# Check if we should include sensitive data
597-
should_include_data = (
598-
should_send_default_pii() and integration.include_prompts
599-
)
630+
should_include_result_data = False
631+
if has_data_collection_enabled(client.options):
632+
if client.options["data_collection"]["gen_ai"]["inputs"]:
633+
should_include_result_data = True
634+
elif should_send_default_pii() and integration.include_prompts:
635+
should_include_result_data = True
600636

601637
# For prompts, count messages and set role/content only for single-message prompts
602638
try:
@@ -619,7 +655,7 @@ async def _prompt_handler_wrapper(
619655
)
620656

621657
# Only set role and content for single-message prompts if PII is allowed
622-
if message_count == 1 and should_include_data and messages:
658+
if message_count == 1 and should_include_result_data and messages:
623659
first_message = messages[0]
624660
# Extract role
625661
role = None
@@ -675,15 +711,24 @@ async def _instrument_v2_prompt_get(
675711
if ctx.params is None or ctx.params.get("name") is None:
676712
return await call_next(ctx)
677713

714+
client = sentry_sdk.get_client()
678715
handler_name = ctx.params["name"]
716+
679717
arguments = ctx.params.get("arguments")
718+
680719
if arguments is None:
681720
arguments = {}
682721

722+
if has_data_collection_enabled(client.options):
723+
if not client.options["data_collection"]["gen_ai"]["inputs"]:
724+
# Arguments can contain sensitive data and shouldn't be added to the span
725+
# if the user has opted out via this config.
726+
arguments = {}
727+
683728
# Get request ID, session ID, and transport from context
684729
request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx)
685730

686-
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
731+
span_streaming = has_span_streaming_enabled(client.options)
687732

688733
# Start span and execute
689734
with _active_http_scopes(ctx=ctx):
@@ -726,14 +771,17 @@ async def _instrument_v2_prompt_get(
726771
return result
727772

728773
# Get integration to check PII settings
729-
integration = sentry_sdk.get_client().get_integration(MCPIntegration)
774+
integration = client.get_integration(MCPIntegration)
730775
if integration is None:
731776
return result
732777

733778
# Check if we should include sensitive data
734-
should_include_data = (
735-
should_send_default_pii() and integration.include_prompts
736-
)
779+
should_include_result_data = False
780+
if has_data_collection_enabled(client.options):
781+
if client.options["data_collection"]["gen_ai"]["inputs"]:
782+
should_include_result_data = True
783+
elif should_send_default_pii() and integration.include_prompts:
784+
should_include_result_data = True
737785

738786
# For prompts, count messages and set role/content only for single-message prompts
739787
try:
@@ -751,7 +799,7 @@ async def _instrument_v2_prompt_get(
751799
)
752800

753801
# Only set role and content for single-message prompts if PII is allowed
754-
if message_count == 1 and should_include_data and messages:
802+
if message_count == 1 and should_include_result_data and messages:
755803
first_message = messages[0]
756804
# Extract role
757805
role = None

sentry_sdk/scope.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -861,10 +861,7 @@ def set_transaction_name(self, name: str, source: "Optional[str]" = None) -> Non
861861
"""Set the transaction name and optionally the transaction source."""
862862
self._transaction = name
863863
if self._span:
864-
if isinstance(self._span, NoOpStreamedSpan):
865-
return
866-
867-
elif isinstance(self._span, StreamedSpan):
864+
if isinstance(self._span, StreamedSpan):
868865
self._span._segment.name = name
869866
if source:
870867
self._span._segment.set_attribute(
@@ -925,12 +922,24 @@ def streamed_span(self, span: "Optional[StreamedSpan]") -> None:
925922

926923
# Also set _transaction and _transaction_info in streaming mode as this
927924
# is used for populating events and linking them to segments
928-
if type(span) is StreamedSpan and span._is_segment():
925+
if not isinstance(span, StreamedSpan) or not span._is_segment():
926+
return
927+
928+
if type(span) is StreamedSpan:
929929
self._transaction = span.name
930930
if span._attributes.get("sentry.segment.name.source"):
931931
self._transaction_info["source"] = str(
932932
span._attributes["sentry.segment.name.source"]
933933
)
934+
return
935+
936+
if type(span) is NoOpStreamedSpan:
937+
if span._name is not None:
938+
self._transaction = span.name
939+
if span._attributes.get("sentry.segment.name.source"):
940+
self._transaction_info["source"] = str(
941+
span._attributes["sentry.segment.name.source"]
942+
)
934943

935944
@property
936945
def profile(self) -> "Optional[Profile]":
@@ -1309,6 +1318,8 @@ def start_streamed_span(
13091318

13101319
if is_ignored_span(name, attributes):
13111320
return NoOpStreamedSpan(
1321+
name=name,
1322+
attributes=attributes,
13121323
scope=self,
13131324
segment=None,
13141325
trace_id=propagation_context.trace_id,
@@ -1329,6 +1340,8 @@ def start_streamed_span(
13291340

13301341
if sampled is False or sampled is None:
13311342
return NoOpStreamedSpan(
1343+
name=name,
1344+
attributes=attributes,
13321345
scope=self,
13331346
segment=None,
13341347
trace_id=propagation_context.trace_id,
@@ -1359,6 +1372,8 @@ def start_streamed_span(
13591372
with new_scope():
13601373
if is_ignored_span(name, attributes):
13611374
return NoOpStreamedSpan(
1375+
name=name,
1376+
attributes=attributes,
13621377
segment=parent_span._segment,
13631378
trace_id=parent_span.trace_id,
13641379
parent_span_id=parent_span.span_id,
@@ -1368,6 +1383,8 @@ def start_streamed_span(
13681383

13691384
if isinstance(parent_span, NoOpStreamedSpan):
13701385
return NoOpStreamedSpan(
1386+
name=name,
1387+
attributes=attributes,
13711388
segment=parent_span._segment,
13721389
trace_id=parent_span.trace_id,
13731390
parent_span_id=parent_span.span_id,

sentry_sdk/traces.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,8 @@ class NoOpStreamedSpan(StreamedSpan):
632632

633633
def __init__(
634634
self,
635+
name: "Optional[str]" = None,
636+
attributes: "Optional[Attributes]" = None,
635637
segment: "Optional[StreamedSpan]" = None,
636638
trace_id: "Optional[str]" = None,
637639
parent_span_id: "Optional[str]" = None,
@@ -643,6 +645,13 @@ def __init__(
643645
sample_rand: "Optional[float]" = None,
644646
sample_rate: "Optional[float]" = None,
645647
) -> None:
648+
self._name = name # type: ignore[assignment]
649+
self._attributes = {}
650+
if attributes is not None and "sentry.segment.name.source" in attributes:
651+
self.set_attribute(
652+
"sentry.segment.name.source", attributes["sentry.segment.name.source"]
653+
)
654+
646655
self._span_id: "Optional[str]" = None
647656

648657
self._sampled = sampled
@@ -721,10 +730,17 @@ def get_attributes(self) -> "Attributes":
721730
return {}
722731

723732
def set_attribute(self, key: str, value: "AttributeValue") -> None:
724-
pass
733+
if key != "sentry.segment.name.source":
734+
return
735+
736+
super().set_attribute("sentry.segment.name.source", value)
725737

726738
def set_attributes(self, attributes: "Attributes") -> None:
727-
pass
739+
for key, value in attributes.items():
740+
if key != "sentry.segment.name.source":
741+
continue
742+
743+
self.set_attribute("sentry.segment.name.source", value)
728744

729745
def remove_attribute(self, key: str) -> None:
730746
pass
@@ -739,11 +755,11 @@ def status(self, status: "Union[SpanStatus, str]") -> None:
739755

740756
@property
741757
def name(self) -> str:
742-
return ""
758+
return self._name or ""
743759

744760
@name.setter
745-
def name(self, value: str) -> None:
746-
pass
761+
def name(self, name: str) -> None:
762+
self._name = name
747763

748764
@property
749765
def active(self) -> bool:

0 commit comments

Comments
 (0)