From 70dca2abe81b786685d089e3e7e541290268e317 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Mon, 26 Oct 2026 18:31:21 +0100 Subject: [PATCH 1/8] . --- sentry_sdk/consts.py | 20 ++ .../integrations/boto3/_instrumentation.py | 5 + .../integrations/boto3/_services/base.py | 6 + .../integrations/boto3/_services/sqs.py | 97 +++++-- tests/integrations/boto3/test_sqs.py | 272 ++++++++++++++++-- 5 files changed, 361 insertions(+), 39 deletions(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index f7a673f1bc..8cda281a25 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -471,6 +471,12 @@ class SPANDATA: Example: "db-password" """ + AWS_SQS_QUEUE_URL = "aws.sqs.queue.url" + """ + The URL of the AWS SQS queue the request refers to. + Example: "https://sqs.us-east-1.amazonaws.com/123456789012/orders" + """ + CACHE_HIT = "cache.hit" """ A boolean indicating whether the requested data was found in the cache. @@ -1090,6 +1096,18 @@ class SPANDATA: Number of retries/attempts to process a message. """ + MESSAGING_OPERATION_NAME = "messaging.operation.name" + """ + The system-specific name of the messaging operation. + Example: "publish" + """ + + MESSAGING_OPERATION_TYPE = "messaging.operation.type" + """ + The type of the messaging operation: `create`, `send`, `receive`, `process` or `settle`. + Example: "send" + """ + MESSAGING_SYSTEM = "messaging.system" """ The messaging system's name, e.g. `kafka`, `aws_sqs` @@ -1489,6 +1507,8 @@ class OP: OBJECT_UPLOAD_PART = "object.upload_part" QUEUE_PROCESS = "queue.process" QUEUE_PUBLISH = "queue.publish" + QUEUE_RECEIVE = "queue.receive" + QUEUE_SETTLE = "queue.settle" QUEUE_SUBMIT_ARQ = "queue.submit.arq" QUEUE_TASK_ARQ = "queue.task.arq" QUEUE_SUBMIT_CELERY = "queue.submit.celery" diff --git a/sentry_sdk/integrations/boto3/_instrumentation.py b/sentry_sdk/integrations/boto3/_instrumentation.py index 731dac6fa3..8b7e34da87 100644 --- a/sentry_sdk/integrations/boto3/_instrumentation.py +++ b/sentry_sdk/integrations/boto3/_instrumentation.py @@ -180,6 +180,11 @@ def _start_client_span( with capture_internal_exceptions(): attributes.update(service_ext.get_request_attributes(ctx)) + with capture_internal_exceptions(): + service_span_name = service_ext.get_span_name(ctx, attributes) + if service_span_name is not None: + span_name = service_span_name + # `start_span()` evaluates `ignore_spans` against the initial attributes. # https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/#rpc-client-span attributes.update( diff --git a/sentry_sdk/integrations/boto3/_services/base.py b/sentry_sdk/integrations/boto3/_services/base.py index ec7ac1df1c..edff2d31a9 100644 --- a/sentry_sdk/integrations/boto3/_services/base.py +++ b/sentry_sdk/integrations/boto3/_services/base.py @@ -21,6 +21,12 @@ def get_span_op(self, ctx: "AwsCallContext") -> "Optional[str]": """Return an optional `sentry.op` override, or `None` to keep the default.""" return None + def get_span_name( + self, ctx: "AwsCallContext", attributes: "Attributes" + ) -> "Optional[str]": + """Return an optional span name override from the span's start attributes, or `None` to keep the default.""" + return None + def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes": """Return request attributes to add before the AWS request is made.""" return {} diff --git a/sentry_sdk/integrations/boto3/_services/sqs.py b/sentry_sdk/integrations/boto3/_services/sqs.py index e9ae02142e..833c6f7b4b 100644 --- a/sentry_sdk/integrations/boto3/_services/sqs.py +++ b/sentry_sdk/integrations/boto3/_services/sqs.py @@ -2,36 +2,97 @@ from typing import TYPE_CHECKING from urllib.parse import unquote, urlsplit -from sentry_sdk.consts import SPANDATA +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations.boto3._services.base import _ServiceExtension from sentry_sdk.integrations.boto3._utils import _get_aws_arn_attributes -from sentry_sdk.utils import capture_internal_exceptions if TYPE_CHECKING: + from typing import Any, Optional + from sentry_sdk._types import Attributes from sentry_sdk.integrations.boto3._context import AwsCallContext +# operation -> (`sentry.op`, `messaging.operation.type`) +_MESSAGING_OPERATIONS = { + "SendMessage": (OP.QUEUE_PUBLISH, "send"), + "SendMessageBatch": (OP.QUEUE_PUBLISH, "send"), + "ReceiveMessage": (OP.QUEUE_RECEIVE, "receive"), + "DeleteMessage": (OP.QUEUE_SETTLE, "settle"), + "DeleteMessageBatch": (OP.QUEUE_SETTLE, "settle"), +} + class _SQSExtension(_ServiceExtension): + def get_span_op(self, ctx: "AwsCallContext") -> "Optional[str]": + if ctx.operation_name in _MESSAGING_OPERATIONS: + return _MESSAGING_OPERATIONS[ctx.operation_name][0] + return None + + def get_span_name( + self, ctx: "AwsCallContext", attributes: "Attributes" + ) -> "Optional[str]": + if ctx.operation_name not in _MESSAGING_OPERATIONS: + return None + return f"{attributes[SPANDATA.MESSAGING_OPERATION_TYPE]} {attributes[SPANDATA.MESSAGING_DESTINATION_NAME]}" + def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes": - attributes: "Attributes" = {} - with capture_internal_exceptions(): + attributes = _get_aws_arn_attributes( + (ctx.params.get("DestinationArn"), ctx.params.get("SourceArn")), ("sqs",) + ) + + queue_url = ctx.params.get("QueueUrl") + if queue_url is not None: + attributes[SPANDATA.AWS_SQS_QUEUE_URL] = queue_url + # https://sqs..amazonaws.com// + path = unquote(urlsplit(queue_url).path).strip("/").split("/") + attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = path[-1] + if re.fullmatch(r"[0-9]{12}", path[0]): + attributes[SPANDATA.CLOUD_ACCOUNT_ID] = path[0] + elif "QueueName" in ctx.params: + # `CreateQueue`, `GetQueueUrl`. + attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = ctx.params["QueueName"] + if "QueueOwnerAWSAccountId" in ctx.params: + attributes[SPANDATA.CLOUD_ACCOUNT_ID] = ctx.params[ + "QueueOwnerAWSAccountId" + ] + elif "SourceArn" in ctx.params: + # message move tasks: arn:aws:sqs::: + attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = ctx.params[ + "SourceArn" + ].split(":")[-1] + + if ctx.operation_name in _MESSAGING_OPERATIONS: + operation_type = _MESSAGING_OPERATIONS[ctx.operation_name][1] attributes.update( - _get_aws_arn_attributes( - ( - ctx.params.get("DestinationArn"), - ctx.params.get("SourceArn"), - ), - ("sqs",), - ) + { + SPANDATA.MESSAGING_SYSTEM: "aws_sqs", + SPANDATA.MESSAGING_OPERATION_TYPE: operation_type, + SPANDATA.MESSAGING_OPERATION_NAME: operation_type, + } ) + if operation_type == "send": + attributes[SPANDATA.SENTRY_KIND] = "producer" + if "Entries" in ctx.params: + attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = len( + ctx.params["Entries"] + ) + return attributes - queue_url = ctx.params.get("QueueUrl") - if queue_url is None: + def get_response_attributes( + self, ctx: "AwsCallContext", response: "Any" + ) -> "Attributes": + attributes: "Attributes" = {} + # a `ClientError` response holds no results. + if "Error" in response: return attributes - - with capture_internal_exceptions(): - account_id = unquote(urlsplit(queue_url).path).strip("/").split("/", 1)[0] - if re.fullmatch(r"^[0-9]{12}$", account_id): - attributes[SPANDATA.CLOUD_ACCOUNT_ID] = account_id + if ctx.operation_name == "SendMessage": + attributes[SPANDATA.MESSAGING_MESSAGE_ID] = response["MessageId"] + elif ctx.operation_name == "ReceiveMessage": + # `Messages` is left out when no message was received. + attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = len( + response.get("Messages", ()) + ) + # failed entries come back in a `200 OK` response. + if response.get("Failed"): + attributes[SPANDATA.ERROR_TYPE] = "aws_batch_partial_failure" return attributes diff --git a/tests/integrations/boto3/test_sqs.py b/tests/integrations/boto3/test_sqs.py index ecc81c69d9..2bd6bf56a0 100644 --- a/tests/integrations/boto3/test_sqs.py +++ b/tests/integrations/boto3/test_sqs.py @@ -1,4 +1,5 @@ import pytest +from botocore.exceptions import ClientError from botocore.stub import Stubber from sentry_sdk.consts import OP, SPANDATA @@ -9,23 +10,59 @@ from tests.integrations.boto3.helpers import client_factory as client_factory -@pytest.mark.parametrize("source", ["arn", "queue-url"]) -def test_request_attributes(client_factory, capture_items, source): +@pytest.mark.parametrize( + "method,params,response,queue_url,account_id,resource_id", + [ + pytest.param( + "delete_queue", + {"QueueUrl": "https://sqs.eu-north-1.amazonaws.com/123456789012/orders"}, + {}, + "https://sqs.eu-north-1.amazonaws.com/123456789012/orders", + "123456789012", + None, + id="queue-url", + ), + pytest.param( + "create_queue", + {"QueueName": "orders"}, + {"QueueUrl": "https://sqs.eu-north-1.amazonaws.com/123456789012/orders"}, + None, + None, + None, + id="create-queue", + ), + pytest.param( + "get_queue_url", + {"QueueName": "orders", "QueueOwnerAWSAccountId": "123456789012"}, + {"QueueUrl": "https://sqs.eu-north-1.amazonaws.com/123456789012/orders"}, + None, + "123456789012", + None, + id="get-queue-url", + ), + pytest.param( + "start_message_move_task", + {"SourceArn": "arn:aws:sqs:eu-north-1:123456789012:orders"}, + {"TaskHandle": "task-handle"}, + None, + "123456789012", + "arn:aws:sqs:eu-north-1:123456789012:orders", + id="source-arn", + ), + ], +) +def test_request_attributes( + client_factory, + capture_items, + method, + params, + response, + queue_url, + account_id, + resource_id, +): client = client_factory("sqs") - - if source == "arn": - require_botocore_model_fields(client, "start_message_move_task") - resource_id = "arn:aws:sqs:eu-north-1:123456789012:orders-dlq" - method = "start_message_move_task" - params = {"SourceArn": resource_id} - response = {"TaskHandle": "task-handle"} - else: - resource_id = None - method = "delete_queue" - params = { - "QueueUrl": "https://sqs.eu-north-1.amazonaws.com/123456789012/orders" - } - response = {} + require_botocore_model_fields(client, method) with Stubber(client) as stubber: stubber.add_response(method, response, params) @@ -35,8 +72,201 @@ def test_request_attributes(client_factory, capture_items, source): (span,) = spans[OP.RPC] attributes = span["attributes"] - assert attributes[SPANDATA.CLOUD_ACCOUNT_ID] == "123456789012" - if resource_id is None: - assert SPANDATA.CLOUD_RESOURCE_ID not in attributes - else: - assert attributes[SPANDATA.CLOUD_RESOURCE_ID] == resource_id + assert attributes[SPANDATA.MESSAGING_DESTINATION_NAME] == "orders" + assert attributes.get(SPANDATA.AWS_SQS_QUEUE_URL) == queue_url + assert attributes.get(SPANDATA.CLOUD_ACCOUNT_ID) == account_id + assert attributes.get(SPANDATA.CLOUD_RESOURCE_ID) == resource_id + assert SPANDATA.MESSAGING_SYSTEM not in attributes + + +def test_send_message(client_factory, capture_items): + client = client_factory("sqs") + + with Stubber(client) as stubber: + stubber.add_response( + "send_message", {"MessageId": "5fea7756-0ea4-451a-a703-a558b933e274"} + ) + spans = capture_spans_by_op( + lambda: client.send_message( + QueueUrl="https://sqs.eu-north-1.amazonaws.com/123456789012/orders", + MessageBody="hi", + ), + capture_items, + ) + + (span,) = spans[OP.QUEUE_PUBLISH] + assert span["name"] == "send orders" + attributes = span["attributes"] + assert attributes[SPANDATA.SENTRY_KIND] == "producer" + assert attributes[SPANDATA.MESSAGING_SYSTEM] == "aws_sqs" + assert attributes[SPANDATA.MESSAGING_OPERATION_TYPE] == "send" + assert attributes[SPANDATA.MESSAGING_OPERATION_NAME] == "send" + assert attributes[SPANDATA.MESSAGING_DESTINATION_NAME] == "orders" + assert ( + attributes[SPANDATA.MESSAGING_MESSAGE_ID] + == "5fea7756-0ea4-451a-a703-a558b933e274" + ) + assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in attributes + + +@pytest.mark.parametrize( + "failed,error_type", + [ + pytest.param([], None, id="ok"), + pytest.param( + [{"Id": "2", "Code": "InternalError", "SenderFault": False}], + "aws_batch_partial_failure", + id="partial-failure", + ), + ], +) +def test_send_message_batch(client_factory, capture_items, failed, error_type): + client = client_factory("sqs") + + with Stubber(client) as stubber: + stubber.add_response( + "send_message_batch", + { + "Successful": [ + { + "Id": "1", + "MessageId": "5fea7756", + "MD5OfMessageBody": "49f68a5c8493ec2c0bf489821c21fc3b", + } + ], + "Failed": failed, + }, + ) + spans = capture_spans_by_op( + lambda: client.send_message_batch( + QueueUrl="https://sqs.eu-north-1.amazonaws.com/123456789012/orders", + Entries=[ + {"Id": "1", "MessageBody": "hi"}, + {"Id": "2", "MessageBody": "hi"}, + ], + ), + capture_items, + ) + + (span,) = spans[OP.QUEUE_PUBLISH] + assert span["name"] == "send orders" + assert span["status"] == ("ok" if error_type is None else "error") + attributes = span["attributes"] + assert attributes[SPANDATA.SENTRY_KIND] == "producer" + assert attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] == 2 + assert attributes.get(SPANDATA.ERROR_TYPE) == error_type + assert SPANDATA.MESSAGING_MESSAGE_ID not in attributes + + +@pytest.mark.parametrize( + "response,message_count", + [ + # `Messages` is absent when the queue is empty. + pytest.param({}, 0, id="empty"), + pytest.param( + { + "Messages": [ + {"MessageId": "1", "ReceiptHandle": "receipt-handle-1"}, + {"MessageId": "2", "ReceiptHandle": "receipt-handle-2"}, + ] + }, + 2, + id="messages", + ), + ], +) +def test_receive_message(client_factory, capture_items, response, message_count): + client = client_factory("sqs") + + with Stubber(client) as stubber: + stubber.add_response("receive_message", response) + spans = capture_spans_by_op( + lambda: client.receive_message( + QueueUrl="https://sqs.eu-north-1.amazonaws.com/123456789012/orders", + MaxNumberOfMessages=10, + ), + capture_items, + ) + + (span,) = spans[OP.QUEUE_RECEIVE] + assert span["name"] == "receive orders" + attributes = span["attributes"] + assert attributes[SPANDATA.SENTRY_KIND] == "client" + assert attributes[SPANDATA.MESSAGING_SYSTEM] == "aws_sqs" + assert attributes[SPANDATA.MESSAGING_OPERATION_TYPE] == "receive" + assert attributes[SPANDATA.MESSAGING_OPERATION_NAME] == "receive" + assert attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] == message_count + assert SPANDATA.MESSAGING_MESSAGE_ID not in attributes + assert "receipt-handle" not in str(span) + + +def test_receive_message_error(client_factory, capture_items): + client = client_factory("sqs") + + with Stubber(client) as stubber: + stubber.add_client_error( + "receive_message", + service_error_code="AWS.SimpleQueueService.NonExistentQueue", + http_status_code=400, + ) + + def call(): + with pytest.raises(ClientError): + client.receive_message( + QueueUrl="https://sqs.eu-north-1.amazonaws.com/123456789012/orders" + ) + + spans = capture_spans_by_op(call, capture_items) + + (span,) = spans[OP.QUEUE_RECEIVE] + assert span["status"] == "error" + assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in span["attributes"] + + +@pytest.mark.parametrize( + "method,params,response,message_count", + [ + pytest.param( + "delete_message", + {"ReceiptHandle": "receipt-handle-1"}, + {}, + None, + id="single", + ), + pytest.param( + "delete_message_batch", + { + "Entries": [ + {"Id": "1", "ReceiptHandle": "receipt-handle-1"}, + {"Id": "2", "ReceiptHandle": "receipt-handle-2"}, + ] + }, + {"Successful": [{"Id": "1"}, {"Id": "2"}], "Failed": []}, + 2, + id="batch", + ), + ], +) +def test_delete_message( + client_factory, capture_items, method, params, response, message_count +): + client = client_factory("sqs") + + with Stubber(client) as stubber: + stubber.add_response(method, response) + spans = capture_spans_by_op( + lambda: getattr(client, method)( + QueueUrl="https://sqs.eu-north-1.amazonaws.com/123456789012/orders", + **params, + ), + capture_items, + ) + + (span,) = spans[OP.QUEUE_SETTLE] + assert span["name"] == "settle orders" + attributes = span["attributes"] + assert attributes[SPANDATA.SENTRY_KIND] == "client" + assert attributes[SPANDATA.MESSAGING_OPERATION_TYPE] == "settle" + assert attributes[SPANDATA.MESSAGING_OPERATION_NAME] == "settle" + assert attributes.get(SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT) == message_count + assert "receipt-handle" not in str(span) From 0f904231b43598cf6c6c16dd20933b0b2f8c3bf2 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 27 Oct 2026 17:41:36 +0100 Subject: [PATCH 2/8] wip --- .../integrations/boto3/_instrumentation.py | 7 +- .../integrations/boto3/_services/base.py | 2 +- .../integrations/boto3/_services/sqs.py | 33 +++- tests/integrations/boto3/test_sqs.py | 154 +++++++++++------- 4 files changed, 123 insertions(+), 73 deletions(-) diff --git a/sentry_sdk/integrations/boto3/_instrumentation.py b/sentry_sdk/integrations/boto3/_instrumentation.py index 8b7e34da87..9be195bd95 100644 --- a/sentry_sdk/integrations/boto3/_instrumentation.py +++ b/sentry_sdk/integrations/boto3/_instrumentation.py @@ -180,10 +180,9 @@ def _start_client_span( with capture_internal_exceptions(): attributes.update(service_ext.get_request_attributes(ctx)) - with capture_internal_exceptions(): - service_span_name = service_ext.get_span_name(ctx, attributes) - if service_span_name is not None: - span_name = service_span_name + service_span_name = service_ext.get_span_name(ctx, attributes) + if service_span_name is not None: + span_name = service_span_name # `start_span()` evaluates `ignore_spans` against the initial attributes. # https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/#rpc-client-span diff --git a/sentry_sdk/integrations/boto3/_services/base.py b/sentry_sdk/integrations/boto3/_services/base.py index edff2d31a9..397499bffb 100644 --- a/sentry_sdk/integrations/boto3/_services/base.py +++ b/sentry_sdk/integrations/boto3/_services/base.py @@ -24,7 +24,7 @@ def get_span_op(self, ctx: "AwsCallContext") -> "Optional[str]": def get_span_name( self, ctx: "AwsCallContext", attributes: "Attributes" ) -> "Optional[str]": - """Return an optional span name override from the span's start attributes, or `None` to keep the default.""" + """Return an optional span name override, or `None` to keep the default.""" return None def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes": diff --git a/sentry_sdk/integrations/boto3/_services/sqs.py b/sentry_sdk/integrations/boto3/_services/sqs.py index 833c6f7b4b..1be4fc1483 100644 --- a/sentry_sdk/integrations/boto3/_services/sqs.py +++ b/sentry_sdk/integrations/boto3/_services/sqs.py @@ -33,6 +33,8 @@ def get_span_name( ) -> "Optional[str]": if ctx.operation_name not in _MESSAGING_OPERATIONS: return None + # for example: `send ` when `messaging.operation.type=send`. + # https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/#span-name return f"{attributes[SPANDATA.MESSAGING_OPERATION_TYPE]} {attributes[SPANDATA.MESSAGING_DESTINATION_NAME]}" def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes": @@ -43,20 +45,23 @@ def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes": queue_url = ctx.params.get("QueueUrl") if queue_url is not None: attributes[SPANDATA.AWS_SQS_QUEUE_URL] = queue_url - # https://sqs..amazonaws.com// + + # `https://sqs.eu-north-1.amazonaws.com/123456789012/orders` => ['123456789012', 'orders'] path = unquote(urlsplit(queue_url).path).strip("/").split("/") + attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = path[-1] if re.fullmatch(r"[0-9]{12}", path[0]): attributes[SPANDATA.CLOUD_ACCOUNT_ID] = path[0] elif "QueueName" in ctx.params: - # `CreateQueue`, `GetQueueUrl`. + # `CreateQueue` & `GetQueueUrl` operations. attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = ctx.params["QueueName"] + if "QueueOwnerAWSAccountId" in ctx.params: attributes[SPANDATA.CLOUD_ACCOUNT_ID] = ctx.params[ "QueueOwnerAWSAccountId" ] elif "SourceArn" in ctx.params: - # message move tasks: arn:aws:sqs::: + # `arn:aws:sqs:eu-north-1:123456789012:orders` => `orders` attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = ctx.params[ "SourceArn" ].split(":")[-1] @@ -82,17 +87,27 @@ def get_response_attributes( self, ctx: "AwsCallContext", response: "Any" ) -> "Attributes": attributes: "Attributes" = {} - # a `ClientError` response holds no results. + + # `ClientError` response holds no results. if "Error" in response: return attributes + if ctx.operation_name == "SendMessage": attributes[SPANDATA.MESSAGING_MESSAGE_ID] = response["MessageId"] elif ctx.operation_name == "ReceiveMessage": # `Messages` is left out when no message was received. - attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = len( - response.get("Messages", ()) + messages = response.get("Messages", ()) + attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = len(messages) + if len(messages) == 1: + attributes[SPANDATA.MESSAGING_MESSAGE_ID] = messages[0]["MessageId"] + + # failed entries have a `200 OK` response; use their code when they share one. + # https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_BatchResultErrorEntry.html + error_codes = {entry["Code"] for entry in response.get("Failed", ())} + if error_codes: + attributes[SPANDATA.ERROR_TYPE] = ( + error_codes.pop() + if len(error_codes) == 1 + else "aws_batch_partial_failure" ) - # failed entries come back in a `200 OK` response. - if response.get("Failed"): - attributes[SPANDATA.ERROR_TYPE] = "aws_batch_partial_failure" return attributes diff --git a/tests/integrations/boto3/test_sqs.py b/tests/integrations/boto3/test_sqs.py index 2bd6bf56a0..f28f8cb74b 100644 --- a/tests/integrations/boto3/test_sqs.py +++ b/tests/integrations/boto3/test_sqs.py @@ -9,6 +9,11 @@ ) from tests.integrations.boto3.helpers import client_factory as client_factory +_BATCH_ENTRIES = [ + {"Id": "1", "MessageBody": "hi"}, + {"Id": "2", "MessageBody": "hi"}, +] + @pytest.mark.parametrize( "method,params,response,queue_url,account_id,resource_id", @@ -79,71 +84,94 @@ def test_request_attributes( assert SPANDATA.MESSAGING_SYSTEM not in attributes -def test_send_message(client_factory, capture_items): - client = client_factory("sqs") - - with Stubber(client) as stubber: - stubber.add_response( - "send_message", {"MessageId": "5fea7756-0ea4-451a-a703-a558b933e274"} - ) - spans = capture_spans_by_op( - lambda: client.send_message( - QueueUrl="https://sqs.eu-north-1.amazonaws.com/123456789012/orders", - MessageBody="hi", - ), - capture_items, - ) - - (span,) = spans[OP.QUEUE_PUBLISH] - assert span["name"] == "send orders" - attributes = span["attributes"] - assert attributes[SPANDATA.SENTRY_KIND] == "producer" - assert attributes[SPANDATA.MESSAGING_SYSTEM] == "aws_sqs" - assert attributes[SPANDATA.MESSAGING_OPERATION_TYPE] == "send" - assert attributes[SPANDATA.MESSAGING_OPERATION_NAME] == "send" - assert attributes[SPANDATA.MESSAGING_DESTINATION_NAME] == "orders" - assert ( - attributes[SPANDATA.MESSAGING_MESSAGE_ID] - == "5fea7756-0ea4-451a-a703-a558b933e274" - ) - assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in attributes - - @pytest.mark.parametrize( - "failed,error_type", + "method,params,response,message_count,message_id,error_type", [ - pytest.param([], None, id="ok"), pytest.param( - [{"Id": "2", "Code": "InternalError", "SenderFault": False}], - "aws_batch_partial_failure", - id="partial-failure", + "send_message", + {"MessageBody": "hi"}, + {"MessageId": "5fea7756-0ea4-451a-a703-a558b933e274"}, + None, + "5fea7756-0ea4-451a-a703-a558b933e274", + None, + id="single", ), - ], -) -def test_send_message_batch(client_factory, capture_items, failed, error_type): - client = client_factory("sqs") - - with Stubber(client) as stubber: - stubber.add_response( + pytest.param( "send_message_batch", + {"Entries": _BATCH_ENTRIES}, { "Successful": [ { "Id": "1", - "MessageId": "5fea7756", + "MessageId": "message-1", + "MD5OfMessageBody": "49f68a5c8493ec2c0bf489821c21fc3b", + }, + { + "Id": "2", + "MessageId": "message-2", + "MD5OfMessageBody": "49f68a5c8493ec2c0bf489821c21fc3b", + }, + ], + "Failed": [], + }, + 2, + None, + None, + id="batch", + ), + pytest.param( + "send_message_batch", + {"Entries": _BATCH_ENTRIES}, + { + "Successful": [ + { + "Id": "1", + "MessageId": "message-1", "MD5OfMessageBody": "49f68a5c8493ec2c0bf489821c21fc3b", } ], - "Failed": failed, + "Failed": [{"Id": "2", "Code": "InternalError", "SenderFault": False}], }, - ) + 2, + None, + "InternalError", + id="batch-shared-error-code", + ), + pytest.param( + "send_message_batch", + {"Entries": _BATCH_ENTRIES}, + { + "Successful": [], + "Failed": [ + {"Id": "1", "Code": "InternalError", "SenderFault": False}, + {"Id": "2", "Code": "InvalidParameterValue", "SenderFault": True}, + ], + }, + 2, + None, + "aws_batch_partial_failure", + id="batch-mixed-error-codes", + ), + ], +) +def test_send_message( + client_factory, + capture_items, + method, + params, + response, + message_count, + message_id, + error_type, +): + client = client_factory("sqs") + + with Stubber(client) as stubber: + stubber.add_response(method, response) spans = capture_spans_by_op( - lambda: client.send_message_batch( + lambda: getattr(client, method)( QueueUrl="https://sqs.eu-north-1.amazonaws.com/123456789012/orders", - Entries=[ - {"Id": "1", "MessageBody": "hi"}, - {"Id": "2", "MessageBody": "hi"}, - ], + **params, ), capture_items, ) @@ -153,16 +181,23 @@ def test_send_message_batch(client_factory, capture_items, failed, error_type): assert span["status"] == ("ok" if error_type is None else "error") attributes = span["attributes"] assert attributes[SPANDATA.SENTRY_KIND] == "producer" - assert attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] == 2 + assert attributes[SPANDATA.MESSAGING_OPERATION_TYPE] == "send" + assert attributes[SPANDATA.MESSAGING_OPERATION_NAME] == "send" + assert attributes.get(SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT) == message_count + assert attributes.get(SPANDATA.MESSAGING_MESSAGE_ID) == message_id assert attributes.get(SPANDATA.ERROR_TYPE) == error_type - assert SPANDATA.MESSAGING_MESSAGE_ID not in attributes @pytest.mark.parametrize( - "response,message_count", + "response,message_count,message_id", [ - # `Messages` is absent when the queue is empty. - pytest.param({}, 0, id="empty"), + pytest.param({}, 0, None, id="empty"), + pytest.param( + {"Messages": [{"MessageId": "1", "ReceiptHandle": "receipt-handle-1"}]}, + 1, + "1", + id="single-message", + ), pytest.param( { "Messages": [ @@ -171,11 +206,14 @@ def test_send_message_batch(client_factory, capture_items, failed, error_type): ] }, 2, + None, id="messages", ), ], ) -def test_receive_message(client_factory, capture_items, response, message_count): +def test_receive_message( + client_factory, capture_items, response, message_count, message_id +): client = client_factory("sqs") with Stubber(client) as stubber: @@ -192,11 +230,10 @@ def test_receive_message(client_factory, capture_items, response, message_count) assert span["name"] == "receive orders" attributes = span["attributes"] assert attributes[SPANDATA.SENTRY_KIND] == "client" - assert attributes[SPANDATA.MESSAGING_SYSTEM] == "aws_sqs" assert attributes[SPANDATA.MESSAGING_OPERATION_TYPE] == "receive" assert attributes[SPANDATA.MESSAGING_OPERATION_NAME] == "receive" assert attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] == message_count - assert SPANDATA.MESSAGING_MESSAGE_ID not in attributes + assert attributes.get(SPANDATA.MESSAGING_MESSAGE_ID) == message_id assert "receipt-handle" not in str(span) @@ -219,7 +256,6 @@ def call(): spans = capture_spans_by_op(call, capture_items) (span,) = spans[OP.QUEUE_RECEIVE] - assert span["status"] == "error" assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in span["attributes"] From c73d9ba872907ab99c77eb7206f968b61f9220e6 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 28 Oct 2026 12:56:06 +0100 Subject: [PATCH 3/8] fix(boto3): Add `capture_internal_exceptions()` guard to `get_span_name()` --- sentry_sdk/integrations/boto3/_instrumentation.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/boto3/_instrumentation.py b/sentry_sdk/integrations/boto3/_instrumentation.py index 9be195bd95..8b7e34da87 100644 --- a/sentry_sdk/integrations/boto3/_instrumentation.py +++ b/sentry_sdk/integrations/boto3/_instrumentation.py @@ -180,9 +180,10 @@ def _start_client_span( with capture_internal_exceptions(): attributes.update(service_ext.get_request_attributes(ctx)) - service_span_name = service_ext.get_span_name(ctx, attributes) - if service_span_name is not None: - span_name = service_span_name + with capture_internal_exceptions(): + service_span_name = service_ext.get_span_name(ctx, attributes) + if service_span_name is not None: + span_name = service_span_name # `start_span()` evaluates `ignore_spans` against the initial attributes. # https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/#rpc-client-span From 61994bf639603921585f14f8a313facf4b0493aa Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Wed, 28 Oct 2026 13:00:09 +0100 Subject: [PATCH 4/8] fix(boto3): Simplify error type handling in batch operations --- sentry_sdk/integrations/boto3/_services/sqs.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/integrations/boto3/_services/sqs.py b/sentry_sdk/integrations/boto3/_services/sqs.py index 1be4fc1483..274f6d21dc 100644 --- a/sentry_sdk/integrations/boto3/_services/sqs.py +++ b/sentry_sdk/integrations/boto3/_services/sqs.py @@ -102,12 +102,10 @@ def get_response_attributes( attributes[SPANDATA.MESSAGING_MESSAGE_ID] = messages[0]["MessageId"] # failed entries have a `200 OK` response; use their code when they share one. - # https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_BatchResultErrorEntry.html + # https://docs.aws.amazon.com/sns/latest/api/API_BatchResultErrorEntry.html error_codes = {entry["Code"] for entry in response.get("Failed", ())} - if error_codes: - attributes[SPANDATA.ERROR_TYPE] = ( - error_codes.pop() - if len(error_codes) == 1 - else "aws_batch_partial_failure" - ) + if len(error_codes) == 1: + attributes[SPANDATA.ERROR_TYPE] = error_codes.pop() + elif error_codes: + attributes[SPANDATA.ERROR_TYPE] = "aws_batch_partial_failure" return attributes From c640799da086e733083e4f808952eb51e95d0eef Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Fri, 30 Oct 2026 13:54:10 +0100 Subject: [PATCH 5/8] fix(boto3): make sure span is sent - stop reading operation type from attributes in `get_span_name()` so that failure in `get_request_attributes()` can't make this fail as well - wrap URL parsing in `get_request_attributes()` so failure just loses the attributes that come from url --- .../integrations/boto3/_services/sqs.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/boto3/_services/sqs.py b/sentry_sdk/integrations/boto3/_services/sqs.py index 274f6d21dc..7a1bce2955 100644 --- a/sentry_sdk/integrations/boto3/_services/sqs.py +++ b/sentry_sdk/integrations/boto3/_services/sqs.py @@ -5,6 +5,7 @@ from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations.boto3._services.base import _ServiceExtension from sentry_sdk.integrations.boto3._utils import _get_aws_arn_attributes +from sentry_sdk.utils import capture_internal_exceptions if TYPE_CHECKING: from typing import Any, Optional @@ -35,7 +36,13 @@ def get_span_name( return None # for example: `send ` when `messaging.operation.type=send`. # https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/#span-name - return f"{attributes[SPANDATA.MESSAGING_OPERATION_TYPE]} {attributes[SPANDATA.MESSAGING_DESTINATION_NAME]}" + operation_type = _MESSAGING_OPERATIONS[ctx.operation_name][1] + destination_name = attributes.get(SPANDATA.MESSAGING_DESTINATION_NAME) + return ( + f"{operation_type} {destination_name}" + if destination_name + else operation_type + ) def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes": attributes = _get_aws_arn_attributes( @@ -46,12 +53,13 @@ def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes": if queue_url is not None: attributes[SPANDATA.AWS_SQS_QUEUE_URL] = queue_url - # `https://sqs.eu-north-1.amazonaws.com/123456789012/orders` => ['123456789012', 'orders'] - path = unquote(urlsplit(queue_url).path).strip("/").split("/") + with capture_internal_exceptions(): + # `https://sqs.eu-north-1.amazonaws.com/123456789012/orders` => ['123456789012', 'orders'] + path = unquote(urlsplit(queue_url).path).strip("/").split("/") - attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = path[-1] - if re.fullmatch(r"[0-9]{12}", path[0]): - attributes[SPANDATA.CLOUD_ACCOUNT_ID] = path[0] + attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = path[-1] + if re.fullmatch(r"[0-9]{12}", path[0]): + attributes[SPANDATA.CLOUD_ACCOUNT_ID] = path[0] elif "QueueName" in ctx.params: # `CreateQueue` & `GetQueueUrl` operations. attributes[SPANDATA.MESSAGING_DESTINATION_NAME] = ctx.params["QueueName"] From 48eae1cfb41695e21fd7ebec5664e8e646bf42f1 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 12:31:53 +0000 Subject: [PATCH 6/8] ref(boto3): Guard SQS ARN and batch-count reads separately MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Botocore validates params after our hook, so a bad Entries or ARN value now only loses that attribute, not every SQS request attribute. Move the batch error.type code to _utils so SNS can share it, and link the SQS docs for BatchResultErrorEntry. Co-Authored-By: Johannes Daxböck <114897+dingsdax@users.noreply.github.com> --- .../integrations/boto3/_services/sqs.py | 33 +++++++++++-------- sentry_sdk/integrations/boto3/_utils.py | 15 +++++++++ 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/integrations/boto3/_services/sqs.py b/sentry_sdk/integrations/boto3/_services/sqs.py index 7a1bce2955..d488ee4456 100644 --- a/sentry_sdk/integrations/boto3/_services/sqs.py +++ b/sentry_sdk/integrations/boto3/_services/sqs.py @@ -4,7 +4,10 @@ from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations.boto3._services.base import _ServiceExtension -from sentry_sdk.integrations.boto3._utils import _get_aws_arn_attributes +from sentry_sdk.integrations.boto3._utils import ( + _get_aws_arn_attributes, + _get_batch_error_type, +) from sentry_sdk.utils import capture_internal_exceptions if TYPE_CHECKING: @@ -45,9 +48,14 @@ def get_span_name( ) def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes": - attributes = _get_aws_arn_attributes( - (ctx.params.get("DestinationArn"), ctx.params.get("SourceArn")), ("sqs",) - ) + attributes: "Attributes" = {} + with capture_internal_exceptions(): + attributes.update( + _get_aws_arn_attributes( + (ctx.params.get("DestinationArn"), ctx.params.get("SourceArn")), + ("sqs",), + ) + ) queue_url = ctx.params.get("QueueUrl") if queue_url is not None: @@ -86,9 +94,10 @@ def get_request_attributes(self, ctx: "AwsCallContext") -> "Attributes": if operation_type == "send": attributes[SPANDATA.SENTRY_KIND] = "producer" if "Entries" in ctx.params: - attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = len( - ctx.params["Entries"] - ) + with capture_internal_exceptions(): + attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = len( + ctx.params["Entries"] + ) return attributes def get_response_attributes( @@ -109,11 +118,7 @@ def get_response_attributes( if len(messages) == 1: attributes[SPANDATA.MESSAGING_MESSAGE_ID] = messages[0]["MessageId"] - # failed entries have a `200 OK` response; use their code when they share one. - # https://docs.aws.amazon.com/sns/latest/api/API_BatchResultErrorEntry.html - error_codes = {entry["Code"] for entry in response.get("Failed", ())} - if len(error_codes) == 1: - attributes[SPANDATA.ERROR_TYPE] = error_codes.pop() - elif error_codes: - attributes[SPANDATA.ERROR_TYPE] = "aws_batch_partial_failure" + error_type = _get_batch_error_type(response) + if error_type is not None: + attributes[SPANDATA.ERROR_TYPE] = error_type return attributes diff --git a/sentry_sdk/integrations/boto3/_utils.py b/sentry_sdk/integrations/boto3/_utils.py index 9f124639a6..1d20103313 100644 --- a/sentry_sdk/integrations/boto3/_utils.py +++ b/sentry_sdk/integrations/boto3/_utils.py @@ -38,3 +38,18 @@ def _get_aws_arn_attributes( return attributes return {} + + +def _get_batch_error_type(response: "Any") -> "Optional[str]": + """ + `error.type` for a batch call whose response has `Failed` entries. + Failed entries come back in a `200 OK` response; use their code when they share one. + https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_BatchResultErrorEntry.html + https://docs.aws.amazon.com/sns/latest/api/API_BatchResultErrorEntry.html + """ + error_codes = {entry["Code"] for entry in response.get("Failed", ())} + if len(error_codes) == 1: + return error_codes.pop() + if error_codes: + return "aws_batch_partial_failure" + return None From 89270849599cfdc5200b671eef6d70e9f770cd7a Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 11 Nov 2026 10:08:37 +0000 Subject: [PATCH 7/8] fix(boto3): Don't raise on unexpected SQS response shapes on_success_response() ran outside capture_internal_exceptions(), so a SendMessage or ReceiveMessage response without MessageId (e.g. from a botocore Stubber or an SQS-compatible service) raised a KeyError into the caller after a successful call and left the span open. Co-Authored-By: Ivana Kellyer <131587164+sentrivana@users.noreply.github.com> --- .../integrations/boto3/_services/sqs.py | 29 ++++++++++--------- tests/integrations/boto3/test_sqs.py | 15 ++++++++++ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/sentry_sdk/integrations/boto3/_services/sqs.py b/sentry_sdk/integrations/boto3/_services/sqs.py index 47fca08ec3..21cee61ada 100644 --- a/sentry_sdk/integrations/boto3/_services/sqs.py +++ b/sentry_sdk/integrations/boto3/_services/sqs.py @@ -107,19 +107,22 @@ def on_success_response( ) -> None: attributes: "Attributes" = {} - if ctx.operation_name == "SendMessage": - attributes[SPANDATA.MESSAGING_MESSAGE_ID] = response["MessageId"] - elif ctx.operation_name == "ReceiveMessage": - # `Messages` is left out when no message was received. - messages = response.get("Messages", ()) - attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = len(messages) - if len(messages) == 1: - attributes[SPANDATA.MESSAGING_MESSAGE_ID] = messages[0]["MessageId"] - - error_type = _get_batch_error_type(response) - if error_type is not None: - span.status = SpanStatus.ERROR - attributes[SPANDATA.ERROR_TYPE] = error_type + # the call already succeeded; an unexpected response shape, e.g. from a + # `Stubber` or an SQS-compatible service, must only lose these attributes. + with capture_internal_exceptions(): + if ctx.operation_name == "SendMessage" and "MessageId" in response: + attributes[SPANDATA.MESSAGING_MESSAGE_ID] = response["MessageId"] + elif ctx.operation_name == "ReceiveMessage": + # `Messages` is left out when no message was received. + messages = response.get("Messages", ()) + attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = len(messages) + if len(messages) == 1 and "MessageId" in messages[0]: + attributes[SPANDATA.MESSAGING_MESSAGE_ID] = messages[0]["MessageId"] + + error_type = _get_batch_error_type(response) + if error_type is not None: + span.status = SpanStatus.ERROR + attributes[SPANDATA.ERROR_TYPE] = error_type span.set_attributes(attributes) span.end() diff --git a/tests/integrations/boto3/test_sqs.py b/tests/integrations/boto3/test_sqs.py index f28f8cb74b..5305f1414e 100644 --- a/tests/integrations/boto3/test_sqs.py +++ b/tests/integrations/boto3/test_sqs.py @@ -96,6 +96,15 @@ def test_request_attributes( None, id="single", ), + pytest.param( + "send_message", + {"MessageBody": "hi"}, + {}, + None, + None, + None, + id="single-without-message-id", + ), pytest.param( "send_message_batch", {"Entries": _BATCH_ENTRIES}, @@ -198,6 +207,12 @@ def test_send_message( "1", id="single-message", ), + pytest.param( + {"Messages": [{"Body": "hi"}]}, + 1, + None, + id="single-message-without-message-id", + ), pytest.param( { "Messages": [ From 4d4d66c1efcd162ac2efc5137cbd3833bc9c3fe3 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 11 Nov 2026 10:08:43 +0000 Subject: [PATCH 8/8] docs(boto3): Document SQS span names and ops in migration guide SQS messaging calls no longer follow the . name and rpc op the guide describes for boto3 spans. Co-Authored-By: Ivana Kellyer <131587164+sentrivana@users.noreply.github.com> --- MIGRATION_GUIDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index b9e08520a6..8f6b7e1a69 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -47,7 +47,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - The `RustTracingIntegration` no longer includes the values of tracing fields if `send_default_pii` is enabled. Pass `include_tracing_fields=True` to the integration to include them. - Whether the Dramatiq integration attaches message bodies to events is now controlled by `data_collection`'s `queues` setting instead of `http_bodies`. - The Celery integration no longer sets the status of task spans to error when a task raises a control flow exception (`Retry`, `Ignore`, or `Reject`). -- The boto3 integration now names spans `.` (for example, `S3.GetObject` or `DynamoDB.GetItem`), following OpenTelemetry semantic conventions. Previously, spans were named `aws..` (for example, `aws.s3.GetObject`). Its spans, including the span for reading a streamed response body, now also have the origin `auto.aws.boto3` instead of `auto.http.boto3`. Update any alerts, dashboards, or `ignore_spans` rules that match on the old names or origin. +- The boto3 integration now names spans `.` (for example, `S3.GetObject` or `DynamoDB.GetItem`), following OpenTelemetry semantic conventions. Previously, spans were named `aws..` (for example, `aws.s3.GetObject`). Its spans, including the span for reading a streamed response body, now also have the origin `auto.aws.boto3` instead of `auto.http.boto3`. SQS messaging calls are the exception: `SendMessage` and `SendMessageBatch` are named `send `, `ReceiveMessage` is named `receive `, and `DeleteMessage` and `DeleteMessageBatch` are named `settle `. Update any alerts, dashboards, or `ignore_spans` rules that match on the old names or origin. - Enabling an AI agent integration (LangChain, OpenAI Agents, or Pydantic AI) no longer disables the OpenAI, Anthropic, and Google GenAI integrations. Instead, those integrations don't create chat spans for requests made from within an agent framework's chat span, but still create other spans, such as embedding spans. The LangChain integration no longer instruments embeddings directly; embedding spans are now created by the integration of the underlying client library. - Whether the MCP integration records tool arguments, tool results, and prompt contents is now controlled by `data_collection`'s new `mcp` setting (`{"inputs": bool, "outputs": bool}`, both `True` by default) instead of the `gen_ai` setting. - With `handled_tool_call_exceptions=True` (the default), the Pydantic AI integration now reports an error for every failed tool argument validation attempt, including retries. Previously, only one error was reported per tool call. @@ -77,7 +77,7 @@ Span ops are now a smaller, framework-neutral set. - The Django integration now emits `view.render` instead of `view.response.render`, `view` instead of `view.authenticate`, and `function` instead of `event.django`. - The Django, Starlette, Starlite, and Litestar integrations now emit `middleware` instead of `middleware.django`, `middleware.starlette`, `middleware.starlette.receive`, `middleware.starlette.send`, `middleware.starlite`, `middleware.starlite.receive`, `middleware.starlite.send`, `middleware.litestar`, `middleware.litestar.receive`, and `middleware.litestar.send`. - The ARQ, Celery, Django, Dramatiq, Huey, RQ, and Ray integrations now emit `queue.publish` and `queue.process` instead of `queue.submit.arq`, `queue.task.arq`, `queue.submit.celery`, `queue.task.celery`, `queue.submit.django`, `queue.task.dramatiq`, `queue.submit.huey`, `queue.task.huey`, `queue.task.rq`, `queue.submit.ray`, and `queue.task.ray`. -- The Boto3 integration now emits `rpc` instead of `http.client` for AWS API calls. +- The Boto3 integration now emits `rpc` instead of `http.client` for AWS API calls. SQS messaging calls emit `queue.publish` (`SendMessage`, `SendMessageBatch`), `queue.receive` (`ReceiveMessage`), and `queue.settle` (`DeleteMessage`, `DeleteMessageBatch`) instead. #### GenAI span attributes