Skip to content

Commit 59a480a

Browse files
committed
test(aws): Run E2E test in AWS SAM
1 parent 373746f commit 59a480a

File tree

8 files changed

+299
-0
lines changed

8 files changed

+299
-0
lines changed

.github/workflows/build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,12 @@ jobs:
10311031
uses: actions/setup-node@v4
10321032
with:
10331033
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
1034+
- name: Set up AWS SAM
1035+
if: matrix.test-application == 'aws-lambda-sam'
1036+
uses: aws-actions/setup-sam@v2
1037+
with:
1038+
use-installer: true
1039+
token: ${{ secrets.GITHUB_TOKEN }}
10341040
- name: Restore caches
10351041
uses: ./.github/actions/restore-cache
10361042
with:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports.handler = async (event, context) => {
2+
return {
3+
statusCode: 200,
4+
body: JSON.stringify({
5+
event,
6+
context,
7+
}),
8+
};
9+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/aws-serverless';
2+
3+
export const handler = Sentry.wrapHandler(async (event, context) => {
4+
return {
5+
statusCode: 200,
6+
body: JSON.stringify({
7+
event,
8+
}),
9+
};
10+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "aws-lambda-sam",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "commonjs",
6+
"scripts": {
7+
"test": "playwright test",
8+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
9+
"test:build": "pnpm install && npx rimraf node_modules/@sentry/aws-serverless-layer/nodejs",
10+
"test:assert": "pnpm test"
11+
},
12+
"dependencies": {
13+
"@sentry/aws-serverless": "* || latest"
14+
},
15+
"//": "@sentry/aws-serverless-layer is not a package, but we use it to copy the layer zip file to the test app",
16+
"devDependencies": {
17+
"@aws-sdk/client-lambda": "^3.863.0",
18+
"@playwright/test": "~1.53.2",
19+
"@sentry-internal/test-utils": "link:../../../test-utils",
20+
"@sentry/aws-serverless-layer": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/",
21+
"aws-cdk-lib": "^2.210.0",
22+
"constructs": "^10.4.2",
23+
"glob": "^11.0.3"
24+
},
25+
"volta": {
26+
"extends": "../../package.json"
27+
},
28+
"sentryTest": {
29+
"optional": true
30+
}
31+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
export default getPlaywrightConfig();
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Stack, CfnResource, StackProps } from 'aws-cdk-lib';
2+
import { Construct } from 'constructs';
3+
import * as path from 'node:path';
4+
import * as fs from 'node:fs';
5+
import * as os from 'node:os';
6+
import * as dns from 'node:dns/promises';
7+
import { platform } from 'node:process';
8+
import { globSync } from 'glob';
9+
10+
const LAMBDA_FUNCTION_DIR = './lambda-functions';
11+
const LAMBDA_FUNCTION_TIMEOUT = 10;
12+
const LAYER_DIR = './node_modules/@sentry/aws-serverless-layer/';
13+
export const SAM_PORT = 3001;
14+
const NODE_RUNTIME = `nodejs${process.version.split('.').at(0)?.replace('v', '')}.x`;
15+
16+
export class LocalLambdaStack extends Stack {
17+
sentryLayer: CfnResource;
18+
19+
constructor(scope: Construct, id: string, props: StackProps, hostIp: string) {
20+
console.log('[LocalLambdaStack] Creating local SAM Lambda Stack');
21+
super(scope, id, props);
22+
23+
this.templateOptions.templateFormatVersion = '2010-09-09';
24+
this.templateOptions.transforms = ['AWS::Serverless-2016-10-31'];
25+
26+
console.log('[LocalLambdaStack] Add Sentry Lambda layer containing the Sentry SDK to the SAM stack');
27+
28+
const [layerZipFile] = globSync('sentry-node-serverless-*.zip', { cwd: LAYER_DIR });
29+
30+
if (!layerZipFile) {
31+
throw new Error(`[LocalLambdaStack] Could not find sentry-node-serverless zip file in ${LAYER_DIR}`);
32+
}
33+
34+
this.sentryLayer = new CfnResource(this, 'SentryNodeServerlessSDK', {
35+
type: 'AWS::Serverless::LayerVersion',
36+
properties: {
37+
ContentUri: path.join(LAYER_DIR, layerZipFile),
38+
CompatibleRuntimes: ['nodejs18.x', 'nodejs20.x', 'nodejs22.x'],
39+
},
40+
});
41+
42+
const dsn = `http://public@${hostIp}:3031/1337`;
43+
console.log(`[LocalLambdaStack] Using Sentry DSN: ${dsn}`);
44+
45+
console.log('[LocalLambdaStack] Add all Lambda function defined in ./lambda-functions/ to the SAM stack');
46+
47+
const lambdaDirs = fs
48+
.readdirSync(LAMBDA_FUNCTION_DIR)
49+
.filter(dir => fs.statSync(path.join(LAMBDA_FUNCTION_DIR, dir)).isDirectory());
50+
51+
for (const lambdaDir of lambdaDirs) {
52+
const isEsm = fs.existsSync(path.join(LAMBDA_FUNCTION_DIR, lambdaDir, 'index.mjs'));
53+
54+
new CfnResource(this, lambdaDir, {
55+
type: 'AWS::Serverless::Function',
56+
properties: {
57+
CodeUri: path.join(LAMBDA_FUNCTION_DIR, lambdaDir),
58+
Handler: 'index.handler',
59+
Runtime: NODE_RUNTIME,
60+
Timeout: LAMBDA_FUNCTION_TIMEOUT,
61+
Layers: [
62+
{
63+
Ref: this.sentryLayer.logicalId,
64+
},
65+
],
66+
Environment: {
67+
Variables: {
68+
SENTRY_DSN: dsn,
69+
SENTRY_TRACES_SAMPLE_RATE: 1.0,
70+
SENTRY_DEBUG: true,
71+
NODE_OPTIONS: `--${isEsm ? 'import' : 'require'}=@sentry/aws-serverless/awslambda-auto`,
72+
},
73+
},
74+
},
75+
});
76+
77+
console.log(`[LocalLambdaStack] Added Lambda function: ${lambdaDir}`);
78+
}
79+
}
80+
81+
static async waitForStack(timeout = 60000, port = SAM_PORT) {
82+
const startTime = Date.now();
83+
const maxWaitTime = timeout;
84+
85+
while (Date.now() - startTime < maxWaitTime) {
86+
try {
87+
const response = await fetch(`http://127.0.0.1:${port}/`);
88+
89+
if (response.ok || response.status === 404) {
90+
console.log(`[LocalLambdaStack] SAM stack is ready`);
91+
return;
92+
}
93+
} catch {
94+
await new Promise(resolve => setTimeout(resolve, 1000));
95+
}
96+
}
97+
98+
throw new Error(`[LocalLambdaStack] Failed to start SAM stack after ${timeout}ms`);
99+
}
100+
}
101+
102+
export async function getHostIp() {
103+
if (process.env.GITHUB_ACTIONS) {
104+
const host = await dns.lookup(os.hostname());
105+
return host.address;
106+
}
107+
108+
if (platform === 'darwin' || platform === 'win32') {
109+
return 'host.docker.internal';
110+
}
111+
112+
const host = await dns.lookup(os.hostname());
113+
return host.address;
114+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/test-utils';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'aws-serverless-lambda-sam',
6+
});
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { test as base } from '@playwright/test';
2+
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
import { App } from 'aws-cdk-lib';
4+
import { LocalLambdaStack, SAM_PORT, getHostIp } from '../stack.js';
5+
import { writeFileSync, openSync } from 'node:fs';
6+
import { tmpdir } from 'node:os';
7+
import * as path from 'node:path';
8+
import { spawn, execSync } from 'node:child_process';
9+
import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda';
10+
11+
const DOCKER_NETWORK_NAME = 'lambda-test-network';
12+
const SAM_TEMPLATE_FILE = 'sam.template.yml';
13+
14+
const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClient: LambdaClient }>({
15+
testEnvironment: [
16+
async ({}, use) => {
17+
console.log('[testEnvironment fixture] Setting up AWS Lambda test infrastructure');
18+
19+
execSync('docker network prune -f');
20+
execSync(`docker network create --driver bridge ${DOCKER_NETWORK_NAME}`);
21+
22+
const hostIp = await getHostIp();
23+
const app = new App();
24+
25+
const stack = new LocalLambdaStack(app, 'LocalLambdaStack', {}, hostIp);
26+
const template = app.synth().getStackByName('LocalLambdaStack').template;
27+
writeFileSync(SAM_TEMPLATE_FILE, JSON.stringify(template, null, 2));
28+
29+
const debugLogFile = path.join(tmpdir(), 'sentry_aws_lambda_tests_sam_debug.log');
30+
const debugLogFd = openSync(debugLogFile, 'w');
31+
console.log(`[test_environment fixture] Writing SAM debug log to: ${debugLogFile}`);
32+
33+
const process = spawn(
34+
'sam',
35+
[
36+
'local',
37+
'start-lambda',
38+
'--debug',
39+
'--template',
40+
SAM_TEMPLATE_FILE,
41+
'--warm-containers',
42+
'EAGER',
43+
'--docker-network',
44+
DOCKER_NETWORK_NAME,
45+
],
46+
{
47+
stdio: ['ignore', debugLogFd, debugLogFd],
48+
},
49+
);
50+
51+
try {
52+
await LocalLambdaStack.waitForStack();
53+
54+
await use(stack);
55+
} finally {
56+
console.log('[testEnvironment fixture] Tearing down AWS Lambda test infrastructure');
57+
58+
process.kill('SIGTERM');
59+
await new Promise(resolve => {
60+
process.once('exit', resolve);
61+
setTimeout(() => {
62+
if (!process.killed) {
63+
process.kill('SIGKILL');
64+
}
65+
resolve(void 0);
66+
}, 5000);
67+
});
68+
}
69+
},
70+
{ scope: 'worker', auto: true },
71+
],
72+
lambdaClient: async ({}, use) => {
73+
const lambdaClient = new LambdaClient({
74+
endpoint: `http://127.0.0.1:${SAM_PORT}`,
75+
region: 'us-east-1',
76+
credentials: {
77+
accessKeyId: 'dummy',
78+
secretAccessKey: 'dummy',
79+
},
80+
});
81+
82+
await use(lambdaClient);
83+
},
84+
});
85+
86+
test('basic no exception', async ({ lambdaClient }) => {
87+
const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
88+
return transactionEvent?.transaction === 'basic';
89+
});
90+
91+
await lambdaClient.send(
92+
new InvokeCommand({
93+
FunctionName: 'basic',
94+
Payload: JSON.stringify({}),
95+
}),
96+
);
97+
98+
const transactionEvent = await transactionEventPromise;
99+
100+
console.log('Transaction event received');
101+
102+
console.log(transactionEvent);
103+
});
104+
105+
test('esm', async ({ lambdaClient }) => {
106+
const transactionEventPromise = waitForTransaction('aws-serverless-lambda-sam', transactionEvent => {
107+
return transactionEvent?.transaction === 'esm';
108+
});
109+
110+
await lambdaClient.send(
111+
new InvokeCommand({
112+
FunctionName: 'esm',
113+
Payload: JSON.stringify({}),
114+
}),
115+
);
116+
117+
const transactionEvent = await transactionEventPromise;
118+
119+
console.log(transactionEvent);
120+
});

0 commit comments

Comments
 (0)