From a16a8f4235c2199d9b4b953823907943884ac8af Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 22 Apr 2026 09:48:59 +0200 Subject: [PATCH] test(aws-serverless): Ensure aws-serverless E2E tests run locally I ran into a bunch of issues trying to run this locally. This adjusts things slightly so that it runs locally, taking care of setting up sam consistently. --- .../aws-serverless/package.json | 17 ++++--- .../aws-serverless/pull-sam-image.sh | 8 ++-- .../aws-serverless/samconfig.toml | 10 +++++ .../aws-serverless/src/stack.ts | 16 +++++-- .../aws-serverless/start-event-proxy.mjs | 2 +- .../aws-serverless/tests/lambda-fixtures.ts | 45 ++++++++++++++----- .../aws-serverless/tests/layer.test.ts | 10 ++--- .../aws-serverless/tests/npm.test.ts | 4 +- 8 files changed, 75 insertions(+), 37 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/aws-serverless/samconfig.toml diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/package.json b/dev-packages/e2e-tests/test-applications/aws-serverless/package.json index b417d8022667..84076d8393c2 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/package.json +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/package.json @@ -1,12 +1,13 @@ { - "name": "aws-lambda-sam", + "name": "aws-serverless", "version": "1.0.0", "private": true, "type": "commonjs", "scripts": { "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && npx rimraf node_modules/@sentry/aws-serverless/nodejs", + "test:pull-sam-image": "./pull-sam-image.sh", + "test:build": "pnpm test:pull-sam-image && pnpm install && npx rimraf node_modules/@sentry/aws-serverless/nodejs", "test:assert": "pnpm test" }, "//": "We just need the @sentry/aws-serverless layer zip file, not the NPM package", @@ -15,12 +16,10 @@ "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/", - "@types/tmp": "^0.2.6", "aws-cdk-lib": "^2.210.0", "constructs": "^10.4.2", "glob": "^11.0.3", - "rimraf": "^5.0.10", - "tmp": "^0.2.5" + "rimraf": "^5.0.10" }, "volta": { "extends": "../../package.json" @@ -34,12 +33,12 @@ "sentryTest": { "variants": [ { - "build-command": "NODE_VERSION=20 ./pull-sam-image.sh && pnpm test:build", - "assert-command": "NODE_VERSION=20 pnpm test:assert", - "label": "aws-serverless (Node 20)" + "build-command": "NODE_VERSION=22 pnpm test:build", + "assert-command": "NODE_VERSION=22 pnpm test:assert", + "label": "aws-serverless (Node 22)" }, { - "build-command": "NODE_VERSION=18 ./pull-sam-image.sh && pnpm test:build", + "build-command": "NODE_VERSION=18 pnpm test:build", "assert-command": "NODE_VERSION=18 pnpm test:assert", "label": "aws-serverless (Node 18)" } diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/pull-sam-image.sh b/dev-packages/e2e-tests/test-applications/aws-serverless/pull-sam-image.sh index d6790c2c2c49..c9659547a4ea 100755 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/pull-sam-image.sh +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/pull-sam-image.sh @@ -1,13 +1,11 @@ #!/bin/bash -# Script to pull the correct Lambda docker image based on the NODE_VERSION environment variable. +# Pull the Lambda Node docker image for SAM local. NODE_VERSION should be the major only (e.g. 20). +# Defaults to 20 to match the repo's Volta Node major (see root package.json "volta.node"). set -e -if [[ -z "$NODE_VERSION" ]]; then - echo "Error: NODE_VERSION not set" - exit 1 -fi +NODE_VERSION="${NODE_VERSION:-20}" echo "Pulling Lambda Node $NODE_VERSION docker image..." docker pull "public.ecr.aws/lambda/nodejs:${NODE_VERSION}" diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/samconfig.toml b/dev-packages/e2e-tests/test-applications/aws-serverless/samconfig.toml new file mode 100644 index 000000000000..6771bf7d7900 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/samconfig.toml @@ -0,0 +1,10 @@ +# SAM CLI expects this file in the project root; without it, `sam local start-lambda` logs +# OSError / missing file errors when run from the e2e temp directory. +# These values are placeholders — this app only uses `sam local`, not deploy. +version = 0.1 + +[default.deploy.parameters] +stack_name = "sentry-e2e-aws-serverless-local" +region = "us-east-1" +confirm_changeset = false +capabilities = "CAPABILITY_IAM" diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts index 63463c914e1d..792bd4308c51 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts @@ -4,7 +4,7 @@ import * as path from 'node:path'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as dns from 'node:dns/promises'; -import { platform } from 'node:process'; +import { arch, platform } from 'node:process'; import { globSync } from 'glob'; import { execFileSync } from 'node:child_process'; @@ -12,9 +12,13 @@ const LAMBDA_FUNCTIONS_WITH_LAYER_DIR = './src/lambda-functions-layer'; const LAMBDA_FUNCTIONS_WITH_NPM_DIR = './src/lambda-functions-npm'; const LAMBDA_FUNCTION_TIMEOUT = 10; const LAYER_DIR = './node_modules/@sentry/aws-serverless/'; -const DEFAULT_NODE_VERSION = '22'; export const SAM_PORT = 3001; +/** Match SAM / Docker to this machine so Apple Silicon does not mix arm64 images with an x86_64 template default. */ +function samLambdaArchitecture(): 'arm64' | 'x86_64' { + return arch === 'arm64' ? 'arm64' : 'x86_64'; +} + function resolvePackagesDir(): string { // When running via the e2e test runner, tests are copied to a temp directory // so we need the workspace root passed via env var @@ -49,6 +53,7 @@ export class LocalLambdaStack extends Stack { properties: { ContentUri: path.join(LAYER_DIR, layerZipFile), CompatibleRuntimes: ['nodejs18.x', 'nodejs20.x', 'nodejs22.x'], + CompatibleArchitectures: [samLambdaArchitecture()], }, }); @@ -122,12 +127,17 @@ export class LocalLambdaStack extends Stack { execFileSync('npm', ['install', '--install-links', '--prefix', lambdaPath], { stdio: 'inherit' }); } + if (!process.env.NODE_VERSION) { + throw new Error('[LocalLambdaStack] NODE_VERSION is not set'); + } + new CfnResource(this, functionName, { type: 'AWS::Serverless::Function', properties: { + Architectures: [samLambdaArchitecture()], CodeUri: path.join(functionsDir, lambdaDir), Handler: 'index.handler', - Runtime: `nodejs${process.env.NODE_VERSION ?? DEFAULT_NODE_VERSION}.x`, + Runtime: `nodejs${process.env.NODE_VERSION}.x`, Timeout: LAMBDA_FUNCTION_TIMEOUT, Layers: addLayer ? [{ Ref: this.sentryLayer.logicalId }] : undefined, Environment: { diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless/start-event-proxy.mjs index 196ae2471c69..4eff620fa17f 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'aws-serverless-lambda-sam', + proxyServerName: 'aws-serverless', }); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/lambda-fixtures.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/lambda-fixtures.ts index 23aab3a7d683..4df52b322d26 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/lambda-fixtures.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/lambda-fixtures.ts @@ -1,14 +1,19 @@ import { test as base, expect } from '@playwright/test'; import { App } from 'aws-cdk-lib'; -import * as tmp from 'tmp'; import { LocalLambdaStack, SAM_PORT, getHostIp } from '../src/stack'; import { writeFileSync } from 'node:fs'; -import { spawn, execSync } from 'node:child_process'; +import { execSync, spawn } from 'node:child_process'; import { LambdaClient } from '@aws-sdk/client-lambda'; const DOCKER_NETWORK_NAME = 'lambda-test-network'; const SAM_TEMPLATE_FILE = 'sam.template.yml'; +/** Major Node for SAM `--invoke-image`; default matches root `package.json` `volta.node` and `pull-sam-image.sh`. */ +const DEFAULT_NODE_VERSION_MAJOR = '20'; + +const SAM_INSTALL_ERROR = + 'You need to install sam, e.g. run `brew install aws-sam-cli`. Ensure `sam` is on your PATH when running tests.'; + export { expect }; export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClient: LambdaClient }>({ @@ -16,6 +21,11 @@ export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClien async ({}, use) => { console.log('[testEnvironment fixture] Setting up AWS Lambda test infrastructure'); + const nodeVersionMajor = process.env.NODE_VERSION?.trim() || DEFAULT_NODE_VERSION_MAJOR; + process.env.NODE_VERSION = nodeVersionMajor; + + assertSamOnPath(); + execSync('docker network prune -f'); createDockerNetwork(); @@ -26,11 +36,6 @@ export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClien const template = app.synth().getStackByName('LocalLambdaStack').template; writeFileSync(SAM_TEMPLATE_FILE, JSON.stringify(template, null, 2)); - const debugLog = tmp.fileSync({ prefix: 'sentry_aws_lambda_tests_sam_debug', postfix: '.log' }); - if (!process.env.CI) { - console.log(`[test_environment fixture] Writing SAM debug log to: ${debugLog.name}`); - } - const args = [ 'local', 'start-lambda', @@ -42,16 +47,15 @@ export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClien '--docker-network', DOCKER_NETWORK_NAME, '--skip-pull-image', + '--invoke-image', + `public.ecr.aws/lambda/nodejs:${nodeVersionMajor}`, ]; - if (process.env.NODE_VERSION) { - args.push('--invoke-image', `public.ecr.aws/lambda/nodejs:${process.env.NODE_VERSION}`); - } - console.log(`[testEnvironment fixture] Running SAM with args: ${args.join(' ')}`); const samProcess = spawn('sam', args, { - stdio: process.env.CI ? 'inherit' : ['ignore', debugLog.fd, debugLog.fd], + stdio: process.env.DEBUG ? 'inherit' : 'ignore', + env: envForSamChild(), }); try { @@ -91,6 +95,23 @@ export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClien }, }); +/** Avoid forcing linux/amd64 on Apple Silicon when `DOCKER_DEFAULT_PLATFORM` is set globally. */ +function envForSamChild(): NodeJS.ProcessEnv { + const env = { ...process.env }; + if (process.arch === 'arm64') { + delete env.DOCKER_DEFAULT_PLATFORM; + } + return env; +} + +function assertSamOnPath(): void { + try { + execSync('sam --version', { encoding: 'utf-8', stdio: 'pipe' }); + } catch { + throw new Error(SAM_INSTALL_ERROR); + } +} + function createDockerNetwork() { try { execSync(`docker network create --driver bridge ${DOCKER_NETWORK_NAME}`); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts index 966ddf032218..fb3d45c5c188 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts @@ -4,7 +4,7 @@ import { test, expect } from './lambda-fixtures'; test.describe('Lambda layer', () => { test('tracing in CJS works', async ({ lambdaClient }) => { - const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => { + const transactionEventPromise = waitForTransaction('aws-serverless', transactionEvent => { return transactionEvent?.transaction === 'LayerTracingCjs'; }); @@ -72,7 +72,7 @@ test.describe('Lambda layer', () => { }); test('tracing in ESM works', async ({ lambdaClient }) => { - const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => { + const transactionEventPromise = waitForTransaction('aws-serverless', transactionEvent => { return transactionEvent?.transaction === 'LayerTracingEsm'; }); @@ -140,7 +140,7 @@ test.describe('Lambda layer', () => { }); test('capturing errors works', async ({ lambdaClient }) => { - const errorEventPromise = waitForError('aws-serverless-lambda-sam', errorEvent => { + const errorEventPromise = waitForError('aws-serverless', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'test'; }); @@ -168,7 +168,7 @@ test.describe('Lambda layer', () => { }); test('capturing errors works in ESM', async ({ lambdaClient }) => { - const errorEventPromise = waitForError('aws-serverless-lambda-sam', errorEvent => { + const errorEventPromise = waitForError('aws-serverless', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'test esm'; }); @@ -196,7 +196,7 @@ test.describe('Lambda layer', () => { }); test('streaming handlers work', async ({ lambdaClient }) => { - const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => { + const transactionEventPromise = waitForTransaction('aws-serverless', transactionEvent => { return transactionEvent?.transaction === 'LayerStreaming'; }); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/npm.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/npm.test.ts index e5b6ee1b9f32..943d5a2ab0f3 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/npm.test.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/tests/npm.test.ts @@ -4,7 +4,7 @@ import { test, expect } from './lambda-fixtures'; test.describe('NPM package', () => { test('tracing in CJS works', async ({ lambdaClient }) => { - const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => { + const transactionEventPromise = waitForTransaction('aws-serverless', transactionEvent => { return transactionEvent?.transaction === 'NpmTracingCjs'; }); @@ -72,7 +72,7 @@ test.describe('NPM package', () => { }); test('tracing in ESM works', async ({ lambdaClient }) => { - const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => { + const transactionEventPromise = waitForTransaction('aws-serverless', transactionEvent => { return transactionEvent?.transaction === 'NpmTracingEsm'; });