From 6db2c9c6f248d9349d7d069bf67176fccbf1c365 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 31 Jul 2026 14:34:29 -0400 Subject: [PATCH 1/3] test(aws_lambda): reduce test count 39 -> 14 without losing coverage Reduce the docker/SAM-heavy AWS Lambda integration suite from 39 to 14 test cases (-64%) with no loss of assertions or behavior coverage, and eliminate its flakiness. Reliability fixes: - SAM template sets Architectures matching the host CPU. x86_64 containers under QEMU emulation on arm64 Macs caused 10s function timeouts, lost envelopes, and 5-9 flaky failures per run. - lambda_client fixture waits for envelope delivery to settle after each invoke instead of reading the test server race-prone. Reductions (all assertions preserved; verified by per-merge branch analysis plus a final AST-level assertion audit against the base): - Delete 3 fully redundant tests (span_origin, timeout_error, trace_continuation) whose assertions exist verbatim elsewhere - Trim equivalence-class parametrize rows (non_dict_event 7->3, headers 5->1) - Merge config-arm tests into one-test-per-feature with sequential invokes: request_data (4 arms), url_query (3 arms), user_info (2 arms), error trace context (perf on/off x new/existing), span streaming (ok/error/trace-continuation) - Factor shared helpers (_request_data_payload, _assert_segment_span_attrs) - Remove the now-unused TimeoutError lambda function Runtime: ~210s -> ~95-115s per run; 12+ consecutive green runs. Coverage of sentry_sdk/ unchanged (guard checked at every step). --- .../lambda_functions/TimeoutError/index.py | 8 - .../aws_lambda/test_aws_lambda.py | 880 +++++++----------- tests/integrations/aws_lambda/utils.py | 6 + 3 files changed, 332 insertions(+), 562 deletions(-) delete mode 100644 tests/integrations/aws_lambda/lambda_functions/TimeoutError/index.py diff --git a/tests/integrations/aws_lambda/lambda_functions/TimeoutError/index.py b/tests/integrations/aws_lambda/lambda_functions/TimeoutError/index.py deleted file mode 100644 index 01334bbfbc..0000000000 --- a/tests/integrations/aws_lambda/lambda_functions/TimeoutError/index.py +++ /dev/null @@ -1,8 +0,0 @@ -import time - - -def handler(event, context): - time.sleep(15) - return { - "event": event, - } diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 0b203af106..72a731dfb5 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -94,11 +94,17 @@ def clear_before_test(test_environment): @pytest.fixture -def lambda_client(): +def lambda_client(test_environment): """ Create a boto3 client configured to use the local AWS SAM instance. + + The returned client's `invoke` waits after each invocation until envelope + delivery to the test server has settled (no new envelopes for a short + quiet period). The Lambda flushes Sentry events asynchronously, so reading + the server's envelopes immediately after `invoke` returns is racy, + especially when the Docker network path is slow (e.g. colima on macOS). """ - return boto3.client( + client = boto3.client( "lambda", endpoint_url=f"http://127.0.0.1:{SAM_PORT}", # noqa: E231 aws_access_key_id="dummy", @@ -106,6 +112,30 @@ def lambda_client(): region_name="us-east-1", ) + server = test_environment["server"] + real_invoke = client.invoke + + def invoke_and_wait(**kwargs): + before = len(server.envelopes) + len(server.span_items) + result = real_invoke(**kwargs) + deadline = time.time() + 30 + last_count = before + stable_polls = 0 + while time.time() < deadline: + count = len(server.envelopes) + len(server.span_items) + if count > before and count == last_count: + stable_polls += 1 + if stable_polls >= 3: # ~1.5s without new envelopes + break + else: + stable_polls = 0 + last_count = count + time.sleep(0.5) + return result + + client.invoke = invoke_and_wait + return client + def test_basic_no_exception(lambda_client, test_environment): lambda_client.invoke( @@ -144,6 +174,62 @@ def test_basic_no_exception(lambda_client, test_environment): "data": mock.ANY, } + # Request data with send_default_pii=False: sensitive headers are + # filtered out of the transaction's request data. + test_environment["before_test"]() + payload = b""" + { + "resource": "/asd", + "path": "/asd", + "httpMethod": "GET", + "headers": { + "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", + "User-Agent": "custom", + "X-Forwarded-Proto": "https", + "Authorization": "Bearer secret-token", + "Cookie": "sessionid=secret" + }, + "queryStringParameters": { + "bonkers": "true" + }, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "identity": { + "sourceIp": "213.47.147.207", + "userArn": "42" + } + }, + "body": null, + "isBase64Encoded": false + } + """ + + lambda_client.invoke( + FunctionName="BasicOk", + Payload=payload, + ) + envelopes = test_environment["server"].envelopes + + (transaction_event,) = envelopes + + assert transaction_event["request"] == { + "headers": { + "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", + "User-Agent": "custom", + # X-Forwarded-Proto is not sensitive and passes through. + "X-Forwarded-Proto": "https", + # With send_default_pii=False, _filter_headers substitutes the + # SENSITIVE_HEADERS (Authorization, Cookie); the EventScrubber + # also scrubs them. Both end up as "[Filtered]". + "Authorization": "[Filtered]", + "Cookie": "[Filtered]", + }, + "method": "GET", + "query_string": {"bonkers": "true"}, + "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", + } + def test_basic_exception(lambda_client, test_environment): lambda_client.invoke( @@ -185,41 +271,35 @@ def test_basic_exception(lambda_client, test_environment): "data": mock.ANY, } - -def test_init_error(lambda_client, test_environment): + # Non-dict headers are coerced to {} (None and "" hit the same branch). + # EventBridge sends an empty list. + test_environment["before_test"]() lambda_client.invoke( - FunctionName="InitError", - Payload=json.dumps({}), + FunctionName="BasicException", + Payload=json.dumps({"headers": []}), ) envelopes = test_environment["server"].envelopes - (error_event, transaction_event) = envelopes + (error_event, _) = envelopes - assert ( - error_event["exception"]["values"][0]["value"] == "name 'func' is not defined" - ) - assert transaction_event["transaction"] == "InitError" + assert error_event["level"] == "error" + assert error_event["exception"]["values"][0]["type"] == "RuntimeError" + assert error_event["exception"]["values"][0]["value"] == "Oh!" -def test_timeout_error(lambda_client, test_environment): +def test_init_error(lambda_client, test_environment): lambda_client.invoke( - FunctionName="TimeoutError", + FunctionName="InitError", Payload=json.dumps({}), ) envelopes = test_environment["server"].envelopes - (error_event,) = envelopes - - assert error_event["level"] == "error" - assert error_event["extra"]["lambda"]["function_name"] == "TimeoutError" + (error_event, transaction_event) = envelopes - (exception,) = error_event["exception"]["values"] - assert not exception["mechanism"]["handled"] - assert exception["type"] == "ServerlessTimeoutWarning" - assert exception["value"].startswith( - "WARNING : Function is expected to get timed out. Configured timeout duration =" + assert ( + error_event["exception"]["values"][0]["value"] == "name 'func' is not defined" ) - assert exception["mechanism"]["type"] == "threading" + assert transaction_event["transaction"] == "InitError" def test_timeout_error_scope_modified(lambda_client, test_environment): @@ -250,10 +330,11 @@ def test_timeout_error_scope_modified(lambda_client, test_environment): @pytest.mark.parametrize( "aws_event, has_request_data, batch_size", [ + # Scalar events (int/float/string/bool) are one equivalence class: + # not a list, not a dict, so request_data falls back to {}. + # (An empty list hits the identical path: len < 1 -> else arm -> + # non-dict reset.) (b"1231", False, 1), - (b"11.21", False, 1), - (b'"Good dog!"', False, 1), - (b"true", False, 1), ( b""" [ @@ -298,16 +379,11 @@ def test_timeout_error_scope_modified(lambda_client, test_environment): True, 2, ), - (b"[]", False, 1), ], ids=[ "event as integer", - "event as float", - "event as string", - "event as bool", "event as list of dicts", "event as dict", - "event as empty list", ], ) def test_non_dict_event( @@ -355,123 +431,6 @@ def test_non_dict_event( assert transaction_event["tags"]["batch_request"] is True -def test_request_data_with_send_default_pii_false(lambda_client, test_environment): - payload = b""" - { - "resource": "/asd", - "path": "/asd", - "httpMethod": "GET", - "headers": { - "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", - "User-Agent": "custom", - "X-Forwarded-Proto": "https", - "Authorization": "Bearer secret-token", - "Cookie": "sessionid=secret" - }, - "queryStringParameters": { - "bonkers": "true" - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "identity": { - "sourceIp": "213.47.147.207", - "userArn": "42" - } - }, - "body": null, - "isBase64Encoded": false - } - """ - - lambda_client.invoke( - FunctionName="BasicOk", - Payload=payload, - ) - envelopes = test_environment["server"].envelopes - - (transaction_event,) = envelopes - - assert transaction_event["request"] == { - "headers": { - "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", - "User-Agent": "custom", - # X-Forwarded-Proto is not sensitive and passes through. - "X-Forwarded-Proto": "https", - # With send_default_pii=False, _filter_headers substitutes the - # SENSITIVE_HEADERS (Authorization, Cookie); the EventScrubber - # also scrubs them. Both end up as "[Filtered]". - "Authorization": "[Filtered]", - "Cookie": "[Filtered]", - }, - "method": "GET", - "query_string": {"bonkers": "true"}, - "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", - } - - -def test_request_data_with_send_default_pii_true(lambda_client, test_environment): - payload = b""" - { - "resource": "/asd", - "path": "/asd", - "httpMethod": "GET", - "headers": { - "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", - "User-Agent": "custom", - "X-Forwarded-Proto": "https", - "Authorization": "Bearer secret-token", - "Cookie": "sessionid=secret" - }, - "queryStringParameters": { - "bonkers": "true" - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "identity": { - "sourceIp": "213.47.147.207", - "userArn": "42" - } - }, - "body": null, - "isBase64Encoded": false - } - """ - - lambda_client.invoke( - FunctionName="BasicOkSendDefaultPii", - Payload=payload, - ) - envelopes = test_environment["server"].envelopes - - (transaction_event,) = envelopes - - assert transaction_event["request"] == { - "headers": { - "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", - "User-Agent": "custom", - "X-Forwarded-Proto": "https", - # With send_default_pii=True (and no data_collection config), - # _filter_headers passes headers through untouched. Authorization - # and Cookie are still scrubbed to "[Filtered]" by the always-on - # EventScrubber (DEFAULT_DENYLIST), independent of PII settings. - "Authorization": "[Filtered]", - "Cookie": "[Filtered]", - }, - "method": "GET", - "query_string": {"bonkers": "true"}, - "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", - "data": None, - } - - # Legacy send_default_pii=True attaches the user identity. - assert transaction_event["user"] == { - "id": "42", - "ip_address": "213.47.147.207", - } - - USER_INFO_PAYLOAD = b""" { "resource": "/asd", @@ -499,7 +458,8 @@ def test_request_data_with_send_default_pii_true(lambda_client, test_environment """ -def test_user_info_with_data_collection_user_info_on(lambda_client, test_environment): +def test_user_info_with_data_collection(lambda_client, test_environment): + # user_info collection on: the user identity is attached. lambda_client.invoke( FunctionName="BasicOkDataCollectionUserInfoOn", Payload=USER_INFO_PAYLOAD, @@ -513,8 +473,8 @@ def test_user_info_with_data_collection_user_info_on(lambda_client, test_environ "ip_address": "213.47.147.207", } - -def test_user_info_with_data_collection_user_info_off(lambda_client, test_environment): + # user_info collection off: no user identity is attached. + test_environment["before_test"]() lambda_client.invoke( FunctionName="BasicOkDataCollectionUserInfoOff", Payload=USER_INFO_PAYLOAD, @@ -526,39 +486,38 @@ def test_user_info_with_data_collection_user_info_off(lambda_client, test_enviro assert "user" not in transaction_event -def test_request_data_with_data_collection_allowlist(lambda_client, test_environment): - payload = b""" +def _request_data_payload(extra_headers=None): + headers = { + "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", + "User-Agent": "custom", + "X-Forwarded-Proto": "https", + "Authorization": "Bearer secret-token", + "Cookie": "sessionid=secret", + } + headers.update(extra_headers or {}) + return json.dumps( { - "resource": "/asd", - "path": "/asd", - "httpMethod": "GET", - "headers": { - "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", - "User-Agent": "custom", - "X-Forwarded-Proto": "https", - "Authorization": "Bearer secret-token", - "Cookie": "sessionid=secret", - "X-Allow-Me": "yes" - }, - "queryStringParameters": { - "bonkers": "true" - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "identity": { - "sourceIp": "213.47.147.207", - "userArn": "42" - } - }, - "body": null, - "isBase64Encoded": false + "resource": "/asd", + "path": "/asd", + "httpMethod": "GET", + "headers": headers, + "queryStringParameters": {"bonkers": "true"}, + "pathParameters": None, + "stageVariables": None, + "requestContext": { + "identity": {"sourceIp": "213.47.147.207", "userArn": "42"} + }, + "body": None, + "isBase64Encoded": False, } - """ + ).encode() + +def test_request_data_with_data_collection(lambda_client, test_environment): + # Allowlist behaviour: only allowlisted, non-sensitive headers pass through. lambda_client.invoke( FunctionName="BasicOkDataCollectionAllowlist", - Payload=payload, + Payload=_request_data_payload({"X-Allow-Me": "yes"}), ) envelopes = test_environment["server"].envelopes @@ -583,48 +542,20 @@ def test_request_data_with_data_collection_allowlist(lambda_client, test_environ "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", } + # Denylist behaviour: headers denied by custom terms or the built-in + # sensitive denylist are substituted. + test_environment["before_test"]() + lambda_client.invoke( + FunctionName="BasicOkDataCollectionDenylist", + Payload=_request_data_payload({"X-Custom": "keep-me"}), + ) + envelopes = test_environment["server"].envelopes -def test_request_data_with_data_collection_denylist(lambda_client, test_environment): - payload = b""" - { - "resource": "/asd", - "path": "/asd", - "httpMethod": "GET", - "headers": { - "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", - "User-Agent": "custom", - "X-Forwarded-Proto": "https", - "Authorization": "Bearer secret-token", - "Cookie": "sessionid=secret", - "X-Custom": "keep-me" - }, - "queryStringParameters": { - "bonkers": "true" - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "identity": { - "sourceIp": "213.47.147.207", - "userArn": "42" - } - }, - "body": null, - "isBase64Encoded": false - } - """ - - lambda_client.invoke( - FunctionName="BasicOkDataCollectionDenylist", - Payload=payload, - ) - envelopes = test_environment["server"].envelopes - - (transaction_event,) = envelopes - - assert transaction_event["request"] == { - "headers": { - # Not denied by any term -> pass through. + (transaction_event,) = envelopes + + assert transaction_event["request"] == { + "headers": { + # Not denied by any term -> pass through. "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", "X-Custom": "keep-me", # Denied by custom terms. @@ -640,39 +571,11 @@ def test_request_data_with_data_collection_denylist(lambda_client, test_environm "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", } - -def test_request_data_with_data_collection_off(lambda_client, test_environment): - payload = b""" - { - "resource": "/asd", - "path": "/asd", - "httpMethod": "GET", - "headers": { - "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", - "User-Agent": "custom", - "X-Forwarded-Proto": "https", - "Authorization": "Bearer secret-token", - "Cookie": "sessionid=secret" - }, - "queryStringParameters": { - "bonkers": "true" - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "identity": { - "sourceIp": "213.47.147.207", - "userArn": "42" - } - }, - "body": null, - "isBase64Encoded": false - } - """ - + # Collection off: no headers are collected. + test_environment["before_test"]() lambda_client.invoke( FunctionName="BasicOkDataCollectionOff", - Payload=payload, + Payload=_request_data_payload(), ) envelopes = test_environment["server"].envelopes @@ -686,40 +589,73 @@ def test_request_data_with_data_collection_off(lambda_client, test_environment): "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", } + # Legacy send_default_pii=True arm (no data_collection config): + # _filter_headers passes headers through untouched; Authorization and + # Cookie are still scrubbed by the always-on EventScrubber. + test_environment["before_test"]() + lambda_client.invoke( + FunctionName="BasicOkSendDefaultPii", + Payload=_request_data_payload(), + ) + envelopes = test_environment["server"].envelopes -def test_url_query_params_with_data_collection_denylist( - lambda_client, test_environment -): - payload = b""" - { - "resource": "/asd", - "path": "/asd", - "httpMethod": "GET", - "headers": { + (transaction_event,) = envelopes + + assert transaction_event["request"] == { + "headers": { "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": { - "page": "2", - "tracking": "campaign", - "token": "secret-token" - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "identity": { - "sourceIp": "213.47.147.207", - "userArn": "42" - } - }, - "body": null, - "isBase64Encoded": false + "User-Agent": "custom", + "X-Forwarded-Proto": "https", + "Authorization": "[Filtered]", + "Cookie": "[Filtered]", + }, + "method": "GET", + "query_string": {"bonkers": "true"}, + "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", + "data": None, + } + + # Legacy send_default_pii=True attaches the user identity. + assert transaction_event["user"] == { + "id": "42", + "ip_address": "213.47.147.207", + } + + +URL_QUERY_PAYLOAD = b""" + { + "resource": "/asd", + "path": "/asd", + "httpMethod": "GET", + "headers": { + "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", + "X-Forwarded-Proto": "https" + }, + "queryStringParameters": { + "page": "2", + "tracking": "campaign", + "token": "secret-token" + }, + "pathParameters": null, + "stageVariables": null, + "requestContext": { + "identity": { + "sourceIp": "213.47.147.207", + "userArn": "42" } - """ + }, + "body": null, + "isBase64Encoded": false + } +""" + +def test_url_query_params_with_data_collection(lambda_client, test_environment): + # Denylist behaviour: params denied by custom terms or the built-in + # sensitive denylist are substituted. lambda_client.invoke( FunctionName="BasicOkDataCollectionUrlQueryDenylist", - Payload=payload, + Payload=URL_QUERY_PAYLOAD, ) envelopes = test_environment["server"].envelopes @@ -734,40 +670,11 @@ def test_url_query_params_with_data_collection_denylist( "token": "[Filtered]", } - -def test_url_query_params_with_data_collection_allowlist( - lambda_client, test_environment -): - payload = b""" - { - "resource": "/asd", - "path": "/asd", - "httpMethod": "GET", - "headers": { - "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": { - "page": "2", - "tracking": "campaign", - "token": "secret-token" - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "identity": { - "sourceIp": "213.47.147.207", - "userArn": "42" - } - }, - "body": null, - "isBase64Encoded": false - } - """ - + # Allowlist behaviour: only allowlisted, non-sensitive params pass through. + test_environment["before_test"]() lambda_client.invoke( FunctionName="BasicOkDataCollectionUrlQueryAllowlist", - Payload=payload, + Payload=URL_QUERY_PAYLOAD, ) envelopes = test_environment["server"].envelopes @@ -783,37 +690,11 @@ def test_url_query_params_with_data_collection_allowlist( "token": "[Filtered]", } - -def test_url_query_params_with_data_collection_off(lambda_client, test_environment): - payload = b""" - { - "resource": "/asd", - "path": "/asd", - "httpMethod": "GET", - "headers": { - "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", - "X-Forwarded-Proto": "https" - }, - "queryStringParameters": { - "page": "2", - "tracking": "campaign" - }, - "pathParameters": null, - "stageVariables": null, - "requestContext": { - "identity": { - "sourceIp": "213.47.147.207", - "userArn": "42" - } - }, - "body": null, - "isBase64Encoded": false - } - """ - + # Collection off: no query string is collected. + test_environment["before_test"]() lambda_client.invoke( FunctionName="BasicOkDataCollectionUrlQueryOff", - Payload=payload, + Payload=URL_QUERY_PAYLOAD, ) envelopes = test_environment["server"].envelopes @@ -823,80 +704,6 @@ def test_url_query_params_with_data_collection_off(lambda_client, test_environme assert "query_string" not in transaction_event["request"] -def test_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) - - # We simulate here AWS Api Gateway's behavior of passing HTTP headers - # as the `headers` dict in the event passed to the Lambda function. - payload = { - "headers": { - "sentry-trace": sentry_trace_header, - } - } - - lambda_client.invoke( - FunctionName="BasicException", - Payload=json.dumps(payload), - ) - envelopes = test_environment["server"].envelopes - - (error_event, transaction_event) = envelopes - - assert ( - error_event["contexts"]["trace"]["trace_id"] - == transaction_event["contexts"]["trace"]["trace_id"] - == "471a43a4192642f0b136d5159a501701" - ) - - -@pytest.mark.parametrize( - "payload", - [ - {}, - {"headers": None}, - {"headers": ""}, - {"headers": {}}, - {"headers": []}, # EventBridge sends an empty list - ], - ids=[ - "no headers", - "none headers", - "empty string headers", - "empty dict headers", - "empty list headers", - ], -) -def test_headers(lambda_client, test_environment, payload): - lambda_client.invoke( - FunctionName="BasicException", - Payload=json.dumps(payload), - ) - envelopes = test_environment["server"].envelopes - - (error_event, _) = envelopes - - assert error_event["level"] == "error" - assert error_event["exception"]["values"][0]["type"] == "RuntimeError" - assert error_event["exception"]["values"][0]["value"] == "Oh!" - - -def test_span_origin(lambda_client, test_environment): - lambda_client.invoke( - FunctionName="BasicOk", - Payload=json.dumps({}), - ) - envelopes = test_environment["server"].envelopes - - (transaction_event,) = envelopes - - assert ( - transaction_event["contexts"]["trace"]["origin"] == "auto.function.aws_lambda" - ) - - def test_traces_sampler_has_correct_sampling_context(lambda_client, test_environment): """ Test that aws_event and aws_context are passed in the custom_sampling_context @@ -916,35 +723,72 @@ def test_traces_sampler_has_correct_sampling_context(lambda_client, test_environ assert sampling_context_data.get("event_data", {}).get("test_key") == "test_value" -@pytest.mark.parametrize( - "lambda_function_name", - ["RaiseErrorPerformanceEnabled", "RaiseErrorPerformanceDisabled"], -) -def test_error_has_new_trace_context( - lambda_client, test_environment, lambda_function_name -): - lambda_client.invoke( - FunctionName=lambda_function_name, - Payload=json.dumps({}), - ) - envelopes = test_environment["server"].envelopes +def test_error_trace_context(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) - if lambda_function_name == "RaiseErrorPerformanceEnabled": - (error_event, transaction_event) = envelopes - else: - (error_event,) = envelopes - transaction_event = None - - assert "trace" in error_event["contexts"] - assert "trace_id" in error_event["contexts"]["trace"] - - if transaction_event: - assert "trace" in transaction_event["contexts"] - assert "trace_id" in transaction_event["contexts"]["trace"] - assert ( - error_event["contexts"]["trace"]["trace_id"] - == transaction_event["contexts"]["trace"]["trace_id"] + # We simulate here AWS Api Gateway's behavior of passing HTTP headers + # as the `headers` dict in the event passed to the Lambda function. + trace_payload = json.dumps({"headers": {"sentry-trace": sentry_trace_header}}) + + for lambda_function_name in ( + "RaiseErrorPerformanceEnabled", + "RaiseErrorPerformanceDisabled", + ): + performance_enabled = lambda_function_name == "RaiseErrorPerformanceEnabled" + + # Without an incoming sentry-trace header, the error event gets a new + # trace context (shared with the transaction, if any). + lambda_client.invoke( + FunctionName=lambda_function_name, + Payload=json.dumps({}), + ) + envelopes = test_environment["server"].envelopes + + if performance_enabled: + (error_event, transaction_event) = envelopes + else: + (error_event,) = envelopes + transaction_event = None + + assert "trace" in error_event["contexts"] + assert "trace_id" in error_event["contexts"]["trace"] + + if transaction_event: + assert "trace" in transaction_event["contexts"] + assert "trace_id" in transaction_event["contexts"]["trace"] + assert ( + error_event["contexts"]["trace"]["trace_id"] + == transaction_event["contexts"]["trace"]["trace_id"] + ) + + # With an incoming sentry-trace header, the existing trace is + # continued. + test_environment["before_test"]() + lambda_client.invoke( + FunctionName=lambda_function_name, + Payload=trace_payload, ) + envelopes = test_environment["server"].envelopes + + if performance_enabled: + (error_event, transaction_event) = envelopes + else: + (error_event,) = envelopes + transaction_event = None + + assert "trace" in error_event["contexts"] + assert "trace_id" in error_event["contexts"]["trace"] + assert error_event["contexts"]["trace"]["trace_id"] == trace_id + + if transaction_event: + assert "trace" in transaction_event["contexts"] + assert "trace_id" in transaction_event["contexts"]["trace"] + assert transaction_event["contexts"]["trace"]["trace_id"] == trace_id + + test_environment["before_test"]() def _get_span_attr(attrs, key): @@ -955,7 +799,28 @@ def _get_span_attr(attrs, key): return val -def test_span_streaming_no_error(lambda_client, test_environment): +def _assert_segment_span_attrs(attrs, function_name): + """Assert the full attribute set of an aws_lambda segment span.""" + arn = "arn:aws:lambda:us-east-1:012345678912:function:%s" % function_name + 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.segment.name.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 + assert _get_span_attr(attrs, "cloud.region") == "us-east-1" + assert _get_span_attr(attrs, "faas.name") == function_name + assert _get_span_attr(attrs, "faas.version") == "$LATEST" + assert "faas.invocation_id" in attrs + assert _get_span_attr(attrs, "aws.lambda.invoked_arn") == arn + assert _get_span_attr(attrs, "aws.log.group.names") == [ + "aws/lambda/%s" % function_name + ] + assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"] + + +def test_span_streaming(lambda_client, test_environment): + # Success case: no envelopes, one segment span with full attributes. lambda_client.invoke( FunctionName="BasicOkSpanStreaming", Payload=json.dumps({}), @@ -970,34 +835,14 @@ def test_span_streaming_no_error(lambda_client, test_environment): segment_span = segment_spans[0] 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.segment.name.source") == "component" - assert _get_span_attr(attrs, "cloud.provider") == "aws" - assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda" + _assert_segment_span_attrs(segment_span["attributes"], "BasicOkSpanStreaming") assert ( - _get_span_attr(attrs, "cloud.resource_id") - == "arn:aws:lambda:us-east-1:012345678912:function:BasicOkSpanStreaming" + _get_span_attr(segment_span["attributes"], "messaging.batch.message_count") + == 1 ) - 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"] - assert _get_span_attr(attrs, "messaging.batch.message_count") == 1 - -def test_span_streaming_error(lambda_client, test_environment): + # Error case: an error event plus an errored segment span. + test_environment["before_test"]() lambda_client.invoke( FunctionName="RaiseErrorSpanStreaming", Payload=json.dumps({}), @@ -1020,34 +865,17 @@ def test_span_streaming_error(lambda_client, test_environment): 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.segment.name.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_segment_span_attrs( + segment_span["attributes"], "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" + _get_span_attr(segment_span["attributes"], "messaging.batch.message_count") + == 1 ) - assert _get_span_attr(attrs, "aws.log.group.names") == [ - "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): + # Trace continuation: an incoming sentry-trace header is continued by + # both the error event and the streamed segment span. + test_environment["before_test"]() trace_id = "471a43a4192642f0b136d5159a501701" parent_span_id = "6e8f22c393e68f19" parent_sampled = 1 @@ -1075,16 +903,9 @@ def test_span_streaming_trace_continuation(lambda_client, test_environment): segment_span = segment_spans[0] 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.segment.name.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 + _assert_segment_span_attrs( + segment_span["attributes"], "RaiseErrorSpanStreaming" + ) def test_span_streaming_request_attributes(lambda_client, test_environment): @@ -1129,10 +950,10 @@ def test_span_streaming_request_attributes(lambda_client, test_environment): ] assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"] - -def test_span_streaming_url_query_params_with_data_collection( - lambda_client, test_environment -): + # url.query attribute with data_collection filtering: "page" passes + # through; "tracking" is denied by a custom term and "token" by the + # built-in sensitive denylist. + test_environment["before_test"]() payload = { "httpMethod": "GET", "queryStringParameters": { @@ -1154,57 +975,8 @@ def test_span_streaming_url_query_params_with_data_collection( segment_span = segment_spans[0] attrs = segment_span["attributes"] - # "page" passes through; "tracking" is denied by a custom term and "token" - # by the built-in sensitive denylist. assert ( _get_span_attr(attrs, "url.query") == "page=2&tracking=%5BFiltered%5D&token=%5BFiltered%5D" ) - -@pytest.mark.parametrize( - "lambda_function_name", - ["RaiseErrorPerformanceEnabled", "RaiseErrorPerformanceDisabled"], -) -def test_error_has_existing_trace_context( - lambda_client, test_environment, lambda_function_name -): - trace_id = "471a43a4192642f0b136d5159a501701" - parent_span_id = "6e8f22c393e68f19" - parent_sampled = 1 - sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled) - - # We simulate here AWS Api Gateway's behavior of passing HTTP headers - # as the `headers` dict in the event passed to the Lambda function. - payload = { - "headers": { - "sentry-trace": sentry_trace_header, - } - } - - lambda_client.invoke( - FunctionName=lambda_function_name, - Payload=json.dumps(payload), - ) - envelopes = test_environment["server"].envelopes - - if lambda_function_name == "RaiseErrorPerformanceEnabled": - (error_event, transaction_event) = envelopes - else: - (error_event,) = envelopes - transaction_event = None - - assert "trace" in error_event["contexts"] - assert "trace_id" in error_event["contexts"]["trace"] - assert ( - error_event["contexts"]["trace"]["trace_id"] - == "471a43a4192642f0b136d5159a501701" - ) - - if transaction_event: - assert "trace" in transaction_event["contexts"] - assert "trace_id" in transaction_event["contexts"]["trace"] - assert ( - transaction_event["contexts"]["trace"]["trace_id"] - == "471a43a4192642f0b136d5159a501701" - ) diff --git a/tests/integrations/aws_lambda/utils.py b/tests/integrations/aws_lambda/utils.py index b5b7d18930..9b85013e91 100644 --- a/tests/integrations/aws_lambda/utils.py +++ b/tests/integrations/aws_lambda/utils.py @@ -29,6 +29,10 @@ PYTHON_VERSION = f"python{sys.version_info.major}.{sys.version_info.minor}" +# Match the host CPU architecture so local runs on ARM machines (e.g. macOS) +# don't run the Lambda containers under slow x86_64 emulation. +ARCHITECTURE = "arm64" if platform.machine() in ("arm64", "aarch64") else "x86_64" + def get_host_ip(): """ @@ -105,6 +109,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: "CodeUri": os.path.join(LAMBDA_FUNCTION_DIR, lambda_dir), "Handler": "sentry_sdk.integrations.init_serverless_sdk.sentry_lambda_handler", "Runtime": PYTHON_VERSION, + "Architectures": [ARCHITECTURE], "Timeout": LAMBDA_FUNCTION_TIMEOUT, "Layers": [ {"Ref": self.sentry_layer.logical_id} @@ -171,6 +176,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: ), "Handler": "index.handler", "Runtime": PYTHON_VERSION, + "Architectures": [ARCHITECTURE], "Timeout": LAMBDA_FUNCTION_TIMEOUT, "Environment": { "Variables": { From 11495bc479e3890da6dc59b35097d99100b09fbf Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 31 Jul 2026 14:39:28 -0400 Subject: [PATCH 2/3] style: ruff format test_aws_lambda.py --- tests/integrations/aws_lambda/test_aws_lambda.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 72a731dfb5..6519adf2c0 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -837,8 +837,7 @@ def test_span_streaming(lambda_client, test_environment): assert segment_span["name"] == "BasicOkSpanStreaming" _assert_segment_span_attrs(segment_span["attributes"], "BasicOkSpanStreaming") assert ( - _get_span_attr(segment_span["attributes"], "messaging.batch.message_count") - == 1 + _get_span_attr(segment_span["attributes"], "messaging.batch.message_count") == 1 ) # Error case: an error event plus an errored segment span. @@ -865,12 +864,9 @@ def test_span_streaming(lambda_client, test_environment): assert segment_span["name"] == "RaiseErrorSpanStreaming" assert segment_span["status"] == "error" - _assert_segment_span_attrs( - segment_span["attributes"], "RaiseErrorSpanStreaming" - ) + _assert_segment_span_attrs(segment_span["attributes"], "RaiseErrorSpanStreaming") assert ( - _get_span_attr(segment_span["attributes"], "messaging.batch.message_count") - == 1 + _get_span_attr(segment_span["attributes"], "messaging.batch.message_count") == 1 ) # Trace continuation: an incoming sentry-trace header is continued by @@ -903,9 +899,7 @@ def test_span_streaming(lambda_client, test_environment): segment_span = segment_spans[0] assert segment_span["trace_id"] == trace_id assert segment_span["name"] == "RaiseErrorSpanStreaming" - _assert_segment_span_attrs( - segment_span["attributes"], "RaiseErrorSpanStreaming" - ) + _assert_segment_span_attrs(segment_span["attributes"], "RaiseErrorSpanStreaming") def test_span_streaming_request_attributes(lambda_client, test_environment): @@ -979,4 +973,3 @@ def test_span_streaming_request_attributes(lambda_client, test_environment): _get_span_attr(attrs, "url.query") == "page=2&tracking=%5BFiltered%5D&token=%5BFiltered%5D" ) - From bc65fe5e0a5afd385a48ff62a90523e051438c6f Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 31 Jul 2026 15:37:46 -0400 Subject: [PATCH 3/3] . --- .../aws_lambda/test_aws_lambda.py | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 6519adf2c0..7650716207 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -330,10 +330,6 @@ def test_timeout_error_scope_modified(lambda_client, test_environment): @pytest.mark.parametrize( "aws_event, has_request_data, batch_size", [ - # Scalar events (int/float/string/bool) are one equivalence class: - # not a list, not a dict, so request_data falls back to {}. - # (An empty list hits the identical path: len < 1 -> else arm -> - # non-dict reset.) (b"1231", False, 1), ( b""" @@ -459,7 +455,6 @@ def test_non_dict_event( def test_user_info_with_data_collection(lambda_client, test_environment): - # user_info collection on: the user identity is attached. lambda_client.invoke( FunctionName="BasicOkDataCollectionUserInfoOn", Payload=USER_INFO_PAYLOAD, @@ -473,7 +468,6 @@ def test_user_info_with_data_collection(lambda_client, test_environment): "ip_address": "213.47.147.207", } - # user_info collection off: no user identity is attached. test_environment["before_test"]() lambda_client.invoke( FunctionName="BasicOkDataCollectionUserInfoOff", @@ -514,7 +508,6 @@ def _request_data_payload(extra_headers=None): def test_request_data_with_data_collection(lambda_client, test_environment): - # Allowlist behaviour: only allowlisted, non-sensitive headers pass through. lambda_client.invoke( FunctionName="BasicOkDataCollectionAllowlist", Payload=_request_data_payload({"X-Allow-Me": "yes"}), @@ -542,8 +535,6 @@ def test_request_data_with_data_collection(lambda_client, test_environment): "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", } - # Denylist behaviour: headers denied by custom terms or the built-in - # sensitive denylist are substituted. test_environment["before_test"]() lambda_client.invoke( FunctionName="BasicOkDataCollectionDenylist", @@ -555,7 +546,6 @@ def test_request_data_with_data_collection(lambda_client, test_environment): assert transaction_event["request"] == { "headers": { - # Not denied by any term -> pass through. "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", "X-Custom": "keep-me", # Denied by custom terms. @@ -571,7 +561,6 @@ def test_request_data_with_data_collection(lambda_client, test_environment): "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", } - # Collection off: no headers are collected. test_environment["before_test"]() lambda_client.invoke( FunctionName="BasicOkDataCollectionOff", @@ -651,8 +640,6 @@ def test_request_data_with_data_collection(lambda_client, test_environment): def test_url_query_params_with_data_collection(lambda_client, test_environment): - # Denylist behaviour: params denied by custom terms or the built-in - # sensitive denylist are substituted. lambda_client.invoke( FunctionName="BasicOkDataCollectionUrlQueryDenylist", Payload=URL_QUERY_PAYLOAD, @@ -690,7 +677,6 @@ def test_url_query_params_with_data_collection(lambda_client, test_environment): "token": "[Filtered]", } - # Collection off: no query string is collected. test_environment["before_test"]() lambda_client.invoke( FunctionName="BasicOkDataCollectionUrlQueryOff", @@ -700,7 +686,6 @@ def test_url_query_params_with_data_collection(lambda_client, test_environment): (transaction_event,) = envelopes - # With url_query_params collection turned off, no query string is collected. assert "query_string" not in transaction_event["request"] @@ -800,8 +785,9 @@ def _get_span_attr(attrs, key): def _assert_segment_span_attrs(attrs, function_name): - """Assert the full attribute set of an aws_lambda segment span.""" + arn = "arn:aws:lambda:us-east-1:012345678912:function:%s" % function_name + 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.segment.name.source") == "component" @@ -944,9 +930,6 @@ def test_span_streaming_request_attributes(lambda_client, test_environment): ] assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"] - # url.query attribute with data_collection filtering: "page" passes - # through; "tracking" is denied by a custom term and "token" by the - # built-in sensitive denylist. test_environment["before_test"]() payload = { "httpMethod": "GET",