diff --git a/scripts/init_serverless_sdk.py b/scripts/init_serverless_sdk.py index d27036af48..2207997192 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/fa2462d93d11da632aad8f8bfb07a3e060438150/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..c589df7c4c --- /dev/null +++ b/tests/integrations/aws_lambda/lambda_functions/PathHandler/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 0bf1cd3670..ad402d347c 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -201,6 +201,20 @@ def test_init_error(lambda_client, test_environment): assert transaction_event["transaction"] == "InitError" +def test_path_handler_loaded_once(lambda_client, test_environment): + num_invocations = [ + json.loads( + lambda_client.invoke(FunctionName="PathHandler", Payload=json.dumps({}))[ + "Payload" + ].read() + )["num_invocations"] + for _ in range(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): 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", } },