diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c92fb6b05838..add193a29d3b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -935,7 +935,7 @@ jobs: matrix.test-application) uses: oven-sh/setup-bun@v2 - name: Set up AWS SAM - if: matrix.test-application == 'aws-serverless' + if: matrix.test-application == 'aws-serverless' || matrix.test-application == 'aws-serverless-layer' uses: aws-actions/setup-sam@v2 with: use-installer: true diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/package.json b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/package.json new file mode 100644 index 000000000000..16acf393e5d4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/package.json @@ -0,0 +1,47 @@ +{ + "name": "aws-serverless-layer", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "scripts": { + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "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", + "devDependencies": { + "@aws-sdk/client-lambda": "^3.863.0", + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/", + "aws-cdk-lib": "^2.210.0", + "constructs": "^10.4.2", + "glob": "^11.0.3", + "rimraf": "^5.0.10" + }, + "volta": { + "extends": "../../package.json" + }, + "//": "We need to override this here again to ensure this is not overwritten by the test runner", + "pnpm": { + "overrides": { + "@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/" + } + }, + "sentryTest": { + "variants": [ + { + "build-command": "NODE_VERSION=22 pnpm test:build", + "assert-command": "NODE_VERSION=22 pnpm test:assert", + "label": "aws-serverless-layer (Node 22)" + }, + { + "build-command": "NODE_VERSION=18 pnpm test:build", + "assert-command": "NODE_VERSION=18 pnpm test:assert", + "label": "aws-serverless-layer (Node 18)" + } + ] + } +} diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/playwright.config.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/playwright.config.ts new file mode 100644 index 000000000000..e47333c66e76 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/playwright.config.ts @@ -0,0 +1,5 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +export default getPlaywrightConfig(undefined, { + timeout: 60 * 1000 * 3, // 3 minutes +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/pull-sam-image.sh b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/pull-sam-image.sh new file mode 100755 index 000000000000..c9659547a4ea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/pull-sam-image.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# 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 + +NODE_VERSION="${NODE_VERSION:-20}" + +echo "Pulling Lambda Node $NODE_VERSION docker image..." +docker pull "public.ecr.aws/lambda/nodejs:${NODE_VERSION}" + +echo "Successfully pulled Lambda Node $NODE_VERSION docker image" diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/samconfig.toml b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/samconfig.toml new file mode 100644 index 000000000000..26f5a51c7a77 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/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-layer-local" +region = "us-east-1" +confirm_changeset = false +capabilities = "CAPABILITY_IAM" diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/Error/index.js b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/Error/index.js similarity index 100% rename from dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/Error/index.js rename to dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/Error/index.js diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/ErrorEsm/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/ErrorEsm/index.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/ErrorEsm/index.mjs rename to dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/ErrorEsm/index.mjs diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/Streaming/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/Streaming/index.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/Streaming/index.mjs rename to dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/Streaming/index.mjs diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingCjs/index.js b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/TracingCjs/index.js similarity index 100% rename from dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingCjs/index.js rename to dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/TracingCjs/index.js diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/TracingEsm/index.mjs similarity index 100% rename from dev-packages/e2e-tests/test-applications/aws-serverless/src/lambda-functions-layer/TracingEsm/index.mjs rename to dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/lambda-functions-layer/TracingEsm/index.mjs diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/stack.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/stack.ts new file mode 100644 index 000000000000..8475ee0a328a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/src/stack.ts @@ -0,0 +1,124 @@ +import { Stack, CfnResource, StackProps } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +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 { arch, platform } from 'node:process'; +import { globSync } from 'glob'; + +const LAMBDA_FUNCTIONS_DIR = './src/lambda-functions-layer'; +const LAMBDA_FUNCTION_TIMEOUT = 10; +const LAYER_DIR = './node_modules/@sentry/aws-serverless/'; +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'; +} + +export class LocalLambdaStack extends Stack { + sentryLayer: CfnResource; + + constructor(scope: Construct, id: string, props: StackProps, hostIp: string) { + console.log('[LocalLambdaStack] Creating local SAM Lambda Stack'); + super(scope, id, props); + + this.templateOptions.templateFormatVersion = '2010-09-09'; + this.templateOptions.transforms = ['AWS::Serverless-2016-10-31']; + + console.log('[LocalLambdaStack] Add Sentry Lambda layer containing the Sentry SDK to the SAM stack'); + + const [layerZipFile] = globSync('sentry-node-serverless-*.zip', { cwd: LAYER_DIR }); + + if (!layerZipFile) { + throw new Error(`[LocalLambdaStack] Could not find sentry-node-serverless zip file in ${LAYER_DIR}`); + } + + this.sentryLayer = new CfnResource(this, 'SentryNodeServerlessSDK', { + type: 'AWS::Serverless::LayerVersion', + properties: { + ContentUri: path.join(LAYER_DIR, layerZipFile), + CompatibleRuntimes: ['nodejs18.x', 'nodejs20.x', 'nodejs22.x'], + CompatibleArchitectures: [samLambdaArchitecture()], + }, + }); + + const dsn = `http://public@${hostIp}:3031/1337`; + console.log(`[LocalLambdaStack] Using Sentry DSN: ${dsn}`); + + this.addLambdaFunctions(dsn); + } + + private addLambdaFunctions(dsn: string) { + console.log(`[LocalLambdaStack] Add all Lambda functions defined in ${LAMBDA_FUNCTIONS_DIR} to the SAM stack`); + + const lambdaDirs = fs + .readdirSync(LAMBDA_FUNCTIONS_DIR) + .filter(dir => fs.statSync(path.join(LAMBDA_FUNCTIONS_DIR, dir)).isDirectory()); + + for (const lambdaDir of lambdaDirs) { + const functionName = `Layer${lambdaDir}`; + + 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(LAMBDA_FUNCTIONS_DIR, lambdaDir), + Handler: 'index.handler', + Runtime: `nodejs${process.env.NODE_VERSION}.x`, + Timeout: LAMBDA_FUNCTION_TIMEOUT, + Layers: [{ Ref: this.sentryLayer.logicalId }], + Environment: { + Variables: { + SENTRY_DSN: dsn, + SENTRY_TRACES_SAMPLE_RATE: 1.0, + SENTRY_DEBUG: true, + NODE_OPTIONS: `--import=@sentry/aws-serverless/awslambda-auto`, + }, + }, + }, + }); + + console.log(`[LocalLambdaStack] Added Lambda function: ${functionName}`); + } + } + + static async waitForStack(timeout = 60000, port = SAM_PORT) { + const startTime = Date.now(); + const maxWaitTime = timeout; + + while (Date.now() - startTime < maxWaitTime) { + try { + const response = await fetch(`http://127.0.0.1:${port}/`); + + if (response.ok || response.status === 404) { + console.log(`[LocalLambdaStack] SAM stack is ready`); + return; + } + } catch { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + throw new Error(`[LocalLambdaStack] Failed to start SAM stack after ${timeout}ms`); + } +} + +export async function getHostIp() { + if (process.env.GITHUB_ACTIONS) { + const host = await dns.lookup(os.hostname()); + return host.address; + } + + if (platform === 'darwin' || platform === 'win32') { + return 'host.docker.internal'; + } + + const host = await dns.lookup(os.hostname()); + return host.address; +} diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/start-event-proxy.mjs new file mode 100644 index 000000000000..dfc97cb7f3f3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'aws-serverless-layer', +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/lambda-fixtures.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/lambda-fixtures.ts new file mode 100644 index 000000000000..4df52b322d26 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/lambda-fixtures.ts @@ -0,0 +1,137 @@ +import { test as base, expect } from '@playwright/test'; +import { App } from 'aws-cdk-lib'; +import { LocalLambdaStack, SAM_PORT, getHostIp } from '../src/stack'; +import { writeFileSync } from 'node:fs'; +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 }>({ + testEnvironment: [ + 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(); + + const hostIp = await getHostIp(); + const app = new App(); + + const stack = new LocalLambdaStack(app, 'LocalLambdaStack', {}, hostIp); + const template = app.synth().getStackByName('LocalLambdaStack').template; + writeFileSync(SAM_TEMPLATE_FILE, JSON.stringify(template, null, 2)); + + const args = [ + 'local', + 'start-lambda', + '--debug', + '--template', + SAM_TEMPLATE_FILE, + '--warm-containers', + 'EAGER', + '--docker-network', + DOCKER_NETWORK_NAME, + '--skip-pull-image', + '--invoke-image', + `public.ecr.aws/lambda/nodejs:${nodeVersionMajor}`, + ]; + + console.log(`[testEnvironment fixture] Running SAM with args: ${args.join(' ')}`); + + const samProcess = spawn('sam', args, { + stdio: process.env.DEBUG ? 'inherit' : 'ignore', + env: envForSamChild(), + }); + + try { + await LocalLambdaStack.waitForStack(); + + await use(stack); + } finally { + console.log('[testEnvironment fixture] Tearing down AWS Lambda test infrastructure'); + + samProcess.kill('SIGTERM'); + await new Promise(resolve => { + samProcess.once('exit', resolve); + setTimeout(() => { + if (!samProcess.killed) { + samProcess.kill('SIGKILL'); + } + resolve(void 0); + }, 5000); + }); + + removeDockerNetwork(); + } + }, + { scope: 'worker', auto: true }, + ], + lambdaClient: async ({}, use) => { + const lambdaClient = new LambdaClient({ + endpoint: `http://127.0.0.1:${SAM_PORT}`, + region: 'us-east-1', + credentials: { + accessKeyId: 'dummy', + secretAccessKey: 'dummy', + }, + }); + + await use(lambdaClient); + }, +}); + +/** 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}`); + } catch (error) { + const stderr = (error as { stderr?: Buffer }).stderr?.toString() ?? ''; + if (stderr.includes('already exists')) { + console.log(`[testEnvironment fixture] Reusing existing docker network ${DOCKER_NETWORK_NAME}`); + return; + } + throw error; + } +} + +function removeDockerNetwork() { + try { + execSync(`docker network rm ${DOCKER_NETWORK_NAME}`); + } catch (error) { + const stderr = (error as { stderr?: Buffer }).stderr?.toString() ?? ''; + if (!stderr.includes('No such network')) { + console.warn(`[testEnvironment fixture] Failed to remove docker network ${DOCKER_NETWORK_NAME}: ${stderr}`); + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/layer.test.ts similarity index 96% rename from dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts rename to dev-packages/e2e-tests/test-applications/aws-serverless-layer/tests/layer.test.ts index fb3d45c5c188..c32dbfea7435 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/tests/layer.test.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless-layer/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', transactionEvent => { + const transactionEventPromise = waitForTransaction('aws-serverless-layer', 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', transactionEvent => { + const transactionEventPromise = waitForTransaction('aws-serverless-layer', transactionEvent => { return transactionEvent?.transaction === 'LayerTracingEsm'; }); @@ -140,7 +140,7 @@ test.describe('Lambda layer', () => { }); test('capturing errors works', async ({ lambdaClient }) => { - const errorEventPromise = waitForError('aws-serverless', errorEvent => { + const errorEventPromise = waitForError('aws-serverless-layer', 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', errorEvent => { + const errorEventPromise = waitForError('aws-serverless-layer', 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', transactionEvent => { + const transactionEventPromise = waitForTransaction('aws-serverless-layer', transactionEvent => { return transactionEvent?.transaction === 'LayerStreaming'; }); 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 84076d8393c2..f74e7a670c50 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/package.json +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/package.json @@ -7,41 +7,19 @@ "test": "playwright test", "clean": "npx rimraf node_modules pnpm-lock.yaml", "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:build": "pnpm test:pull-sam-image && pnpm install", "test:assert": "pnpm test" }, - "//": "We just need the @sentry/aws-serverless layer zip file, not the NPM package", "devDependencies": { "@aws-sdk/client-lambda": "^3.863.0", "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", - "@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/", + "@sentry/aws-serverless": "file:../../packed/sentry-aws-serverless-packed.tgz", "aws-cdk-lib": "^2.210.0", "constructs": "^10.4.2", - "glob": "^11.0.3", "rimraf": "^5.0.10" }, "volta": { "extends": "../../package.json" - }, - "//": "We need to override this here again to ensure this is not overwritten by the test runner", - "pnpm": { - "overrides": { - "@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/" - } - }, - "sentryTest": { - "variants": [ - { - "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 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/src/stack.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts index 792bd4308c51..07bf6f7e3a49 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 @@ -5,13 +5,10 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as dns from 'node:dns/promises'; import { arch, platform } from 'node:process'; -import { globSync } from 'glob'; import { execFileSync } from 'node:child_process'; -const LAMBDA_FUNCTIONS_WITH_LAYER_DIR = './src/lambda-functions-layer'; -const LAMBDA_FUNCTIONS_WITH_NPM_DIR = './src/lambda-functions-npm'; +const LAMBDA_FUNCTIONS_DIR = './src/lambda-functions-npm'; const LAMBDA_FUNCTION_TIMEOUT = 10; -const LAYER_DIR = './node_modules/@sentry/aws-serverless/'; 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. */ @@ -20,19 +17,14 @@ function samLambdaArchitecture(): '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 const workspaceRoot = process.env.SENTRY_E2E_WORKSPACE_ROOT; if (workspaceRoot) { return path.join(workspaceRoot, 'packages'); } - // Fallback for local development when running from the original location return path.resolve(__dirname, '../../../../../packages'); } export class LocalLambdaStack extends Stack { - sentryLayer: CfnResource; - constructor(scope: Construct, id: string, props: StackProps, hostIp: string) { console.log('[LocalLambdaStack] Creating local SAM Lambda Stack'); super(scope, id, props); @@ -40,92 +32,57 @@ export class LocalLambdaStack extends Stack { this.templateOptions.templateFormatVersion = '2010-09-09'; this.templateOptions.transforms = ['AWS::Serverless-2016-10-31']; - console.log('[LocalLambdaStack] Add Sentry Lambda layer containing the Sentry SDK to the SAM stack'); - - const [layerZipFile] = globSync('sentry-node-serverless-*.zip', { cwd: LAYER_DIR }); - - if (!layerZipFile) { - throw new Error(`[LocalLambdaStack] Could not find sentry-node-serverless zip file in ${LAYER_DIR}`); - } - - this.sentryLayer = new CfnResource(this, 'SentryNodeServerlessSDK', { - type: 'AWS::Serverless::LayerVersion', - properties: { - ContentUri: path.join(LAYER_DIR, layerZipFile), - CompatibleRuntimes: ['nodejs18.x', 'nodejs20.x', 'nodejs22.x'], - CompatibleArchitectures: [samLambdaArchitecture()], - }, - }); - const dsn = `http://public@${hostIp}:3031/1337`; console.log(`[LocalLambdaStack] Using Sentry DSN: ${dsn}`); - this.addLambdaFunctions({ functionsDir: LAMBDA_FUNCTIONS_WITH_LAYER_DIR, dsn, addLayer: true }); - this.addLambdaFunctions({ functionsDir: LAMBDA_FUNCTIONS_WITH_NPM_DIR, dsn, addLayer: false }); + this.addLambdaFunctions(dsn); } - private addLambdaFunctions({ - functionsDir, - dsn, - addLayer, - }: { - functionsDir: string; - dsn: string; - addLayer: boolean; - }) { - console.log(`[LocalLambdaStack] Add all Lambda functions defined in ${functionsDir} to the SAM stack`); + private addLambdaFunctions(dsn: string) { + console.log(`[LocalLambdaStack] Add all Lambda functions defined in ${LAMBDA_FUNCTIONS_DIR} to the SAM stack`); const lambdaDirs = fs - .readdirSync(functionsDir) - .filter(dir => fs.statSync(path.join(functionsDir, dir)).isDirectory()); + .readdirSync(LAMBDA_FUNCTIONS_DIR) + .filter(dir => fs.statSync(path.join(LAMBDA_FUNCTIONS_DIR, dir)).isDirectory()); for (const lambdaDir of lambdaDirs) { - const functionName = `${addLayer ? 'Layer' : 'Npm'}${lambdaDir}`; - - if (!addLayer) { - const lambdaPath = path.resolve(functionsDir, lambdaDir); - const packageLockPath = path.join(lambdaPath, 'package-lock.json'); - const nodeModulesPath = path.join(lambdaPath, 'node_modules'); - - // Point the dependency at the locally built packages so tests use the current workspace bits - // We need to link all @sentry/* packages that are dependencies of aws-serverless - // because otherwise npm will try to install them from the registry, where the current version is not yet published - const packagesToLink = ['aws-serverless', 'node', 'core', 'node-core', 'opentelemetry']; - const dependencies: Record = {}; - - const packagesDir = resolvePackagesDir(); - for (const pkgName of packagesToLink) { - const pkgDir = path.join(packagesDir, pkgName); - if (!fs.existsSync(pkgDir)) { - throw new Error( - `[LocalLambdaStack] Workspace package ${pkgName} not found at ${pkgDir}. Did you run the build?`, - ); - } - const relativePath = path.relative(lambdaPath, pkgDir); - dependencies[`@sentry/${pkgName}`] = `file:${relativePath.replace(/\\/g, '/')}`; + const functionName = `Npm${lambdaDir}`; + + const lambdaPath = path.resolve(LAMBDA_FUNCTIONS_DIR, lambdaDir); + const packageLockPath = path.join(lambdaPath, 'package-lock.json'); + const nodeModulesPath = path.join(lambdaPath, 'node_modules'); + + const packagesToLink = ['aws-serverless', 'node', 'core', 'node-core', 'opentelemetry']; + const dependencies: Record = {}; + + const packagesDir = resolvePackagesDir(); + for (const pkgName of packagesToLink) { + const pkgDir = path.join(packagesDir, pkgName); + if (!fs.existsSync(pkgDir)) { + throw new Error( + `[LocalLambdaStack] Workspace package ${pkgName} not found at ${pkgDir}. Did you run the build?`, + ); } + const relativePath = path.relative(lambdaPath, pkgDir); + dependencies[`@sentry/${pkgName}`] = `file:${relativePath.replace(/\\/g, '/')}`; + } - console.log(`[LocalLambdaStack] Install dependencies for ${functionName}`); + console.log(`[LocalLambdaStack] Install dependencies for ${functionName}`); - if (fs.existsSync(packageLockPath)) { - // Prevent stale lock files from pinning the published package version - fs.rmSync(packageLockPath); - } + if (fs.existsSync(packageLockPath)) { + fs.rmSync(packageLockPath); + } - if (fs.existsSync(nodeModulesPath)) { - // Ensure we reinstall from the workspace instead of reusing cached dependencies - fs.rmSync(nodeModulesPath, { recursive: true, force: true }); - } + if (fs.existsSync(nodeModulesPath)) { + fs.rmSync(nodeModulesPath, { recursive: true, force: true }); + } - const packageJson = { - dependencies, - }; + const packageJson = { + dependencies, + }; - fs.writeFileSync(path.join(lambdaPath, 'package.json'), JSON.stringify(packageJson, null, 2)); - // Use --install-links to copy files instead of creating symlinks for file: dependencies. - // Symlinks don't work inside the Docker container because the target paths don't exist there. - execFileSync('npm', ['install', '--install-links', '--prefix', lambdaPath], { stdio: 'inherit' }); - } + fs.writeFileSync(path.join(lambdaPath, 'package.json'), JSON.stringify(packageJson, null, 2)); + execFileSync('npm', ['install', '--install-links', '--prefix', lambdaPath], { stdio: 'inherit' }); if (!process.env.NODE_VERSION) { throw new Error('[LocalLambdaStack] NODE_VERSION is not set'); @@ -135,11 +92,10 @@ export class LocalLambdaStack extends Stack { type: 'AWS::Serverless::Function', properties: { Architectures: [samLambdaArchitecture()], - CodeUri: path.join(functionsDir, lambdaDir), + CodeUri: path.join(LAMBDA_FUNCTIONS_DIR, lambdaDir), Handler: 'index.handler', Runtime: `nodejs${process.env.NODE_VERSION}.x`, Timeout: LAMBDA_FUNCTION_TIMEOUT, - Layers: addLayer ? [{ Ref: this.sentryLayer.logicalId }] : undefined, Environment: { Variables: { SENTRY_DSN: dsn,