Skip to content
Merged
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
23 changes: 20 additions & 3 deletions sentry_sdk/integrations/boto3/_services/bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@


if TYPE_CHECKING:
from typing import Any, Iterator, Optional, Union
from typing import Any, Iterator, Mapping, Optional, Union

from mypy_boto3_bedrock_runtime.type_defs import (
ConverseResponseTypeDef,
Expand Down Expand Up @@ -171,7 +171,7 @@ def _transform_system_instructions(


def _transform_messages(
input_messages: "Union[list[dict[str, Any]], tuple[dict[str, Any], ...]]",
input_messages: "Union[list[Mapping[str, Any]], tuple[Mapping[str, Any], ...]]",
) -> "list[InputMessage]":
"""
Unlike other providers, AWS declares that input and output messages use the same message type.
Expand Down Expand Up @@ -488,7 +488,8 @@ def on_success_response(
)

if ctx.operation_name == "Converse":
usage = cast("ConverseResponseTypeDef", response)["usage"]
response = cast("ConverseResponseTypeDef", response)
usage = response["usage"]

set_on_span(SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS, usage["outputTokens"])
set_on_span(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS, usage["totalTokens"])
Expand All @@ -511,6 +512,22 @@ def on_success_response(

set_on_span(SPANDATA.GEN_AI_USAGE_INPUT_TOKENS, input_tokens)

client = sentry_sdk.get_client()
if "message" in response["output"] and (
(
has_data_collection_enabled(client.options)
and client.options["data_collection"]["gen_ai"]["outputs"]
)
or (
not has_data_collection_enabled(client.options)
and should_send_default_pii()
)
):
set_on_span(
SPANDATA.GEN_AI_OUTPUT_MESSAGES,
json.dumps(_transform_messages([response["output"]["message"]])),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Output messages use legacy PII gate

Medium Severity

The new gen_ai.output.messages recording still gates on has_data_collection_enabled and should_send_default_pii instead of reading data_collection["gen_ai"]["outputs"] directly. data_collection is already fully resolved at init, including the send_default_pii mapping, so those helpers are not needed. The new test also parametrizes a send_default_pii=True path and never covers mixed inputs/outputs (especially outputs: False).

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: Read data_collection directly; no send_default_pii

Reviewed by Cursor Bugbot for commit fc6b034. Configure here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think it's better to be explicit to be honest 🤷


if isinstance(span, StreamedSpan):
span.end()
return
Expand Down
90 changes: 90 additions & 0 deletions tests/integrations/boto3/test_bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,96 @@ def test_converse_input_attributes(
)


@pytest.mark.parametrize("data_collection", [True, False])
@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_converse_output_attributes(
sentry_init,
capture_items,
capture_events,
data_collection,
span_streaming,
stream_gen_ai_spans,
converse_response,
content_block_messages,
):
if data_collection:
sentry_init(
traces_sample_rate=1.0,
integrations=[Boto3Integration()],
data_collection={"gen_ai": {"outputs": True}},
stream_gen_ai_spans=stream_gen_ai_spans,
trace_lifecycle="stream" if span_streaming else "static",
)
else:
sentry_init(
traces_sample_rate=1.0,
integrations=[Boto3Integration()],
send_default_pii=True,
stream_gen_ai_spans=stream_gen_ai_spans,
trace_lifecycle="stream" if span_streaming else "static",
)

messages, expected_messages = content_block_messages
converse_response["output"]["message"] = {
"role": "assistant",
"content": [block for message in messages for block in message["content"]],
}
expected_output_messages = [
{
"role": "assistant",
"parts": [
part for message in expected_messages for part in message["parts"]
],
}
]

session = boto3.Session(
aws_access_key_id="-", aws_secret_access_key="-", region_name="us-east-1"
)
client = session.client("bedrock-runtime")
stubber = Stubber(client)
stubber.add_response("converse", converse_response)

if span_streaming or stream_gen_ai_spans:
items = capture_items("span")

with stubber, sentry_sdk.start_transaction(name="parent"):
client.converse(
modelId="anthropic.claude-3-sonnet-20240229-v1:0",
messages=[{"role": "user", "content": [{"text": "Hi"}]}],
)

sentry_sdk.flush()
spans = [item.payload for item in items]
(span,) = (
span
for span in spans
if span["attributes"].get("sentry.op") == OP.GEN_AI_CHAT
)

assert (
json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES])
== expected_output_messages
)
else:
events = capture_events()

with stubber, sentry_sdk.start_transaction(name="parent"):
client.converse(
modelId="anthropic.claude-3-sonnet-20240229-v1:0",
messages=[{"role": "user", "content": [{"text": "Hi"}]}],
)

(transaction,) = events
(span,) = transaction["spans"]

assert (
json.loads(span["data"][SPANDATA.GEN_AI_OUTPUT_MESSAGES])
== expected_output_messages
)


@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
def test_converse_stream(
Expand Down
Loading