Skip to content

Commit 12ab6e8

Browse files
HazATclaude
andcommitted
feat(lighthouse-ci): bundle script targets new single-app static matrix
Rewrites bundle-and-upload.mjs for 1 app × 3 modes = 3 static cells. - Replace the 3-app APPS array (default-browser, react-19, nextjs-16) with single constants: APP='lighthouse-react', MODES=['no-sentry','init-only','tracing-replay']. - Delete prepareSsrBundle() and all SSR plumbing (installCmd, readyPattern, startCmd, serve:'server' branch). The new fixture is static-only. - Build invocation: pnpm install + pnpm build:<mode> — mode selection lives inside each per-mode npm script via Vite's --mode flag. No SENTRY_LIGHTHOUSE_MODE / NEXT_PUBLIC_* / REACT_APP_* env vars are passed. - Pass VITE_E2E_TEST_DSN so the tracing-replay Sentry.init has a DSN at build time. - Static output dir is dist/ (Vite default), not build/. - Wire protocol (multipart POST, cell schema, metadata shape) is unchanged. - Zero runtime deps rule preserved; execFileSync with argv arrays throughout. Dry-run verified: all 3 tarballs built successfully, upload failed with expected 401 on dummy token. Co-Authored-By: claude-sonnet-4-5 <noreply@anthropic.com>
1 parent ca35738 commit 12ab6e8

1 file changed

Lines changed: 35 additions & 137 deletions

File tree

dev-packages/lighthouse-bundle/bundle-and-upload.mjs

Lines changed: 35 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
/**
2-
* Bundle the instrumented Sentry test apps for every (app, mode) cell and POST
3-
* the tarballs to the Sentry Lighthouse lab (https://lighthouse.sentry.gg). The
4-
* lab runs Lighthouse asynchronously and ships results to Sentry on its own
5-
* schedule — this script exits as soon as the upload succeeds, it does NOT wait
6-
* for the lab to finish.
2+
* Bundle the `lighthouse-react` test app for each mode (no-sentry, init-only,
3+
* tracing-replay) and POST the three tarballs to the Sentry Lighthouse lab
4+
* (https://lighthouse.sentry.gg). The lab runs Lighthouse asynchronously and
5+
* ships results to Sentry on its own schedule — this script exits as soon as
6+
* the upload succeeds.
7+
*
8+
* Single-app static matrix: 1 app × 3 modes = 3 cells.
9+
* See plan scratchpad #182 for design details.
710
*
811
* Wire protocol: ~/Projects/sentry-lhci/docs/sentry-javascript-handoff.md
912
*
@@ -17,7 +20,7 @@
1720
/* eslint-disable no-console */
1821

1922
import { execFileSync } from 'node:child_process';
20-
import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
23+
import { mkdir, readFile, rm, stat } from 'node:fs/promises';
2124
import path from 'node:path';
2225

2326
const LAB_URL = process.env.LIGHTHOUSE_LAB_URL;
@@ -31,31 +34,10 @@ const RUNNER_TEMP = process.env.RUNNER_TEMP ?? path.join(WORKSPACE, '.tmp');
3134
const PACKED_DIR = path.join(WORKSPACE, 'dev-packages/e2e-tests/packed');
3235
const E2E_DIR = path.join(WORKSPACE, 'dev-packages/e2e-tests');
3336

34-
/**
35-
* The matrix. Adding an app here requires:
36-
* 1. The test app reads SENTRY_LIGHTHOUSE_MODE (or its bundler-specific
37-
* prefix variant) and branches its Sentry init.
38-
* 2. For SSR apps, the lab's runner must accept a startCmd + readyPattern.
39-
*/
40-
const APPS = [
41-
{ app: 'default-browser', serve: 'static', staticDir: 'build' },
42-
{ app: 'react-19', serve: 'static', staticDir: 'build' },
43-
{
44-
app: 'nextjs-16',
45-
serve: 'server',
46-
startCmd: 'pnpm start',
47-
readyPattern: 'Ready in',
48-
// Lab side: pnpm 9.15.9 is on the image via corepack. We strip the lockfile
49-
// and devDependencies from the SSR bundle (CI generates the lockfile with
50-
// workspace-absolute paths that don't survive the move; devDeps include
51-
// workspace links like @sentry-internal/test-utils that would also fail).
52-
// --no-frozen-lockfile lets pnpm regenerate from the rewritten package.json,
53-
// --prefer-offline uses the lab's persistent pnpm store (/data/.pnpm-store).
54-
installCmd: 'pnpm install --no-frozen-lockfile --prefer-offline',
55-
},
56-
];
57-
37+
const APP = 'lighthouse-react';
38+
const APP_DIR = 'lighthouse-react';
5839
const MODES = ['no-sentry', 'init-only', 'tracing-replay'];
40+
const STATIC_DIR = 'dist';
5941

6042
async function run() {
6143
// Fail fast if the lab is down so we don't waste minutes building bundles.
@@ -69,13 +51,10 @@ async function run() {
6951
await mkdir(path.join(RUNNER_TEMP, 'bundles'), { recursive: true });
7052
const bundles = [];
7153

72-
for (const def of APPS) {
73-
for (const mode of MODES) {
74-
const fieldName = `bundle-${bundles.length}`;
75-
console.log(`\n=== Preparing ${def.app} (${mode}) → ${fieldName} ===`);
76-
const bundle = await prepareCell(def, mode, fieldName);
77-
bundles.push(bundle);
78-
}
54+
for (const mode of MODES) {
55+
const fieldName = `bundle-${bundles.length}`;
56+
console.log(`\n=== Preparing ${APP} (${mode}) → ${fieldName} ===`);
57+
bundles.push(await prepareCell(mode, fieldName));
7958
}
8059

8160
console.log(`\n=== Uploading ${bundles.length} bundles to ${LAB_URL}/api/builds ===`);
@@ -87,24 +66,19 @@ async function run() {
8766
}
8867

8968
/**
90-
* Build a single (app, mode) cell:
91-
* 1. Copy the app to a unique temp dir (concurrent cells can't collide).
69+
* Build a single (mode) cell:
70+
* 1. Copy the app to a unique temp dir.
9271
* 2. Apply pnpm overrides (existing helper).
93-
* 3. Run `pnpm test:build` with SENTRY_LIGHTHOUSE_MODE + the framework
94-
* prefix variants (NEXT_PUBLIC_*, REACT_APP_*) — each app's bundler picks
95-
* up whichever it knows about.
96-
* 4. Static cells: tar just the build dir.
97-
* SSR cells: copy packed tgzs into the bundle, rewrite package.json to
98-
* relative `file:./packed/...` paths, drop devDependencies, tar without
99-
* node_modules and the lockfile.
72+
* 3. Run `pnpm install` then `pnpm build:<mode>`.
73+
* 4. Tar the `dist/` output dir.
10074
* 5. Return cell metadata for the upload.
10175
*/
102-
async function prepareCell(def, mode, fieldName) {
103-
const tempApp = path.join(RUNNER_TEMP, `app-${def.app}-${mode}`);
76+
async function prepareCell(mode, fieldName) {
77+
const tempApp = path.join(RUNNER_TEMP, `app-${APP}-${mode}`);
10478
await rm(tempApp, { recursive: true, force: true });
10579

10680
// Copy app to temp (fixes file:/link: deps to workspace-absolute paths)
107-
execFileSync('yarn', ['ci:copy-to-temp', `./test-applications/${def.app}`, tempApp], {
81+
execFileSync('yarn', ['ci:copy-to-temp', `./test-applications/${APP_DIR}`, tempApp], {
10882
cwd: E2E_DIR,
10983
stdio: 'inherit',
11084
});
@@ -115,106 +89,30 @@ async function prepareCell(def, mode, fieldName) {
11589
stdio: 'inherit',
11690
});
11791

118-
// Build with the right mode env var. We set all common bundler prefixes so each
119-
// app's bundler picks up whichever variant it knows about — apps that don't read
120-
// a prefix simply ignore extra vars.
121-
execFileSync('pnpm', ['test:build'], {
92+
// Install deps
93+
execFileSync('pnpm', ['install'], { cwd: tempApp, stdio: 'inherit' });
94+
95+
// Build with the Vite mode — mode selection lives inside the per-mode npm
96+
// script, no extra env vars needed for routing. VITE_E2E_TEST_DSN is passed
97+
// so the tracing-replay build's Sentry.init has a DSN at build time.
98+
execFileSync('pnpm', [`build:${mode}`], {
12299
cwd: tempApp,
123100
stdio: 'inherit',
124101
env: {
125102
...process.env,
126-
SENTRY_E2E_WORKSPACE_ROOT: WORKSPACE,
127-
SENTRY_LIGHTHOUSE_MODE: mode,
128-
NEXT_PUBLIC_SENTRY_LIGHTHOUSE_MODE: mode,
129-
REACT_APP_SENTRY_LIGHTHOUSE_MODE: mode,
103+
VITE_E2E_TEST_DSN: process.env.VITE_E2E_TEST_DSN ?? 'https://username@domain/123',
130104
},
131105
});
132106

133-
const tarPath = path.join(RUNNER_TEMP, 'bundles', `${def.app}-${mode}.tar.gz`);
134-
135-
if (def.serve === 'static') {
136-
// Static cell — tar just the build dir. Lab serves it with a static HTTP server.
137-
execFileSync('tar', ['-czf', tarPath, '-C', tempApp, def.staticDir], { stdio: 'inherit' });
138-
console.log(`Static bundle: ${tarPath} (${await formatSize(tarPath)})`);
139-
return {
140-
fieldName,
141-
tarPath,
142-
cell: {
143-
app: def.app,
144-
mode,
145-
bundleField: fieldName,
146-
serve: 'static',
147-
staticDir: def.staticDir,
148-
},
149-
};
150-
}
107+
const tarPath = path.join(RUNNER_TEMP, 'bundles', `${APP}-${mode}.tar.gz`);
108+
execFileSync('tar', ['-czf', tarPath, '-C', tempApp, STATIC_DIR], { stdio: 'inherit' });
109+
console.log(`Static bundle: ${tarPath} (${await formatSize(tarPath)})`);
151110

152-
// SSR cell — prep for `pnpm install && pnpm start` on the lab.
153-
await prepareSsrBundle(tempApp);
154-
execFileSync(
155-
'tar',
156-
[
157-
'-czf',
158-
tarPath,
159-
'--exclude=node_modules',
160-
'--exclude=.git',
161-
'--exclude=pnpm-lock.yaml',
162-
'-C',
163-
path.dirname(tempApp),
164-
path.basename(tempApp),
165-
],
166-
{ stdio: 'inherit' },
167-
);
168-
console.log(`SSR bundle: ${tarPath} (${await formatSize(tarPath)})`);
169111
return {
170112
fieldName,
171113
tarPath,
172-
cell: {
173-
app: def.app,
174-
mode,
175-
bundleField: fieldName,
176-
serve: 'server',
177-
startCmd: def.startCmd,
178-
readyPattern: def.readyPattern,
179-
installCmd: def.installCmd,
180-
},
181-
};
182-
}
183-
184-
/**
185-
* For SSR cells: copy the packed Sentry tarballs into the bundle, rewrite
186-
* package.json deps + pnpm.overrides to relative `file:./packed/...` paths so
187-
* the lab's `pnpm install` can resolve them from inside the extracted bundle,
188-
* and drop devDependencies entirely (not needed at runtime; some are workspace
189-
* links like @sentry-internal/test-utils that don't survive the move).
190-
*/
191-
async function prepareSsrBundle(tempApp) {
192-
// Copy packed tgz files into <bundle>/packed/
193-
const inBundlePacked = path.join(tempApp, 'packed');
194-
await mkdir(inBundlePacked, { recursive: true });
195-
for (const name of await readdir(PACKED_DIR)) {
196-
if (name.endsWith('.tgz')) {
197-
await copyFile(path.join(PACKED_DIR, name), path.join(inBundlePacked, name));
198-
}
199-
}
200-
201-
// Rewrite all workspace-absolute `file:.../sentry-*-packed.tgz` references in
202-
// package.json to `./packed/<file>`, and drop devDependencies wholesale.
203-
const pkgPath = path.join(tempApp, 'package.json');
204-
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
205-
const rewrite = obj => {
206-
if (!obj) return;
207-
for (const [name, val] of Object.entries(obj)) {
208-
const m = typeof val === 'string' ? val.match(/sentry-[a-z0-9-]+-packed\.tgz$/) : null;
209-
if (m && (val.startsWith('file:') || val.startsWith('link:'))) {
210-
obj[name] = `file:./packed/${m[0]}`;
211-
}
212-
}
114+
cell: { app: APP, mode, bundleField: fieldName, serve: 'static', staticDir: STATIC_DIR },
213115
};
214-
rewrite(pkg.dependencies);
215-
rewrite(pkg.pnpm?.overrides);
216-
delete pkg.devDependencies;
217-
await writeFile(pkgPath, JSON.stringify(pkg, null, 2));
218116
}
219117

220118
/**

0 commit comments

Comments
 (0)