From 552017a4d53ba6af13020337588de94d476dced8 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 30 Oct 2023 14:20:50 +0100 Subject: [PATCH] Load AWS Lambda secrets in Github CI (#2153) Make sure our AWS Lambda test setup is correct and the tests work as expected and also in a timely manner. We run our test in AWS Lambda and then parse the log output to see what events/envelopes where sent. Because Lambda truncates this log output to 4kb, I had to change the tests to make the events/envelopes smaller in size to get the whole event/envelop in the log output. When the AWS env vars where not set, the tests where skipped but it looked like they where successful. I made them now fail loudly in that case, so we see if they do not run. Also made the code easier to comprehend. --------- Co-authored-by: Ivana Kellyerova --- .craft.yml | 2 + .../workflows/test-integration-aws_lambda.yml | 4 +- Makefile | 3 +- aws-lambda-layer-requirements.txt | 7 + scripts/aws-cleanup.sh | 15 +- scripts/aws-deploy-local-layer.sh | 2 +- scripts/build_aws_lambda_layer.py | 63 ++- .../ci-yaml-aws-credentials.txt | 2 + scripts/split-tox-gh-actions/ci-yaml.txt | 1 + .../split-tox-gh-actions.py | 10 + tests/integrations/aws_lambda/client.py | 425 ++++++++++++------ tests/integrations/aws_lambda/test_aws.py | 317 +++++++------ tox.ini | 10 +- 13 files changed, 581 insertions(+), 280 deletions(-) create mode 100644 aws-lambda-layer-requirements.txt mode change 100644 => 100755 scripts/aws-cleanup.sh create mode 100644 scripts/split-tox-gh-actions/ci-yaml-aws-credentials.txt diff --git a/.craft.yml b/.craft.yml index 3f8433d9fc..21d4fc7496 100644 --- a/.craft.yml +++ b/.craft.yml @@ -18,6 +18,8 @@ targets: # On the other hand, AWS Lambda does not support every Python runtime. # The supported runtimes are available in the following link: # https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html + - python3.7 + - python3.8 - python3.9 - python3.10 - python3.11 diff --git a/.github/workflows/test-integration-aws_lambda.yml b/.github/workflows/test-integration-aws_lambda.yml index 62bfab90f2..385bb4b13a 100644 --- a/.github/workflows/test-integration-aws_lambda.yml +++ b/.github/workflows/test-integration-aws_lambda.yml @@ -18,6 +18,8 @@ permissions: contents: read env: + SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID: ${{ secrets.SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID }} + SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY: ${{ secrets.SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY }} BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless @@ -31,7 +33,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7"] + python-version: ["3.9"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 diff --git a/Makefile b/Makefile index 2011b1b63e..4d93d5341f 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,6 @@ apidocs-hotfix: apidocs .PHONY: apidocs-hotfix aws-lambda-layer: dist - $(VENV_PATH)/bin/pip install urllib3 - $(VENV_PATH)/bin/pip install certifi + $(VENV_PATH)/bin/pip install -r aws-lambda-layer-requirements.txt $(VENV_PATH)/bin/python -m scripts.build_aws_lambda_layer .PHONY: aws-lambda-layer diff --git a/aws-lambda-layer-requirements.txt b/aws-lambda-layer-requirements.txt new file mode 100644 index 0000000000..8986fdafc0 --- /dev/null +++ b/aws-lambda-layer-requirements.txt @@ -0,0 +1,7 @@ +certifi + +# In Lambda functions botocore is used, and botocore is not +# yet supporting urllib3 1.27.0 never mind 2+. +# So we pin this here to make our Lambda layer work with +# Lambda Function using Python 3.7+ +urllib3<1.27 diff --git a/scripts/aws-cleanup.sh b/scripts/aws-cleanup.sh old mode 100644 new mode 100755 index 1219668855..982835c283 --- a/scripts/aws-cleanup.sh +++ b/scripts/aws-cleanup.sh @@ -1,11 +1,18 @@ #!/bin/sh -# Delete all AWS Lambda functions +# +# Helper script to clean up AWS Lambda functions created +# by the test suite (tests/integrations/aws_lambda/test_aws.py). +# +# This will delete all Lambda functions named `test_function_*`. +# +export AWS_DEFAULT_REGION="us-east-1" export AWS_ACCESS_KEY_ID="$SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID" export AWS_SECRET_ACCESS_KEY="$SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY" -export AWS_IAM_ROLE="$SENTRY_PYTHON_TEST_AWS_IAM_ROLE" -for func in $(aws lambda list-functions | jq -r .Functions[].FunctionName); do +for func in $(aws lambda list-functions --output text --query 'Functions[?starts_with(FunctionName, `test_`) == `true`].FunctionName'); do echo "Deleting $func" - aws lambda delete-function --function-name $func + aws lambda delete-function --function-name "$func" done + +echo "All done! Have a nice day!" diff --git a/scripts/aws-deploy-local-layer.sh b/scripts/aws-deploy-local-layer.sh index 3f213849f3..56f2087596 100755 --- a/scripts/aws-deploy-local-layer.sh +++ b/scripts/aws-deploy-local-layer.sh @@ -22,7 +22,7 @@ aws lambda publish-layer-version \ --region "eu-central-1" \ --zip-file "fileb://dist/$ZIP" \ --description "Local test build of SentryPythonServerlessSDK (can be deleted)" \ - --compatible-runtimes python3.6 python3.7 python3.8 python3.9 + --compatible-runtimes python3.7 python3.8 python3.9 python3.10 python3.11 \ --no-cli-pager echo "Done deploying zipped Lambda layer to AWS as 'SentryPythonServerlessSDK-local-dev'." diff --git a/scripts/build_aws_lambda_layer.py b/scripts/build_aws_lambda_layer.py index d551097649..8704e4de01 100644 --- a/scripts/build_aws_lambda_layer.py +++ b/scripts/build_aws_lambda_layer.py @@ -1,10 +1,15 @@ import os import shutil import subprocess +import sys import tempfile +from typing import TYPE_CHECKING from sentry_sdk.consts import VERSION as SDK_VERSION +if TYPE_CHECKING: + from typing import Optional + DIST_PATH = "dist" # created by "make dist" that is called by "make aws-lambda-layer" PYTHON_SITE_PACKAGES = "python" # see https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html#configuration-layers-path @@ -13,11 +18,16 @@ class LayerBuilder: def __init__( self, base_dir, # type: str + out_zip_filename=None, # type: Optional[str] ): # type: (...) -> None self.base_dir = base_dir self.python_site_packages = os.path.join(self.base_dir, PYTHON_SITE_PACKAGES) - self.out_zip_filename = f"sentry-python-serverless-{SDK_VERSION}.zip" + self.out_zip_filename = ( + f"sentry-python-serverless-{SDK_VERSION}.zip" + if out_zip_filename is None + else out_zip_filename + ) def make_directories(self): # type: (...) -> None @@ -25,6 +35,21 @@ def make_directories(self): def install_python_packages(self): # type: (...) -> None + # Install requirements for Lambda Layer (these are more limited than the SDK requirements, + # because Lambda does not support the newest versions of some packages) + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "-r", + "aws-lambda-layer-requirements.txt", + "--target", + self.python_site_packages, + ], + ) + sentry_python_sdk = os.path.join( DIST_PATH, f"sentry_sdk-{SDK_VERSION}-py2.py3-none-any.whl", # this is generated by "make dist" that is called by "make aws-lamber-layer" @@ -34,6 +59,7 @@ def install_python_packages(self): "pip", "install", "--no-cache-dir", # always access PyPI + "--no-deps", # the right depencencies have been installed in the call above "--quiet", sentry_python_sdk, "--target", @@ -80,13 +106,34 @@ def zip(self): ) -def build_packaged_zip(): - with tempfile.TemporaryDirectory() as base_dir: - layer_builder = LayerBuilder(base_dir) - layer_builder.make_directories() - layer_builder.install_python_packages() - layer_builder.create_init_serverless_sdk_package() - layer_builder.zip() +def build_packaged_zip(base_dir=None, make_dist=False, out_zip_filename=None): + if base_dir is None: + base_dir = tempfile.mkdtemp() + + if make_dist: + # Same thing that is done by "make dist" + # (which is a dependency of "make aws-lambda-layer") + subprocess.check_call( + [sys.executable, "setup.py", "sdist", "bdist_wheel", "-d", DIST_PATH], + ) + + layer_builder = LayerBuilder(base_dir, out_zip_filename=out_zip_filename) + layer_builder.make_directories() + layer_builder.install_python_packages() + layer_builder.create_init_serverless_sdk_package() + layer_builder.zip() + + # Just for debugging + dist_path = os.path.abspath(DIST_PATH) + print("Created Lambda Layer package with this information:") + print(" - Base directory for generating package: {}".format(layer_builder.base_dir)) + print( + " - Created Python SDK distribution (in `{}`): {}".format(dist_path, make_dist) + ) + if not make_dist: + print(" If 'False' we assume it was already created (by 'make dist')") + print(" - Package zip filename: {}".format(layer_builder.out_zip_filename)) + print(" - Copied package zip to: {}".format(dist_path)) if __name__ == "__main__": diff --git a/scripts/split-tox-gh-actions/ci-yaml-aws-credentials.txt b/scripts/split-tox-gh-actions/ci-yaml-aws-credentials.txt new file mode 100644 index 0000000000..fe4b4104e0 --- /dev/null +++ b/scripts/split-tox-gh-actions/ci-yaml-aws-credentials.txt @@ -0,0 +1,2 @@ + SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID: ${{ secrets.SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID }} + SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY: ${{ secrets.SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY }} diff --git a/scripts/split-tox-gh-actions/ci-yaml.txt b/scripts/split-tox-gh-actions/ci-yaml.txt index 99d8154c60..90bd5c61ce 100644 --- a/scripts/split-tox-gh-actions/ci-yaml.txt +++ b/scripts/split-tox-gh-actions/ci-yaml.txt @@ -18,6 +18,7 @@ permissions: contents: read env: +{{ aws_credentials }} BUILD_CACHE_KEY: ${{ github.sha }} CACHED_BUILD_PATHS: | ${{ github.workspace }}/dist-serverless diff --git a/scripts/split-tox-gh-actions/split-tox-gh-actions.py b/scripts/split-tox-gh-actions/split-tox-gh-actions.py index 15f85391ed..ea187475db 100755 --- a/scripts/split-tox-gh-actions/split-tox-gh-actions.py +++ b/scripts/split-tox-gh-actions/split-tox-gh-actions.py @@ -28,6 +28,7 @@ TEMPLATE_FILE = TEMPLATE_DIR / "ci-yaml.txt" TEMPLATE_FILE_SERVICES = TEMPLATE_DIR / "ci-yaml-services.txt" TEMPLATE_FILE_SETUP_DB = TEMPLATE_DIR / "ci-yaml-setup-db.txt" +TEMPLATE_FILE_AWS_CREDENTIALS = TEMPLATE_DIR / "ci-yaml-aws-credentials.txt" TEMPLATE_SNIPPET_TEST = TEMPLATE_DIR / "ci-yaml-test-snippet.txt" TEMPLATE_SNIPPET_TEST_PY27 = TEMPLATE_DIR / "ci-yaml-test-py27-snippet.txt" @@ -40,6 +41,10 @@ "clickhouse_driver", ] +FRAMEWORKS_NEEDING_AWS = [ + "aws_lambda", +] + MATRIX_DEFINITION = """ strategy: fail-fast: false @@ -128,6 +133,11 @@ def write_yaml_file( f = open(TEMPLATE_FILE_SETUP_DB, "r") out += "".join(f.readlines()) + elif template_line.strip() == "{{ aws_credentials }}": + if current_framework in FRAMEWORKS_NEEDING_AWS: + f = open(TEMPLATE_FILE_AWS_CREDENTIALS, "r") + out += "".join(f.readlines()) + elif template_line.strip() == "{{ additional_uses }}": if current_framework in FRAMEWORKS_NEEDING_CLICKHOUSE: out += ADDITIONAL_USES_CLICKHOUSE diff --git a/tests/integrations/aws_lambda/client.py b/tests/integrations/aws_lambda/client.py index d8e430f3d7..c2bc90df93 100644 --- a/tests/integrations/aws_lambda/client.py +++ b/tests/integrations/aws_lambda/client.py @@ -1,59 +1,206 @@ -import sys +import base64 +import boto3 +import glob +import hashlib import os -import shutil -import tempfile import subprocess -import boto3 -import uuid -import base64 +import sys +import tempfile +from sentry_sdk.consts import VERSION as SDK_VERSION -def get_boto_client(): - return boto3.client( - "lambda", - aws_access_key_id=os.environ["SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID"], - aws_secret_access_key=os.environ["SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY"], - region_name="us-east-1", +AWS_REGION_NAME = "us-east-1" +AWS_CREDENTIALS = { + "aws_access_key_id": os.environ["SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID"], + "aws_secret_access_key": os.environ["SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY"], +} +AWS_LAMBDA_EXECUTION_ROLE_NAME = "lambda-ex" +AWS_LAMBDA_EXECUTION_ROLE_ARN = None + + +def _install_dependencies(base_dir, subprocess_kwargs): + """ + Installs dependencies for AWS Lambda function + """ + setup_cfg = os.path.join(base_dir, "setup.cfg") + with open(setup_cfg, "w") as f: + f.write("[install]\nprefix=") + + # Install requirements for Lambda Layer (these are more limited than the SDK requirements, + # because Lambda does not support the newest versions of some packages) + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "-r", + "aws-lambda-layer-requirements.txt", + "--target", + base_dir, + ], + **subprocess_kwargs, + ) + # Install requirements used for testing + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "mock==3.0.0", + "funcsigs", + "--target", + base_dir, + ], + **subprocess_kwargs, + ) + # Create a source distribution of the Sentry SDK (in parent directory of base_dir) + subprocess.check_call( + [ + sys.executable, + "setup.py", + "sdist", + "--dist-dir", + os.path.dirname(base_dir), + ], + **subprocess_kwargs, + ) + # Install the created Sentry SDK source distribution into the target directory + # Do not install the dependencies of the SDK, because they where installed by aws-lambda-layer-requirements.txt above + source_distribution_archive = glob.glob( + "{}/*.tar.gz".format(os.path.dirname(base_dir)) + )[0] + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + source_distribution_archive, + "--no-deps", + "--target", + base_dir, + ], + **subprocess_kwargs, ) -def build_no_code_serverless_function_and_layer( - client, tmpdir, fn_name, runtime, timeout, initial_handler +def _create_lambda_function_zip(base_dir): + """ + Zips the given base_dir omitting Python cache files + """ + subprocess.run( + [ + "zip", + "-q", + "-x", + "**/__pycache__/*", + "-r", + "lambda-function-package.zip", + "./", + ], + cwd=base_dir, + check=True, + ) + + +def _create_lambda_package( + base_dir, code, initial_handler, layer, syntax_check, subprocess_kwargs ): """ - Util function that auto instruments the no code implementation of the python - sdk by creating a layer containing the Python-sdk, and then creating a func - that uses that layer + Creates deployable packages (as zip files) for AWS Lambda function + and optional the accompanying Sentry Lambda layer """ - from scripts.build_aws_lambda_layer import build_layer_dir + if initial_handler: + # If Initial handler value is provided i.e. it is not the default + # `test_lambda.test_handler`, then create another dir level so that our path is + # test_dir.test_lambda.test_handler + test_dir_path = os.path.join(base_dir, "test_dir") + python_init_file = os.path.join(test_dir_path, "__init__.py") + os.makedirs(test_dir_path) + with open(python_init_file, "w"): + # Create __init__ file to make it a python package + pass + + test_lambda_py = os.path.join(base_dir, "test_dir", "test_lambda.py") + else: + test_lambda_py = os.path.join(base_dir, "test_lambda.py") + + with open(test_lambda_py, "w") as f: + f.write(code) - build_layer_dir(dest_abs_path=tmpdir) + if syntax_check: + # Check file for valid syntax first, and that the integration does not + # crash when not running in Lambda (but rather a local deployment tool + # such as chalice's) + subprocess.check_call([sys.executable, test_lambda_py]) - with open(os.path.join(tmpdir, "serverless-ball.zip"), "rb") as serverless_zip: - response = client.publish_layer_version( - LayerName="python-serverless-sdk-test", - Description="Created as part of testsuite for getsentry/sentry-python", - Content={"ZipFile": serverless_zip.read()}, + if layer is None: + _install_dependencies(base_dir, subprocess_kwargs) + _create_lambda_function_zip(base_dir) + + else: + _create_lambda_function_zip(base_dir) + + # Create Lambda layer zip package + from scripts.build_aws_lambda_layer import build_packaged_zip + + build_packaged_zip( + base_dir=base_dir, + make_dist=True, + out_zip_filename="lambda-layer-package.zip", ) - with open(os.path.join(tmpdir, "ball.zip"), "rb") as zip: - client.create_function( - FunctionName=fn_name, - Runtime=runtime, - Timeout=timeout, - Environment={ - "Variables": { - "SENTRY_INITIAL_HANDLER": initial_handler, - "SENTRY_DSN": "https://123abc@example.com/123", - "SENTRY_TRACES_SAMPLE_RATE": "1.0", - } - }, - Role=os.environ["SENTRY_PYTHON_TEST_AWS_IAM_ROLE"], - Handler="sentry_sdk.integrations.init_serverless_sdk.sentry_lambda_handler", - Layers=[response["LayerVersionArn"]], - Code={"ZipFile": zip.read()}, - Description="Created as part of testsuite for getsentry/sentry-python", + +def _get_or_create_lambda_execution_role(): + global AWS_LAMBDA_EXECUTION_ROLE_ARN + + policy = """{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] + } + """ + iam_client = boto3.client( + "iam", + region_name=AWS_REGION_NAME, + **AWS_CREDENTIALS, + ) + + try: + response = iam_client.get_role(RoleName=AWS_LAMBDA_EXECUTION_ROLE_NAME) + AWS_LAMBDA_EXECUTION_ROLE_ARN = response["Role"]["Arn"] + except iam_client.exceptions.NoSuchEntityException: + # create role for lambda execution + response = iam_client.create_role( + RoleName=AWS_LAMBDA_EXECUTION_ROLE_NAME, + AssumeRolePolicyDocument=policy, ) + AWS_LAMBDA_EXECUTION_ROLE_ARN = response["Role"]["Arn"] + + # attach policy to role + iam_client.attach_role_policy( + RoleName=AWS_LAMBDA_EXECUTION_ROLE_NAME, + PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ) + + +def get_boto_client(): + _get_or_create_lambda_execution_role() + + return boto3.client( + "lambda", + region_name=AWS_REGION_NAME, + **AWS_CREDENTIALS, + ) def run_lambda_function( @@ -68,110 +215,128 @@ def run_lambda_function( initial_handler=None, subprocess_kwargs=(), ): + """ + Creates a Lambda function with the given code, and invokes it. + + If the same code is run multiple times the function will NOT be + created anew each time but the existing function will be reused. + """ subprocess_kwargs = dict(subprocess_kwargs) - with tempfile.TemporaryDirectory() as tmpdir: - if initial_handler: - # If Initial handler value is provided i.e. it is not the default - # `test_lambda.test_handler`, then create another dir level so that our path is - # test_dir.test_lambda.test_handler - test_dir_path = os.path.join(tmpdir, "test_dir") - python_init_file = os.path.join(test_dir_path, "__init__.py") - os.makedirs(test_dir_path) - with open(python_init_file, "w"): - # Create __init__ file to make it a python package - pass - - test_lambda_py = os.path.join(tmpdir, "test_dir", "test_lambda.py") - else: - test_lambda_py = os.path.join(tmpdir, "test_lambda.py") - - with open(test_lambda_py, "w") as f: - f.write(code) - - if syntax_check: - # Check file for valid syntax first, and that the integration does not - # crash when not running in Lambda (but rather a local deployment tool - # such as chalice's) - subprocess.check_call([sys.executable, test_lambda_py]) - - fn_name = "test_function_{}".format(uuid.uuid4()) - - if layer is None: - setup_cfg = os.path.join(tmpdir, "setup.cfg") - with open(setup_cfg, "w") as f: - f.write("[install]\nprefix=") - - subprocess.check_call( - [sys.executable, "setup.py", "sdist", "-d", os.path.join(tmpdir, "..")], - **subprocess_kwargs - ) + # Making a unique function name depending on all the code that is run in it (function code plus SDK version) + # The name needs to be short so the generated event/envelope json blobs are small enough to be output + # in the log result of the Lambda function. + function_hash = hashlib.shake_256((code + SDK_VERSION).encode("utf-8")).hexdigest(5) + fn_name = "test_{}".format(function_hash) + full_fn_name = "{}_{}".format( + fn_name, runtime.replace(".", "").replace("python", "py") + ) - subprocess.check_call( - "pip install mock==3.0.0 funcsigs -t .", - cwd=tmpdir, - shell=True, - **subprocess_kwargs - ) + function_exists_in_aws = True + try: + client.get_function( + FunctionName=full_fn_name, + ) + print( + "Lambda function in AWS already existing, taking it (and do not create a local one)" + ) + except client.exceptions.ResourceNotFoundException: + function_exists_in_aws = False + + if not function_exists_in_aws: + tmp_base_dir = tempfile.gettempdir() + base_dir = os.path.join(tmp_base_dir, fn_name) + dir_already_existing = os.path.isdir(base_dir) + + if dir_already_existing: + print("Local Lambda function directory already exists, skipping creation") - # https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html - subprocess.check_call( - "pip install ../*.tar.gz -t .", - cwd=tmpdir, - shell=True, - **subprocess_kwargs + if not dir_already_existing: + os.mkdir(base_dir) + _create_lambda_package( + base_dir, code, initial_handler, layer, syntax_check, subprocess_kwargs ) - shutil.make_archive(os.path.join(tmpdir, "ball"), "zip", tmpdir) + @add_finalizer + def clean_up(): + # this closes the web socket so we don't get a + # ResourceWarning: unclosed + # warning on every test + # based on https://github.com/boto/botocore/pull/1810 + # (if that's ever merged, this can just become client.close()) + session = client._endpoint.http_session + managers = [session._manager] + list(session._proxy_managers.values()) + for manager in managers: + manager.clear() + + layers = [] + environment = {} + handler = initial_handler or "test_lambda.test_handler" + + if layer is not None: + with open( + os.path.join(base_dir, "lambda-layer-package.zip"), "rb" + ) as lambda_layer_zip: + response = client.publish_layer_version( + LayerName="python-serverless-sdk-test", + Description="Created as part of testsuite for getsentry/sentry-python", + Content={"ZipFile": lambda_layer_zip.read()}, + ) - with open(os.path.join(tmpdir, "ball.zip"), "rb") as zip: + layers = [response["LayerVersionArn"]] + handler = ( + "sentry_sdk.integrations.init_serverless_sdk.sentry_lambda_handler" + ) + environment = { + "Variables": { + "SENTRY_INITIAL_HANDLER": initial_handler + or "test_lambda.test_handler", + "SENTRY_DSN": "https://123abc@example.com/123", + "SENTRY_TRACES_SAMPLE_RATE": "1.0", + } + } + + try: + with open( + os.path.join(base_dir, "lambda-function-package.zip"), "rb" + ) as lambda_function_zip: client.create_function( - FunctionName=fn_name, + Description="Created as part of testsuite for getsentry/sentry-python", + FunctionName=full_fn_name, Runtime=runtime, Timeout=timeout, - Role=os.environ["SENTRY_PYTHON_TEST_AWS_IAM_ROLE"], - Handler="test_lambda.test_handler", - Code={"ZipFile": zip.read()}, - Description="Created as part of testsuite for getsentry/sentry-python", + Role=AWS_LAMBDA_EXECUTION_ROLE_ARN, + Handler=handler, + Code={"ZipFile": lambda_function_zip.read()}, + Environment=environment, + Layers=layers, ) - else: - subprocess.run( - ["zip", "-q", "-x", "**/__pycache__/*", "-r", "ball.zip", "./"], - cwd=tmpdir, - check=True, + + waiter = client.get_waiter("function_active_v2") + waiter.wait(FunctionName=full_fn_name) + except client.exceptions.ResourceConflictException: + print( + "Lambda function already exists, this is fine, we will just invoke it." ) - # Default initial handler - if not initial_handler: - initial_handler = "test_lambda.test_handler" + response = client.invoke( + FunctionName=full_fn_name, + InvocationType="RequestResponse", + LogType="Tail", + Payload=payload, + ) - build_no_code_serverless_function_and_layer( - client, tmpdir, fn_name, runtime, timeout, initial_handler - ) + assert 200 <= response["StatusCode"] < 300, response + return response - @add_finalizer - def clean_up(): - client.delete_function(FunctionName=fn_name) - - # this closes the web socket so we don't get a - # ResourceWarning: unclosed - # warning on every test - # based on https://github.com/boto/botocore/pull/1810 - # (if that's ever merged, this can just become client.close()) - session = client._endpoint.http_session - managers = [session._manager] + list(session._proxy_managers.values()) - for manager in managers: - manager.clear() - - response = client.invoke( - FunctionName=fn_name, - InvocationType="RequestResponse", - LogType="Tail", - Payload=payload, - ) - assert 200 <= response["StatusCode"] < 300, response - return response +# This is for inspecting new Python runtime environments in AWS Lambda +# If you need to debug a new runtime, use this REPL to run arbitrary Python or bash commands +# in that runtime in a Lambda function: +# +# pip3 install click +# python3 tests/integrations/aws_lambda/client.py --runtime=python4.0 +# _REPL_CODE = """ @@ -197,7 +362,7 @@ def test_handler(event, context): @click.command() @click.option( - "--runtime", required=True, help="name of the runtime to use, eg python3.8" + "--runtime", required=True, help="name of the runtime to use, eg python3.11" ) @click.option("--verbose", is_flag=True, default=False) def repl(runtime, verbose): diff --git a/tests/integrations/aws_lambda/test_aws.py b/tests/integrations/aws_lambda/test_aws.py index 5825e5fca9..8904de1e52 100644 --- a/tests/integrations/aws_lambda/test_aws.py +++ b/tests/integrations/aws_lambda/test_aws.py @@ -1,22 +1,36 @@ """ -# AWS Lambda system tests +# AWS Lambda System Tests -This testsuite uses boto3 to upload actual lambda functions to AWS, execute -them and assert some things about the externally observed behavior. What that -means for you is that those tests won't run without AWS access keys: +This testsuite uses boto3 to upload actual Lambda functions to AWS Lambda and invoke them. - export SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID=.. - export SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY=... - export SENTRY_PYTHON_TEST_AWS_IAM_ROLE="arn:aws:iam::920901907255:role/service-role/lambda" +For running test locally you need to set these env vars: +(You can find the values in the Sentry password manager by searching for "AWS Lambda for Python SDK Tests"). -If you need to debug a new runtime, use this REPL to figure things out: + export SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID="..." + export SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY="..." + + +You can use `scripts/aws-cleanup.sh` to delete all files generated by this test suite. + + +If you need to debug a new runtime, use this REPL to run arbitrary Python or bash commands +in that runtime in a Lambda function: (see the bottom of client.py for more information.) pip3 install click python3 tests/integrations/aws_lambda/client.py --runtime=python4.0 + +IMPORTANT: + +During running of this test suite temporary folders will be created for compiling the Lambda functions. +This temporary folders will not be cleaned up. This is because in CI generated files have to be shared +between tests and thus the folders can not be deleted right after use. + +If you run your tests locally, you need to clean up the temporary folders manually. The location of +the temporary folders is printed when running a test. """ + import base64 import json -import os import re from textwrap import dedent @@ -31,56 +45,84 @@ from sentry_sdk.transport import HttpTransport -def event_processor(event): +def truncate_data(data): # AWS Lambda truncates the log output to 4kb, which is small enough to miss # parts of even a single error-event/transaction-envelope pair if considered # in full, so only grab the data we need. - event_data = {} - event_data["contexts"] = {} - event_data["contexts"]["trace"] = event.get("contexts", {}).get("trace") - event_data["exception"] = event.get("exception") - event_data["extra"] = event.get("extra") - event_data["level"] = event.get("level") - event_data["request"] = event.get("request") - event_data["tags"] = event.get("tags") - event_data["transaction"] = event.get("transaction") + cleaned_data = {} - return event_data + if data.get("type") is not None: + cleaned_data["type"] = data["type"] -def envelope_processor(envelope): - # AWS Lambda truncates the log output to 4kb, which is small enough to miss - # parts of even a single error-event/transaction-envelope pair if considered - # in full, so only grab the data we need. + if data.get("contexts") is not None: + cleaned_data["contexts"] = {} - (item,) = envelope.items - envelope_json = json.loads(item.get_bytes()) + if data["contexts"].get("trace") is not None: + cleaned_data["contexts"]["trace"] = data["contexts"].get("trace") + + if data.get("transaction") is not None: + cleaned_data["transaction"] = data.get("transaction") + + if data.get("request") is not None: + cleaned_data["request"] = data.get("request") - envelope_data = {} - envelope_data["contexts"] = {} - envelope_data["type"] = envelope_json["type"] - envelope_data["transaction"] = envelope_json["transaction"] - envelope_data["contexts"]["trace"] = envelope_json["contexts"]["trace"] - envelope_data["request"] = envelope_json["request"] - envelope_data["tags"] = envelope_json["tags"] + if data.get("tags") is not None: + cleaned_data["tags"] = data.get("tags") - return envelope_data + if data.get("exception") is not None: + cleaned_data["exception"] = data.get("exception") + + for value in cleaned_data["exception"]["values"]: + for frame in value.get("stacktrace", {}).get("frames", []): + del frame["vars"] + del frame["pre_context"] + del frame["context_line"] + del frame["post_context"] + + if data.get("extra") is not None: + cleaned_data["extra"] = {} + + for key in data["extra"].keys(): + if key == "lambda": + for lambda_key in data["extra"]["lambda"].keys(): + if lambda_key in ["function_name"]: + cleaned_data["extra"].setdefault("lambda", {})[lambda_key] = data["extra"]["lambda"][lambda_key] + elif key == "cloudwatch logs": + for cloudwatch_key in data["extra"]["cloudwatch logs"].keys(): + if cloudwatch_key in ["url", "log_group", "log_stream"]: + cleaned_data["extra"].setdefault("cloudwatch logs", {})[cloudwatch_key] = data["extra"]["cloudwatch logs"][cloudwatch_key] + + if data.get("level") is not None: + cleaned_data["level"] = data.get("level") + + if data.get("message") is not None: + cleaned_data["message"] = data.get("message") + + if "contexts" not in cleaned_data: + raise Exception(json.dumps(data)) + + return cleaned_data + +def event_processor(event): + return truncate_data(event) + +def envelope_processor(envelope): + (item,) = envelope.items + item_json = json.loads(item.get_bytes()) + + return truncate_data(item_json) class TestTransport(HttpTransport): def _send_event(self, event): event = event_processor(event) - # Writing a single string to stdout holds the GIL (seems like) and - # therefore cannot be interleaved with other threads. This is why we - # explicitly add a newline at the end even though `print` would provide - # us one. print("\\nEVENT: {}\\n".format(json.dumps(event))) def _send_envelope(self, envelope): envelope = envelope_processor(envelope) print("\\nENVELOPE: {}\\n".format(json.dumps(envelope))) - def init_sdk(timeout_warning=False, **extra_init_args): sentry_sdk.init( dsn="https://123abc@example.com/123", @@ -94,9 +136,6 @@ def init_sdk(timeout_warning=False, **extra_init_args): @pytest.fixture def lambda_client(): - if "SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID" not in os.environ: - pytest.skip("AWS environ vars not set") - from tests.integrations.aws_lambda.client import get_boto_client return get_boto_client() @@ -107,6 +146,8 @@ def lambda_client(): "python3.7", "python3.8", "python3.9", + "python3.10", + "python3.11", ] ) def lambda_runtime(request): @@ -132,8 +173,13 @@ def inner( initial_handler=initial_handler, ) - # for better debugging - response["LogResult"] = base64.b64decode(response["LogResult"]).splitlines() + # Make sure the "ENVELOPE:" and "EVENT:" log entries are always starting a new line. (Sometimes they don't.) + response["LogResult"] = ( + base64.b64decode(response["LogResult"]) + .replace(b"EVENT:", b"\nEVENT:") + .replace(b"ENVELOPE:", b"\nENVELOPE:") + .splitlines() + ) response["Payload"] = json.loads(response["Payload"].read().decode("utf-8")) del response["ResponseMetadata"] @@ -157,19 +203,14 @@ def inner( def test_basic(run_lambda_function): - envelopes, events, response = run_lambda_function( + _, events, response = run_lambda_function( LAMBDA_PRELUDE + dedent( """ init_sdk() - def event_processor(event): - # Delay event output like this to test proper shutdown - time.sleep(1) - return event - def test_handler(event, context): - raise Exception("something went wrong") + raise Exception("Oh!") """ ), b'{"foo": "bar"}', @@ -181,7 +222,7 @@ def test_handler(event, context): assert event["level"] == "error" (exception,) = event["exception"]["values"] assert exception["type"] == "Exception" - assert exception["value"] == "something went wrong" + assert exception["value"] == "Oh!" (frame1,) = exception["stacktrace"]["frames"] assert frame1["filename"] == "test_lambda.py" @@ -193,13 +234,13 @@ def test_handler(event, context): assert exception["mechanism"]["type"] == "aws_lambda" assert not exception["mechanism"]["handled"] - assert event["extra"]["lambda"]["function_name"].startswith("test_function_") + assert event["extra"]["lambda"]["function_name"].startswith("test_") logs_url = event["extra"]["cloudwatch logs"]["url"] assert logs_url.startswith("https://console.aws.amazon.com/cloudwatch/home?region=") assert not re.search("(=;|=$)", logs_url) assert event["extra"]["cloudwatch logs"]["log_group"].startswith( - "/aws/lambda/test_function_" + "/aws/lambda/test_" ) log_stream_re = "^[0-9]{4}/[0-9]{2}/[0-9]{2}/\\[[^\\]]+][a-f0-9]+$" @@ -213,27 +254,28 @@ def test_initialization_order(run_lambda_function): as seen by AWS already runs. At this point at least draining the queue should work.""" - envelopes, events, _response = run_lambda_function( + _, events, _ = run_lambda_function( LAMBDA_PRELUDE + dedent( """ def test_handler(event, context): init_sdk() - sentry_sdk.capture_exception(Exception("something went wrong")) + sentry_sdk.capture_exception(Exception("Oh!")) """ ), b'{"foo": "bar"}', ) (event,) = events + assert event["level"] == "error" (exception,) = event["exception"]["values"] assert exception["type"] == "Exception" - assert exception["value"] == "something went wrong" + assert exception["value"] == "Oh!" def test_request_data(run_lambda_function): - envelopes, events, _response = run_lambda_function( + _, events, _ = run_lambda_function( LAMBDA_PRELUDE + dedent( """ @@ -250,7 +292,7 @@ def test_handler(event, context): "httpMethod": "GET", "headers": { "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:62.0) Gecko/20100101 Firefox/62.0", + "User-Agent": "custom", "X-Forwarded-Proto": "https" }, "queryStringParameters": { @@ -275,7 +317,7 @@ def test_handler(event, context): assert event["request"] == { "headers": { "Host": "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:62.0) Gecko/20100101 Firefox/62.0", + "User-Agent": "custom", "X-Forwarded-Proto": "https", }, "method": "GET", @@ -285,24 +327,24 @@ def test_handler(event, context): def test_init_error(run_lambda_function, lambda_runtime): - envelopes, events, response = run_lambda_function( + _, events, _ = run_lambda_function( LAMBDA_PRELUDE - + ( - "def event_processor(event):\n" - ' return event["exception"]["values"][0]["value"]\n' - "init_sdk()\n" - "func()" + + dedent( + """ + init_sdk() + func() + """ ), b'{"foo": "bar"}', syntax_check=False, ) (event,) = events - assert "name 'func' is not defined" in event + assert event["exception"]["values"][0]["value"] == "name 'func' is not defined" def test_timeout_error(run_lambda_function): - envelopes, events, response = run_lambda_function( + _, events, _ = run_lambda_function( LAMBDA_PRELUDE + dedent( """ @@ -314,7 +356,7 @@ def test_handler(event, context): """ ), b'{"foo": "bar"}', - timeout=3, + timeout=2, ) (event,) = events @@ -322,20 +364,20 @@ def test_handler(event, context): (exception,) = event["exception"]["values"] assert exception["type"] == "ServerlessTimeoutWarning" assert exception["value"] in ( - "WARNING : Function is expected to get timed out. Configured timeout duration = 4 seconds.", "WARNING : Function is expected to get timed out. Configured timeout duration = 3 seconds.", + "WARNING : Function is expected to get timed out. Configured timeout duration = 2 seconds.", ) assert exception["mechanism"]["type"] == "threading" assert not exception["mechanism"]["handled"] - assert event["extra"]["lambda"]["function_name"].startswith("test_function_") + assert event["extra"]["lambda"]["function_name"].startswith("test_") logs_url = event["extra"]["cloudwatch logs"]["url"] assert logs_url.startswith("https://console.aws.amazon.com/cloudwatch/home?region=") assert not re.search("(=;|=$)", logs_url) assert event["extra"]["cloudwatch logs"]["log_group"].startswith( - "/aws/lambda/test_function_" + "/aws/lambda/test_" ) log_stream_re = "^[0-9]{4}/[0-9]{2}/[0-9]{2}/\\[[^\\]]+][a-f0-9]+$" @@ -345,7 +387,7 @@ def test_handler(event, context): def test_performance_no_error(run_lambda_function): - envelopes, events, response = run_lambda_function( + envelopes, _, _ = run_lambda_function( LAMBDA_PRELUDE + dedent( """ @@ -359,40 +401,41 @@ def test_handler(event, context): ) (envelope,) = envelopes + assert envelope["type"] == "transaction" - assert envelope["contexts"]["trace"]["op"] == "function.aws.lambda" - assert envelope["transaction"].startswith("test_function_") - assert envelope["transaction_info"] == {"source": "component"} + assert envelope["contexts"]["trace"]["op"] == "function.aws" + assert envelope["transaction"].startswith("test_") assert envelope["transaction"] in envelope["request"]["url"] def test_performance_error(run_lambda_function): - envelopes, events, response = run_lambda_function( + envelopes, _, _ = run_lambda_function( LAMBDA_PRELUDE + dedent( """ init_sdk(traces_sample_rate=1.0) def test_handler(event, context): - raise Exception("something went wrong") + raise Exception("Oh!") """ ), b'{"foo": "bar"}', ) - (event,) = events - assert event["level"] == "error" - (exception,) = event["exception"]["values"] - assert exception["type"] == "Exception" - assert exception["value"] == "something went wrong" + ( + error_event, + transaction_event, + ) = envelopes - (envelope,) = envelopes + assert error_event["level"] == "error" + (exception,) = error_event["exception"]["values"] + assert exception["type"] == "Exception" + assert exception["value"] == "Oh!" - assert envelope["type"] == "transaction" - assert envelope["contexts"]["trace"]["op"] == "function.aws.lambda" - assert envelope["transaction"].startswith("test_function_") - assert envelope["transaction_info"] == {"source": "component"} - assert envelope["transaction"] in envelope["request"]["url"] + assert transaction_event["type"] == "transaction" + assert transaction_event["contexts"]["trace"]["op"] == "function.aws" + assert transaction_event["transaction"].startswith("test_") + assert transaction_event["transaction"] in transaction_event["request"]["url"] @pytest.mark.parametrize( @@ -419,29 +462,25 @@ def test_handler(event, context): [ { "headers": { - "Host": "dogs.are.great", + "Host": "x.io", "X-Forwarded-Proto": "http" }, "httpMethod": "GET", - "path": "/tricks/kangaroo", + "path": "/somepath", "queryStringParameters": { - "completed_successfully": "true", - "treat_provided": "true", - "treat_type": "cheese" + "done": "true" }, "dog": "Maisey" }, { "headers": { - "Host": "dogs.are.great", + "Host": "x.io", "X-Forwarded-Proto": "http" }, "httpMethod": "GET", - "path": "/tricks/kangaroo", + "path": "/somepath", "queryStringParameters": { - "completed_successfully": "true", - "treat_provided": "true", - "treat_type": "cheese" + "done": "true" }, "dog": "Charlie" } @@ -459,14 +498,14 @@ def test_non_dict_event( batch_size, DictionaryContaining, # noqa:N803 ): - envelopes, events, response = run_lambda_function( + envelopes, _, response = run_lambda_function( LAMBDA_PRELUDE + dedent( """ init_sdk(traces_sample_rate=1.0) def test_handler(event, context): - raise Exception("More treats, please!") + raise Exception("Oh?") """ ), aws_event, @@ -474,50 +513,50 @@ def test_handler(event, context): assert response["FunctionError"] == "Unhandled" - error_event = events[0] + ( + error_event, + transaction_event, + ) = envelopes assert error_event["level"] == "error" - assert error_event["contexts"]["trace"]["op"] == "function.aws.lambda" + assert error_event["contexts"]["trace"]["op"] == "function.aws" function_name = error_event["extra"]["lambda"]["function_name"] - assert function_name.startswith("test_function_") + assert function_name.startswith("test_") assert error_event["transaction"] == function_name exception = error_event["exception"]["values"][0] assert exception["type"] == "Exception" - assert exception["value"] == "More treats, please!" + assert exception["value"] == "Oh?" assert exception["mechanism"]["type"] == "aws_lambda" - envelope = envelopes[0] - assert envelope["type"] == "transaction" - assert envelope["contexts"]["trace"] == DictionaryContaining( + assert transaction_event["type"] == "transaction" + assert transaction_event["contexts"]["trace"] == DictionaryContaining( error_event["contexts"]["trace"] ) - assert envelope["contexts"]["trace"]["status"] == "internal_error" - assert envelope["transaction"] == error_event["transaction"] - assert envelope["request"]["url"] == error_event["request"]["url"] + assert transaction_event["contexts"]["trace"]["status"] == "internal_error" + assert transaction_event["transaction"] == error_event["transaction"] + assert transaction_event["request"]["url"] == error_event["request"]["url"] if has_request_data: request_data = { - "headers": {"Host": "dogs.are.great", "X-Forwarded-Proto": "http"}, + "headers": {"Host": "x.io", "X-Forwarded-Proto": "http"}, "method": "GET", - "url": "http://dogs.are.great/tricks/kangaroo", + "url": "http://x.io/somepath", "query_string": { - "completed_successfully": "true", - "treat_provided": "true", - "treat_type": "cheese", + "done": "true", }, } else: request_data = {"url": "awslambda:///{}".format(function_name)} assert error_event["request"] == request_data - assert envelope["request"] == request_data + assert transaction_event["request"] == request_data if batch_size > 1: assert error_event["tags"]["batch_size"] == batch_size assert error_event["tags"]["batch_request"] is True - assert envelope["tags"]["batch_size"] == batch_size - assert envelope["tags"]["batch_request"] is True + assert transaction_event["tags"]["batch_size"] == batch_size + assert transaction_event["tags"]["batch_request"] is True def test_traces_sampler_gets_correct_values_in_sampling_context( @@ -554,7 +593,7 @@ def test_traces_sampler_gets_correct_values_in_sampling_context( import inspect - envelopes, events, response = run_lambda_function( + _, _, response = run_lambda_function( LAMBDA_PRELUDE + dedent(inspect.getsource(StringContaining)) + dedent(inspect.getsource(DictionaryContaining)) @@ -589,12 +628,12 @@ def test_handler(event, context): "aws_event": DictionaryContaining({ "httpMethod": "GET", "path": "/sit/stay/rollover", - "headers": {"Host": "dogs.are.great", "X-Forwarded-Proto": "http"}, + "headers": {"Host": "x.io", "X-Forwarded-Proto": "http"}, }), "aws_context": ObjectDescribedBy( type=get_lambda_bootstrap().LambdaContext, attrs={ - 'function_name': StringContaining("test_function"), + 'function_name': StringContaining("test_"), 'function_version': '$LATEST', } ) @@ -616,7 +655,7 @@ def test_handler(event, context): ) """ ), - b'{"httpMethod": "GET", "path": "/sit/stay/rollover", "headers": {"Host": "dogs.are.great", "X-Forwarded-Proto": "http"}}', + b'{"httpMethod": "GET", "path": "/sit/stay/rollover", "headers": {"Host": "x.io", "X-Forwarded-Proto": "http"}}', ) assert response["Payload"]["AssertionError raised"] is False @@ -648,7 +687,7 @@ def test_handler(event, context): assert isinstance(current_client.options['integrations'][0], sentry_sdk.integrations.aws_lambda.AwsLambdaIntegration) - raise Exception("something went wrong") + raise Exception("Oh!") """ ), b'{"foo": "bar"}', @@ -661,7 +700,7 @@ def test_handler(event, context): assert response["Payload"]["errorType"] != "AssertionError" assert response["Payload"]["errorType"] == "Exception" - assert response["Payload"]["errorMessage"] == "something went wrong" + assert response["Payload"]["errorMessage"] == "Oh!" assert "sentry_handler" in response["LogResult"][3].decode("utf-8") @@ -675,7 +714,7 @@ def test_error_has_new_trace_context_performance_enabled(run_lambda_function): def test_handler(event, context): sentry_sdk.capture_message("hi") - raise Exception("something went wrong") + raise Exception("Oh!") """ ), payload=b'{"foo": "bar"}', @@ -708,7 +747,7 @@ def test_error_has_new_trace_context_performance_disabled(run_lambda_function): def test_handler(event, context): sentry_sdk.capture_message("hi") - raise Exception("something went wrong") + raise Exception("Oh!") """ ), payload=b'{"foo": "bar"}', @@ -734,6 +773,14 @@ def test_error_has_existing_trace_context_performance_enabled(run_lambda_functio 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, + } + } + envelopes, _, _ = run_lambda_function( LAMBDA_PRELUDE + dedent( @@ -742,10 +789,10 @@ def test_error_has_existing_trace_context_performance_enabled(run_lambda_functio def test_handler(event, context): sentry_sdk.capture_message("hi") - raise Exception("something went wrong") + raise Exception("Oh!") """ ), - payload=b'{"sentry_trace": "%s"}' % sentry_trace_header.encode(), + payload=json.dumps(payload).encode(), ) (msg_event, error_event, transaction_event) = envelopes @@ -773,6 +820,14 @@ def test_error_has_existing_trace_context_performance_disabled(run_lambda_functi 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, + } + } + _, events, _ = run_lambda_function( LAMBDA_PRELUDE + dedent( @@ -781,10 +836,10 @@ def test_error_has_existing_trace_context_performance_disabled(run_lambda_functi def test_handler(event, context): sentry_sdk.capture_message("hi") - raise Exception("something went wrong") + raise Exception("Oh!") """ ), - payload=b'{"sentry_trace": "%s"}' % sentry_trace_header.encode(), + payload=json.dumps(payload).encode(), ) (msg_event, error_event) = events diff --git a/tox.ini b/tox.ini index d2741320c3..625482d5b8 100644 --- a/tox.ini +++ b/tox.ini @@ -35,8 +35,10 @@ envlist = {py3.7,py3.8,py3.9,py3.10,py3.11}-asyncpg # AWS Lambda - # The aws_lambda tests deploy to the real AWS and have their own matrix of Python versions. - {py3.7}-aws_lambda + # The aws_lambda tests deploy to the real AWS and have their own + # matrix of Python versions to run the test lambda function in. + # see `lambda_runtime` fixture in tests/integrations/aws_lambda.py + {py3.9}-aws_lambda # Beam {py3.7}-beam-v{2.12,2.13,2.32,2.33} @@ -410,12 +412,15 @@ deps = quart-v0.16: blinker<1.6 quart-v0.16: jinja2<3.1.0 quart-v0.16: Werkzeug<2.1.0 + quart-v0.16: hypercorn<0.15.0 quart-v0.16: quart>=0.16.1,<0.17.0 quart-v0.17: Werkzeug<3.0.0 quart-v0.17: blinker<1.6 + quart-v0.17: hypercorn<0.15.0 quart-v0.17: quart>=0.17.0,<0.18.0 quart-v0.18: Werkzeug<3.0.0 quart-v0.18: quart>=0.18.0,<0.19.0 + quart-v0.18: hypercorn<0.15.0 quart-v0.19: Werkzeug>=3.0.0 quart-v0.19: quart>=0.19.0,<0.20.0 @@ -572,7 +577,6 @@ setenv = passenv = SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY - SENTRY_PYTHON_TEST_AWS_IAM_ROLE SENTRY_PYTHON_TEST_POSTGRES_USER SENTRY_PYTHON_TEST_POSTGRES_PASSWORD SENTRY_PYTHON_TEST_POSTGRES_NAME