Skip to content
Open
Show file tree
Hide file tree
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
219 changes: 149 additions & 70 deletions sentry_sdk/integrations/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
has_data_collection_enabled,
package_version,
reraise,
safe_serialize,
Expand Down Expand Up @@ -390,74 +391,6 @@ def _set_common_input_data(
)
set_on_span(SPANDATA.GEN_AI_SYSTEM, "anthropic")
set_on_span(SPANDATA.GEN_AI_OPERATION_NAME, "chat")
if (
messages is not None
and len(messages) > 0 # type: ignore
and should_send_default_pii()
and integration.include_prompts
):
if isinstance(system, str) or isinstance(system, Iterable):
set_on_span(
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
json.dumps(_transform_system_instructions(system)),
)

normalized_messages = []
for message in messages:
if (
message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER
and "content" in message
and isinstance(message["content"], (list, tuple))
):
transformed_content = []
for item in message["content"]:
Comment on lines -409 to -413

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Tool collection is incorrectly gated by the gen_ai.inputs setting, contradicting the PR's goal to always collect them and causing a regression for users migrating to data_collection.
Severity: MEDIUM

Suggested Fix

Move the tool collection logic outside of the conditional check for gen_ai.inputs within the data_collection path. This will make tool collection unconditional, aligning the behavior with the legacy path and the PR's description.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: sentry_sdk/integrations/anthropic.py#L409-L413

Potential issue: The code in `anthropic.py` gates tool collection behind the
`client.options["data_collection"]["gen_ai"]["inputs"]` setting. This contradicts the
PR's stated goal that "Tools are always collected regardless of the message collection
setting." When users enable the new `data_collection` feature and set `gen_ai.inputs` to
`False`, they will unexpectedly lose tool data that was previously collected by default.
This creates a behavioral inconsistency between the legacy path and the new
`data_collection` path, and represents a regression for migrating users. The tests
confirm this conditional behavior, but they contradict the intended functionality
described in the pull request.

Also affects:

  • tests/integrations/anthropic/test_anthropic.py:329~336

Did we get this right? 👍 / 👎 to inform future reviews.

# Skip tool_result items - they can contain images/documents
# with nested structures that are difficult to redact properly
if isinstance(item, dict) and item.get("type") == "tool_result":
continue

# Transform content blocks (images, documents, etc.)
transformed_content.append(
_transform_anthropic_content_block(item)
if isinstance(item, dict)
else item
)

# If there are non-tool-result items, add them as a message
if transformed_content:
normalized_messages.append(
{
"role": message.get("role"),
"content": transformed_content,
}
)
else:
# Transform content for non-list messages or assistant messages
transformed_message = message.copy()
if "content" in transformed_message:
content = transformed_message["content"]
if isinstance(content, (list, tuple)):
transformed_message["content"] = [
_transform_anthropic_content_block(item)
if isinstance(item, dict)
else item
for item in content
]
normalized_messages.append(transformed_message)

role_normalized_messages = normalize_message_roles(normalized_messages)

client = sentry_sdk.get_client()
scope = sentry_sdk.get_current_scope()
messages_data = (
truncate_and_annotate_messages(role_normalized_messages, span, scope)
if should_truncate_gen_ai_input(client.options)
else role_normalized_messages
)
if messages_data is not None:
set_data_normalized(
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False
)

if max_tokens is not None and _is_given(max_tokens):
set_on_span(SPANDATA.GEN_AI_REQUEST_MAX_TOKENS, max_tokens)
Expand All @@ -470,8 +403,154 @@ def _set_common_input_data(
if top_p is not None and _is_given(top_p):
set_on_span(SPANDATA.GEN_AI_REQUEST_TOP_P, top_p)

if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore
set_on_span(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools))
client = sentry_sdk.get_client()

if has_data_collection_enabled(client.options):
if client.options["data_collection"]["gen_ai"]["inputs"]:
if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore
set_on_span(
SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)
)
else:
# Tools were unconditionally added pre-data collection configuration.
# This can be removed once data collection is fully rolled out
if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore
set_on_span(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools))

if messages is not None and len(messages) > 0: # type: ignore
if has_data_collection_enabled(client.options):
if client.options["data_collection"]["gen_ai"]["inputs"]:
if isinstance(system, str) or isinstance(system, Iterable):
set_on_span(
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
json.dumps(_transform_system_instructions(system)),
)

normalized_messages = []
for message in messages:
if (
message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER
and "content" in message
and isinstance(message["content"], (list, tuple))
):
transformed_content = []
for item in message["content"]:
# Skip tool_result items - they can contain images/documents
# with nested structures that are difficult to redact properly
if (
isinstance(item, dict)
and item.get("type") == "tool_result"
):
continue

# Transform content blocks (images, documents, etc.)
transformed_content.append(
_transform_anthropic_content_block(item)
if isinstance(item, dict)
else item
)

# If there are non-tool-result items, add them as a message
if transformed_content:
normalized_messages.append(
{
"role": message.get("role"),
"content": transformed_content,
}
)
else:
# Transform content for non-list messages or assistant messages
transformed_message = message.copy()
if "content" in transformed_message:
content = transformed_message["content"]
if isinstance(content, (list, tuple)):
transformed_message["content"] = [
_transform_anthropic_content_block(item)
if isinstance(item, dict)
else item
for item in content
]
normalized_messages.append(transformed_message)

role_normalized_messages = normalize_message_roles(normalized_messages)

scope = sentry_sdk.get_current_scope()
messages_data = (
truncate_and_annotate_messages(
role_normalized_messages, span, scope
)
if should_truncate_gen_ai_input(client.options)
else role_normalized_messages
)
if messages_data is not None:
set_data_normalized(
span,
SPANDATA.GEN_AI_REQUEST_MESSAGES,
messages_data,
unpack=False,
)
elif should_send_default_pii() and integration.include_prompts:
if isinstance(system, str) or isinstance(system, Iterable):
set_on_span(
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
json.dumps(_transform_system_instructions(system)),
)

normalized_messages = []
for message in messages:
if (
message.get("role") == GEN_AI_ALLOWED_MESSAGE_ROLES.USER
and "content" in message
and isinstance(message["content"], (list, tuple))
):
transformed_content = []
for item in message["content"]:
# Skip tool_result items - they can contain images/documents
# with nested structures that are difficult to redact properly
if isinstance(item, dict) and item.get("type") == "tool_result":
continue

# Transform content blocks (images, documents, etc.)
transformed_content.append(
_transform_anthropic_content_block(item)
if isinstance(item, dict)
else item
)

# If there are non-tool-result items, add them as a message
if transformed_content:
normalized_messages.append(
{
"role": message.get("role"),
"content": transformed_content,
}
)
else:
# Transform content for non-list messages or assistant messages
transformed_message = message.copy()
if "content" in transformed_message:
content = transformed_message["content"]
if isinstance(content, (list, tuple)):
transformed_message["content"] = [
_transform_anthropic_content_block(item)
if isinstance(item, dict)
else item
for item in content
]
normalized_messages.append(transformed_message)

role_normalized_messages = normalize_message_roles(normalized_messages)

scope = sentry_sdk.get_current_scope()
messages_data = (
truncate_and_annotate_messages(role_normalized_messages, span, scope)
if should_truncate_gen_ai_input(client.options)
else role_normalized_messages
)
if messages_data is not None:
set_data_normalized(
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False
)


def _set_create_input_data(
Expand Down
Loading
Loading