From 46ed68d43d4a57fa06dc7d4457a7f073f1d4c3ad Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 07:10:32 +0000 Subject: [PATCH 1/6] fix(aws-lambda): Only set messaging.batch.message_count for SQS and SNS batches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Lambda span set messaging.batch.message_count to 1 for every non-list event and to len(event) for a top-level list. An SQS or SNS delivery is a dict with Records, so a batch of 10 messages reported 1, and HTTP or direct invocations reported a batch they don't have. Set it only when all records come from SQS or SNS, to the number of records. Co-Authored-By: Johannes Daxböck <114897+dingsdax@users.noreply.github.com> --- .../integrations/aws_lambda/_handler.py | 34 +++++++++++++-- .../aws_lambda/test_aws_lambda.py | 41 ++++++++++++++++++- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda/_handler.py b/sentry_sdk/integrations/aws_lambda/_handler.py index 89ba0089b7..735551fef3 100644 --- a/sentry_sdk/integrations/aws_lambda/_handler.py +++ b/sentry_sdk/integrations/aws_lambda/_handler.py @@ -29,7 +29,7 @@ ) if TYPE_CHECKING: - from typing import Any, Callable, Mapping, TypeVar + from typing import Any, Callable, Mapping, Optional, TypeVar from sentry_sdk._types import Attributes @@ -38,6 +38,30 @@ TIMEOUT_WARNING_BUFFER = 1500 MILLIS_TO_SECONDS = 1000.0 +# `eventSource` of SQS records, `EventSource` of SNS records. +MESSAGING_EVENT_SOURCES = ("aws:sqs", "aws:sns") + + +def _get_messaging_batch_size(aws_event: "Any") -> "Optional[int]": + """The number of SQS or SNS messages in the event, or `None` for any other event.""" + if not isinstance(aws_event, dict): + return None + + records = aws_event.get("Records") + if not isinstance(records, list) or not records: + return None + + sources = { + record.get("eventSource") or record.get("EventSource") + if isinstance(record, dict) + else None + for record in records + } + if len(sources) != 1 or sources.pop() not in MESSAGING_EVENT_SOURCES: + return None + + return len(records) + def _wrap_handler(handler: "F") -> "F": from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration @@ -64,10 +88,8 @@ def sentry_handler( if isinstance(aws_event, list) and len(aws_event) >= 1: request_data = aws_event[0] - batch_size = len(aws_event) else: request_data = aws_event - batch_size = 1 if not isinstance(request_data, dict): # If we're not dealing with a dictionary, we won't be able to get @@ -160,11 +182,15 @@ def sentry_handler( SPANDATA.SENTRY_SEGMENT_NAME_SOURCE: SegmentNameSource.COMPONENT, SPANDATA.CLOUD_PROVIDER: CLOUD_PROVIDER, SPANDATA.CLOUD_PLATFORM: CLOUD_PLATFORM, - SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT: batch_size, **header_attributes, **additional_attributes, } + with capture_internal_exceptions(): + batch_size = _get_messaging_batch_size(aws_event) + if batch_size is not None: + attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = batch_size + with capture_internal_exceptions(): invoked_function_arn = aws_context.invoked_function_arn function_version = aws_context.function_version diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 2d7712b2d2..d039f6f956 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -756,7 +756,7 @@ def test_no_error(lambda_client, test_environment): assert _get_span_attr(attrs, SPANDATA.AWS_LOG_STREAM_NAMES) == [ LATEST_FUNCTION_VERSION ] - assert _get_span_attr(attrs, SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT) == 1 + assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in attrs def test_error(lambda_client, test_environment): @@ -810,7 +810,44 @@ def test_error(lambda_client, test_environment): assert _get_span_attr(attrs, SPANDATA.AWS_LOG_STREAM_NAMES) == [ LATEST_FUNCTION_VERSION ] - assert _get_span_attr(attrs, SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT) == 1 + assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in attrs + + +_SQS_RECORD = {"eventSource": "aws:sqs"} +_SNS_RECORD = {"EventSource": "aws:sns"} + + +@pytest.mark.parametrize( + "aws_event,batch_count", + [ + pytest.param({"Records": [_SQS_RECORD, _SQS_RECORD]}, 2, id="sqs"), + pytest.param({"Records": [_SNS_RECORD]}, 1, id="sns"), + pytest.param({"Records": [{"eventSource": "aws:s3"}]}, None, id="s3"), + pytest.param({"Records": [_SQS_RECORD, _SNS_RECORD]}, None, id="mixed"), + pytest.param({"httpMethod": "GET", "path": "/"}, None, id="http"), + pytest.param( + [{"good dog": "Maisey"}, {"good dog": "Charlie"}], None, id="list" + ), + ], +) +def test_messaging_batch_message_count( + lambda_client, test_environment, aws_event, batch_count +): + lambda_client.invoke( + FunctionName="BasicOkSpanStreaming", + Payload=json.dumps(aws_event), + ) + + (segment_span,) = [ + s for s in test_environment["server"].span_items if s["is_segment"] + ] + attrs = segment_span["attributes"] + if batch_count is None: + assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in attrs + else: + assert ( + _get_span_attr(attrs, SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT) == batch_count + ) def test_trace_continuation(lambda_client, test_environment): From 9337a81c30dd0a475c05e0c50b4dce36fc4813cf Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 07:55:45 +0000 Subject: [PATCH 2/6] fix(aws-lambda): Remove messaging.batch.message_count from the Lambda span Replace the SQS/SNS record counting with a plain removal. The count belongs on messaging spans (boto3 receive and batch-send spans, and the queue.process span for SQS/SNS triggers), as in the OTel messaging conventions. The JS SDK doesn't set it on the Lambda span either. The attribute was added in 3.0 and never shipped in 2.x. Co-Authored-By: johdax <114897+dingsdax@users.noreply.github.com> --- .../integrations/aws_lambda/_handler.py | 31 +--------------- .../aws_lambda/test_aws_lambda.py | 37 ------------------- 2 files changed, 1 insertion(+), 67 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda/_handler.py b/sentry_sdk/integrations/aws_lambda/_handler.py index 735551fef3..7a825bed28 100644 --- a/sentry_sdk/integrations/aws_lambda/_handler.py +++ b/sentry_sdk/integrations/aws_lambda/_handler.py @@ -29,7 +29,7 @@ ) if TYPE_CHECKING: - from typing import Any, Callable, Mapping, Optional, TypeVar + from typing import Any, Callable, Mapping, TypeVar from sentry_sdk._types import Attributes @@ -38,30 +38,6 @@ TIMEOUT_WARNING_BUFFER = 1500 MILLIS_TO_SECONDS = 1000.0 -# `eventSource` of SQS records, `EventSource` of SNS records. -MESSAGING_EVENT_SOURCES = ("aws:sqs", "aws:sns") - - -def _get_messaging_batch_size(aws_event: "Any") -> "Optional[int]": - """The number of SQS or SNS messages in the event, or `None` for any other event.""" - if not isinstance(aws_event, dict): - return None - - records = aws_event.get("Records") - if not isinstance(records, list) or not records: - return None - - sources = { - record.get("eventSource") or record.get("EventSource") - if isinstance(record, dict) - else None - for record in records - } - if len(sources) != 1 or sources.pop() not in MESSAGING_EVENT_SOURCES: - return None - - return len(records) - def _wrap_handler(handler: "F") -> "F": from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration @@ -186,11 +162,6 @@ def sentry_handler( **additional_attributes, } - with capture_internal_exceptions(): - batch_size = _get_messaging_batch_size(aws_event) - if batch_size is not None: - attributes[SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT] = batch_size - with capture_internal_exceptions(): invoked_function_arn = aws_context.invoked_function_arn function_version = aws_context.function_version diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index d039f6f956..37dfbcac50 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -813,43 +813,6 @@ def test_error(lambda_client, test_environment): assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in attrs -_SQS_RECORD = {"eventSource": "aws:sqs"} -_SNS_RECORD = {"EventSource": "aws:sns"} - - -@pytest.mark.parametrize( - "aws_event,batch_count", - [ - pytest.param({"Records": [_SQS_RECORD, _SQS_RECORD]}, 2, id="sqs"), - pytest.param({"Records": [_SNS_RECORD]}, 1, id="sns"), - pytest.param({"Records": [{"eventSource": "aws:s3"}]}, None, id="s3"), - pytest.param({"Records": [_SQS_RECORD, _SNS_RECORD]}, None, id="mixed"), - pytest.param({"httpMethod": "GET", "path": "/"}, None, id="http"), - pytest.param( - [{"good dog": "Maisey"}, {"good dog": "Charlie"}], None, id="list" - ), - ], -) -def test_messaging_batch_message_count( - lambda_client, test_environment, aws_event, batch_count -): - lambda_client.invoke( - FunctionName="BasicOkSpanStreaming", - Payload=json.dumps(aws_event), - ) - - (segment_span,) = [ - s for s in test_environment["server"].span_items if s["is_segment"] - ] - attrs = segment_span["attributes"] - if batch_count is None: - assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in attrs - else: - assert ( - _get_span_attr(attrs, SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT) == batch_count - ) - - def test_trace_continuation(lambda_client, test_environment): trace_id = "471a43a4192642f0b136d5159a501701" parent_span_id = "6e8f22c393e68f19" From 1c106688df3edd0644472fa57121235cc65323ff Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:28:56 +0000 Subject: [PATCH 3/6] ci: Trigger CI (the previous push started no workflows) From c42e110e3f1768f0557ecf4824550aac692c4435 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:19:33 +0000 Subject: [PATCH 4/6] test(aws-lambda): Drop the unused batch_size column in test_non_dict_event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Johannes Daxböck <114897+dingsdax@users.noreply.github.com> --- .../integrations/aws_lambda/test_aws_lambda.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 37dfbcac50..501d6096ba 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -159,12 +159,12 @@ def test_timeout_error_scope_modified(lambda_client, test_environment): @pytest.mark.parametrize( - "aws_event, has_request_data, batch_size", + "aws_event, has_request_data", [ - (b"1231", False, 1), - (b"11.21", False, 1), - (b'"Good dog!"', False, 1), - (b"true", False, 1), + (b"1231", False), + (b"11.21", False), + (b'"Good dog!"', False), + (b"true", False), ( b""" [ @@ -175,7 +175,6 @@ def test_timeout_error_scope_modified(lambda_client, test_environment): ] """, False, - 4, ), ( b""" @@ -207,9 +206,8 @@ def test_timeout_error_scope_modified(lambda_client, test_environment): ] """, True, - 2, ), - (b"[]", False, 1), + (b"[]", False), ], ids=[ "event as integer", @@ -221,9 +219,7 @@ def test_timeout_error_scope_modified(lambda_client, test_environment): "event as empty list", ], ) -def test_non_dict_event( - lambda_client, test_environment, aws_event, has_request_data, batch_size -): +def test_non_dict_event(lambda_client, test_environment, aws_event, has_request_data): lambda_client.invoke( FunctionName="BasicException", Payload=aws_event, From 32d1d2200c420a36b4c3924b5805bb45a31648e6 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:20:05 +0000 Subject: [PATCH 5/6] test(aws-lambda): Drop not-in asserts for batch count --- tests/integrations/aws_lambda/test_aws_lambda.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 501d6096ba..4d0b7bbce5 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -752,7 +752,6 @@ def test_no_error(lambda_client, test_environment): assert _get_span_attr(attrs, SPANDATA.AWS_LOG_STREAM_NAMES) == [ LATEST_FUNCTION_VERSION ] - assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in attrs def test_error(lambda_client, test_environment): @@ -806,7 +805,6 @@ def test_error(lambda_client, test_environment): assert _get_span_attr(attrs, SPANDATA.AWS_LOG_STREAM_NAMES) == [ LATEST_FUNCTION_VERSION ] - assert SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT not in attrs def test_trace_continuation(lambda_client, test_environment): From d3069702c37ff53e682b0e438965ff2848ca2f71 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 12 Nov 2026 13:25:41 +0000 Subject: [PATCH 6/6] docs: Note batch count removal from Lambda span in migration guide --- MIGRATION_GUIDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index e71c2450c0..1103152a13 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -38,6 +38,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - The `UnraisableHookIntegration` is now enabled by default. - We now don't suppress chained exceptions in the ASGI and asyncio integrations by default. The related `suppress_asgi_chained_exceptions` experimental option was removed. - In the AWS Lambda and GCP integrations, the text of the message of the warning the SDK optionally emits if a function is about to time out has changed. +- The AWS Lambda integration no longer sets the `messaging.batch.message_count` attribute on the function span. In 2.x span streaming mode, it was the length of the event if the event was a list, and `1` otherwise, so an SQS or SNS batch was always reported as `1`. The attribute is now only set on messaging spans, such as boto3 SQS and SNS spans. - The `http.request.header.` span attributes set by server integrations are now lists of strings instead of strings. - The HTTP breadcrumbs created by the stdlib (`http.client`) and boto3 integrations now use different keys: `http.method` is now `http.request.method`, `url` (or `aws.request.url` for boto3) is now `url.full`, `http.query` is now `url.query`, and `http.fragment` is now `url.fragment`. The stdlib breadcrumb no longer contains `reason`. The query string is filtered according to `data_collection`'s `url_query_params` setting. - The `url` field of HTTP breadcrumbs created by the HTTPX, httpx2, pyreqwest, and AIOHTTP client integrations now contains the full URL, including the query string and fragment. The query string is filtered according to `data_collection`'s `url_query_params` setting.