Skip to content

Commit 5eb036f

Browse files
authored
feat(aws): Create unified lambda layer for ESM and CJS (#17012)
This introduces a new AWS lambda layer that supports both ESM and CJS. Instead of bundling the whole SDK, we install the local NPM package and then prune all not strictly necessary files from `node_modules` by using `@vercel/nft` to keep the layer size as small as possible. closes #16876 closes #16883 closes #16886 closes #16879
1 parent d059b06 commit 5eb036f

File tree

14 files changed

+248
-83
lines changed

14 files changed

+248
-83
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "aws-lambda-layer-esm",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"start": "node src/run.mjs",
7+
"test": "playwright test",
8+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
9+
"test:build": "pnpm install",
10+
"test:assert": "pnpm test"
11+
},
12+
"//": "Link from local Lambda layer build",
13+
"dependencies": {
14+
"@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless"
15+
},
16+
"devDependencies": {
17+
"@sentry-internal/test-utils": "link:../../../test-utils",
18+
"@playwright/test": "~1.53.2"
19+
},
20+
"volta": {
21+
"extends": "../../package.json"
22+
}
23+
}
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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as Sentry from '@sentry/aws-serverless';
2+
3+
import * as http from 'node:http';
4+
5+
async function handle() {
6+
await Sentry.startSpan({ name: 'manual-span', op: 'test' }, async () => {
7+
await new Promise(resolve => {
8+
http.get('http://example.com', res => {
9+
res.on('data', d => {
10+
process.stdout.write(d);
11+
});
12+
13+
res.on('end', () => {
14+
resolve();
15+
});
16+
});
17+
});
18+
});
19+
}
20+
21+
export { handle };
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { handle } from './lambda-function.mjs';
2+
3+
const event = {};
4+
const context = {
5+
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
6+
functionName: 'my-lambda',
7+
};
8+
await handle(event, context);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import child_process from 'node:child_process';
2+
3+
child_process.execSync('node ./src/run-lambda.mjs', {
4+
stdio: 'inherit',
5+
env: {
6+
...process.env,
7+
// On AWS, LAMBDA_TASK_ROOT is usually /var/task but for testing, we set it to the CWD to correctly apply our handler
8+
LAMBDA_TASK_ROOT: process.cwd(),
9+
_HANDLER: 'src/lambda-function.handle',
10+
11+
NODE_OPTIONS: '--import @sentry/aws-serverless/awslambda-auto',
12+
SENTRY_DSN: 'http://public@localhost:3031/1337',
13+
SENTRY_TRACES_SAMPLE_RATE: '1.0',
14+
SENTRY_DEBUG: 'true',
15+
},
16+
cwd: process.cwd(),
17+
});
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-lambda-layer-esm',
6+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as child_process from 'child_process';
2+
import { expect, test } from '@playwright/test';
3+
import { waitForTransaction } from '@sentry-internal/test-utils';
4+
5+
test('Lambda layer SDK bundle sends events', async ({ request }) => {
6+
const transactionEventPromise = waitForTransaction('aws-lambda-layer-esm', transactionEvent => {
7+
return transactionEvent?.transaction === 'my-lambda';
8+
});
9+
10+
// Waiting for 1s here because attaching the listener for events in `waitForTransaction` is not synchronous
11+
// Since in this test, we don't start a browser via playwright, we don't have the usual delays (page.goto, etc)
12+
// which are usually enough for us to never have noticed this race condition before.
13+
// This is a workaround but probably sufficient as long as we only experience it in this test.
14+
await new Promise<void>(resolve =>
15+
setTimeout(() => {
16+
resolve();
17+
}, 1000),
18+
);
19+
20+
child_process.execSync('pnpm start', {
21+
stdio: 'ignore',
22+
});
23+
24+
const transactionEvent = await transactionEventPromise;
25+
26+
// shows the SDK sent a transaction
27+
expect(transactionEvent.transaction).toEqual('my-lambda'); // name should be the function name
28+
expect(transactionEvent.contexts?.trace).toEqual({
29+
data: {
30+
'sentry.sample_rate': 1,
31+
'sentry.source': 'custom',
32+
'sentry.origin': 'auto.otel.aws-lambda',
33+
'sentry.op': 'function.aws.lambda',
34+
'cloud.account.id': '123453789012',
35+
'faas.id': 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda',
36+
'faas.coldstart': true,
37+
'otel.kind': 'SERVER',
38+
},
39+
op: 'function.aws.lambda',
40+
origin: 'auto.otel.aws-lambda',
41+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
42+
status: 'ok',
43+
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
44+
});
45+
46+
expect(transactionEvent.spans).toHaveLength(2);
47+
48+
// shows that the Otel Http instrumentation is working
49+
expect(transactionEvent.spans).toContainEqual(
50+
expect.objectContaining({
51+
data: expect.objectContaining({
52+
'sentry.op': 'http.client',
53+
'sentry.origin': 'auto.http.otel.http',
54+
url: 'http://example.com/',
55+
}),
56+
description: 'GET http://example.com/',
57+
op: 'http.client',
58+
}),
59+
);
60+
61+
// shows that the manual span creation is working
62+
expect(transactionEvent.spans).toContainEqual(
63+
expect.objectContaining({
64+
data: expect.objectContaining({
65+
'sentry.op': 'test',
66+
'sentry.origin': 'manual',
67+
}),
68+
description: 'manual-span',
69+
op: 'test',
70+
}),
71+
);
72+
});

dev-packages/rollup-utils/bundleHelpers.mjs

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -90,32 +90,6 @@ export function makeBaseBundleConfig(options) {
9090
plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin],
9191
};
9292

93-
// used by `@sentry/aws-serverless`, when creating the lambda layer
94-
const awsLambdaBundleConfig = {
95-
output: {
96-
format: 'cjs',
97-
},
98-
plugins: [
99-
jsonPlugin,
100-
commonJSPlugin,
101-
// Temporary fix for the lambda layer SDK bundle.
102-
// This is necessary to apply to our lambda layer bundle because calling `new ImportInTheMiddle()` will throw an
103-
// that `ImportInTheMiddle` is not a constructor. Instead we modify the code to call `new ImportInTheMiddle.default()`
104-
// TODO: Remove this plugin once the weird import-in-the-middle exports are fixed, released and we use the respective
105-
// version in our SDKs. See: https://github.com/getsentry/sentry-javascript/issues/12009#issuecomment-2126211967
106-
{
107-
name: 'aws-serverless-lambda-layer-fix',
108-
transform: code => {
109-
if (code.includes('ImportInTheMiddle')) {
110-
return code.replaceAll(/new\s+(ImportInTheMiddle.*)\(/gm, 'new $1.default(');
111-
}
112-
},
113-
},
114-
],
115-
// Don't bundle any of Node's core modules
116-
external: builtinModules,
117-
};
118-
11993
const workerBundleConfig = {
12094
output: {
12195
format: 'esm',
@@ -143,7 +117,6 @@ export function makeBaseBundleConfig(options) {
143117
const bundleTypeConfigMap = {
144118
standalone: standAloneBundleConfig,
145119
addon: addOnBundleConfig,
146-
'aws-lambda': awsLambdaBundleConfig,
147120
'node-worker': workerBundleConfig,
148121
};
149122

packages/aws-serverless/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"/build/loader-hook.mjs"
1616
],
1717
"main": "build/npm/cjs/index.js",
18+
"module": "build/npm/esm/index.js",
1819
"types": "build/npm/types/index.d.ts",
1920
"exports": {
2021
"./package.json": "./package.json",
@@ -73,10 +74,11 @@
7374
"@types/aws-lambda": "^8.10.62"
7475
},
7576
"devDependencies": {
76-
"@types/node": "^18.19.1"
77+
"@types/node": "^18.19.1",
78+
"@vercel/nft": "^0.29.4"
7779
},
7880
"scripts": {
79-
"build": "run-p build:transpile build:types build:bundle",
81+
"build": "run-p build:transpile build:types",
8082
"build:bundle": "yarn build:layer",
8183
"build:layer": "yarn ts-node scripts/buildLambdaLayer.ts",
8284
"build:dev": "run-p build:transpile build:types",

0 commit comments

Comments
 (0)