diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index dfa4aef34c..ecbd732967 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -16,7 +16,10 @@ ) from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_inputs, + should_collect_gen_ai_outputs, +) from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import Span from sentry_sdk.tracing_utils import ( @@ -146,7 +149,9 @@ class AnthropicIntegration(Integration): identifier = "anthropic" origin = f"auto.ai.{identifier}" - def __init__(self: "AnthropicIntegration", include_prompts: bool = True) -> None: + def __init__( + self: "AnthropicIntegration", include_prompts: "Optional[bool]" = None + ) -> None: self.include_prompts = include_prompts @staticmethod @@ -393,8 +398,7 @@ def _set_common_input_data( if ( messages is not None and len(messages) > 0 # type: ignore - and should_send_default_pii() - and integration.include_prompts + and should_collect_gen_ai_inputs(integration.include_prompts) ): if isinstance(system, str) or isinstance(system, Iterable): set_on_span( @@ -586,7 +590,7 @@ def _set_output_data( set_on_span(SPANDATA.GEN_AI_RESPONSE_ID, response_id) if finish_reason is not None: set_on_span(SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, [finish_reason]) - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_outputs(integration.include_prompts): output_messages: "dict[str, list[Any]]" = { "response": [], "tool": [], diff --git a/sentry_sdk/integrations/cohere.py b/sentry_sdk/integrations/cohere.py index 7abf3f6808..adc16d0780 100644 --- a/sentry_sdk/integrations/cohere.py +++ b/sentry_sdk/integrations/cohere.py @@ -10,13 +10,16 @@ from sentry_sdk.tracing_utils import has_span_streaming_enabled if TYPE_CHECKING: - from typing import Any, Callable, Iterator, Union + from typing import Any, Callable, Iterator, Optional, Union from sentry_sdk.tracing import Span import sentry_sdk from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_inputs, + should_collect_gen_ai_outputs, +) from sentry_sdk.utils import capture_internal_exceptions, event_from_exception, reraise try: @@ -73,7 +76,9 @@ class CohereIntegration(Integration): identifier = "cohere" origin = f"auto.ai.{identifier}" - def __init__(self: "CohereIntegration", include_prompts: bool = True) -> None: + def __init__( + self: "CohereIntegration", include_prompts: "Optional[bool]" = None + ) -> None: self.include_prompts = include_prompts @staticmethod @@ -179,7 +184,7 @@ def new_chat(*args: "Any", **kwargs: "Any") -> "Any": reraise(*exc_info) with capture_internal_exceptions(): - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_inputs(integration.include_prompts): set_data_normalized( span, SPANDATA.AI_INPUT_MESSAGES, @@ -215,8 +220,9 @@ def new_iterator() -> "Iterator[StreamedChatResponse]": collect_chat_response_fields( span, x.response, - include_pii=should_send_default_pii() - and integration.include_prompts, + include_pii=should_collect_gen_ai_outputs( + integration.include_prompts + ), ) yield x _end_span(span) @@ -226,8 +232,9 @@ def new_iterator() -> "Iterator[StreamedChatResponse]": collect_chat_response_fields( span, res, - include_pii=should_send_default_pii() - and integration.include_prompts, + include_pii=should_collect_gen_ai_outputs( + integration.include_prompts + ), ) _end_span(span) else: @@ -265,8 +272,8 @@ def new_embed(*args: "Any", **kwargs: "Any") -> "Any": ) with span_ctx as span: - if "texts" in kwargs and ( - should_send_default_pii() and integration.include_prompts + if "texts" in kwargs and should_collect_gen_ai_inputs( + integration.include_prompts ): if isinstance(kwargs["texts"], str): set_data_normalized(span, SPANDATA.AI_TEXTS, [kwargs["texts"]]) diff --git a/sentry_sdk/integrations/google_genai/__init__.py b/sentry_sdk/integrations/google_genai/__init__.py index 45652c3f71..ca56663117 100644 --- a/sentry_sdk/integrations/google_genai/__init__.py +++ b/sentry_sdk/integrations/google_genai/__init__.py @@ -5,6 +5,7 @@ Callable, Iterator, List, + Optional, ) import sentry_sdk @@ -41,7 +42,10 @@ class GoogleGenAIIntegration(Integration): identifier = IDENTIFIER origin = ORIGIN - def __init__(self: "GoogleGenAIIntegration", include_prompts: bool = True) -> None: + def __init__( + self: "GoogleGenAIIntegration", + include_prompts: "Optional[bool]" = None, + ) -> None: self.include_prompts = include_prompts @staticmethod diff --git a/sentry_sdk/integrations/google_genai/streaming.py b/sentry_sdk/integrations/google_genai/streaming.py index 8414ea4f21..0609be033c 100644 --- a/sentry_sdk/integrations/google_genai/streaming.py +++ b/sentry_sdk/integrations/google_genai/streaming.py @@ -2,7 +2,7 @@ from sentry_sdk.ai.utils import set_data_normalized from sentry_sdk.consts import SPANDATA -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import should_collect_gen_ai_outputs from sentry_sdk.traces import StreamedSpan from sentry_sdk.utils import ( safe_serialize, @@ -107,10 +107,8 @@ def set_span_data_for_streaming_response( span.set_attribute if isinstance(span, StreamedSpan) else span.set_data ) - if ( - should_send_default_pii() - and integration.include_prompts - and accumulated_response.get("text") + if should_collect_gen_ai_outputs(integration.include_prompts) and ( + accumulated_response.get("text") ): set_on_span( SPANDATA.GEN_AI_RESPONSE_TEXT, diff --git a/sentry_sdk/integrations/google_genai/utils.py b/sentry_sdk/integrations/google_genai/utils.py index 464a812680..e2bf45fe7e 100644 --- a/sentry_sdk/integrations/google_genai/utils.py +++ b/sentry_sdk/integrations/google_genai/utils.py @@ -27,7 +27,10 @@ truncate_and_annotate_messages, ) from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_inputs, + should_collect_gen_ai_outputs, +) from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing_utils import ( has_span_streaming_enabled, @@ -899,7 +902,7 @@ def set_span_data_for_request( config: "Optional[GenerateContentConfig]" = kwargs.get("config") # Set input messages/prompts if PII is allowed - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_inputs(integration.include_prompts): messages = [] # Add system instruction if present @@ -977,7 +980,7 @@ def set_span_data_for_response( set_on_span = ( span.set_attribute if isinstance(span, StreamedSpan) else span.set_data ) - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_outputs(integration.include_prompts): response_texts = _extract_response_text(response) if response_texts: # Format as JSON string array as per documentation @@ -1063,7 +1066,7 @@ def set_span_data_for_embed_request( ) -> None: """Set span data for embedding request.""" # Include input contents if PII is allowed - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_inputs(integration.include_prompts): if contents: # For embeddings, contents is typically a list of strings/texts input_texts = [] diff --git a/sentry_sdk/integrations/huggingface_hub.py b/sentry_sdk/integrations/huggingface_hub.py index 835acc7279..ab04e24ef0 100644 --- a/sentry_sdk/integrations/huggingface_hub.py +++ b/sentry_sdk/integrations/huggingface_hub.py @@ -12,7 +12,10 @@ ) from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_inputs, + should_collect_gen_ai_outputs, +) from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( @@ -22,7 +25,7 @@ ) if TYPE_CHECKING: - from typing import Any, Callable, Iterable, Union + from typing import Any, Callable, Iterable, Optional, Union from sentry_sdk.tracing import Span @@ -37,7 +40,8 @@ class HuggingfaceHubIntegration(Integration): origin = f"auto.ai.{identifier}" def __init__( - self: "HuggingfaceHubIntegration", include_prompts: bool = True + self: "HuggingfaceHubIntegration", + include_prompts: "Optional[bool]" = None, ) -> None: self.include_prompts = include_prompts @@ -114,7 +118,7 @@ def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any": _set_span_data_attribute(span, SPANDATA.GEN_AI_REQUEST_MODEL, model) # Input attributes - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_inputs(integration.include_prompts): set_data_normalized( span, SPANDATA.GEN_AI_REQUEST_MESSAGES, prompt, unpack=False ) @@ -206,7 +210,7 @@ def new_huggingface_task(*args: "Any", **kwargs: "Any") -> "Any": finish_reason, ) - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_outputs(integration.include_prompts): if tool_calls is not None and len(tool_calls) > 0: set_data_normalized( span, @@ -280,7 +284,7 @@ def new_details_iterator() -> "Iterable[Any]": finish_reason, ) - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_outputs(integration.include_prompts): if len(response_text_buffer) > 0: text_response = "".join(response_text_buffer) if text_response: @@ -359,7 +363,7 @@ def new_iterator() -> "Iterable[str]": finish_reason, ) - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_outputs(integration.include_prompts): if tool_calls is not None and len(tool_calls) > 0: set_data_normalized( span, diff --git a/sentry_sdk/integrations/langchain.py b/sentry_sdk/integrations/langchain.py index 9dcbb189ce..228acd581b 100644 --- a/sentry_sdk/integrations/langchain.py +++ b/sentry_sdk/integrations/langchain.py @@ -17,7 +17,10 @@ ) from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_inputs, + should_collect_gen_ai_outputs, +) from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing_utils import ( _get_value, @@ -214,7 +217,7 @@ class LangchainIntegration(Integration): def __init__( self: "LangchainIntegration", - include_prompts: bool = True, + include_prompts: "Optional[bool]" = None, max_spans: "Optional[int]" = None, ) -> None: self.include_prompts = include_prompts @@ -251,7 +254,7 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc] """Callback handler that creates Sentry spans.""" def __init__( - self, max_span_map_size: "Optional[int]", include_prompts: bool + self, max_span_map_size: "Optional[int]", include_prompts: "Optional[bool]" ) -> None: self.span_map: "OrderedDict[UUID, Union[sentry_sdk.tracing.Span, StreamedSpan]]" = OrderedDict() self.max_span_map_size = max_span_map_size @@ -401,7 +404,7 @@ def on_llm_start( _set_tools_on_span(span, all_params.get("tools")) - if should_send_default_pii() and self.include_prompts: + if should_collect_gen_ai_inputs(self.include_prompts): normalized_messages = [ { "role": GEN_AI_ALLOWED_MESSAGE_ROLES.USER, @@ -484,7 +487,7 @@ def on_chat_model_start( _set_tools_on_span(span, all_params.get("tools")) - if should_send_default_pii() and self.include_prompts: + if should_collect_gen_ai_inputs(self.include_prompts): system_instructions = _get_system_instructions(messages) if len(system_instructions) > 0: set_on_span( @@ -532,7 +535,7 @@ def on_chat_model_end( span = self.span_map[run_id] - if should_send_default_pii() and self.include_prompts: + if should_collect_gen_ai_outputs(self.include_prompts): set_data_normalized( span, SPANDATA.GEN_AI_RESPONSE_TEXT, @@ -588,7 +591,7 @@ def on_llm_end( pass try: - if should_send_default_pii() and self.include_prompts: + if should_collect_gen_ai_outputs(self.include_prompts): tool_calls = getattr(generation.message, "tool_calls", None) if tool_calls is not None and tool_calls != []: set_data_normalized( @@ -600,7 +603,7 @@ def on_llm_end( except AttributeError: pass - if should_send_default_pii() and self.include_prompts: + if should_collect_gen_ai_outputs(self.include_prompts): set_data_normalized( span, SPANDATA.GEN_AI_RESPONSE_TEXT, @@ -643,7 +646,7 @@ def on_agent_finish( span = self.span_map[run_id] - if should_send_default_pii() and self.include_prompts: + if should_collect_gen_ai_outputs(self.include_prompts): set_data_normalized( span, SPANDATA.GEN_AI_RESPONSE_TEXT, finish.return_values.items() ) @@ -695,7 +698,7 @@ def on_tool_start( run_name, ) - if should_send_default_pii() and self.include_prompts: + if should_collect_gen_ai_inputs(self.include_prompts): set_data_normalized( span, SPANDATA.GEN_AI_TOOL_INPUT, @@ -712,7 +715,7 @@ def on_tool_end( span = self.span_map[run_id] - if should_send_default_pii() and self.include_prompts: + if should_collect_gen_ai_outputs(self.include_prompts): set_data_normalized(span, SPANDATA.GEN_AI_TOOL_OUTPUT, output) self._exit_span(span, run_id) @@ -1038,10 +1041,8 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": result = f(self, *args, **kwargs) input = result.get("input") - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts + if input is not None and should_collect_gen_ai_inputs( + integration.include_prompts ): normalized_messages = normalize_message_roles([input]) @@ -1061,10 +1062,8 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": ) output = result.get("output") - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts + if output is not None and should_collect_gen_ai_outputs( + integration.include_prompts ): set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) @@ -1089,10 +1088,8 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": result = f(self, *args, **kwargs) input = result.get("input") - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts + if input is not None and should_collect_gen_ai_inputs( + integration.include_prompts ): normalized_messages = normalize_message_roles([input]) @@ -1112,10 +1109,8 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": ) output = result.get("output") - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts + if output is not None and should_collect_gen_ai_outputs( + integration.include_prompts ): set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) @@ -1166,10 +1161,8 @@ def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": _set_tools_on_span(span, tools) input = args[0].get("input") if len(args) >= 1 else None - if ( - input is not None - and should_send_default_pii() - and integration.include_prompts + if input is not None and should_collect_gen_ai_inputs( + integration.include_prompts ): normalized_messages = normalize_message_roles([input]) @@ -1204,10 +1197,8 @@ def new_iterator() -> "Iterator[Any]": except Exception: output = None - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts + if output is not None and should_collect_gen_ai_outputs( + integration.include_prompts ): set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) @@ -1229,10 +1220,8 @@ async def new_iterator_async() -> "AsyncIterator[Any]": except Exception: output = None - if ( - output is not None - and should_send_default_pii() - and integration.include_prompts + if output is not None and should_collect_gen_ai_outputs( + integration.include_prompts ): set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output) @@ -1300,8 +1289,7 @@ def new_embedding_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": # Capture input if PII is allowed if ( - should_send_default_pii() - and integration.include_prompts + should_collect_gen_ai_inputs(integration.include_prompts) and len(args) > 0 ): input_data = args[0] @@ -1325,8 +1313,7 @@ def new_embedding_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": # Capture input if PII is allowed if ( - should_send_default_pii() - and integration.include_prompts + should_collect_gen_ai_inputs(integration.include_prompts) and len(args) > 0 ): input_data = args[0] @@ -1370,8 +1357,7 @@ async def new_async_embedding_method( # Capture input if PII is allowed if ( - should_send_default_pii() - and integration.include_prompts + should_collect_gen_ai_inputs(integration.include_prompts) and len(args) > 0 ): input_data = args[0] @@ -1395,8 +1381,7 @@ async def new_async_embedding_method( # Capture input if PII is allowed if ( - should_send_default_pii() - and integration.include_prompts + should_collect_gen_ai_inputs(integration.include_prompts) and len(args) > 0 ): input_data = args[0] diff --git a/sentry_sdk/integrations/langgraph.py b/sentry_sdk/integrations/langgraph.py index 3d3856a913..2797ce5bd5 100644 --- a/sentry_sdk/integrations/langgraph.py +++ b/sentry_sdk/integrations/langgraph.py @@ -13,7 +13,10 @@ # This is fine because langgraph depends on langchain-base, and LangchainIntegration only imports from langchain-base. from sentry_sdk.integrations.langchain import LangchainIntegration -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_inputs, + should_collect_gen_ai_outputs, +) from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing_utils import ( has_span_streaming_enabled, @@ -33,7 +36,9 @@ class LanggraphIntegration(Integration): identifier = "langgraph" origin = f"auto.ai.{identifier}" - def __init__(self: "LanggraphIntegration", include_prompts: bool = True) -> None: + def __init__( + self: "LanggraphIntegration", include_prompts: "Optional[bool]" = None + ) -> None: self.include_prompts = include_prompts @staticmethod @@ -188,10 +193,8 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": # Store input messages to later compare with output input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts + if len(args) > 0 and should_collect_gen_ai_inputs( + integration.include_prompts ): input_messages = _parse_langgraph_messages(args[0]) if input_messages: @@ -235,10 +238,8 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": # Store input messages to later compare with output input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts + if len(args) > 0 and should_collect_gen_ai_inputs( + integration.include_prompts ): input_messages = _parse_langgraph_messages(args[0]) if input_messages: @@ -299,10 +300,8 @@ async def new_ainvoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": span.set_attribute(SPANDATA.GEN_AI_AGENT_NAME, graph_name) input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts + if len(args) > 0 and should_collect_gen_ai_inputs( + integration.include_prompts ): input_messages = _parse_langgraph_messages(args[0]) if input_messages: @@ -345,10 +344,8 @@ async def new_ainvoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") input_messages = None - if ( - len(args) > 0 - and should_send_default_pii() - and integration.include_prompts + if len(args) > 0 and should_collect_gen_ai_inputs( + integration.include_prompts ): input_messages = _parse_langgraph_messages(args[0]) if input_messages: @@ -494,7 +491,7 @@ def _set_response_attributes( _set_usage_data(span, new_messages) _set_response_model_name(span, new_messages) - if not (should_send_default_pii() and integration.include_prompts): + if not should_collect_gen_ai_outputs(integration.include_prompts): return llm_response_text = _extract_llm_response_text(new_messages) diff --git a/sentry_sdk/integrations/litellm.py b/sentry_sdk/integrations/litellm.py index 49ead6b068..5c6f98e699 100644 --- a/sentry_sdk/integrations/litellm.py +++ b/sentry_sdk/integrations/litellm.py @@ -13,7 +13,10 @@ ) from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_inputs, + should_collect_gen_ai_outputs, +) from sentry_sdk.tracing_utils import ( has_span_streaming_enabled, should_truncate_gen_ai_input, @@ -22,7 +25,7 @@ if TYPE_CHECKING: from datetime import datetime - from typing import Any, Dict, List + from typing import Any, Dict, List, Optional try: import litellm # type: ignore[import-not-found] @@ -129,7 +132,7 @@ def _input_callback(kwargs: "Dict[str, Any]") -> None: set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, operation) # Record input/messages if allowed - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_inputs(integration.include_prompts): if operation == "embeddings": # For embeddings, look for the 'input' parameter embedding_input = kwargs.get("input") @@ -218,7 +221,7 @@ def _success_callback( ) # Record response content if allowed - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_outputs(integration.include_prompts): if hasattr(completion_response, "choices"): response_messages = [] for choice in completion_response.choices: @@ -353,7 +356,9 @@ class LiteLLMIntegration(Integration): identifier = "litellm" origin = f"auto.ai.{identifier}" - def __init__(self: "LiteLLMIntegration", include_prompts: bool = True) -> None: + def __init__( + self: "LiteLLMIntegration", include_prompts: "Optional[bool]" = None + ) -> None: self.include_prompts = include_prompts @staticmethod diff --git a/sentry_sdk/integrations/mcp.py b/sentry_sdk/integrations/mcp.py index 7ec7a22c21..2bd0ccc933 100644 --- a/sentry_sdk/integrations/mcp.py +++ b/sentry_sdk/integrations/mcp.py @@ -16,7 +16,9 @@ from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations._wsgi_common import nullcontext -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_outputs, +) from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import package_version, safe_serialize @@ -65,13 +67,15 @@ class MCPIntegration(Integration): identifier = "mcp" origin = "auto.ai.mcp" - def __init__(self, include_prompts: bool = True) -> None: + def __init__(self, include_prompts: "Optional[bool]" = None) -> None: """ Initialize the MCP integration. Args: - include_prompts: Whether to include prompts (tool results and prompt content) - in span data. Requires send_default_pii=True. Default is True. + include_prompts: Override for whether to include prompts (tool results and + prompt content) in span data. Requires send_default_pii=True. + None (default) means no override; the global gen_ai + data_collection setting is used. """ self.include_prompts = include_prompts @@ -283,8 +287,8 @@ def _set_span_output_data( if integration is None: return - # Check if we should include sensitive data - should_include_data = should_send_default_pii() and integration.include_prompts + # Check if we should include sensitive output data + should_include_data = should_collect_gen_ai_outputs(integration.include_prompts) # For tools, extract the meaningful content if handler_type == "tool": diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 186c665ed1..b2ceb25e3c 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -33,7 +33,10 @@ ) from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_inputs, + should_collect_gen_ai_outputs, +) from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing_utils import ( has_span_streaming_enabled, @@ -108,7 +111,7 @@ class OpenAIIntegration(Integration): def __init__( self: "OpenAIIntegration", - include_prompts: bool = True, + include_prompts: "Optional[bool]" = None, tiktoken_encoding_name: "Optional[str]" = None, ) -> None: self.include_prompts = include_prompts @@ -362,7 +365,7 @@ def _set_responses_api_input_data( if conversation_id is not None: set_on_span(SPANDATA.GEN_AI_CONVERSATION_ID, conversation_id) - if not should_send_default_pii() or not integration.include_prompts: + if not should_collect_gen_ai_inputs(integration.include_prompts): set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses") return @@ -490,8 +493,7 @@ def _set_completions_api_input_data( set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p) if ( - not should_send_default_pii() - or not integration.include_prompts + not should_collect_gen_ai_inputs(integration.include_prompts) or messages is None ): set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat") @@ -567,8 +569,7 @@ def _set_embeddings_input_data( set_on_span(SPANDATA.GEN_AI_REQUEST_MODEL, model) if ( - not should_send_default_pii() - or not integration.include_prompts + not should_collect_gen_ai_inputs(integration.include_prompts) or messages is None ): set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings") @@ -630,7 +631,7 @@ def _set_common_output_data( # Chat Completions API if hasattr(response, "choices") and response.choices is not None: - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_outputs(integration.include_prompts): response_text = [ choice.message.model_dump() for choice in response.choices @@ -653,7 +654,7 @@ def _set_common_output_data( # Responses API elif hasattr(response, "output"): - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_outputs(integration.include_prompts): output_messages: "dict[str, list[Any]]" = { "response": [], "tool": [], @@ -937,7 +938,7 @@ def _wrap_synchronous_completions_chunk_iterator( all_responses = None if len(data_buf) > 0: all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_outputs(integration.include_prompts): set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) _calculate_completions_token_usage( @@ -1002,7 +1003,7 @@ async def _wrap_asynchronous_completions_chunk_iterator( all_responses = None if len(data_buf) > 0: all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_outputs(integration.include_prompts): set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) _calculate_completions_token_usage( @@ -1069,7 +1070,7 @@ def _wrap_synchronous_responses_event_iterator( ) if len(data_buf) > 0: all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_outputs(integration.include_prompts): set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) if count_tokens_manually: @@ -1136,7 +1137,7 @@ async def _wrap_asynchronous_responses_event_iterator( ) if len(data_buf) > 0: all_responses = ["".join(chunk) for chunk in data_buf] - if should_send_default_pii() and integration.include_prompts: + if should_collect_gen_ai_outputs(integration.include_prompts): set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, all_responses) if count_tokens_manually: _calculate_responses_token_usage( diff --git a/sentry_sdk/integrations/openai_agents/__init__.py b/sentry_sdk/integrations/openai_agents/__init__.py index 5895f53ad3..e5c05d59c0 100644 --- a/sentry_sdk/integrations/openai_agents/__init__.py +++ b/sentry_sdk/integrations/openai_agents/__init__.py @@ -41,7 +41,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional from agents.run_internal.run_steps import SingleStepResult @@ -89,6 +89,12 @@ class OpenAIAgentsIntegration(Integration): identifier = "openai_agents" + def __init__( + self: "OpenAIAgentsIntegration", + include_prompts: "Optional[bool]" = None, + ) -> None: + self.include_prompts = include_prompts + @staticmethod def setup_once() -> None: _patch_error_tracing() diff --git a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py index fd3a430951..71bf666bac 100644 --- a/sentry_sdk/integrations/openai_agents/spans/execute_tool.py +++ b/sentry_sdk/integrations/openai_agents/spans/execute_tool.py @@ -2,12 +2,15 @@ import sentry_sdk from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_inputs, + should_collect_gen_ai_outputs, +) from sentry_sdk.traces import SpanStatus, StreamedSpan from sentry_sdk.tracing_utils import has_span_streaming_enabled from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data +from ..utils import _get_include_prompts, _set_agent_data if TYPE_CHECKING: from typing import Any, Union @@ -46,7 +49,7 @@ def execute_tool_span( set_on_span = span.set_data - if should_send_default_pii(): + if should_collect_gen_ai_inputs(_get_include_prompts()): input = args[1] set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, input) @@ -73,7 +76,7 @@ def update_execute_tool_span( span.set_attribute if isinstance(span, StreamedSpan) else span.set_data ) - if should_send_default_pii(): + if should_collect_gen_ai_outputs(_get_include_prompts()): set_on_span(SPANDATA.GEN_AI_TOOL_OUTPUT, result) # Add conversation ID from agent diff --git a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py index c21145ac4a..16c707512e 100644 --- a/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +++ b/sentry_sdk/integrations/openai_agents/spans/invoke_agent.py @@ -8,7 +8,10 @@ truncate_and_annotate_messages, ) from sentry_sdk.consts import OP, SPANDATA -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_inputs, + should_collect_gen_ai_outputs, +) from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing_utils import ( has_span_streaming_enabled, @@ -17,7 +20,7 @@ from sentry_sdk.utils import safe_serialize from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data, _set_usage_data +from ..utils import _get_include_prompts, _set_agent_data, _set_usage_data if TYPE_CHECKING: from typing import Any, Union @@ -49,7 +52,7 @@ def invoke_agent_span( span.set_data(SPANDATA.GEN_AI_OPERATION_NAME, "invoke_agent") - if should_send_default_pii(): + if should_collect_gen_ai_inputs(_get_include_prompts()): messages = [] if agent.instructions: message = ( @@ -110,7 +113,7 @@ def update_invoke_agent_span( if hasattr(context, "usage"): _set_usage_data(span, context.usage) - if should_send_default_pii(): + if should_collect_gen_ai_outputs(_get_include_prompts()): set_data_normalized(span, SPANDATA.GEN_AI_RESPONSE_TEXT, output, unpack=False) # Add conversation ID from agent diff --git a/sentry_sdk/integrations/openai_agents/utils.py b/sentry_sdk/integrations/openai_agents/utils.py index 224a5f66ba..5d0a63c263 100644 --- a/sentry_sdk/integrations/openai_agents/utils.py +++ b/sentry_sdk/integrations/openai_agents/utils.py @@ -16,13 +16,16 @@ ) from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_inputs, + should_collect_gen_ai_outputs, +) from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing_utils import should_truncate_gen_ai_input from sentry_sdk.utils import event_from_exception, safe_serialize if TYPE_CHECKING: - from typing import Any, Union + from typing import Any, Optional, Union from agents import TResponseInputItem, Usage @@ -116,11 +119,18 @@ def _set_usage_data( set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) +def _get_include_prompts() -> "Optional[bool]": + from sentry_sdk.integrations.openai_agents import OpenAIAgentsIntegration + + integration = sentry_sdk.get_client().get_integration(OpenAIAgentsIntegration) + return integration.include_prompts if integration is not None else None + + def _set_input_data( span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", get_response_kwargs: "dict[str, Any]", ) -> None: - if not should_send_default_pii(): + if not should_collect_gen_ai_inputs(_get_include_prompts()): return request_messages = [] @@ -207,7 +217,7 @@ def _set_input_data( def _set_output_data( span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", result: "Any" ) -> None: - if not should_send_default_pii(): + if not should_collect_gen_ai_outputs(_get_include_prompts()): return output_messages: "dict[str, list[Any]]" = { diff --git a/sentry_sdk/integrations/pydantic_ai/__init__.py b/sentry_sdk/integrations/pydantic_ai/__init__.py index 81e7cf8090..bcc02148f0 100644 --- a/sentry_sdk/integrations/pydantic_ai/__init__.py +++ b/sentry_sdk/integrations/pydantic_ai/__init__.py @@ -21,7 +21,7 @@ from .spans.ai_client import ai_client_span, update_ai_client_span if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional from pydantic_ai import ModelRequestContext, RunContext from pydantic_ai.capabilities import Hooks # type: ignore @@ -132,14 +132,17 @@ class PydanticAIIntegration(Integration): using_request_hooks = False def __init__( - self, include_prompts: bool = True, handled_tool_call_exceptions: bool = True + self, + include_prompts: "Optional[bool]" = None, + handled_tool_call_exceptions: bool = True, ) -> None: """ Initialize the Pydantic AI integration. Args: include_prompts: Whether to include prompts and messages in span data. - Requires send_default_pii=True. Defaults to True. + Requires send_default_pii=True. Overrides the global gen_ai data + collection setting when set. Defaults to None (use global setting). handled_tool_exceptions: Capture tool call exceptions that Pydantic AI internally prevents from bubbling up. """ diff --git a/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py b/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py index dfe898d139..5a08f44891 100644 --- a/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +++ b/sentry_sdk/integrations/pydantic_ai/spans/ai_client.py @@ -18,7 +18,8 @@ _set_agent_data, _set_available_tools, _set_model_data, - _should_send_prompts, + _should_send_input_prompts, + _should_send_output_prompts, get_current_agent, get_is_streaming, ) @@ -103,7 +104,7 @@ def _set_input_messages( span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", messages: "Any" ) -> None: """Set input messages data on a span.""" - if not _should_send_prompts(): + if not _should_send_input_prompts(): return if not messages: @@ -216,7 +217,7 @@ def _set_output_data( span: "Union[sentry_sdk.tracing.Span, StreamedSpan]", response: "Any" ) -> None: """Set output data on a span.""" - if not _should_send_prompts(): + if not _should_send_output_prompts(): return if not response: diff --git a/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py b/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py index 7648c1418a..9fc0cee95a 100644 --- a/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +++ b/sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py @@ -7,7 +7,11 @@ from sentry_sdk.utils import safe_serialize from ..consts import SPAN_ORIGIN -from ..utils import _set_agent_data, _should_send_prompts +from ..utils import ( + _set_agent_data, + _should_send_input_prompts, + _should_send_output_prompts, +) if TYPE_CHECKING: from typing import Any, Optional, Union @@ -62,7 +66,7 @@ def execute_tool_span( _set_agent_data(span, agent) - if _should_send_prompts() and tool_args is not None: + if _should_send_input_prompts() and tool_args is not None: set_on_span(SPANDATA.GEN_AI_TOOL_INPUT, safe_serialize(tool_args)) return span @@ -75,7 +79,7 @@ def update_execute_tool_span( if not span: return - if not _should_send_prompts() or result is None: + if not _should_send_output_prompts() or result is None: return if isinstance(span, StreamedSpan): diff --git a/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py b/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py index e4021f3bfa..7e0fdc8bb5 100644 --- a/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +++ b/sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py @@ -16,7 +16,8 @@ _set_agent_data, _set_available_tools, _set_model_data, - _should_send_prompts, + _should_send_input_prompts, + _should_send_output_prompts, ) from .utils import ( _serialize_binary_content_item, @@ -70,7 +71,7 @@ def invoke_agent_span( _set_available_tools(span, agent) # Add user prompt and system prompts if available and prompts are enabled - if _should_send_prompts(): + if _should_send_input_prompts(): messages = [] # Add system prompts (both instructions and system_prompt) @@ -160,7 +161,7 @@ def update_invoke_agent_span( output = getattr(result, "output", None) # Set response text if prompts are enabled - if _should_send_prompts() and output: + if _should_send_output_prompts() and output: set_data_normalized( span, SPANDATA.GEN_AI_RESPONSE_TEXT, str(output), unpack=False ) diff --git a/sentry_sdk/integrations/pydantic_ai/utils.py b/sentry_sdk/integrations/pydantic_ai/utils.py index 340dcf8953..f0b2891f67 100644 --- a/sentry_sdk/integrations/pydantic_ai/utils.py +++ b/sentry_sdk/integrations/pydantic_ai/utils.py @@ -3,7 +3,10 @@ import sentry_sdk from sentry_sdk.consts import SPANDATA -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.scope import ( + should_collect_gen_ai_inputs, + should_collect_gen_ai_outputs, +) from sentry_sdk.traces import StreamedSpan from sentry_sdk.utils import event_from_exception, safe_serialize @@ -49,15 +52,31 @@ def get_is_streaming() -> bool: return False -def _should_send_prompts() -> bool: +def _should_send_input_prompts() -> bool: """ - Check if prompts should be sent to Sentry. + Check if input prompts/messages should be sent to Sentry. - This checks both send_default_pii and the include_prompts integration setting. + This routes through the gen_ai inputs data collection setting, using the + integration's include_prompts as an override. """ - if not should_send_default_pii(): + from . import PydanticAIIntegration + + # Get the integration instance from the client + integration = sentry_sdk.get_client().get_integration(PydanticAIIntegration) + + if integration is None: return False + return should_collect_gen_ai_inputs(integration.include_prompts) + + +def _should_send_output_prompts() -> bool: + """ + Check if output completions/responses should be sent to Sentry. + + This routes through the gen_ai outputs data collection setting, using the + integration's include_prompts as an override. + """ from . import PydanticAIIntegration # Get the integration instance from the client @@ -66,7 +85,7 @@ def _should_send_prompts() -> bool: if integration is None: return False - return getattr(integration, "include_prompts", False) + return should_collect_gen_ai_outputs(integration.include_prompts) def _set_agent_data( diff --git a/tests/integrations/langgraph/test_langgraph.py b/tests/integrations/langgraph/test_langgraph.py index 200bd58838..6c206bc98f 100644 --- a/tests/integrations/langgraph/test_langgraph.py +++ b/tests/integrations/langgraph/test_langgraph.py @@ -141,7 +141,10 @@ def _generate(self, messages, stop=None, run_manager=None, **kwargs) -> ChatResu def test_langgraph_integration_init(): """Test LanggraphIntegration initialization with different parameters.""" integration = LanggraphIntegration() - assert integration.include_prompts is True + # Defaults to None (no per-integration override -> defer to the global + # data_collection.gen_ai setting). Runtime behavior is unchanged: in legacy + # mode this still gates on send_default_pii. + assert integration.include_prompts is None assert integration.identifier == "langgraph" assert integration.origin == "auto.ai.langgraph" diff --git a/tests/integrations/pydantic_ai/test_pydantic_ai.py b/tests/integrations/pydantic_ai/test_pydantic_ai.py index 71b865b117..863ac0cf95 100644 --- a/tests/integrations/pydantic_ai/test_pydantic_ai.py +++ b/tests/integrations/pydantic_ai/test_pydantic_ai.py @@ -3441,9 +3441,12 @@ async def test_set_model_data_with_none_settings_values(sentry_init, capture_ite @pytest.mark.asyncio async def test_should_send_prompts_without_pii(sentry_init, capture_items): """ - Test that _should_send_prompts returns False when PII disabled. + Test that the input/output prompt gates return False when PII disabled. """ - from sentry_sdk.integrations.pydantic_ai.utils import _should_send_prompts + from sentry_sdk.integrations.pydantic_ai.utils import ( + _should_send_input_prompts, + _should_send_output_prompts, + ) sentry_init( integrations=[PydanticAIIntegration(include_prompts=True)], @@ -3452,8 +3455,8 @@ async def test_should_send_prompts_without_pii(sentry_init, capture_items): ) # Should return False - result = _should_send_prompts() - assert result is False + assert _should_send_input_prompts() is False + assert _should_send_output_prompts() is False @pytest.mark.asyncio