diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d7781c5f43f..34e5c0be6d54 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,7 @@ on: - v8 - release/** pull_request: + types: [opened, synchronize, reopened] merge_group: types: [checks_requested] workflow_dispatch: diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml new file mode 100644 index 000000000000..81a25c967c58 --- /dev/null +++ b/.github/workflows/lighthouse.yml @@ -0,0 +1,143 @@ +name: 'Nightly: Lighthouse' + +# Builds the instrumented Sentry test apps (default-browser, react-19, nextjs-16) +# in three Sentry feature modes (no-sentry, init-only, tracing-replay), tars each +# of the 9 cells, and uploads them to the Sentry Lighthouse lab at +# https://lighthouse.sentry.gg. +# +# The lab runs Lighthouse on dedicated hardware (no shared-runner flake), saves +# results to its own DB + dashboard, and ships metrics to Sentry. See the wire +# protocol in ~/Projects/sentry-lhci/docs/sentry-javascript-handoff.md. +# +# This workflow only builds + uploads — no Lighthouse runs on GitHub Actions. +# Never blocks merges (not in build.yml's required-jobs gate). + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +# Independent of build.yml so nothing here can cancel or queue against main CI. +concurrency: + group: lighthouse-${{ github.run_id }} + cancel-in-progress: false + +env: + CACHED_DEPENDENCY_PATHS: | + ${{ github.workspace }}/node_modules + ${{ github.workspace }}/packages/*/node_modules + ${{ github.workspace }}/dev-packages/*/node_modules + ~/.cache/mongodb-binaries/ + TARBALL_ARTIFACT_GLOB: packages/*/*.tgz + TARBALL_ARTIFACT_DOWNLOAD_PATH: ${{ github.workspace }}/packages + DISABLE_V8_COMPILE_CACHE: '1' + +jobs: + job_build: + name: Build SDK + Tarballs + runs-on: ubuntu-24.04 + timeout-minutes: 20 + outputs: + dependency_cache_key: ${{ steps.install_dependencies.outputs.cache_key }} + steps: + - name: Check out current commit + uses: actions/checkout@v6 + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version-file: 'package.json' + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + id: install_dependencies + + - name: Build packages + run: yarn build:ci + + - name: Compute build artifact paths from Nx + id: nx_build_paths + run: | + { + echo 'paths<> "$GITHUB_OUTPUT" + + - name: Upload build artifacts + # Required by .github/actions/restore-cache in the downstream job — mirrors + # the `build-output` artifact that build.yml's job_build produces. Without + # it, the bundle job fails on restore-cache with "Artifact not found". + uses: actions/upload-artifact@v7 + with: + name: build-output + path: ${{ steps.nx_build_paths.outputs.paths }} + retention-days: 4 + compression-level: 6 + overwrite: true + + - name: Build tarballs + run: yarn build:tarball + + - name: Upload tarball artifacts + uses: actions/upload-artifact@v7 + with: + name: lighthouse-build-tarball-output + path: ${{ env.TARBALL_ARTIFACT_GLOB }} + if-no-files-found: error + retention-days: 4 + compression-level: 6 + overwrite: true + + job_bundle_and_upload: + name: Bundle test apps and upload to Lighthouse lab + needs: [job_build] + runs-on: ubuntu-24.04 + timeout-minutes: 30 + env: + LIGHTHOUSE_LAB_URL: ${{ secrets.LIGHTHOUSE_LAB_URL }} + LIGHTHOUSE_UPLOAD_TOKEN: ${{ secrets.LIGHTHOUSE_UPLOAD_TOKEN }} + # Synthetic DSNs — the apps only need *something* parseable at build time; + # nothing actually phones home (the Lighthouse run is what we care about). + E2E_TEST_DSN: 'https://username@domain/123' + NEXT_PUBLIC_E2E_TEST_DSN: 'https://username@domain/123' + PUBLIC_E2E_TEST_DSN: 'https://username@domain/123' + REACT_APP_E2E_TEST_DSN: 'https://username@domain/123' + steps: + - name: Check out current commit + uses: actions/checkout@v6 + + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 + with: + version: 9.15.9 + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version-file: 'package.json' + + - name: Restore caches + uses: ./.github/actions/restore-cache + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} + + - name: Restore tarball artifacts + uses: actions/download-artifact@v7 + with: + name: lighthouse-build-tarball-output + path: ${{ env.TARBALL_ARTIFACT_DOWNLOAD_PATH }} + + - name: Prepare E2E tests (copy packed tgzs into dev-packages/e2e-tests/packed) + run: yarn test:prepare + working-directory: dev-packages/e2e-tests + + - name: Validate E2E tests setup + run: yarn test:validate + working-directory: dev-packages/e2e-tests + + - name: Bundle every cell and upload to the lab + # bundle-and-upload.mjs handles the full 3 × 3 matrix in one process: + # copies each app to a temp dir, applies pnpm overrides, builds with the + # right SENTRY_LIGHTHOUSE_MODE env var, tars, POSTs all 9 bundles to the + # lab, polls until terminal, writes a Job Summary. + run: node dev-packages/lighthouse-bundle/bundle-and-upload.mjs diff --git a/dev-packages/e2e-tests/test-applications/default-browser/build.mjs b/dev-packages/e2e-tests/test-applications/default-browser/build.mjs index aeaad894bdbd..6a4254cc8238 100644 --- a/dev-packages/e2e-tests/test-applications/default-browser/build.mjs +++ b/dev-packages/e2e-tests/test-applications/default-browser/build.mjs @@ -17,8 +17,15 @@ webpack( minimize: true, minimizer: [new TerserPlugin()], }, + // The Lighthouse-CI baseline mode (SENTRY_LIGHTHOUSE_MODE=no-sentry) emits the + // @sentry/browser chunk as a separately code-split asset (~265 KiB raw). Webpack's + // default performance.hints='warning' fires AssetsOverSizeLimitWarning for that + // chunk, and build.mjs's `if (stats.hasWarnings()) process.exit(1)` would then + // abort the build. This is a test app, not a size-tracked production bundle, so + // hints are disabled — size is tracked separately via .size-limit.js at the repo root. + performance: { hints: false }, plugins: [ - new webpack.EnvironmentPlugin(['E2E_TEST_DSN']), + new webpack.EnvironmentPlugin({ E2E_TEST_DSN: '', SENTRY_LIGHTHOUSE_MODE: '' }), new HtmlWebpackPlugin({ template: path.join(__dirname, 'public/index.html'), }), diff --git a/dev-packages/e2e-tests/test-applications/default-browser/src/index.js b/dev-packages/e2e-tests/test-applications/default-browser/src/index.js index d3eea216fe84..1288bfcfab09 100644 --- a/dev-packages/e2e-tests/test-applications/default-browser/src/index.js +++ b/dev-packages/e2e-tests/test-applications/default-browser/src/index.js @@ -1,14 +1,10 @@ -import * as Sentry from '@sentry/browser'; - -Sentry.init({ - dsn: process.env.E2E_TEST_DSN, - integrations: [Sentry.browserTracingIntegration()], - tracesSampleRate: 1.0, - release: 'e2e-test', - environment: 'qa', - tunnel: 'http://localhost:3031', -}); +const lighthouseMode = process.env.SENTRY_LIGHTHOUSE_MODE; +// Event listeners are attached synchronously at module top-level so E2E tests that do +// `page.goto('/')` followed immediately by `button.click()` cannot race the dynamic +// `import('@sentry/browser')` below. The handlers don't depend on Sentry being +// initialized — Sentry's global error/transaction handlers attach via window-level +// listeners installed by `Sentry.init()` and pick up the thrown error regardless. document.getElementById('exception-button').addEventListener('click', () => { throw new Error('I am an error!'); }); @@ -16,3 +12,38 @@ document.getElementById('exception-button').addEventListener('click', () => { document.getElementById('navigation-link').addEventListener('click', () => { document.getElementById('navigation-target').scrollIntoView({ behavior: 'smooth' }); }); + +// Sentry is loaded via dynamic `import()` so the `no-sentry` Lighthouse build can +// tree-shake the SDK out completely. Wrapped in an async IIFE because top-level await +// isn't supported by the webpack target used for this app's bundle. +if (lighthouseMode !== 'no-sentry') { + void (async () => { + const Sentry = await import('@sentry/browser'); + + const integrations = []; + + // Existing E2E behavior (empty string) and 'tracing-replay' mode both include tracing. + // 'init-only' mode omits all integrations so we can measure SDK-core overhead. + if (lighthouseMode !== 'init-only') { + integrations.push(Sentry.browserTracingIntegration()); + } + + // Replay is gated to 'tracing-replay' so we can measure its overhead independently + // from tracing. Existing E2E behavior (unset env var) did not include replay, so we + // preserve that. + if (lighthouseMode === 'tracing-replay') { + integrations.push(Sentry.replayIntegration()); + } + + Sentry.init({ + dsn: process.env.E2E_TEST_DSN, + integrations, + tracesSampleRate: 1.0, + replaysSessionSampleRate: lighthouseMode === 'tracing-replay' ? 1.0 : 0, + replaysOnErrorSampleRate: lighthouseMode === 'tracing-replay' ? 1.0 : 0, + release: 'e2e-test', + environment: 'qa', + tunnel: 'http://localhost:3031', + }); + })(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts index 991c6009ed02..cede9c55c688 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts @@ -1,22 +1,42 @@ import * as Sentry from '@sentry/nextjs'; import type { Log } from '@sentry/nextjs'; -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, - tunnel: `http://localhost:3031/`, // proxy server - tracesSampleRate: 1.0, - sendDefaultPii: true, - integrations: [ - Sentry.thirdPartyErrorFilterIntegration({ - filterKeys: ['nextjs-16-e2e'], - behaviour: 'apply-tag-if-contains-third-party-frames', - }), - ], - // Verify Log type is available - beforeSendLog(log: Log) { - return log; - }, -}); +// SENTRY_LIGHTHOUSE_MODE values: +// ''/undefined - existing E2E behavior (no tracing/replay; third-party-error-filter on) +// 'no-sentry' - Sentry.init() is skipped entirely (SDK is still in the bundle because +// Next.js doesn't reliably treeshake @sentry/nextjs; see plan Risk 5) +// 'init-only' - Sentry.init() with no integrations (measures SDK-core overhead) +// 'tracing-replay' - Sentry.init() with browserTracing + replay (measures feature overhead) +const lighthouseMode = process.env.NEXT_PUBLIC_SENTRY_LIGHTHOUSE_MODE; -export const onRouterTransitionStart = Sentry.captureRouterTransitionStart; +if (lighthouseMode !== 'no-sentry') { + Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + replaysSessionSampleRate: lighthouseMode === 'tracing-replay' ? 1.0 : 0, + replaysOnErrorSampleRate: lighthouseMode === 'tracing-replay' ? 1.0 : 0, + sendDefaultPii: true, + // Existing E2E behavior (mode unset/'') keeps third-party-error-filter on. + // init-only / tracing-replay drop it so we measure SDK overhead without app-specific noise. + // tracing-replay additionally enables browserTracing + replay. + integrations: [ + ...(lighthouseMode === undefined || lighthouseMode === '' + ? [ + Sentry.thirdPartyErrorFilterIntegration({ + filterKeys: ['nextjs-16-e2e'], + behaviour: 'apply-tag-if-contains-third-party-frames', + }), + ] + : []), + ...(lighthouseMode === 'tracing-replay' ? [Sentry.browserTracingIntegration(), Sentry.replayIntegration()] : []), + ], + // Verify Log type is available + beforeSendLog(log: Log) { + return log; + }, + }); +} + +export const onRouterTransitionStart = lighthouseMode !== 'no-sentry' ? Sentry.captureRouterTransitionStart : undefined; diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx index 16209793ae07..365319374905 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx @@ -1,26 +1,58 @@ -import * as Sentry from '@sentry/react'; import React from 'react'; import ReactDOM from 'react-dom/client'; import Index from './pages/Index'; -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: process.env.REACT_APP_E2E_TEST_DSN, - release: 'e2e-test', - tunnel: 'http://localhost:3031/', // proxy server -}); +const lighthouseMode = process.env.REACT_APP_SENTRY_LIGHTHOUSE_MODE; -const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement, { - onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => { - console.warn(error, errorInfo); - }), - onCaughtError: Sentry.reactErrorHandler((error, errorInfo) => { - console.warn(error, errorInfo); - }), -}); +if (lighthouseMode === 'no-sentry') { + // No Sentry at all — sync render so webpack/CRA can fully dead-code-eliminate the + // @sentry/react import below. CRA inlines `process.env.REACT_APP_*` at build time, + // so this branch becomes the only one in the bundle when the env var is set. + const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); + root.render( +
+ +
, + ); +} else { + // Dynamic-import Sentry so it lives in a separate chunk that webpack/CRA drops + // entirely from the no-sentry build above. Preserves existing E2E behavior when + // the env var is unset (init + error handlers, no tracing/replay). + void (async () => { + const Sentry = await import('@sentry/react'); -root.render( -
- -
, -); + const integrations: unknown[] = []; + if (lighthouseMode === 'tracing-replay') { + integrations.push(Sentry.browserTracingIntegration()); + integrations.push(Sentry.replayIntegration()); + } + + Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.REACT_APP_E2E_TEST_DSN, + release: 'e2e-test', + tunnel: 'http://localhost:3031/', // proxy server + integrations: integrations as Parameters[0]['integrations'], + tracesSampleRate: lighthouseMode === 'tracing-replay' ? 1.0 : undefined, + replaysSessionSampleRate: lighthouseMode === 'tracing-replay' ? 1.0 : 0, + replaysOnErrorSampleRate: lighthouseMode === 'tracing-replay' ? 1.0 : 0, + }); + + const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement, { + onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => { + // oxlint-disable-next-line no-console + console.warn(error, errorInfo); + }), + onCaughtError: Sentry.reactErrorHandler((error, errorInfo) => { + // oxlint-disable-next-line no-console + console.warn(error, errorInfo); + }), + }); + + root.render( +
+ +
, + ); + })(); +} diff --git a/dev-packages/lighthouse-bundle/bundle-and-upload.mjs b/dev-packages/lighthouse-bundle/bundle-and-upload.mjs new file mode 100644 index 000000000000..f33aa8f77eb6 --- /dev/null +++ b/dev-packages/lighthouse-bundle/bundle-and-upload.mjs @@ -0,0 +1,263 @@ +/** + * Bundle the instrumented Sentry test apps for every (app, mode) cell and POST + * the tarballs to the Sentry Lighthouse lab (https://lighthouse.sentry.gg). The + * lab runs Lighthouse asynchronously and ships results to Sentry on its own + * schedule — this script exits as soon as the upload succeeds, it does NOT wait + * for the lab to finish. + * + * Wire protocol: ~/Projects/sentry-lhci/docs/sentry-javascript-handoff.md + * + * Zero runtime dependencies — uses Node 22 builtins (fetch, FormData, Blob) and + * the system `tar`. Every external command is invoked via `execFileSync` with + * an argv array so no shell interpolation happens — needed both for safety + * (CodeQL flags any env-derived string concatenated into a shell command, even + * when the inputs are controlled) and to keep paths with spaces working. + */ + +/* eslint-disable no-console */ + +import { execFileSync } from 'node:child_process'; +import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +const LAB_URL = process.env.LIGHTHOUSE_LAB_URL; +const TOKEN = process.env.LIGHTHOUSE_UPLOAD_TOKEN; +if (!LAB_URL || !TOKEN) { + throw new Error('LIGHTHOUSE_LAB_URL and LIGHTHOUSE_UPLOAD_TOKEN must be set'); +} + +const WORKSPACE = process.env.GITHUB_WORKSPACE ?? process.cwd(); +const RUNNER_TEMP = process.env.RUNNER_TEMP ?? path.join(WORKSPACE, '.tmp'); +const PACKED_DIR = path.join(WORKSPACE, 'dev-packages/e2e-tests/packed'); +const E2E_DIR = path.join(WORKSPACE, 'dev-packages/e2e-tests'); + +/** + * The matrix. Adding an app here requires: + * 1. The test app reads SENTRY_LIGHTHOUSE_MODE (or its bundler-specific + * prefix variant) and branches its Sentry init. + * 2. For SSR apps, the lab's runner must accept a startCmd + readyPattern. + */ +const APPS = [ + { app: 'default-browser', serve: 'static', staticDir: 'build' }, + { app: 'react-19', serve: 'static', staticDir: 'build' }, + { + app: 'nextjs-16', + serve: 'server', + startCmd: 'pnpm start', + readyPattern: 'Ready in', + // Lab side: pnpm 9.15.9 is on the image via corepack. We strip the lockfile + // and devDependencies from the SSR bundle (CI generates the lockfile with + // workspace-absolute paths that don't survive the move; devDeps include + // workspace links like @sentry-internal/test-utils that would also fail). + // --no-frozen-lockfile lets pnpm regenerate from the rewritten package.json, + // --prefer-offline uses the lab's persistent pnpm store (/data/.pnpm-store). + installCmd: 'pnpm install --no-frozen-lockfile --prefer-offline', + }, +]; + +const MODES = ['no-sentry', 'init-only', 'tracing-replay']; + +async function run() { + // Fail fast if the lab is down so we don't waste minutes building bundles. + console.log(`Liveness check: ${LAB_URL}/healthz`); + const health = await fetch(`${LAB_URL}/healthz`); + if (!health.ok) { + throw new Error(`Lab healthcheck failed: ${health.status} ${await health.text()}`); + } + console.log('Lab is reachable.'); + + await mkdir(path.join(RUNNER_TEMP, 'bundles'), { recursive: true }); + const bundles = []; + + for (const def of APPS) { + for (const mode of MODES) { + const fieldName = `bundle-${bundles.length}`; + console.log(`\n=== Preparing ${def.app} (${mode}) → ${fieldName} ===`); + const bundle = await prepareCell(def, mode, fieldName); + bundles.push(bundle); + } + } + + console.log(`\n=== Uploading ${bundles.length} bundles to ${LAB_URL}/api/builds ===`); + const buildResp = await uploadBundles(bundles); + console.log(`Build queued: ${buildResp.buildId}`); + console.log(`Dashboard: ${LAB_URL}${buildResp.dashboardUrl}`); + console.log(`API: ${LAB_URL}${buildResp.buildUrl}`); + console.log('\nUpload succeeded. The lab runs Lighthouse asynchronously — track results in the Sentry dashboard.'); +} + +/** + * Build a single (app, mode) cell: + * 1. Copy the app to a unique temp dir (concurrent cells can't collide). + * 2. Apply pnpm overrides (existing helper). + * 3. Run `pnpm test:build` with SENTRY_LIGHTHOUSE_MODE + the framework + * prefix variants (NEXT_PUBLIC_*, REACT_APP_*) — each app's bundler picks + * up whichever it knows about. + * 4. Static cells: tar just the build dir. + * SSR cells: copy packed tgzs into the bundle, rewrite package.json to + * relative `file:./packed/...` paths, drop devDependencies, tar without + * node_modules and the lockfile. + * 5. Return cell metadata for the upload. + */ +async function prepareCell(def, mode, fieldName) { + const tempApp = path.join(RUNNER_TEMP, `app-${def.app}-${mode}`); + await rm(tempApp, { recursive: true, force: true }); + + // Copy app to temp (fixes file:/link: deps to workspace-absolute paths) + execFileSync('yarn', ['ci:copy-to-temp', `./test-applications/${def.app}`, tempApp], { + cwd: E2E_DIR, + stdio: 'inherit', + }); + + // Add pnpm overrides (workspace-absolute paths pointing at packed dir) + execFileSync('yarn', ['ci:pnpm-overrides', tempApp, PACKED_DIR], { + cwd: E2E_DIR, + stdio: 'inherit', + }); + + // Build with the right mode env var. We set all common bundler prefixes so each + // app's bundler picks up whichever variant it knows about — apps that don't read + // a prefix simply ignore extra vars. + execFileSync('pnpm', ['test:build'], { + cwd: tempApp, + stdio: 'inherit', + env: { + ...process.env, + SENTRY_E2E_WORKSPACE_ROOT: WORKSPACE, + SENTRY_LIGHTHOUSE_MODE: mode, + NEXT_PUBLIC_SENTRY_LIGHTHOUSE_MODE: mode, + REACT_APP_SENTRY_LIGHTHOUSE_MODE: mode, + }, + }); + + const tarPath = path.join(RUNNER_TEMP, 'bundles', `${def.app}-${mode}.tar.gz`); + + if (def.serve === 'static') { + // Static cell — tar just the build dir. Lab serves it with a static HTTP server. + execFileSync('tar', ['-czf', tarPath, '-C', tempApp, def.staticDir], { stdio: 'inherit' }); + console.log(`Static bundle: ${tarPath} (${await formatSize(tarPath)})`); + return { + fieldName, + tarPath, + cell: { + app: def.app, + mode, + bundleField: fieldName, + serve: 'static', + staticDir: def.staticDir, + }, + }; + } + + // SSR cell — prep for `pnpm install && pnpm start` on the lab. + await prepareSsrBundle(tempApp); + execFileSync( + 'tar', + [ + '-czf', + tarPath, + '--exclude=node_modules', + '--exclude=.git', + '--exclude=pnpm-lock.yaml', + '-C', + path.dirname(tempApp), + path.basename(tempApp), + ], + { stdio: 'inherit' }, + ); + console.log(`SSR bundle: ${tarPath} (${await formatSize(tarPath)})`); + return { + fieldName, + tarPath, + cell: { + app: def.app, + mode, + bundleField: fieldName, + serve: 'server', + startCmd: def.startCmd, + readyPattern: def.readyPattern, + installCmd: def.installCmd, + }, + }; +} + +/** + * For SSR cells: copy the packed Sentry tarballs into the bundle, rewrite + * package.json deps + pnpm.overrides to relative `file:./packed/...` paths so + * the lab's `pnpm install` can resolve them from inside the extracted bundle, + * and drop devDependencies entirely (not needed at runtime; some are workspace + * links like @sentry-internal/test-utils that don't survive the move). + */ +async function prepareSsrBundle(tempApp) { + // Copy packed tgz files into /packed/ + const inBundlePacked = path.join(tempApp, 'packed'); + await mkdir(inBundlePacked, { recursive: true }); + for (const name of await readdir(PACKED_DIR)) { + if (name.endsWith('.tgz')) { + await copyFile(path.join(PACKED_DIR, name), path.join(inBundlePacked, name)); + } + } + + // Rewrite all workspace-absolute `file:.../sentry-*-packed.tgz` references in + // package.json to `./packed/`, and drop devDependencies wholesale. + const pkgPath = path.join(tempApp, 'package.json'); + const pkg = JSON.parse(await readFile(pkgPath, 'utf8')); + const rewrite = obj => { + if (!obj) return; + for (const [name, val] of Object.entries(obj)) { + const m = typeof val === 'string' ? val.match(/sentry-[a-z0-9-]+-packed\.tgz$/) : null; + if (m && (val.startsWith('file:') || val.startsWith('link:'))) { + obj[name] = `file:./packed/${m[0]}`; + } + } + }; + rewrite(pkg.dependencies); + rewrite(pkg.pnpm?.overrides); + delete pkg.devDependencies; + await writeFile(pkgPath, JSON.stringify(pkg, null, 2)); +} + +/** + * POST the multipart form. Returns the parsed 202 response body. + */ +async function uploadBundles(bundles) { + const metadata = { + commit: process.env.GITHUB_SHA ?? 'unknown', + branch: process.env.GITHUB_REF_NAME ?? 'unknown', + triggeredBy: 'github-actions', + workflowRunUrl: + process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID + ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` + : undefined, + cells: bundles.map(b => b.cell), + }; + + const form = new FormData(); + form.append('metadata', JSON.stringify(metadata)); + for (const b of bundles) { + const buf = await readFile(b.tarPath); + form.append(b.fieldName, new Blob([buf], { type: 'application/gzip' }), path.basename(b.tarPath)); + } + + const res = await fetch(`${LAB_URL}/api/builds`, { + method: 'POST', + headers: { Authorization: `Bearer ${TOKEN}` }, + body: form, + }); + if (!res.ok) { + throw new Error(`Upload failed: ${res.status} ${await res.text()}`); + } + return res.json(); +} + +async function formatSize(filePath) { + const { size } = await stat(filePath); + if (size < 1024) return `${size} B`; + if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`; + return `${(size / 1024 / 1024).toFixed(1)} MB`; +} + +run().catch(err => { + console.error(err.stack || err.message); + process.exit(1); +}); diff --git a/dev-packages/lighthouse-bundle/package.json b/dev-packages/lighthouse-bundle/package.json new file mode 100644 index 000000000000..14480a2ebb34 --- /dev/null +++ b/dev-packages/lighthouse-bundle/package.json @@ -0,0 +1,12 @@ +{ + "name": "@sentry-internal/lighthouse-bundle", + "version": "0.0.0", + "private": true, + "type": "module", + "volta": { + "extends": "../../package.json" + }, + "scripts": { + "bundle-and-upload": "node bundle-and-upload.mjs" + } +} diff --git a/package.json b/package.json index bca64a9f863f..ddd85333d3f3 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,8 @@ "dev-packages/clear-cache-gh-action", "dev-packages/external-contributor-gh-action", "dev-packages/rollup-utils", - "dev-packages/bundler-tests" + "dev-packages/bundler-tests", + "dev-packages/lighthouse-bundle" ], "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7",