Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d496226
feat(lighthouse-ci): scaffold lighthouse-tests dev-package
HazAT May 12, 2026
36c3f76
feat(lighthouse-ci): add SENTRY_LIGHTHOUSE_MODE guard to default-brow…
HazAT May 12, 2026
cc37d4d
fix(lighthouse-ci): enable replay in tracing-replay mode for default-…
HazAT May 12, 2026
ae9b8b8
feat(lighthouse-ci): add nightly + label-gated lighthouse jobs to bui…
HazAT May 12, 2026
2a7c378
fix(lighthouse-ci): align workflow with actual matrix schema and ligh…
HazAT May 12, 2026
0119764
feat(lighthouse-ci): add post-comment.mjs to render Lighthouse result…
HazAT May 12, 2026
cfa2092
fix(lighthouse-ci): drop unusable local-path links from PR comment
HazAT May 12, 2026
e8cb7f8
chore(lighthouse-ci): pin treosh/lighthouse-ci-action to commit SHA
HazAT May 12, 2026
e04bc37
fix(lighthouse-ci): unblock webpack build and surface LCP/TBT/bytes i…
HazAT May 12, 2026
9e28f29
chore(lighthouse-ci): drop angular, remix, ember, solidstart from matrix
HazAT May 12, 2026
254f973
fix(lighthouse-ci): rename lighthouserc.js to .cjs and switch to modu…
HazAT May 12, 2026
b766f80
fix(lighthouse-ci): drop redundant uploadArtifacts from treosh action
HazAT May 12, 2026
db8a4bb
fix(lighthouse-ci): unblock nextjs-16 + astro-5; drop react-router-7-spa
HazAT May 12, 2026
b29aada
fix(lighthouse-ci): upload artifacts from the treosh action's results…
HazAT May 12, 2026
b96bb65
fix(lighthouse-ci): include hidden files when uploading .lighthouseci…
HazAT May 12, 2026
9809ea1
fix(lighthouse-ci): attach default-browser listeners synchronously to…
HazAT May 13, 2026
1549ea8
chore(lighthouse-ci): trim matrix to instrumented apps only (default-…
HazAT May 13, 2026
760b128
fix(lighthouse-ci): run on every PR + nightly, drop label gate
HazAT May 13, 2026
8288dcb
feat(lighthouse-ci): render report as Job Summary; sync APPS list to …
HazAT May 13, 2026
7c24cff
chore(lighthouse-ci): extract to standalone nightly workflow
HazAT May 13, 2026
06f87a5
refactor(lighthouse-ci): drop PR-commenting logic from report script
HazAT May 13, 2026
bb5ee63
feat(react-19-e2e): add SENTRY_LIGHTHOUSE_MODE instrumentation
HazAT May 13, 2026
df529c5
feat(lighthouse-ci): bundle test apps and upload to lighthouse.sentry…
HazAT May 13, 2026
d876721
ci(lighthouse): TEMP — fire workflow on push to feat/lighthouse-ci
HazAT May 13, 2026
a271b87
fix(lighthouse-ci): update root workspaces to lighthouse-bundle
HazAT May 13, 2026
1800bf0
fix(lighthouse-ci): upload build-output artifact for restore-cache
HazAT May 13, 2026
48c2db9
refactor(lighthouse-ci): fire-and-forget upload, fix SSR install, har…
HazAT May 13, 2026
cad23e0
revert(react-router-7-spa-e2e): restore original entry, drop dead lig…
HazAT May 13, 2026
9ba92f4
ci(lighthouse): drop temp push trigger on feat/lighthouse-ci
HazAT May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- v8
- release/**
pull_request:
types: [opened, synchronize, reopened]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated to this here and can likely be removed from this PR?

merge_group:
types: [checks_requested]
workflow_dispatch:
Expand Down
143 changes: 143 additions & 0 deletions .github/workflows/lighthouse.yml
Original file line number Diff line number Diff line change
@@ -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).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# 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:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO we can just combine all of this into one job and avoid all the caching and artifact handling, making this much simpler!

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<<EOF'
yarn --silent ci:print-build-artifact-paths
echo 'EOF'
} >> "$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
Original file line number Diff line number Diff line change
Expand Up @@ -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: '' }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing DSN no longer fails the build

Low Severity

Changing webpack.EnvironmentPlugin(['E2E_TEST_DSN']) to webpack.EnvironmentPlugin({ E2E_TEST_DSN: '', SENTRY_LIGHTHOUSE_MODE: '' }) means a missing E2E_TEST_DSN no longer causes a build failure — it silently defaults to ''. With an empty DSN, Sentry.init won't capture events, so any E2E test using waitForError or waitForTransaction would time out instead of surfacing a clear build-time error. The SENTRY_LIGHTHOUSE_MODE default could be kept separate (e.g. a second EnvironmentPlugin call) to preserve the fail-fast behavior for the DSN.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9ba92f4. Configure here.

new HtmlWebpackPlugin({
template: path.join(__dirname, 'public/index.html'),
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,49 @@
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!');
});

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') {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm not a fan of this, this complicates/changes the core thing we are testing here, which is a really straightforward sentry setup. this is not really the recommended way to run sentry at all and we heavily discourage users from doing this - it will also have different performance implications and lighthouse scores compared to using sentry "normally".

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',
});
})();
}
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Exporting onRouterTransitionStart as undefined in no-sentry mode will likely cause a TypeError during navigation in Next.js, crashing the test application.
Severity: MEDIUM

Suggested Fix

To fix this, avoid exporting onRouterTransitionStart as undefined. Instead, ensure that the module does not export onRouterTransitionStart at all when it's not needed. This allows the Next.js framework to correctly treat the hook as non-existent rather than attempting to call an undefined value.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
dev-packages/e2e-tests/test-applications/nextjs-16/instrumentation-client.ts#L42

Potential issue: In the `no-sentry` Lighthouse mode, `onRouterTransitionStart` is
exported as `undefined`. The Next.js framework, which consumes this hook, likely does
not check if the value is a function before calling it during router transitions. This
will cause a `TypeError: onRouterTransitionStart is not a function` at runtime, crashing
the application. This behavior is confined to the Lighthouse lab test environment and
will cause those specific tests to fail.

72 changes: 52 additions & 20 deletions dev-packages/e2e-tests/test-applications/react-19/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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(
<div>
<Index />
</div>,
);
} 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(
<div>
<Index />
</div>,
);
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<typeof Sentry.init>[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(
<div>
<Index />
</div>,
);
})();
Comment thread
sentry[bot] marked this conversation as resolved.
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Async rendering changes E2E test app behavior

Low Severity

For normal E2E runs (env var unset), the react-19 app now renders the entire React tree inside an async IIFE after a dynamic import('@sentry/react'). Previously, rendering was synchronous. Since webpack code-splits the dynamic import into a separate chunk fetched over the network, the DOM stays empty until that fetch completes. Similarly, default-browser now initializes Sentry asynchronously, so window.onerror isn't hooked until the chunk loads. While Playwright's auto-wait mitigates this, it introduces a race between chunk loading and test interactions that could cause flaky timeouts under CI load.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9ba92f4. Configure here.

Loading
Loading