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..7650716207 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): @@ -251,9 +331,6 @@ def test_timeout_error_scope_modified(lambda_client, test_environment): "aws_event, has_request_data, batch_size", [ (b"1231", False, 1), - (b"11.21", False, 1), - (b'"Good dog!"', False, 1), - (b"true", False, 1), ( b""" [ @@ -298,16 +375,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 +427,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 +454,7 @@ 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): lambda_client.invoke( FunctionName="BasicOkDataCollectionUserInfoOn", Payload=USER_INFO_PAYLOAD, @@ -513,8 +468,7 @@ 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): + test_environment["before_test"]() lambda_client.invoke( FunctionName="BasicOkDataCollectionUserInfoOff", Payload=USER_INFO_PAYLOAD, @@ -526,39 +480,37 @@ 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): lambda_client.invoke( FunctionName="BasicOkDataCollectionAllowlist", - Payload=payload, + Payload=_request_data_payload({"X-Allow-Me": "yes"}), ) envelopes = test_environment["server"].envelopes @@ -583,48 +535,17 @@ def test_request_data_with_data_collection_allowlist(lambda_client, test_environ "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", } + 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": { "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", "X-Custom": "keep-me", # Denied by custom terms. @@ -640,39 +561,10 @@ 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 - } - """ - + test_environment["before_test"]() lambda_client.invoke( FunctionName="BasicOkDataCollectionOff", - Payload=payload, + Payload=_request_data_payload(), ) envelopes = test_environment["server"].envelopes @@ -686,40 +578,71 @@ 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 + + (transaction_event,) = envelopes -def test_url_query_params_with_data_collection_denylist( - lambda_client, test_environment -): - payload = b""" - { - "resource": "/asd", - "path": "/asd", - "httpMethod": "GET", - "headers": { + 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): lambda_client.invoke( FunctionName="BasicOkDataCollectionUrlQueryDenylist", - Payload=payload, + Payload=URL_QUERY_PAYLOAD, ) envelopes = test_environment["server"].envelopes @@ -734,40 +657,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,120 +677,18 @@ 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 - } - """ - + test_environment["before_test"]() lambda_client.invoke( FunctionName="BasicOkDataCollectionUrlQueryOff", - Payload=payload, + Payload=URL_QUERY_PAYLOAD, ) envelopes = test_environment["server"].envelopes (transaction_event,) = envelopes - # With url_query_params collection turned off, no query string is collected. 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 +708,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 +784,29 @@ 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): + + 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 +821,13 @@ 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 ( - _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_segment_span_attrs(segment_span["attributes"], "BasicOkSpanStreaming") assert ( - _get_span_attr(attrs, "aws.lambda.invoked_arn") - == "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, "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 +850,14 @@ 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 _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") 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 +885,7 @@ 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 +930,7 @@ 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 -): + test_environment["before_test"]() payload = { "httpMethod": "GET", "queryStringParameters": { @@ -1154,57 +952,7 @@ 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": {