diff --git a/src/sentry/integrations/aws_lambda/utils.py b/src/sentry/integrations/aws_lambda/utils.py index c65ae1c756c259..17a125487bc445 100644 --- a/src/sentry/integrations/aws_lambda/utils.py +++ b/src/sentry/integrations/aws_lambda/utils.py @@ -204,6 +204,35 @@ def get_dsn_for_project(organization_id, project_id): return enabled_dsn.dsn_public +def get_node_options_for_layer(layer_name: str, layer_version: int | None) -> str: + """ + Depending on the SDK major our Lambda Layer represents, a different SDK + package has to be used when setting `NODE_OPTIONS`. + This helper generates the correct options for all the layers we support. + """ + # Lambda layers for v7 of our AWS SDK use the older `@sentry/serverless` SDK + # + # These are specifically + # - `SentryNodeServerlessSDKv7` + # - `SentryNodeServerlessSDK:235` and lower + if layer_name == "SentryNodeServerlessSDKv7" or ( + layer_name == "SentryNodeServerlessSDK" + and layer_version is not None + and layer_version <= 235 + ): + return "-r @sentry/serverless/dist/awslambda-auto" + + # Lambda layers for v8 and above of our AWS SDK use + # the newer `@sentry/aws-serverless` SDK + # + # These are specifically + # - `SentryNodeServerlessSDK:236` and above + # - `SentryNodeServerlessSDKv8` + # - and any other layer with a version suffix above, e.g. + # `SentryNodeServerlessSDKv9` + return "-r @sentry/aws-serverless/awslambda-auto" + + def enable_single_lambda(lambda_client, function, sentry_project_dsn, retries_left=3): # find the latest layer for this function layer_arn = get_latest_layer_for_function(function) @@ -226,6 +255,7 @@ def enable_single_lambda(lambda_client, function, sentry_project_dsn, retries_le if runtime.startswith("nodejs"): # note the env variables would be different for non-Node runtimes + layer_name = get_option_value(function, OPTION_LAYER_NAME) version = get_option_value(function, OPTION_VERSION) try: parsed_version = int(version) @@ -233,24 +263,12 @@ def enable_single_lambda(lambda_client, function, sentry_project_dsn, retries_le sentry_sdk.capture_message("Invariant: Unable to parse AWS lambda version") parsed_version = None - if ( - # Lambda layer version 235 was the latest version using `@sentry/serverless` before we switched to `@sentry/aws-serverless` - parsed_version is not None - and parsed_version <= 235 - ): - env_variables.update( - { - "NODE_OPTIONS": "-r @sentry/serverless/dist/awslambda-auto", - **sentry_env_variables, - } - ) - else: - env_variables.update( - { - "NODE_OPTIONS": "-r @sentry/aws-serverless/awslambda-auto", - **sentry_env_variables, - } - ) + env_variables.update( + { + "NODE_OPTIONS": get_node_options_for_layer(layer_name, parsed_version), + **sentry_env_variables, + } + ) elif runtime.startswith("python"): # Check if we are trying to re-enable an already enabled python, and if diff --git a/tests/sentry/integrations/api/endpoints/test_organization_integration_serverless_functions.py b/tests/sentry/integrations/api/endpoints/test_organization_integration_serverless_functions.py index c2ca11d0584240..976a66d3affe13 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_integration_serverless_functions.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_integration_serverless_functions.py @@ -253,7 +253,7 @@ def test_enable_node_layer(self, mock_gen_aws_client, mock_get_serialized_lambda ], "Environment": { "Variables": { - "NODE_OPTIONS": "-r @sentry/serverless/dist/awslambda-auto", + "NODE_OPTIONS": "-r @sentry/aws-serverless/awslambda-auto", "SENTRY_DSN": self.sentry_dsn, "SENTRY_TRACES_SAMPLE_RATE": "1.0", } @@ -286,7 +286,7 @@ def test_enable_node_layer(self, mock_gen_aws_client, mock_get_serialized_lambda ], Environment={ "Variables": { - "NODE_OPTIONS": "-r @sentry/serverless/dist/awslambda-auto", + "NODE_OPTIONS": "-r @sentry/aws-serverless/awslambda-auto", "SENTRY_DSN": self.sentry_dsn, "SENTRY_TRACES_SAMPLE_RATE": "1.0", } diff --git a/tests/sentry/integrations/aws_lambda/test_integration.py b/tests/sentry/integrations/aws_lambda/test_integration.py index f2ff544fe5846a..f15492700b03cd 100644 --- a/tests/sentry/integrations/aws_lambda/test_integration.py +++ b/tests/sentry/integrations/aws_lambda/test_integration.py @@ -14,6 +14,7 @@ from sentry.projects.services.project import project_service from sentry.silo.base import SiloMode from sentry.testutils.cases import IntegrationTestCase +from sentry.testutils.helpers.options import override_options from sentry.testutils.silo import assume_test_silo_mode, control_silo_test from sentry.users.services.user.serial import serialize_rpc_user @@ -157,67 +158,94 @@ def test_lambda_list(self, mock_react_view, mock_gen_aws_client, mock_get_suppor @patch("sentry.integrations.aws_lambda.integration.get_supported_functions") @patch("sentry.integrations.aws_lambda.integration.gen_aws_client") - def test_lambda_setup_layer_success(self, mock_gen_aws_client, mock_get_supported_functions): - mock_client = mock_gen_aws_client.return_value - mock_client.update_function_configuration = MagicMock() - mock_client.describe_account = MagicMock(return_value={"Account": {"Name": "my_name"}}) - - mock_get_supported_functions.return_value = [ - { - "FunctionName": "lambdaA", - "Runtime": "nodejs12.x", - "FunctionArn": "arn:aws:lambda:us-east-2:599817902985:function:lambdaA", - }, - { - "FunctionName": "lambdaB", - "Runtime": "nodejs10.x", - "FunctionArn": "arn:aws:lambda:us-east-2:599817902985:function:lambdaB", - }, - ] - - aws_external_id = "12-323" - self.pipeline.state.step_index = 2 - self.pipeline.state.data = { - "region": region, - "account_number": account_number, - "aws_external_id": aws_external_id, - "project_id": self.projectA.id, - } - - with assume_test_silo_mode(SiloMode.REGION): - sentry_project_dsn = ProjectKey.get_default(project=self.projectA).get_dsn(public=True) - - # TODO: pass in lambdaA=false - # having issues with reading json data - # request.POST looks like {"lambdaB": "True"} - # string instead of boolean - resp = self.client.post(self.setup_path, {"lambdaB": "true", "lambdaA": "false"}) - - assert resp.status_code == 200 - - mock_client.update_function_configuration.assert_called_once_with( - FunctionName="lambdaB", - Layers=["arn:aws:lambda:us-east-2:1234:layer:my-layer:3"], - Environment={ - "Variables": { - "NODE_OPTIONS": "-r @sentry/serverless/dist/awslambda-auto", - "SENTRY_DSN": sentry_project_dsn, - "SENTRY_TRACES_SAMPLE_RATE": "1.0", + def test_node_lambda_setup_layer_success( + self, + mock_gen_aws_client, + mock_get_supported_functions, + ): + for layer_name, layer_version, expected_node_options in [ + ("SentryNodeServerlessSDKv7", "5", "-r @sentry/serverless/dist/awslambda-auto"), + ("SentryNodeServerlessSDK", "168", "-r @sentry/serverless/dist/awslambda-auto"), + ("SentryNodeServerlessSDK", "235", "-r @sentry/serverless/dist/awslambda-auto"), + ("SentryNodeServerlessSDK", "236", "-r @sentry/aws-serverless/awslambda-auto"), + ("SentryNodeServerlessSDKv8", "3", "-r @sentry/aws-serverless/awslambda-auto"), + ("SentryNodeServerlessSDKv9", "235", "-r @sentry/aws-serverless/awslambda-auto"), + ]: + with override_options( + { + "aws-lambda.node.layer-name": layer_name, + "aws-lambda.node.layer-version": layer_version, + } + ): + # Ensure we reset everything + self.setUp() + mock_get_supported_functions.reset_mock() + mock_gen_aws_client.reset_mock() + + mock_client = mock_gen_aws_client.return_value + mock_client.update_function_configuration = MagicMock() + mock_client.describe_account = MagicMock( + return_value={"Account": {"Name": "my_name"}} + ) + + mock_get_supported_functions.return_value = [ + { + "FunctionName": "lambdaA", + "Runtime": "nodejs12.x", + "FunctionArn": "arn:aws:lambda:us-east-2:599817902985:function:lambdaA", + }, + { + "FunctionName": "lambdaB", + "Runtime": "nodejs10.x", + "FunctionArn": "arn:aws:lambda:us-east-2:599817902985:function:lambdaB", + }, + ] + + aws_external_id = "12-323" + self.pipeline.state.step_index = 2 + self.pipeline.state.data = { + "region": region, + "account_number": account_number, + "aws_external_id": aws_external_id, + "project_id": self.projectA.id, } - }, - ) - integration = Integration.objects.get(provider=self.provider.key) - assert integration.name == "my_name us-east-2" - assert integration.external_id == "599817902985-us-east-2" - assert integration.metadata == { - "region": region, - "account_number": account_number, - "aws_external_id": aws_external_id, - } - assert OrganizationIntegration.objects.filter( - integration=integration, organization_id=self.organization.id - ) + with assume_test_silo_mode(SiloMode.REGION): + sentry_project_dsn = ProjectKey.get_default(project=self.projectA).get_dsn( + public=True + ) + + # TODO: pass in lambdaA=false + # having issues with reading json data + # request.POST looks like {"lambdaB": "True"} + # string instead of boolean + resp = self.client.post(self.setup_path, {"lambdaB": "true", "lambdaA": "false"}) + + assert resp.status_code == 200 + + mock_client.update_function_configuration.assert_called_once_with( + FunctionName="lambdaB", + Layers=[f"arn:aws:lambda:us-east-2:1234:layer:{layer_name}:{layer_version}"], + Environment={ + "Variables": { + "NODE_OPTIONS": expected_node_options, + "SENTRY_DSN": sentry_project_dsn, + "SENTRY_TRACES_SAMPLE_RATE": "1.0", + } + }, + ) + + integration = Integration.objects.get(provider=self.provider.key) + assert integration.name == "my_name us-east-2" + assert integration.external_id == "599817902985-us-east-2" + assert integration.metadata == { + "region": region, + "account_number": account_number, + "aws_external_id": aws_external_id, + } + assert OrganizationIntegration.objects.filter( + integration=integration, organization_id=self.organization.id + ) @patch("sentry.integrations.aws_lambda.integration.get_supported_functions") @patch("sentry.integrations.aws_lambda.integration.gen_aws_client")