From 5bef11cd271de6ca6204fc9f82dd04806782e961 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Wed, 3 Jun 2026 15:03:11 -0400 Subject: [PATCH 1/6] feat(aws-lambda): Add span streaming support to AWS Lambda integration When span streaming is enabled via the trace_lifecycle experiment, use sentry_sdk.traces.start_span instead of start_transaction so the function span is emitted as a span envelope item with OTel-compatible attributes (cloud.provider, cloud.platform, faas.*, aws.lambda.*). Also adds CLOUD_PLATFORM.AWS_LAMBDA constant and updates the test server helper to route span envelope items into a separate span_items list so tests can assert on streamed spans independently of error envelopes. Depends on https://github.com/getsentry/sentry-conventions/pull/414 being shipped first as it introduces a number of the conventions used here Fixes PY-2307 Fixes #6005 --- sentry_sdk/integrations/aws_lambda.py | 90 +++++++-- .../integrations/cloud_resource_context.py | 1 + .../BasicOkSpanStreaming/.gitignore | 11 ++ .../BasicOkSpanStreaming/index.py | 15 ++ .../BasicOkSpanStreamingPii/.gitignore | 11 ++ .../BasicOkSpanStreamingPii/index.py | 16 ++ .../RaiseErrorSpanStreaming/.gitignore | 11 ++ .../RaiseErrorSpanStreaming/index.py | 15 ++ .../aws_lambda/test_aws_lambda.py | 176 ++++++++++++++++++ tests/integrations/aws_lambda/utils.py | 21 ++- 10 files changed, 347 insertions(+), 20 deletions(-) create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreaming/.gitignore create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreaming/index.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingPii/.gitignore create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingPii/index.py create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming/.gitignore create mode 100644 tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming/index.py diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index 3a4050cce9..209f6f1a6a 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -12,8 +12,14 @@ from sentry_sdk.consts import OP from sentry_sdk.integrations import Integration from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.scope import should_send_default_pii +from sentry_sdk.integrations.cloud_resource_context import ( + CLOUD_PLATFORM, + CLOUD_PROVIDER, +) +from sentry_sdk.scope import Scope, should_send_default_pii +from sentry_sdk.traces import SegmentSource from sentry_sdk.tracing import TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( AnnotatedValue, TimeoutThread, @@ -144,20 +150,74 @@ def sentry_handler( if not isinstance(headers, dict): headers = {} - transaction = continue_trace( - headers, - op=OP.FUNCTION_AWS, - name=aws_context.function_name, - source=TransactionSource.COMPONENT, - origin=AwsLambdaIntegration.origin, - ) - with sentry_sdk.start_transaction( - transaction, - custom_sampling_context={ - "aws_event": aws_event, - "aws_context": aws_context, - }, - ): + header_attributes: "dict[str, Any]" = {} + for header, header_value in _filter_headers( + headers, use_annotated_value=False + ).items(): + header_attributes[f"http.request.header.{header.lower()}"] = ( + header_value + ) + + additional_attributes: "dict[str, Any]" = {} + if "httpMethod" in request_data: + additional_attributes["http.request.method"] = request_data[ + "httpMethod" + ] + + if should_send_default_pii() and "queryStringParameters" in request_data: + qs = request_data["queryStringParameters"] + if qs: + additional_attributes["url.query"] = "&".join( + f"{k}={v}" for k, v in qs.items() + ) + + aws_region = aws_context.invoked_function_arn.split(":")[3] + + sampling_context = { + "aws_event": aws_event, + "aws_context": aws_context, + } + + function_name = aws_context.function_name + + if has_span_streaming_enabled(client.options): + sentry_sdk.traces.continue_trace(headers) + Scope.set_custom_sampling_context(sampling_context) + span_ctx = sentry_sdk.traces.start_span( + name=function_name, + parent_span=None, + attributes={ + "sentry.op": OP.FUNCTION_AWS, + "sentry.origin": AwsLambdaIntegration.origin, + "sentry.span.source": SegmentSource.COMPONENT, + "cloud.region": aws_region, + "cloud.resource_id": aws_context.invoked_function_arn, + "cloud.platform": CLOUD_PLATFORM.AWS_LAMBDA, + "cloud.provider": CLOUD_PROVIDER.AWS, + "faas.name": function_name, + "faas.invocation_id": aws_context.aws_request_id, + "faas.version": aws_context.function_version, + "aws.lambda.invoked_arn": aws_context.invoked_function_arn, + "aws.log.group.names": [aws_context.log_group_name], + "aws.log.stream.names": [aws_context.log_stream_name], + **header_attributes, + **additional_attributes, + }, + ) + else: + transaction = continue_trace( + headers, + op=OP.FUNCTION_AWS, + name=function_name, + source=TransactionSource.COMPONENT, + origin=AwsLambdaIntegration.origin, + ) + + span_ctx = sentry_sdk.start_transaction( + transaction, custom_sampling_context=sampling_context + ) + + with span_ctx: try: return handler(aws_event, aws_context, *args, **kwargs) except Exception: diff --git a/sentry_sdk/integrations/cloud_resource_context.py b/sentry_sdk/integrations/cloud_resource_context.py index 87aa07ef4c..f6285d0a9b 100644 --- a/sentry_sdk/integrations/cloud_resource_context.py +++ b/sentry_sdk/integrations/cloud_resource_context.py @@ -48,6 +48,7 @@ class CLOUD_PLATFORM: # noqa: N801 """ AWS_EC2 = "aws_ec2" + AWS_LAMBDA = "aws_lambda" GCP_COMPUTE_ENGINE = "gcp_compute_engine" diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreaming/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreaming/.gitignore new file mode 100644 index 0000000000..1c56884372 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreaming/.gitignore @@ -0,0 +1,11 @@ +# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies +# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry. + +# Ignore everything +* + +# But not index.py +!index.py + +# And not .gitignore itself +!.gitignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreaming/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreaming/index.py new file mode 100644 index 0000000000..6fd958fb7d --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreaming/index.py @@ -0,0 +1,15 @@ +import os + +import sentry_sdk +from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration + +sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + traces_sample_rate=1.0, + integrations=[AwsLambdaIntegration()], + _experiments={"trace_lifecycle": "stream"}, +) + + +def handler(event, context): + return {"event": event} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingPii/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingPii/.gitignore new file mode 100644 index 0000000000..1c56884372 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingPii/.gitignore @@ -0,0 +1,11 @@ +# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies +# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry. + +# Ignore everything +* + +# But not index.py +!index.py + +# And not .gitignore itself +!.gitignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingPii/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingPii/index.py new file mode 100644 index 0000000000..9c9ffb0d0e --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingPii/index.py @@ -0,0 +1,16 @@ +import os + +import sentry_sdk +from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration + +sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + traces_sample_rate=1.0, + send_default_pii=True, + integrations=[AwsLambdaIntegration()], + _experiments={"trace_lifecycle": "stream"}, +) + + +def handler(event, context): + return {"event": event} diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming/.gitignore b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming/.gitignore new file mode 100644 index 0000000000..1c56884372 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming/.gitignore @@ -0,0 +1,11 @@ +# Need to add some ignore rules in this directory, because the unit tests will add the Sentry SDK and its dependencies +# into this directory to create a Lambda function package that contains everything needed to instrument a Lambda function using Sentry. + +# Ignore everything +* + +# But not index.py +!index.py + +# And not .gitignore itself +!.gitignore diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming/index.py new file mode 100644 index 0000000000..24ea6404c5 --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming/index.py @@ -0,0 +1,15 @@ +import os + +import sentry_sdk +from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration + +sentry_sdk.init( + dsn=os.environ.get("SENTRY_DSN"), + traces_sample_rate=1.0, + integrations=[AwsLambdaIntegration()], + _experiments={"trace_lifecycle": "stream"}, +) + + +def handler(event, context): + raise Exception("Oh!") diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index c348a5b7a0..eb09f2f67a 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -525,6 +525,182 @@ def test_error_has_new_trace_context( ) +def _get_span_attr(attrs, key): + """Extract the value from a span attribute, handling both flat and typed formats.""" + val = attrs[key] + if isinstance(val, dict) and "value" in val: + return val["value"] + return val + + +def _get_segment_spans(span_items): + """Filter span items to only segment (root) spans.""" + return [s for s in span_items if s.get("is_segment") is True] + + +def test_span_streaming_no_error(lambda_client, test_environment): + lambda_client.invoke( + FunctionName="BasicOkSpanStreaming", + Payload=json.dumps({}), + ) + envelopes = test_environment["server"].envelopes + segment_spans = _get_segment_spans(test_environment["server"].span_items) + + assert len(envelopes) == 0 + assert len(segment_spans) == 1 + + segment_span = segment_spans[0] + assert segment_span["is_segment"] is True + assert segment_span["name"] == "BasicOkSpanStreaming" + attrs = segment_span["attributes"] + assert _get_span_attr(attrs, "sentry.op") == "function.aws" + assert _get_span_attr(attrs, "sentry.origin") == "auto.function.aws_lambda" + assert _get_span_attr(attrs, "sentry.span.source") == "component" + assert _get_span_attr(attrs, "cloud.provider") == "aws" + assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda" + assert ( + _get_span_attr(attrs, "cloud.resource_id") + == "arn:aws:lambda:us-east-1:012345678912:function:BasicOkSpanStreaming" + ) + assert _get_span_attr(attrs, "cloud.region") == "us-east-1" + assert _get_span_attr(attrs, "faas.name") == "BasicOkSpanStreaming" + assert _get_span_attr(attrs, "faas.version") == "$LATEST" + assert "faas.invocation_id" in attrs + assert ( + _get_span_attr(attrs, "aws.lambda.invoked_arn") + == "arn:aws:lambda:us-east-1:012345678912:function:BasicOkSpanStreaming" + ) + assert _get_span_attr(attrs, "aws.log.group.names") == [ + "aws/lambda/BasicOkSpanStreaming" + ] + assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"] + + +def test_span_streaming_error(lambda_client, test_environment): + lambda_client.invoke( + FunctionName="RaiseErrorSpanStreaming", + Payload=json.dumps({}), + ) + envelopes = test_environment["server"].envelopes + segment_spans = _get_segment_spans(test_environment["server"].span_items) + + assert len(envelopes) == 1 + error_event = envelopes[0] + assert error_event["level"] == "error" + (exception,) = error_event["exception"]["values"] + assert exception["type"] == "Exception" + assert exception["value"] == "Oh!" + assert exception["mechanism"]["type"] == "aws_lambda" + assert not exception["mechanism"]["handled"] + + assert len(segment_spans) == 1 + segment_span = segment_spans[0] + assert segment_span["is_segment"] is True + assert segment_span["name"] == "RaiseErrorSpanStreaming" + assert segment_span["status"] == "error" + attrs = segment_span["attributes"] + assert _get_span_attr(attrs, "sentry.op") == "function.aws" + assert _get_span_attr(attrs, "sentry.origin") == "auto.function.aws_lambda" + assert _get_span_attr(attrs, "sentry.span.source") == "component" + assert _get_span_attr(attrs, "cloud.provider") == "aws" + assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda" + assert ( + _get_span_attr(attrs, "cloud.resource_id") + == "arn:aws:lambda:us-east-1:012345678912:function:RaiseErrorSpanStreaming" + ) + assert _get_span_attr(attrs, "cloud.region") == "us-east-1" + assert _get_span_attr(attrs, "faas.name") == "RaiseErrorSpanStreaming" + assert _get_span_attr(attrs, "faas.version") == "$LATEST" + assert "faas.invocation_id" in attrs + assert ( + _get_span_attr(attrs, "aws.lambda.invoked_arn") + == "arn:aws:lambda:us-east-1:012345678912:function:RaiseErrorSpanStreaming" + ) + assert _get_span_attr(attrs, "aws.log.group.names") == [ + "aws/lambda/RaiseErrorSpanStreaming" + ] + assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"] + + +def test_span_streaming_trace_continuation(lambda_client, test_environment): + trace_id = "471a43a4192642f0b136d5159a501701" + parent_span_id = "6e8f22c393e68f19" + parent_sampled = 1 + sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled) + + payload = { + "headers": { + "sentry-trace": sentry_trace_header, + } + } + + lambda_client.invoke( + FunctionName="RaiseErrorSpanStreaming", + Payload=json.dumps(payload), + ) + envelopes = test_environment["server"].envelopes + segment_spans = _get_segment_spans(test_environment["server"].span_items) + + assert len(envelopes) == 1 + error_event = envelopes[0] + assert error_event["contexts"]["trace"]["trace_id"] == trace_id + + assert len(segment_spans) == 1 + segment_span = segment_spans[0] + assert segment_span["is_segment"] is True + assert segment_span["trace_id"] == trace_id + assert segment_span["name"] == "RaiseErrorSpanStreaming" + attrs = segment_span["attributes"] + assert _get_span_attr(attrs, "sentry.op") == "function.aws" + assert _get_span_attr(attrs, "sentry.origin") == "auto.function.aws_lambda" + assert _get_span_attr(attrs, "sentry.span.source") == "component" + assert _get_span_attr(attrs, "cloud.provider") == "aws" + assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda" + assert _get_span_attr(attrs, "cloud.region") == "us-east-1" + assert _get_span_attr(attrs, "faas.name") == "RaiseErrorSpanStreaming" + assert _get_span_attr(attrs, "faas.version") == "$LATEST" + assert "faas.invocation_id" in attrs + + +def test_span_streaming_request_attributes(lambda_client, test_environment): + payload = { + "headers": { + "Content-Type": "application/json", + "Accept": "text/html", + }, + "httpMethod": "POST", + "queryStringParameters": {"foo": "bar"}, + "path": "/test", + } + + lambda_client.invoke( + FunctionName="BasicOkSpanStreamingPii", + Payload=json.dumps(payload), + ) + segment_spans = _get_segment_spans(test_environment["server"].span_items) + + assert len(segment_spans) == 1 + segment_span = segment_spans[0] + attrs = segment_span["attributes"] + + assert _get_span_attr(attrs, "http.request.method") == "POST" + assert _get_span_attr(attrs, "url.query") == "foo=bar" + assert ( + _get_span_attr(attrs, "http.request.header.content-type") == "application/json" + ) + assert _get_span_attr(attrs, "http.request.header.accept") == "text/html" + assert _get_span_attr(attrs, "faas.name") == "BasicOkSpanStreamingPii" + assert _get_span_attr(attrs, "cloud.provider") == "aws" + assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda" + assert _get_span_attr(attrs, "cloud.region") == "us-east-1" + assert _get_span_attr(attrs, "faas.version") == "$LATEST" + assert "faas.invocation_id" in attrs + assert _get_span_attr(attrs, "aws.log.group.names") == [ + "aws/lambda/BasicOkSpanStreamingPii" + ] + assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"] + + @pytest.mark.parametrize( "lambda_function_name", ["RaiseErrorPerformanceEnabled", "RaiseErrorPerformanceDisabled"], diff --git a/tests/integrations/aws_lambda/utils.py b/tests/integrations/aws_lambda/utils.py index 4782d68275..b5b7d18930 100644 --- a/tests/integrations/aws_lambda/utils.py +++ b/tests/integrations/aws_lambda/utils.py @@ -218,6 +218,7 @@ class SentryServerForTesting: def __init__(self, host="0.0.0.0", port=9999, log_level="warning"): self.envelopes = [] + self.span_items = [] self.host = host self.port = port self.log_level = log_level @@ -246,13 +247,22 @@ async def envelope(request: Request): current_line += 1 continue - # skip envelope item header + # parse envelope item header + item_header = json.loads(lines[current_line]) current_line += 1 - # add envelope item to store - envelope_item = lines[current_line] - if envelope_item.strip(): - self.envelopes.append(json.loads(envelope_item)) + # parse envelope item payload + if current_line < len(lines) and lines[current_line].strip(): + parsed_item = json.loads(lines[current_line]) + if item_header.get("type") == "span": + if "items" in parsed_item: + self.span_items.extend(parsed_item["items"]) + else: + self.span_items.append(parsed_item) + else: + self.envelopes.append(parsed_item) + + current_line += 1 return {"status": "ok"} @@ -269,3 +279,4 @@ def start(self): def clear_envelopes(self): print("[SentryServerForTesting] Clearing envelopes") self.envelopes = [] + self.span_items = [] From 590ec6959df693b40162e8432552df044f8a0b8c Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Wed, 3 Jun 2026 15:39:16 -0400 Subject: [PATCH 2/6] Update tests to not filter for segment spans - we know the order in which they're coming in --- .../aws_lambda/test_aws_lambda.py | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index eb09f2f67a..ef72309f69 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -533,26 +533,24 @@ def _get_span_attr(attrs, key): return val -def _get_segment_spans(span_items): - """Filter span items to only segment (root) spans.""" - return [s for s in span_items if s.get("is_segment") is True] - - def test_span_streaming_no_error(lambda_client, test_environment): lambda_client.invoke( FunctionName="BasicOkSpanStreaming", Payload=json.dumps({}), ) envelopes = test_environment["server"].envelopes - segment_spans = _get_segment_spans(test_environment["server"].span_items) + span_items = test_environment["server"].span_items assert len(envelopes) == 0 - assert len(segment_spans) == 1 + assert len(span_items) == 1 + + segment_span = span_items[0] - segment_span = segment_spans[0] assert segment_span["is_segment"] is True assert segment_span["name"] == "BasicOkSpanStreaming" + attrs = segment_span["attributes"] + assert _get_span_attr(attrs, "sentry.op") == "function.aws" assert _get_span_attr(attrs, "sentry.origin") == "auto.function.aws_lambda" assert _get_span_attr(attrs, "sentry.span.source") == "component" @@ -582,7 +580,7 @@ def test_span_streaming_error(lambda_client, test_environment): Payload=json.dumps({}), ) envelopes = test_environment["server"].envelopes - segment_spans = _get_segment_spans(test_environment["server"].span_items) + span_items = test_environment["server"].span_items assert len(envelopes) == 1 error_event = envelopes[0] @@ -593,12 +591,16 @@ def test_span_streaming_error(lambda_client, test_environment): assert exception["mechanism"]["type"] == "aws_lambda" assert not exception["mechanism"]["handled"] - assert len(segment_spans) == 1 - segment_span = segment_spans[0] + assert len(span_items) == 1 + + segment_span = span_items[0] + assert segment_span["is_segment"] is True assert segment_span["name"] == "RaiseErrorSpanStreaming" assert segment_span["status"] == "error" + attrs = segment_span["attributes"] + assert _get_span_attr(attrs, "sentry.op") == "function.aws" assert _get_span_attr(attrs, "sentry.origin") == "auto.function.aws_lambda" assert _get_span_attr(attrs, "sentry.span.source") == "component" @@ -639,14 +641,14 @@ def test_span_streaming_trace_continuation(lambda_client, test_environment): Payload=json.dumps(payload), ) envelopes = test_environment["server"].envelopes - segment_spans = _get_segment_spans(test_environment["server"].span_items) + span_items = test_environment["server"].span_items assert len(envelopes) == 1 error_event = envelopes[0] assert error_event["contexts"]["trace"]["trace_id"] == trace_id - assert len(segment_spans) == 1 - segment_span = segment_spans[0] + assert len(span_items) == 1 + segment_span = span_items[0] assert segment_span["is_segment"] is True assert segment_span["trace_id"] == trace_id assert segment_span["name"] == "RaiseErrorSpanStreaming" @@ -677,10 +679,10 @@ def test_span_streaming_request_attributes(lambda_client, test_environment): FunctionName="BasicOkSpanStreamingPii", Payload=json.dumps(payload), ) - segment_spans = _get_segment_spans(test_environment["server"].span_items) + span_items = test_environment["server"].span_items - assert len(segment_spans) == 1 - segment_span = segment_spans[0] + assert len(span_items) == 1 + segment_span = span_items[0] attrs = segment_span["attributes"] assert _get_span_attr(attrs, "http.request.method") == "POST" From 1047bdb80200dd58a33631ed63f1d00ece6b1c12 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 4 Jun 2026 08:38:33 -0400 Subject: [PATCH 3/6] Filter for segment to avoid the flaky case where http.client spans are sometimes created --- .../aws_lambda/test_aws_lambda.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index ef72309f69..55321f875b 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -542,11 +542,11 @@ def test_span_streaming_no_error(lambda_client, test_environment): span_items = test_environment["server"].span_items assert len(envelopes) == 0 - assert len(span_items) == 1 - segment_span = span_items[0] + segment_spans = [s for s in span_items if s["is_segment"]] + assert len(segment_spans) == 1 + segment_span = segment_spans[0] - assert segment_span["is_segment"] is True assert segment_span["name"] == "BasicOkSpanStreaming" attrs = segment_span["attributes"] @@ -591,11 +591,10 @@ def test_span_streaming_error(lambda_client, test_environment): assert exception["mechanism"]["type"] == "aws_lambda" assert not exception["mechanism"]["handled"] - assert len(span_items) == 1 + segment_spans = [s for s in span_items if s["is_segment"]] + assert len(segment_spans) == 1 + segment_span = segment_spans[0] - segment_span = span_items[0] - - assert segment_span["is_segment"] is True assert segment_span["name"] == "RaiseErrorSpanStreaming" assert segment_span["status"] == "error" @@ -647,9 +646,9 @@ def test_span_streaming_trace_continuation(lambda_client, test_environment): error_event = envelopes[0] assert error_event["contexts"]["trace"]["trace_id"] == trace_id - assert len(span_items) == 1 - segment_span = span_items[0] - assert segment_span["is_segment"] is True + segment_spans = [s for s in span_items if s["is_segment"]] + assert len(segment_spans) == 1 + segment_span = segment_spans[0] assert segment_span["trace_id"] == trace_id assert segment_span["name"] == "RaiseErrorSpanStreaming" attrs = segment_span["attributes"] @@ -681,8 +680,9 @@ def test_span_streaming_request_attributes(lambda_client, test_environment): ) span_items = test_environment["server"].span_items - assert len(span_items) == 1 - segment_span = span_items[0] + segment_spans = [s for s in span_items if s["is_segment"]] + assert len(segment_spans) == 1 + segment_span = segment_spans[0] attrs = segment_span["attributes"] assert _get_span_attr(attrs, "http.request.method") == "POST" From 976c97d43167c6f7cd2069c30a7c2394f5da3282 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 4 Jun 2026 10:49:41 -0400 Subject: [PATCH 4/6] Add info about the size of the batch that was previously added on tag --- sentry_sdk/integrations/aws_lambda.py | 8 +++----- tests/integrations/aws_lambda/test_aws_lambda.py | 2 ++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index 209f6f1a6a..6bc92be030 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -107,6 +107,7 @@ def sentry_handler( request_data = {} configured_time = aws_context.get_remaining_time_in_millis() + aws_region = aws_context.invoked_function_arn.split(":")[3] with sentry_sdk.isolation_scope() as scope: timeout_thread = None @@ -117,9 +118,7 @@ def sentry_handler( request_data, aws_context, configured_time ) ) - scope.set_tag( - "aws_region", aws_context.invoked_function_arn.split(":")[3] - ) + scope.set_tag("aws_region", aws_region) if batch_size > 1: scope.set_tag("batch_request", True) scope.set_tag("batch_size", batch_size) @@ -171,8 +170,6 @@ def sentry_handler( f"{k}={v}" for k, v in qs.items() ) - aws_region = aws_context.invoked_function_arn.split(":")[3] - sampling_context = { "aws_event": aws_event, "aws_context": aws_context, @@ -200,6 +197,7 @@ def sentry_handler( "aws.lambda.invoked_arn": aws_context.invoked_function_arn, "aws.log.group.names": [aws_context.log_group_name], "aws.log.stream.names": [aws_context.log_stream_name], + "messaging.batch.message_count": batch_size, **header_attributes, **additional_attributes, }, diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 55321f875b..4a8c92d902 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -572,6 +572,7 @@ def test_span_streaming_no_error(lambda_client, test_environment): "aws/lambda/BasicOkSpanStreaming" ] assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"] + assert _get_span_attr(attrs, "messaging.batch.message_count") == 1 def test_span_streaming_error(lambda_client, test_environment): @@ -621,6 +622,7 @@ def test_span_streaming_error(lambda_client, test_environment): "aws/lambda/RaiseErrorSpanStreaming" ] assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"] + assert _get_span_attr(attrs, "messaging.batch.message_count") == 1 def test_span_streaming_trace_continuation(lambda_client, test_environment): From 3d9451fbfa80dabb49069e1cddd5c4a26bd922f3 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 4 Jun 2026 13:23:14 -0400 Subject: [PATCH 5/6] Address the encoding issue raised by warden --- sentry_sdk/integrations/aws_lambda.py | 5 ++--- tests/integrations/aws_lambda/test_aws_lambda.py | 4 ++-- tox.ini | 2 ++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index 6bc92be030..c7fe77714a 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta, timezone from os import environ from typing import TYPE_CHECKING +from urllib.parse import urlencode import sentry_sdk from sentry_sdk.api import continue_trace @@ -166,9 +167,7 @@ def sentry_handler( if should_send_default_pii() and "queryStringParameters" in request_data: qs = request_data["queryStringParameters"] if qs: - additional_attributes["url.query"] = "&".join( - f"{k}={v}" for k, v in qs.items() - ) + additional_attributes["url.query"] = urlencode(qs) sampling_context = { "aws_event": aws_event, diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 4a8c92d902..91e5d0a4f1 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -672,7 +672,7 @@ def test_span_streaming_request_attributes(lambda_client, test_environment): "Accept": "text/html", }, "httpMethod": "POST", - "queryStringParameters": {"foo": "bar"}, + "queryStringParameters": {"foo": "bar", "a-complicated-value": "a=b&c=d"}, "path": "/test", } @@ -688,7 +688,7 @@ def test_span_streaming_request_attributes(lambda_client, test_environment): attrs = segment_span["attributes"] assert _get_span_attr(attrs, "http.request.method") == "POST" - assert _get_span_attr(attrs, "url.query") == "foo=bar" + assert _get_span_attr(attrs, "url.query") == "foo=bar&a-complicated-value=a%3Db%26c%3Dd" assert ( _get_span_attr(attrs, "http.request.header.content-type") == "application/json" ) diff --git a/tox.ini b/tox.ini index 7374816f05..be0a419437 100644 --- a/tox.ini +++ b/tox.ini @@ -13744,6 +13744,8 @@ setenv = unleash: _TESTPATH=tests/integrations/unleash passenv = + DOCKER_BUILDKIT + DOCKER_HOST SENTRY_PYTHON_TEST_POSTGRES_HOST SENTRY_PYTHON_TEST_POSTGRES_USER SENTRY_PYTHON_TEST_POSTGRES_PASSWORD From c744f3e4f9cc5ad0566e812b5dec07030bc816ce Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Thu, 4 Jun 2026 13:27:00 -0400 Subject: [PATCH 6/6] lint --- tests/integrations/aws_lambda/test_aws_lambda.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 91e5d0a4f1..ec01284e58 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -688,7 +688,10 @@ def test_span_streaming_request_attributes(lambda_client, test_environment): attrs = segment_span["attributes"] assert _get_span_attr(attrs, "http.request.method") == "POST" - assert _get_span_attr(attrs, "url.query") == "foo=bar&a-complicated-value=a%3Db%26c%3Dd" + assert ( + _get_span_attr(attrs, "url.query") + == "foo=bar&a-complicated-value=a%3Db%26c%3Dd" + ) assert ( _get_span_attr(attrs, "http.request.header.content-type") == "application/json" )