From b9e1d77dea8cd1ed91a6f0547c53e4467c21c537 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 27 Oct 2026 11:28:19 +0100 Subject: [PATCH 1/4] . --- scripts/init_serverless_sdk.py | 36 +++++-------------- .../lambda_functions/PathHandler/app/index.py | 8 +++++ .../aws_lambda/test_aws_lambda.py | 13 +++++++ tests/integrations/aws_lambda/utils.py | 7 +++- 4 files changed, 35 insertions(+), 29 deletions(-) create mode 100644 tests/integrations/aws_lambda/lambda_functions/PathHandler/app/index.py diff --git a/scripts/init_serverless_sdk.py b/scripts/init_serverless_sdk.py index d27036af48..5423ba19bd 100644 --- a/scripts/init_serverless_sdk.py +++ b/scripts/init_serverless_sdk.py @@ -6,9 +6,8 @@ 'sentry_sdk.integrations.init_serverless_sdk.sentry_lambda_handler' """ +import importlib import os -import re -import sys from typing import TYPE_CHECKING import sentry_sdk @@ -27,8 +26,6 @@ class AWSLambdaModuleLoader: - DIR_PATH_REGEX = r"^(.+)\/([^\/]+)$" - def __init__(self, sentry_initial_handler): try: module_path, self.handler_name = sentry_initial_handler.rsplit(".", 1) @@ -41,35 +38,18 @@ def extract_and_load_lambda_function_module(self, module_path): """ Method that extracts and loads lambda function module from module_path """ - py_version = sys.version_info - - if re.match(self.DIR_PATH_REGEX, module_path): - # With a path like -> `scheduler/scheduler/event` - # `module_name` is `event`, and `module_file_path` is `scheduler/scheduler/event.py` - module_name = module_path.split(os.path.sep)[-1] - module_file_path = module_path + ".py" - - # Supported python versions are 3.6, 3.7, 3.8 - if py_version >= (3, 6): - import importlib.util - - spec = importlib.util.spec_from_file_location( - module_name, module_file_path - ) - self.lambda_function_module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(self.lambda_function_module) - else: - raise ValueError("Python version %s is not supported." % py_version) - else: - import importlib - - self.lambda_function_module = importlib.import_module(module_path) + # like the Lambda runtime: `dir/file` is the module `dir.file`, which `sys.modules` + # keeps, so its code only runs on the first invocation. + # https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/4.2.2/awslambdaric/bootstrap.py#L53 + self.lambda_function_module = importlib.import_module( + module_path.replace("/", ".") + ) def get_lambda_handler(self): return getattr(self.lambda_function_module, self.handler_name) -def sentry_lambda_handler(event: "Any", context: "Any") -> None: +def sentry_lambda_handler(event: "Any", context: "Any") -> "Any": """ Handler function that invokes a lambda handler which path is defined in environment variables as "SENTRY_INITIAL_HANDLER" diff --git a/tests/integrations/aws_lambda/lambda_functions/PathHandler/app/index.py b/tests/integrations/aws_lambda/lambda_functions/PathHandler/app/index.py new file mode 100644 index 0000000000..694df0a7fe --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions/PathHandler/app/index.py @@ -0,0 +1,8 @@ +# counts the invocations since this module was loaded. +invocations = 0 + + +def handler(event, context): + global invocations + invocations += 1 + return {"invocations": invocations} diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 0bf1cd3670..63d58b755d 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -201,6 +201,19 @@ def test_init_error(lambda_client, test_environment): assert transaction_event["transaction"] == "InitError" +def test_path_handler_loaded_once(lambda_client, test_environment): + invocations = [ + json.loads( + lambda_client.invoke(FunctionName="PathHandler", Payload=json.dumps({}))[ + "Payload" + ].read() + )["invocations"] + for _ in range(2) + ] + # the Sentry layer loads the function's module once, not on every invocation. + assert invocations == [1, 2] + + def test_timeout_error_scope_modified(lambda_client, test_environment): lambda_client.invoke( FunctionName="TimeoutErrorScopeModified", diff --git a/tests/integrations/aws_lambda/utils.py b/tests/integrations/aws_lambda/utils.py index 8933d856ed..2d3e3c7fa5 100644 --- a/tests/integrations/aws_lambda/utils.py +++ b/tests/integrations/aws_lambda/utils.py @@ -27,6 +27,9 @@ LAMBDA_FUNCTION_TIMEOUT = 10 SAM_PORT = 3001 +# functions whose handler is a path instead of `index.handler`. +INITIAL_HANDLERS = {"PathHandler": "app/index.handler"} + 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) @@ -117,7 +120,9 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: "Environment": { "Variables": { "SENTRY_DSN": dsn, - "SENTRY_INITIAL_HANDLER": "index.handler", + "SENTRY_INITIAL_HANDLER": INITIAL_HANDLERS.get( + lambda_dir, "index.handler" + ), "SENTRY_TRACES_SAMPLE_RATE": "1.0", } }, From 989a0b9f9ddb71b911253e53ef1b5bee4d987905 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 27 Oct 2026 14:00:06 +0100 Subject: [PATCH 2/4] wip - make more descriptive --- scripts/init_serverless_sdk.py | 2 +- .../lambda_functions/PathHandler/app/index.py | 11 ++++++----- tests/integrations/aws_lambda/test_aws_lambda.py | 9 +++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/scripts/init_serverless_sdk.py b/scripts/init_serverless_sdk.py index 5423ba19bd..2207997192 100644 --- a/scripts/init_serverless_sdk.py +++ b/scripts/init_serverless_sdk.py @@ -40,7 +40,7 @@ def extract_and_load_lambda_function_module(self, module_path): """ # like the Lambda runtime: `dir/file` is the module `dir.file`, which `sys.modules` # keeps, so its code only runs on the first invocation. - # https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/4.2.2/awslambdaric/bootstrap.py#L53 + # https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/fa2462d93d11da632aad8f8bfb07a3e060438150/awslambdaric/bootstrap.py#L53 self.lambda_function_module = importlib.import_module( module_path.replace("/", ".") ) diff --git a/tests/integrations/aws_lambda/lambda_functions/PathHandler/app/index.py b/tests/integrations/aws_lambda/lambda_functions/PathHandler/app/index.py index 694df0a7fe..0a28a52195 100644 --- a/tests/integrations/aws_lambda/lambda_functions/PathHandler/app/index.py +++ b/tests/integrations/aws_lambda/lambda_functions/PathHandler/app/index.py @@ -1,8 +1,9 @@ -# counts the invocations since this module was loaded. -invocations = 0 +# counts the invocations since this module was loaded; gets reset on cold start or +# when the module is reloaded. +num_invocations = 0 def handler(event, context): - global invocations - invocations += 1 - return {"invocations": invocations} + global num_invocations + num_invocations += 1 + return {"invocations": num_invocations} diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 63d58b755d..ad402d347c 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -202,16 +202,17 @@ def test_init_error(lambda_client, test_environment): def test_path_handler_loaded_once(lambda_client, test_environment): - invocations = [ + num_invocations = [ json.loads( lambda_client.invoke(FunctionName="PathHandler", Payload=json.dumps({}))[ "Payload" ].read() - )["invocations"] + )["num_invocations"] for _ in range(2) ] - # the Sentry layer loads the function's module once, not on every invocation. - assert invocations == [1, 2] + # Sentry layer should load the function's module once (on cold start or whenever + # the module is reloaded), not on every warm invocation. + assert num_invocations == [1, 2] def test_timeout_error_scope_modified(lambda_client, test_environment): From fac06dc59f28563f7aa21c2d9f195493d09e45f1 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Tue, 27 Oct 2026 14:20:59 +0100 Subject: [PATCH 3/4] fix test --- .../aws_lambda/lambda_functions/PathHandler/app/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/aws_lambda/lambda_functions/PathHandler/app/index.py b/tests/integrations/aws_lambda/lambda_functions/PathHandler/app/index.py index 0a28a52195..c589df7c4c 100644 --- a/tests/integrations/aws_lambda/lambda_functions/PathHandler/app/index.py +++ b/tests/integrations/aws_lambda/lambda_functions/PathHandler/app/index.py @@ -6,4 +6,4 @@ def handler(event, context): global num_invocations num_invocations += 1 - return {"invocations": num_invocations} + return {"num_invocations": num_invocations} From fb3fbfd4a2d75ac5311e2c6aec7bde1c8a3d1dfd Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Wed, 11 Nov 2026 09:48:22 +0000 Subject: [PATCH 4/4] fix(aws-lambda): Keep accepting ./ and absolute task root handler paths Co-Authored-By: Ivana Kellyer <131587164+sentrivana@users.noreply.github.com> --- scripts/init_serverless_sdk.py | 6 ++++++ .../lambda_functions/AbsolutePathHandler/app/index.py | 9 +++++++++ tests/integrations/aws_lambda/test_aws_lambda.py | 5 +++-- tests/integrations/aws_lambda/utils.py | 5 ++++- 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 tests/integrations/aws_lambda/lambda_functions/AbsolutePathHandler/app/index.py diff --git a/scripts/init_serverless_sdk.py b/scripts/init_serverless_sdk.py index 2207997192..9d4e5cca26 100644 --- a/scripts/init_serverless_sdk.py +++ b/scripts/init_serverless_sdk.py @@ -38,6 +38,12 @@ def extract_and_load_lambda_function_module(self, module_path): """ Method that extracts and loads lambda function module from module_path """ + # the handler used to be loaded from a file path, so keep accepting + # `./dir/file` and absolute `/dir/file` by making them relative + # to the task root, which is on `sys.path`. + task_root = os.environ.get("LAMBDA_TASK_ROOT", os.getcwd()) + module_path = os.path.relpath(os.path.join(task_root, module_path), task_root) + # like the Lambda runtime: `dir/file` is the module `dir.file`, which `sys.modules` # keeps, so its code only runs on the first invocation. # https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/fa2462d93d11da632aad8f8bfb07a3e060438150/awslambdaric/bootstrap.py#L53 diff --git a/tests/integrations/aws_lambda/lambda_functions/AbsolutePathHandler/app/index.py b/tests/integrations/aws_lambda/lambda_functions/AbsolutePathHandler/app/index.py new file mode 100644 index 0000000000..c589df7c4c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions/AbsolutePathHandler/app/index.py @@ -0,0 +1,9 @@ +# counts the invocations since this module was loaded; gets reset on cold start or +# when the module is reloaded. +num_invocations = 0 + + +def handler(event, context): + global num_invocations + num_invocations += 1 + return {"num_invocations": num_invocations} diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index ad402d347c..72c7faa190 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -201,10 +201,11 @@ def test_init_error(lambda_client, test_environment): assert transaction_event["transaction"] == "InitError" -def test_path_handler_loaded_once(lambda_client, test_environment): +@pytest.mark.parametrize("function_name", ["PathHandler", "AbsolutePathHandler"]) +def test_path_handler_loaded_once(lambda_client, test_environment, function_name): num_invocations = [ json.loads( - lambda_client.invoke(FunctionName="PathHandler", Payload=json.dumps({}))[ + lambda_client.invoke(FunctionName=function_name, Payload=json.dumps({}))[ "Payload" ].read() )["num_invocations"] diff --git a/tests/integrations/aws_lambda/utils.py b/tests/integrations/aws_lambda/utils.py index 2d3e3c7fa5..ac14fd56ef 100644 --- a/tests/integrations/aws_lambda/utils.py +++ b/tests/integrations/aws_lambda/utils.py @@ -28,7 +28,10 @@ SAM_PORT = 3001 # functions whose handler is a path instead of `index.handler`. -INITIAL_HANDLERS = {"PathHandler": "app/index.handler"} +INITIAL_HANDLERS = { + "PathHandler": "app/index.handler", + "AbsolutePathHandler": "/var/task/app/index.handler", +} PYTHON_VERSION = f"python{sys.version_info.major}.{sys.version_info.minor}"