Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(aws): Create unified lambda layer for ESM and CJS#17012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
5e835e3c8d2771a1c28f6dd4971f04a8f624ad4e49File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| @sentry:registry=http://127.0.0.1:4873 | ||
| @sentry-internal:registry=http://127.0.0.1:4873 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "name": "aws-lambda-layer-esm", | ||
| "version": "1.0.0", | ||
| "private": true, | ||
| "scripts": { | ||
| "start": "node src/run.mjs", | ||
| "test": "playwright test", | ||
| "clean": "npx rimraf node_modules pnpm-lock.yaml", | ||
| "test:build": "pnpm install", | ||
| "test:assert": "pnpm test" | ||
| }, | ||
| "//": "Link from local Lambda layer build", | ||
| "dependencies": { | ||
| "@sentry/aws-serverless": "link:../../../../packages/aws-serverless/build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless" | ||
| }, | ||
| "devDependencies": { | ||
| "@sentry-internal/test-utils": "link:../../../test-utils", | ||
| "@playwright/test": "~1.53.2" | ||
| }, | ||
| "volta": { | ||
| "extends": "../../package.json" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { getPlaywrightConfig } from '@sentry-internal/test-utils'; | ||
| export default getPlaywrightConfig(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import * as Sentry from '@sentry/aws-serverless'; | ||
| import * as http from 'node:http'; | ||
| async function handle() { | ||
| await Sentry.startSpan({ name: 'manual-span', op: 'test' }, async () => { | ||
| await new Promise(resolve => { | ||
| http.get('http://example.com', res => { | ||
| res.on('data', d => { | ||
| process.stdout.write(d); | ||
| }); | ||
| res.on('end', () => { | ||
| resolve(); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
| export { handle }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { handle } from './lambda-function.mjs'; | ||
| const event = {}; | ||
| const context = { | ||
| invokedFunctionArn: 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda', | ||
| functionName: 'my-lambda', | ||
| }; | ||
| await handle(event, context); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import child_process from 'node:child_process'; | ||
| child_process.execSync('node ./src/run-lambda.mjs', { | ||
| stdio: 'inherit', | ||
| env: { | ||
| ...process.env, | ||
| // On AWS, LAMBDA_TASK_ROOT is usually /var/task but for testing, we set it to the CWD to correctly apply our handler | ||
| LAMBDA_TASK_ROOT: process.cwd(), | ||
| _HANDLER: 'src/lambda-function.handle', | ||
| NODE_OPTIONS: '--import @sentry/aws-serverless/awslambda-auto', | ||
| SENTRY_DSN: 'http://public@localhost:3031/1337', | ||
| SENTRY_TRACES_SAMPLE_RATE: '1.0', | ||
| SENTRY_DEBUG: 'true', | ||
| }, | ||
| cwd: process.cwd(), | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { startEventProxyServer } from '@sentry-internal/test-utils'; | ||
| startEventProxyServer({ | ||
| port: 3031, | ||
| proxyServerName: 'aws-lambda-layer-esm', | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import * as child_process from 'child_process'; | ||
| import { expect, test } from '@playwright/test'; | ||
| import { waitForTransaction } from '@sentry-internal/test-utils'; | ||
| test('Lambda layer SDK bundle sends events', async ({ request }) => { | ||
| const transactionEventPromise = waitForTransaction('aws-lambda-layer-esm', transactionEvent => { | ||
| return transactionEvent?.transaction === 'my-lambda'; | ||
| }); | ||
| // Waiting for 1s here because attaching the listener for events in `waitForTransaction` is not synchronous | ||
| // Since in this test, we don't start a browser via playwright, we don't have the usual delays (page.goto, etc) | ||
| // which are usually enough for us to never have noticed this race condition before. | ||
| // This is a workaround but probably sufficient as long as we only experience it in this test. | ||
| await new Promise<void>(resolve => | ||
| setTimeout(() => { | ||
| resolve(); | ||
| }, 1000), | ||
| ); | ||
| child_process.execSync('pnpm start', { | ||
| stdio: 'ignore', | ||
| }); | ||
| const transactionEvent = await transactionEventPromise; | ||
| // shows the SDK sent a transaction | ||
| expect(transactionEvent.transaction).toEqual('my-lambda'); // name should be the function name | ||
| expect(transactionEvent.contexts?.trace).toEqual({ | ||
| data: { | ||
| 'sentry.sample_rate': 1, | ||
| 'sentry.source': 'custom', | ||
| 'sentry.origin': 'auto.otel.aws-lambda', | ||
| 'sentry.op': 'function.aws.lambda', | ||
| 'cloud.account.id': '123453789012', | ||
| 'faas.id': 'arn:aws:lambda:us-east-1:123453789012:function:my-lambda', | ||
| 'faas.coldstart': true, | ||
| 'otel.kind': 'SERVER', | ||
| }, | ||
| op: 'function.aws.lambda', | ||
| origin: 'auto.otel.aws-lambda', | ||
| span_id: expect.stringMatching(/[a-f0-9]{16}/), | ||
| status: 'ok', | ||
| trace_id: expect.stringMatching(/[a-f0-9]{32}/), | ||
| }); | ||
| expect(transactionEvent.spans).toHaveLength(2); | ||
| // shows that the Otel Http instrumentation is working | ||
| expect(transactionEvent.spans).toContainEqual( | ||
| expect.objectContaining({ | ||
| data: expect.objectContaining({ | ||
| 'sentry.op': 'http.client', | ||
| 'sentry.origin': 'auto.http.otel.http', | ||
| url: 'http://example.com/', | ||
| }), | ||
| description: 'GET http://example.com/', | ||
| op: 'http.client', | ||
| }), | ||
| ); | ||
| // shows that the manual span creation is working | ||
| expect(transactionEvent.spans).toContainEqual( | ||
| expect.objectContaining({ | ||
| data: expect.objectContaining({ | ||
| 'sentry.op': 'test', | ||
| 'sentry.origin': 'manual', | ||
| }), | ||
| description: 'manual-span', | ||
| op: 'test', | ||
| }), | ||
| ); | ||
| }); |
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| /* eslint-disable no-console */ | ||
| import { nodeFileTrace } from '@vercel/nft'; | ||
| import * as childProcess from 'child_process'; | ||
| import * as fs from 'fs'; | ||
| import * as path from 'path'; | ||
| import { version } from '../package.json'; | ||
| /** | ||
| @@ -11,21 +13,20 @@ function run(cmd: string, options?: childProcess.ExecSyncOptions): string { | ||
| return String(childProcess.execSync(cmd, { stdio: 'inherit', ...options })); | ||
| } | ||
| /** | ||
| * Build the AWS lambda layer by first installing the local package into `build/aws/dist-serverless/nodejs`. | ||
| * Then, prune the node_modules directory to remove unused files by first getting all necessary files with | ||
| * `@vercel/nft` and then deleting all other files inside `node_modules`. | ||
| * Finally, create a zip file of the layer. | ||
| */ | ||
| async function buildLambdaLayer(): Promise<void> { | ||
| // Create the main SDK bundle | ||
| run('yarn rollup --config rollup.aws.config.mjs'); | ||
| // We build a minified bundle, but it's standing in for the regular `index.js` file listed in `package.json`'s `main` | ||
| // property, so we have to rename it so it's findable. | ||
| fs.renameSync( | ||
| 'build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/index.debug.min.js', | ||
| 'build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/index.js', | ||
| ); | ||
| console.log('Building Lambda layer.'); | ||
| console.log('Installing local @sentry/aws-serverless into build/aws/dist-serverless/nodejs.'); | ||
msonnb marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| run('npm install . --prefix ./build/aws/dist-serverless/nodejs --install-links --silent'); | ||
msonnb marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // We're creating a bundle for the SDK, but still using it in a Node context, so we need to copy in `package.json`, | ||
| // purely for its `main` property. | ||
| console.log('Copying `package.json` into lambda layer.'); | ||
| fs.copyFileSync('package.json', 'build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/package.json'); | ||
| await pruneNodeModules(); | ||
| fs.rmSync('./build/aws/dist-serverless/nodejs/package.json', { force: true }); | ||
| fs.rmSync('./build/aws/dist-serverless/nodejs/package-lock.json', { force: true }); | ||
| // The layer also includes `awslambda-auto.js`, a helper file which calls `Sentry.init()` and wraps the lambda | ||
| // handler. It gets run when Node is launched inside the lambda, using the environment variable | ||
| @@ -61,3 +62,79 @@ function fsForceMkdirSync(path: string): void { | ||
| fs.rmSync(path, { recursive: true, force: true }); | ||
| fs.mkdirSync(path); | ||
| } | ||
| async function pruneNodeModules(): Promise<void> { | ||
| const entrypoints = [ | ||
| './build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/esm/index.js', | ||
| './build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/index.js', | ||
| './build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/awslambda-auto.js', | ||
| './build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/esm/awslambda-auto.js', | ||
| ]; | ||
| const { fileList } = await nodeFileTrace(entrypoints); | ||
| const allFiles = getAllFiles('./build/aws/dist-serverless/nodejs/node_modules'); | ||
| const filesToDelete = allFiles.filter(file => !fileList.has(file)); | ||
| console.log(`Removing ${filesToDelete.length} unused files from node_modules.`); | ||
| for (const file of filesToDelete) { | ||
| try { | ||
| fs.unlinkSync(file); | ||
| } catch { | ||
| console.error(`Error deleting ${file}`); | ||
| } | ||
| } | ||
| console.log('Cleaning up empty directories.'); | ||
| removeEmptyDirs('./build/aws/dist-serverless/nodejs/node_modules'); | ||
msonnb marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| function removeEmptyDirs(dir: string): void { | ||
| try { | ||
| const entries = fs.readdirSync(dir); | ||
| for (const entry of entries) { | ||
| const fullPath = path.join(dir, entry); | ||
| const stat = fs.statSync(fullPath); | ||
| if (stat.isDirectory()) { | ||
| removeEmptyDirs(fullPath); | ||
| } | ||
| } | ||
| const remainingEntries = fs.readdirSync(dir); | ||
| if (remainingEntries.length === 0) { | ||
| fs.rmdirSync(dir); | ||
| } | ||
| } catch { | ||
| // Directory might not exist or might not be empty, that's ok | ||
| } | ||
| } | ||
| function getAllFiles(dir: string): string[] { | ||
| const files: string[] = []; | ||
| function walkDirectory(currentPath: string): void { | ||
| try { | ||
| const entries = fs.readdirSync(currentPath, { withFileTypes: true }); | ||
| for (const entry of entries) { | ||
| const fullPath = path.join(currentPath, entry.name); | ||
| const relativePath = path.relative(process.cwd(), fullPath); | ||
| if (entry.isDirectory()) { | ||
| walkDirectory(fullPath); | ||
| } else { | ||
| files.push(relativePath); | ||
| } | ||
| } | ||
| } catch { | ||
| console.log(`Skipping directory ${currentPath}`); | ||
| } | ||
| } | ||
| walkDirectory(dir); | ||
| return files; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.