Skip to content

Commit 7203205

Browse files
mydeaclaude
andcommitted
perf(aws-serverless): Speed up Lambda layer build with tarball-based install
Replace `file:` directory references with `npm pack` tarballs when installing @sentry/* packages into the Lambda layer's node_modules. Previously, `yarn install` used `file:` references to workspace packages with a fresh cache folder (to avoid stale cache issues), taking ~52s. With tarballs, yarn can use its global cache and the install completes in ~8s. Only transitive dependencies of @sentry/aws-serverless are packed (~5 packages), keeping the packing step fast. Total layer build time: ~97s → ~15s. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 5f72df5 commit 7203205

1 file changed

Lines changed: 75 additions & 36 deletions

File tree

packages/aws-serverless/scripts/buildLambdaLayer.ts

Lines changed: 75 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import { nodeFileTrace } from '@vercel/nft';
33
import * as childProcess from 'child_process';
44
import * as fs from 'fs';
5-
import * as os from 'os';
65
import * as path from 'path';
76
import { version } from '../package.json';
87

@@ -22,21 +21,14 @@ function run(cmd: string, options?: childProcess.ExecSyncOptions): string {
2221
*/
2322
async function buildLambdaLayer(): Promise<void> {
2423
console.log('Building Lambda layer.');
25-
buildPackageJson();
26-
console.log('Installing local @sentry/aws-serverless into build/aws/dist-serverless/nodejs.');
27-
// Use a temporary cache folder to avoid stale cache references to local file: packages.
28-
// Yarn's global cache can contain outdated references to build artifacts from other
29-
// @sentry/* packages (e.g., build/node_modules paths that no longer exist), causing
30-
// ENOENT errors during file copying.
31-
// The cache folder must be outside the monorepo to avoid recursive nesting when Yarn
32-
// follows file: links and copies package directories.
33-
const cacheFolder = path.join(os.tmpdir(), `sentry-lambda-build-cache-${Date.now()}`);
34-
run(`yarn install --prod --cwd ./build/aws/dist-serverless/nodejs --cache-folder "${cacheFolder}"`);
24+
const tarballDir = buildTarballsAndPackageJson();
25+
console.log('Installing @sentry/aws-serverless from tarballs into build/aws/dist-serverless/nodejs.');
26+
run('yarn install --prod --cwd ./build/aws/dist-serverless/nodejs');
3527

3628
await pruneNodeModules();
3729
fs.rmSync('./build/aws/dist-serverless/nodejs/package.json', { force: true });
3830
fs.rmSync('./build/aws/dist-serverless/nodejs/yarn.lock', { force: true });
39-
fs.rmSync(cacheFolder, { recursive: true, force: true });
31+
fs.rmSync(tarballDir, { recursive: true, force: true });
4032

4133
// The layer also includes `awslambda-auto.js`, a helper file which calls `Sentry.init()` and wraps the lambda
4234
// handler. It gets run when Node is launched inside the lambda, using the environment variable
@@ -170,43 +162,90 @@ function getAllFiles(dir: string): string[] {
170162
return files;
171163
}
172164

173-
function buildPackageJson(): void {
174-
console.log('Building package.json');
165+
/**
166+
* Pack @sentry/* packages into tarballs and generate a package.json that references them.
167+
*
168+
* Using tarballs instead of `file:` directory references avoids stale yarn cache issues
169+
* (where yarn's global cache retains outdated build artifact paths from previous builds)
170+
* and allows us to use yarn's global cache for faster installs.
171+
*
172+
* Only packs packages that are transitive dependencies of @sentry/aws-serverless to keep
173+
* the packing step fast.
174+
*/
175+
function buildTarballsAndPackageJson(): string {
176+
const tarballDir = path.resolve('./build/aws/tarballs');
177+
fsForceMkdirSync(tarballDir);
178+
175179
const packagesDir = path.resolve(__dirname, '../..');
176-
const packageDirs = fs
177-
.readdirSync(packagesDir, { withFileTypes: true })
178-
.filter(dirent => dirent.isDirectory())
179-
.map(dirent => dirent.name)
180-
.filter(name => !name.startsWith('.')) // Skip hidden directories
181-
.sort();
180+
const sentryPackages = collectSentryDependencies(packagesDir);
181+
182+
console.log(`Packing ${sentryPackages.size} @sentry/* packages into tarballs.`);
182183

183184
const resolutions: Record<string, string> = {};
184185

185-
for (const packageDir of packageDirs) {
186-
const packageJsonPath = path.join(packagesDir, packageDir, 'package.json');
187-
if (fs.existsSync(packageJsonPath)) {
188-
try {
189-
const packageContent = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as { name?: string };
190-
const packageName = packageContent.name;
191-
if (typeof packageName === 'string' && packageName) {
192-
resolutions[packageName] = `file:../../../../../../packages/${packageDir}`;
193-
}
194-
} catch {
195-
console.warn(`Warning: Could not read package.json for ${packageDir}`);
196-
}
197-
}
186+
for (const [packageName, packageDir] of sentryPackages) {
187+
run(`npm pack --pack-destination "${tarballDir}"`, {
188+
cwd: path.join(packagesDir, packageDir),
189+
stdio: 'pipe',
190+
});
191+
const tarballName = `${packageName.replace('@', '').replace('/', '-')}-${version}.tgz`;
192+
resolutions[packageName] = `file:${path.join(tarballDir, tarballName)}`;
193+
}
194+
195+
const awsServerlessTarball = resolutions['@sentry/aws-serverless'];
196+
if (!awsServerlessTarball) {
197+
throw new Error('Failed to pack @sentry/aws-serverless');
198198
}
199199

200200
const packageJson = {
201201
dependencies: {
202-
'@sentry/aws-serverless': 'file:../../../../../../packages/aws-serverless',
202+
'@sentry/aws-serverless': awsServerlessTarball,
203203
},
204204
resolutions,
205205
};
206206

207207
fsForceMkdirSync('./build/aws/dist-serverless/nodejs');
208-
const packageJsonPath = './build/aws/dist-serverless/nodejs/package.json';
209-
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
208+
fs.writeFileSync('./build/aws/dist-serverless/nodejs/package.json', JSON.stringify(packageJson, null, 2));
209+
210+
return tarballDir;
211+
}
212+
213+
/**
214+
* Collect all @sentry/* and @sentry-internal/* packages that are transitive
215+
* dependencies of @sentry/aws-serverless.
216+
* Returns a Map of packageName -> directory name.
217+
*/
218+
function collectSentryDependencies(packagesDir: string): Map<string, string> {
219+
const result = new Map<string, string>();
220+
221+
// Build a lookup of package name -> directory name
222+
const nameToDir = new Map<string, string>();
223+
for (const dirent of fs.readdirSync(packagesDir, { withFileTypes: true })) {
224+
if (!dirent.isDirectory() || dirent.name.startsWith('.')) continue;
225+
const pkgPath = path.join(packagesDir, dirent.name, 'package.json');
226+
if (!fs.existsSync(pkgPath)) continue;
227+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { name?: string };
228+
if (pkg.name) {
229+
nameToDir.set(pkg.name, dirent.name);
230+
}
231+
}
232+
233+
function collect(packageName: string): void {
234+
if (result.has(packageName)) return;
235+
const dir = nameToDir.get(packageName);
236+
if (!dir) return;
237+
result.set(packageName, dir);
238+
const pkgPath = path.join(packagesDir, dir, 'package.json');
239+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { dependencies?: Record<string, string> };
240+
for (const dep of Object.keys(pkg.dependencies || {})) {
241+
if (dep.startsWith('@sentry/') || dep.startsWith('@sentry-internal/')) {
242+
collect(dep);
243+
}
244+
}
245+
}
246+
247+
collect('@sentry/aws-serverless');
248+
return result;
210249
}
211250

212251
function replaceSDKSource(): void {

0 commit comments

Comments
 (0)