Skip to content

Commit 182c4c3

Browse files
committed
feat(anthropic): Gate prompt collection on data_collection option
Replace include_prompts and send_default_pii checks with the new data_collection configuration for controlling whether messages and system instructions are captured. Maintain backwards compatibility: when data_collection is not configured, fall back to the legacy pii/include_prompts behavior. Tools are always collected regardless of the message collection setting. Refs PY-2588
1 parent 2cfb819 commit 182c4c3

2 files changed

Lines changed: 623 additions & 70 deletions

File tree

sentry_sdk/integrations/anthropic.py

Lines changed: 149 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from sentry_sdk.utils import (
2727
capture_internal_exceptions,
2828
event_from_exception,
29+
has_data_collection_enabled,
2930
package_version,
3031
reraise,
3132
safe_serialize,
@@ -390,74 +391,6 @@ def _set_common_input_data(
390391
)
391392
set_on_span(SPANDATA.GEN_AI_SYSTEM, "anthropic")
392393
set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat")
393-
if (
394-
messages is not None
395-
and len(messages) > 0 # type: ignore
396-
and should_send_default_pii()
397-
and integration.include_prompts
398-
):
399-
if isinstance(system, str) or isinstance(system, Iterable):
400-
set_on_span(
401-
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
402-
json.dumps(_transform_system_instructions(system)),
403-
)
404-
405-
normalized_messages = []
406-
for message in messages:
407-
if (
408-
message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER
409-
and "content" in message
410-
and isinstance(message["content"], (list, tuple))
411-
):
412-
transformed_content = []
413-
for item in message["content"]:
414-
# Skip tool_result items - they can contain images/documents
415-
# with nested structures that are difficult to redact properly
416-
if isinstance(item, dict) and item.get("type") == "tool_result":
417-
continue
418-
419-
# Transform content blocks (images, documents, etc.)
420-
transformed_content.append(
421-
_transform_anthropic_content_block(item)
422-
if isinstance(item, dict)
423-
else item
424-
)
425-
426-
# If there are non-tool-result items, add them as a message
427-
if transformed_content:
428-
normalized_messages.append(
429-
{
430-
"role": message.get("role"),
431-
"content": transformed_content,
432-
}
433-
)
434-
else:
435-
# Transform content for non-list messages or assistant messages
436-
transformed_message = message.copy()
437-
if "content" in transformed_message:
438-
content = transformed_message["content"]
439-
if isinstance(content, (list, tuple)):
440-
transformed_message["content"] = [
441-
_transform_anthropic_content_block(item)
442-
if isinstance(item, dict)
443-
else item
444-
for item in content
445-
]
446-
normalized_messages.append(transformed_message)
447-
448-
role_normalized_messages = normalize_message_roles(normalized_messages)
449-
450-
client = sentry_sdk.get_client()
451-
scope = sentry_sdk.get_current_scope()
452-
messages_data = (
453-
truncate_and_annotate_messages(role_normalized_messages, span, scope)
454-
if should_truncate_gen_ai_input(client.options)
455-
else role_normalized_messages
456-
)
457-
if messages_data is not None:
458-
set_data_normalized(
459-
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False
460-
)
461394

462395
if max_tokens is not None and _is_given(max_tokens):
463396
set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens)
@@ -470,8 +403,154 @@ def _set_common_input_data(
470403
if top_p is not None and _is_given(top_p):
471404
set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p)
472405

473-
if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore
474-
set_on_span(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools))
406+
client = sentry_sdk.get_client()
407+
408+
if has_data_collection_enabled(client.options):
409+
if client.options["data_collection"]["gen_ai"]["inputs"]:
410+
if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore
411+
set_on_span(
412+
SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)
413+
)
414+
else:
415+
# Tools were unconditionally added pre-data collection configuration.
416+
# This can be removed once data collection is fully rolled out
417+
if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore
418+
set_on_span(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools))
419+
420+
if messages is not None and len(messages) > 0: # type: ignore
421+
if has_data_collection_enabled(client.options):
422+
if client.options["data_collection"]["gen_ai"]["inputs"]:
423+
if isinstance(system, str) or isinstance(system, Iterable):
424+
set_on_span(
425+
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
426+
json.dumps(_transform_system_instructions(system)),
427+
)
428+
429+
normalized_messages = []
430+
for message in messages:
431+
if (
432+
message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER
433+
and "content" in message
434+
and isinstance(message["content"], (list, tuple))
435+
):
436+
transformed_content = []
437+
for item in message["content"]:
438+
# Skip tool_result items - they can contain images/documents
439+
# with nested structures that are difficult to redact properly
440+
if (
441+
isinstance(item, dict)
442+
and item.get("type") == "tool_result"
443+
):
444+
continue
445+
446+
# Transform content blocks (images, documents, etc.)
447+
transformed_content.append(
448+
_transform_anthropic_content_block(item)
449+
if isinstance(item, dict)
450+
else item
451+
)
452+
453+
# If there are non-tool-result items, add them as a message
454+
if transformed_content:
455+
normalized_messages.append(
456+
{
457+
"role": message.get("role"),
458+
"content": transformed_content,
459+
}
460+
)
461+
else:
462+
# Transform content for non-list messages or assistant messages
463+
transformed_message = message.copy()
464+
if "content" in transformed_message:
465+
content = transformed_message["content"]
466+
if isinstance(content, (list, tuple)):
467+
transformed_message["content"] = [
468+
_transform_anthropic_content_block(item)
469+
if isinstance(item, dict)
470+
else item
471+
for item in content
472+
]
473+
normalized_messages.append(transformed_message)
474+
475+
role_normalized_messages = normalize_message_roles(normalized_messages)
476+
477+
scope = sentry_sdk.get_current_scope()
478+
messages_data = (
479+
truncate_and_annotate_messages(
480+
role_normalized_messages, span, scope
481+
)
482+
if should_truncate_gen_ai_input(client.options)
483+
else role_normalized_messages
484+
)
485+
if messages_data is not None:
486+
set_data_normalized(
487+
span,
488+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
489+
messages_data,
490+
unpack=False,
491+
)
492+
elif should_send_default_pii() and integration.include_prompts:
493+
if isinstance(system, str) or isinstance(system, Iterable):
494+
set_on_span(
495+
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
496+
json.dumps(_transform_system_instructions(system)),
497+
)
498+
499+
normalized_messages = []
500+
for message in messages:
501+
if (
502+
message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER
503+
and "content" in message
504+
and isinstance(message["content"], (list, tuple))
505+
):
506+
transformed_content = []
507+
for item in message["content"]:
508+
# Skip tool_result items - they can contain images/documents
509+
# with nested structures that are difficult to redact properly
510+
if isinstance(item, dict) and item.get("type") == "tool_result":
511+
continue
512+
513+
# Transform content blocks (images, documents, etc.)
514+
transformed_content.append(
515+
_transform_anthropic_content_block(item)
516+
if isinstance(item, dict)
517+
else item
518+
)
519+
520+
# If there are non-tool-result items, add them as a message
521+
if transformed_content:
522+
normalized_messages.append(
523+
{
524+
"role": message.get("role"),
525+
"content": transformed_content,
526+
}
527+
)
528+
else:
529+
# Transform content for non-list messages or assistant messages
530+
transformed_message = message.copy()
531+
if "content" in transformed_message:
532+
content = transformed_message["content"]
533+
if isinstance(content, (list, tuple)):
534+
transformed_message["content"] = [
535+
_transform_anthropic_content_block(item)
536+
if isinstance(item, dict)
537+
else item
538+
for item in content
539+
]
540+
normalized_messages.append(transformed_message)
541+
542+
role_normalized_messages = normalize_message_roles(normalized_messages)
543+
544+
scope = sentry_sdk.get_current_scope()
545+
messages_data = (
546+
truncate_and_annotate_messages(role_normalized_messages, span, scope)
547+
if should_truncate_gen_ai_input(client.options)
548+
else role_normalized_messages
549+
)
550+
if messages_data is not None:
551+
set_data_normalized(
552+
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False
553+
)
475554

476555

477556
def _set_create_input_data(

0 commit comments

Comments
 (0)