diff --git a/.craft.yml b/.craft.yml
index 185fa2fd0510..f08ee2832d25 100644
--- a/.craft.yml
+++ b/.craft.yml
@@ -206,6 +206,8 @@ targets:
onlyIfPresent: /^sentry-remix-\d.*\.tgz$/
'npm:@sentry/solid':
onlyIfPresent: /^sentry-solid-\d.*\.tgz$/
+ 'npm:@sentry/solidstart':
+ onlyIfPresent: /^sentry-solidstart-\d.*\.tgz$/
'npm:@sentry/svelte':
onlyIfPresent: /^sentry-svelte-\d.*\.tgz$/
'npm:@sentry/sveltekit':
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8ab03a313253..cd5b7a7447ec 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -96,6 +96,8 @@ jobs:
profiling_node:
- 'packages/profiling-node/**'
- 'dev-packages/e2e-tests/test-applications/node-profiling/**'
+ any_code:
+ - '!**/*.md'
- name: Get PR labels
@@ -109,6 +111,8 @@ jobs:
is_release: ${{ startsWith(github.ref, 'refs/heads/release/') }}
changed_profiling_node: ${{ steps.changed.outputs.profiling_node == 'true' }}
changed_ci: ${{ steps.changed.outputs.workflow == 'true' }}
+ changed_any_code: ${{ steps.changed.outputs.any_code == 'true' }}
+
# When merging into master, or from master
is_gitflow_sync: ${{ github.head_ref == 'master' || github.ref == 'refs/heads/master' }}
has_gitflow_label:
@@ -123,6 +127,7 @@ jobs:
runs-on: ubuntu-20.04
timeout-minutes: 15
if: |
+ needs.job_get_metadata.outputs.changed_any_code == 'true' &&
(needs.job_get_metadata.outputs.is_gitflow_sync == 'false' && needs.job_get_metadata.outputs.has_gitflow_label == 'false')
steps:
- name: Check out base commit (${{ github.event.pull_request.base.sha }})
@@ -274,7 +279,7 @@ jobs:
job_check_format:
name: Check file formatting
- needs: [job_get_metadata, job_build]
+ needs: [job_get_metadata]
timeout-minutes: 10
runs-on: ubuntu-20.04
steps:
@@ -282,16 +287,29 @@ jobs:
uses: actions/checkout@v4
with:
ref: ${{ env.HEAD_COMMIT }}
+
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
+
+ # we use a hash of yarn.lock as our cache key, because if it hasn't changed, our dependencies haven't changed,
+ # so no need to reinstall them
+ - name: Compute dependency cache key
+ id: compute_lockfile_hash
+ run: echo "hash=${{ hashFiles('yarn.lock', '**/package.json') }}" >> "$GITHUB_OUTPUT"
+
- name: Check dependency cache
- uses: actions/cache/restore@v4
+ uses: actions/cache@v4
+ id: cache_dependencies
with:
path: ${{ env.CACHED_DEPENDENCY_PATHS }}
- key: ${{ needs.job_build.outputs.dependency_cache_key }}
- fail-on-cache-miss: true
+ key: ${{ steps.compute_lockfile_hash.outputs.hash }}
+
+ - name: Install dependencies
+ if: steps.cache_dependencies.outputs.cache-hit != 'true'
+ run: yarn install --ignore-engines --frozen-lockfile
+
- name: Check file formatting
run: yarn lint:prettier && yarn lint:biome
@@ -818,10 +836,10 @@ jobs:
pattern: profiling-node-binaries-${{ github.sha }}-*
path: ${{ github.workspace }}/packages/profiling-node/lib/
merge-multiple: true
+ # End rebuild profiling
- - name: Build Profiling tarball
+ - name: Build tarballs
run: yarn build:tarball
- # End rebuild profiling
- name: Stores tarballs in cache
uses: actions/cache/save@v4
@@ -867,6 +885,7 @@ jobs:
'create-remix-app-express',
'create-remix-app-express-legacy',
'create-remix-app-express-vite-dev',
+ 'default-browser',
'node-express-esm-loader',
'node-express-esm-preload',
'node-express-esm-without-loader',
diff --git a/.size-limit.js b/.size-limit.js
index 437e466a89e1..859ce741cc3d 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -55,7 +55,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'),
gzip: true,
- limit: '90 KB',
+ limit: '91 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay, Feedback, metrics)',
@@ -143,7 +143,7 @@ module.exports = [
name: 'CDN Bundle (incl. Tracing)',
path: createCDNPath('bundle.tracing.min.js'),
gzip: true,
- limit: '37 KB',
+ limit: '38 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay)',
@@ -170,7 +170,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.min.js'),
gzip: false,
brotli: false,
- limit: '110 KB',
+ limit: '111 KB',
},
{
name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed',
@@ -193,7 +193,7 @@ module.exports = [
import: createImport('init'),
ignore: ['next/router', 'next/constants'],
gzip: true,
- limit: '38 KB',
+ limit: '39 KB',
},
// SvelteKit SDK (ESM)
{
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 1a8f9ce92cfc..615ca5b24472 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -36,10 +36,11 @@
],
"deno.enablePaths": ["packages/deno/test"],
"editor.codeActionsOnSave": {
- "source.organizeImports.biome": "explicit",
+ "source.organizeImports.biome": "explicit"
},
"editor.defaultFormatter": "biomejs.biome",
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
- }
+ },
+ "cSpell.words": ["arrayify"]
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e8d141efd95..be7298ed213a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,60 @@
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
+## 8.26.0
+
+### Important Changes
+
+- **feat(node): Add `fsInstrumentation` (#13291)**
+
+ This release adds `fsIntegration`, an integration that instruments the `fs` API to the Sentry Node SDK. The
+ integration creates spans with naming patterns of `fs.readFile`, `fs.unlink`, and so on.
+
+ This integration is not enabled by default and needs to be registered in your `Sentry.init` call. You can configure
+ via options whether to include path arguments or error messages as span attributes when an fs call fails:
+
+ ```js
+ Sentry.init({
+ integrations: [
+ Sentry.fsIntegration({
+ recordFilePaths: true,
+ recordErrorMessagesAsSpanAttributes: true,
+ }),
+ ],
+ });
+ ```
+
+ **WARNING:** This integration may add significant overhead to your application. Especially in scenarios with a lot of
+ file I/O, like for example when running a framework dev server, including this integration can massively slow down
+ your application.
+
+### Other Changes
+
+- feat(browser): Add spotlightBrowser integration (#13263)
+- feat(browser): Allow sentry in safari extension background page (#13209)
+- feat(browser): Send CLS as standalone span (experimental) (#13056)
+- feat(core): Add OpenTelemetry-specific `getTraceData` implementation (#13281)
+- feat(nextjs): Always add `browserTracingIntegration` (#13324)
+- feat(nextjs): Always transmit trace data to the client (#13337)
+- feat(nextjs): export SentryBuildOptions (#13296)
+- feat(nextjs): Update `experimental_captureRequestError` to reflect `RequestInfo.path` change in Next.js canary
+ (#13344)
+
+- feat(nuxt): Always add tracing meta tags (#13273)
+- feat(nuxt): Set transaction name for server error (#13292)
+- feat(replay): Add a replay-specific logger (#13256)
+- feat(sveltekit): Add bundle size optimizations to plugin options (#13318)
+- feat(sveltekit): Always add browserTracingIntegration (#13322)
+- feat(tracing): Make long animation frames opt-out (#13255)
+- fix(astro): Correctly extract request data (#13315)
+- fix(astro): Only track access request headers in dynamic page requests (#13306)
+- fix(nuxt): Add import line for disabled `autoImport` (#13342)
+- fix(nuxt): Add vue to excludeEsmLoaderHooks array (#13346)
+- fix(opentelemetry): Do not overwrite http span name if kind is internal (#13282)
+- fix(remix): Ensure `origin` is correctly set for remix server spans (#13305)
+
+Work in this release was contributed by @MonstraG, @undead-voron and @Zen-cronic. Thank you for your contributions!
+
## 8.25.0
### Important Changes
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts
index bb219eda38c7..4ebea3457af6 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts
@@ -33,9 +33,11 @@ sentryTest('should capture interaction transaction. @firefox', async ({ browserN
expect(eventData.contexts).toMatchObject({ trace: { op: 'ui.action.click' } });
expect(eventData.platform).toBe('javascript');
expect(eventData.type).toBe('transaction');
- expect(eventData.spans).toHaveLength(1);
- const interactionSpan = eventData.spans![0];
+ const spans = eventData.spans?.filter(span => !span.op?.startsWith('ui.long-animation-frame'));
+ expect(spans).toHaveLength(1);
+
+ const interactionSpan = spans![0];
expect(interactionSpan.op).toBe('ui.interaction.click');
expect(interactionSpan.description).toBe('body > button.clicked');
expect(interactionSpan.timestamp).toBeDefined();
@@ -63,7 +65,8 @@ sentryTest(
await page.waitForTimeout(1000);
await page.locator('[data-test-id=interaction-button]').click();
const envelope = await envelopePromise;
- expect(envelope[0].spans).toHaveLength(1);
+ const spans = envelope[0].spans?.filter(span => !span.op?.startsWith('ui.long-animation-frame'));
+ expect(spans).toHaveLength(1);
}
},
);
@@ -89,10 +92,10 @@ sentryTest(
const envelopes = await envelopePromise;
expect(envelopes).toHaveLength(1);
const eventData = envelopes[0];
+ const spans = eventData.spans?.filter(span => !span.op?.startsWith('ui.long-animation-frame'));
+ expect(spans).toHaveLength(1);
- expect(eventData.spans).toHaveLength(1);
-
- const interactionSpan = eventData.spans![0];
+ const interactionSpan = spans![0];
expect(interactionSpan.op).toBe('ui.interaction.click');
expect(interactionSpan.description).toBe('body > AnnotatedButton');
},
@@ -120,9 +123,10 @@ sentryTest(
expect(envelopes).toHaveLength(1);
const eventData = envelopes[0];
- expect(eventData.spans).toHaveLength(1);
+ const spans = eventData.spans?.filter(span => !span.op?.startsWith('ui.long-animation-frame'));
+ expect(spans).toHaveLength(1);
- const interactionSpan = eventData.spans![0];
+ const interactionSpan = spans![0];
expect(interactionSpan.op).toBe('ui.interaction.click');
expect(interactionSpan.description).toBe('body > StyledButton');
},
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js
index bde12a1304ed..e1b3f6b13b01 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-disabled/init.js
@@ -4,6 +4,8 @@ window.Sentry = Sentry;
Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
- integrations: [Sentry.browserTracingIntegration({ enableLongTask: false, idleTimeout: 9000 })],
+ integrations: [
+ Sentry.browserTracingIntegration({ enableLongTask: false, enableLongAnimationFrame: false, idleTimeout: 9000 }),
+ ],
tracesSampleRate: 1,
});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js
index ad1d8832b228..319dfaadd4a8 100644
--- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js
+++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/long-tasks-enabled/init.js
@@ -7,6 +7,7 @@ Sentry.init({
integrations: [
Sentry.browserTracingIntegration({
idleTimeout: 9000,
+ enableLongAnimationFrame: false,
}),
],
tracesSampleRate: 1,
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/init.js
new file mode 100644
index 000000000000..32fbb07fbbae
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/init.js
@@ -0,0 +1,17 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ integrations: [
+ Sentry.browserTracingIntegration({
+ idleTimeout: 9000,
+ _experiments: {
+ enableStandaloneClsSpans: true,
+ },
+ }),
+ ],
+ tracesSampleRate: 1,
+ debug: true,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/subject.js
new file mode 100644
index 000000000000..ed1b9b790bb9
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/subject.js
@@ -0,0 +1,17 @@
+import { simulateCLS } from '../../../../utils/web-vitals/cls.ts';
+
+// Simulate Layout shift right at the beginning of the page load, depending on the URL hash
+// don't run if expected CLS is NaN
+const expectedCLS = Number(location.hash.slice(1));
+if (expectedCLS && expectedCLS >= 0) {
+ simulateCLS(expectedCLS).then(() => window.dispatchEvent(new Event('cls-done')));
+}
+
+// Simulate layout shift whenever the trigger-cls event is dispatched
+// Cannot trigger cia a button click because expected layout shift after
+// an interaction doesn't contribute to CLS.
+window.addEventListener('trigger-cls', () => {
+ simulateCLS(0.1).then(() => {
+ window.dispatchEvent(new Event('cls-done'));
+ });
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/template.html
new file mode 100644
index 000000000000..487683893a7f
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/template.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+ Some content
+
+
+
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/test.ts
new file mode 100644
index 000000000000..cdf1e6837ef4
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/test.ts
@@ -0,0 +1,455 @@
+import type { Page } from '@playwright/test';
+import { expect } from '@playwright/test';
+import type { Event as SentryEvent, EventEnvelope, SpanEnvelope } from '@sentry/types';
+
+import { sentryTest } from '../../../../utils/fixtures';
+import {
+ getFirstSentryEnvelopeRequest,
+ getMultipleSentryEnvelopeRequests,
+ properFullEnvelopeRequestParser,
+ shouldSkipTracingTest,
+} from '../../../../utils/helpers';
+
+sentryTest.beforeEach(async ({ browserName, page }) => {
+ if (shouldSkipTracingTest() || browserName !== 'chromium') {
+ sentryTest.skip();
+ }
+
+ await page.setViewportSize({ width: 800, height: 1200 });
+});
+
+function waitForLayoutShift(page: Page): Promise {
+ return page.evaluate(() => {
+ return new Promise(resolve => {
+ window.addEventListener('cls-done', () => resolve());
+ });
+ });
+}
+
+function triggerAndWaitForLayoutShift(page: Page): Promise {
+ return page.evaluate(() => {
+ window.dispatchEvent(new CustomEvent('trigger-cls'));
+ return new Promise(resolve => {
+ window.addEventListener('cls-done', () => resolve());
+ });
+ });
+}
+
+function hidePage(page: Page): Promise {
+ return page.evaluate(() => {
+ window.dispatchEvent(new Event('pagehide'));
+ });
+}
+
+sentryTest('captures a "GOOD" CLS vital with its source as a standalone span', async ({ getLocalTestPath, page }) => {
+ const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'span' },
+ properFullEnvelopeRequestParser,
+ );
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+ await page.goto(`${url}#0.05`);
+
+ await waitForLayoutShift(page);
+
+ await hidePage(page);
+
+ const spanEnvelope = (await spanEnvelopePromise)[0];
+
+ const spanEnvelopeHeaders = spanEnvelope[0];
+ const spanEnvelopeItem = spanEnvelope[1][0][1];
+
+ expect(spanEnvelopeItem).toEqual({
+ data: {
+ 'sentry.exclusive_time': 0,
+ 'sentry.op': 'ui.webvital.cls',
+ 'sentry.origin': 'auto.http.browser.cls',
+ transaction: expect.stringContaining('index.html'),
+ 'user_agent.original': expect.stringContaining('Chrome'),
+ 'sentry.pageload.span_id': expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ description: expect.stringContaining('body > div#content > p'),
+ exclusive_time: 0,
+ measurements: {
+ cls: {
+ unit: '',
+ value: expect.any(Number), // better check below,
+ },
+ },
+ op: 'ui.webvital.cls',
+ origin: 'auto.http.browser.cls',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ segment_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: spanEnvelopeItem.start_timestamp,
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ // Flakey value dependent on timings -> we check for a range
+ expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0.03);
+ expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.07);
+
+ expect(spanEnvelopeHeaders).toEqual({
+ sent_at: expect.any(String),
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: spanEnvelopeItem.trace_id,
+ // no transaction, because span source is URL
+ },
+ });
+});
+
+sentryTest('captures a "MEH" CLS vital with its source as a standalone span', async ({ getLocalTestPath, page }) => {
+ const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'span' },
+ properFullEnvelopeRequestParser,
+ );
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+ await page.goto(`${url}#0.21`);
+
+ await waitForLayoutShift(page);
+
+ // Page hide to trigger CLS emission
+ await page.evaluate(() => {
+ window.dispatchEvent(new Event('pagehide'));
+ });
+
+ const spanEnvelope = (await spanEnvelopePromise)[0];
+
+ const spanEnvelopeHeaders = spanEnvelope[0];
+ const spanEnvelopeItem = spanEnvelope[1][0][1];
+
+ expect(spanEnvelopeItem).toEqual({
+ data: {
+ 'sentry.exclusive_time': 0,
+ 'sentry.op': 'ui.webvital.cls',
+ 'sentry.origin': 'auto.http.browser.cls',
+ transaction: expect.stringContaining('index.html'),
+ 'user_agent.original': expect.stringContaining('Chrome'),
+ 'sentry.pageload.span_id': expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ description: expect.stringContaining('body > div#content > p'),
+ exclusive_time: 0,
+ measurements: {
+ cls: {
+ unit: '',
+ value: expect.any(Number), // better check below,
+ },
+ },
+ op: 'ui.webvital.cls',
+ origin: 'auto.http.browser.cls',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ segment_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: spanEnvelopeItem.start_timestamp,
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ // Flakey value dependent on timings -> we check for a range
+ expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0.18);
+ expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.23);
+
+ expect(spanEnvelopeHeaders).toEqual({
+ sent_at: expect.any(String),
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: spanEnvelopeItem.trace_id,
+ // no transaction, because span source is URL
+ },
+ });
+});
+
+sentryTest('captures a "POOR" CLS vital with its source as a standalone span.', async ({ getLocalTestPath, page }) => {
+ const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'span' },
+ properFullEnvelopeRequestParser,
+ );
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+ await page.goto(`${url}#0.35`);
+
+ await waitForLayoutShift(page);
+
+ // Page hide to trigger CLS emission
+ await hidePage(page);
+
+ const spanEnvelope = (await spanEnvelopePromise)[0];
+
+ const spanEnvelopeHeaders = spanEnvelope[0];
+ const spanEnvelopeItem = spanEnvelope[1][0][1];
+
+ expect(spanEnvelopeItem).toEqual({
+ data: {
+ 'sentry.exclusive_time': 0,
+ 'sentry.op': 'ui.webvital.cls',
+ 'sentry.origin': 'auto.http.browser.cls',
+ transaction: expect.stringContaining('index.html'),
+ 'user_agent.original': expect.stringContaining('Chrome'),
+ 'sentry.pageload.span_id': expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ description: expect.stringContaining('body > div#content > p'),
+ exclusive_time: 0,
+ measurements: {
+ cls: {
+ unit: '',
+ value: expect.any(Number), // better check below,
+ },
+ },
+ op: 'ui.webvital.cls',
+ origin: 'auto.http.browser.cls',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ segment_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: spanEnvelopeItem.start_timestamp,
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ // Flakey value dependent on timings -> we check for a range
+ expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0.33);
+ expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.38);
+
+ expect(spanEnvelopeHeaders).toEqual({
+ sent_at: expect.any(String),
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: spanEnvelopeItem.trace_id,
+ // no transaction, because span source is URL
+ },
+ });
+});
+
+sentryTest(
+ 'captures a 0 CLS vital as a standalone span if no layout shift occurred',
+ async ({ getLocalTestPath, page }) => {
+ const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'span' },
+ properFullEnvelopeRequestParser,
+ );
+
+ const url = await getLocalTestPath({ testDir: __dirname });
+ await page.goto(url);
+
+ await page.waitForTimeout(1000);
+
+ await hidePage(page);
+
+ const spanEnvelope = (await spanEnvelopePromise)[0];
+
+ const spanEnvelopeHeaders = spanEnvelope[0];
+ const spanEnvelopeItem = spanEnvelope[1][0][1];
+
+ expect(spanEnvelopeItem).toEqual({
+ data: {
+ 'sentry.exclusive_time': 0,
+ 'sentry.op': 'ui.webvital.cls',
+ 'sentry.origin': 'auto.http.browser.cls',
+ transaction: expect.stringContaining('index.html'),
+ 'user_agent.original': expect.stringContaining('Chrome'),
+ 'sentry.pageload.span_id': expect.stringMatching(/[a-f0-9]{16}/),
+ },
+ description: 'Layout shift',
+ exclusive_time: 0,
+ measurements: {
+ cls: {
+ unit: '',
+ value: 0,
+ },
+ },
+ op: 'ui.webvital.cls',
+ origin: 'auto.http.browser.cls',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ segment_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: spanEnvelopeItem.start_timestamp,
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ expect(spanEnvelopeHeaders).toEqual({
+ sent_at: expect.any(String),
+ trace: {
+ environment: 'production',
+ public_key: 'public',
+ sample_rate: '1',
+ sampled: 'true',
+ trace_id: spanEnvelopeItem.trace_id,
+ // no transaction, because span source is URL
+ },
+ });
+ },
+);
+
+sentryTest(
+ 'captures CLS increases after the pageload span ended, when page is hidden',
+ async ({ getLocalTestPath, page }) => {
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+
+ expect(eventData.type).toBe('transaction');
+ expect(eventData.contexts?.trace?.op).toBe('pageload');
+
+ const pageloadSpanId = eventData.contexts?.trace?.span_id;
+ const pageloadTraceId = eventData.contexts?.trace?.trace_id;
+
+ expect(pageloadSpanId).toMatch(/[a-f0-9]{16}/);
+ expect(pageloadTraceId).toMatch(/[a-f0-9]{32}/);
+
+ const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'span' },
+ properFullEnvelopeRequestParser,
+ );
+
+ await triggerAndWaitForLayoutShift(page);
+
+ await hidePage(page);
+
+ const spanEnvelope = (await spanEnvelopePromise)[0];
+ const spanEnvelopeItem = spanEnvelope[1][0][1];
+ // Flakey value dependent on timings -> we check for a range
+ expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0.05);
+ expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.15);
+
+ // Ensure the CLS span is connected to the pageload span and trace
+ expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toBe(pageloadSpanId);
+ expect(spanEnvelopeItem.trace_id).toEqual(pageloadTraceId);
+ },
+);
+
+sentryTest('sends CLS of the initial page when soft-navigating to a new page', async ({ getLocalTestPath, page }) => {
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+
+ expect(eventData.type).toBe('transaction');
+ expect(eventData.contexts?.trace?.op).toBe('pageload');
+
+ const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'span' },
+ properFullEnvelopeRequestParser,
+ );
+
+ await triggerAndWaitForLayoutShift(page);
+
+ await page.goto(`${url}#soft-navigation`);
+
+ const spanEnvelope = (await spanEnvelopePromise)[0];
+ const spanEnvelopeItem = spanEnvelope[1][0][1];
+ // Flakey value dependent on timings -> we check for a range
+ expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0.05);
+ expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.15);
+ expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toMatch(/[a-f0-9]{16}/);
+});
+
+sentryTest("doesn't send further CLS after the first navigation", async ({ getLocalTestPath, page }) => {
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+
+ expect(eventData.type).toBe('transaction');
+ expect(eventData.contexts?.trace?.op).toBe('pageload');
+
+ const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'span' },
+ properFullEnvelopeRequestParser,
+ );
+
+ await triggerAndWaitForLayoutShift(page);
+
+ await page.goto(`${url}#soft-navigation`);
+
+ const spanEnvelope = (await spanEnvelopePromise)[0];
+ const spanEnvelopeItem = spanEnvelope[1][0][1];
+ expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0);
+
+ getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'span' }, () => {
+ throw new Error('Unexpected span - This should not happen!');
+ });
+
+ const navigationTxnPromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'transaction' },
+ properFullEnvelopeRequestParser,
+ );
+
+ // activate both CLS emission triggers:
+ await page.goto(`${url}#soft-navigation-2`);
+ await hidePage(page);
+
+ // assumption: If we would send another CLS span on the 2nd navigation, it would be sent before the navigation
+ // transaction ends. This isn't 100% safe to ensure we don't send something but otherwise we'd need to wait for
+ // a timeout or something similar.
+ await navigationTxnPromise;
+});
+
+sentryTest("doesn't send further CLS after the first page hide", async ({ getLocalTestPath, page }) => {
+ const url = await getLocalTestPath({ testDir: __dirname });
+
+ const eventData = await getFirstSentryEnvelopeRequest(page, url);
+
+ expect(eventData.type).toBe('transaction');
+ expect(eventData.contexts?.trace?.op).toBe('pageload');
+
+ const spanEnvelopePromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'span' },
+ properFullEnvelopeRequestParser,
+ );
+
+ await triggerAndWaitForLayoutShift(page);
+
+ await hidePage(page);
+
+ const spanEnvelope = (await spanEnvelopePromise)[0];
+ const spanEnvelopeItem = spanEnvelope[1][0][1];
+ expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0);
+
+ getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'span' }, () => {
+ throw new Error('Unexpected span - This should not happen!');
+ });
+
+ const navigationTxnPromise = getMultipleSentryEnvelopeRequests(
+ page,
+ 1,
+ { envelopeType: 'transaction' },
+ properFullEnvelopeRequestParser,
+ );
+
+ // activate both CLS emission triggers:
+ await page.goto(`${url}#soft-navigation-2`);
+ await hidePage(page);
+
+ // assumption: If we would send another CLS span on the 2nd navigation, it would be sent before the navigation
+ // transaction ends. This isn't 100% safe to ensure we don't send something but otherwise we'd need to wait for
+ // a timeout or something similar.
+ await navigationTxnPromise;
+});
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/.gitignore b/dev-packages/e2e-tests/test-applications/default-browser/.gitignore
new file mode 100644
index 000000000000..84634c973eeb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/default-browser/.gitignore
@@ -0,0 +1,29 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+/test-results/
+/playwright-report/
+/playwright/.cache/
+
+!*.d.ts
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/.npmrc b/dev-packages/e2e-tests/test-applications/default-browser/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/default-browser/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/build.mjs b/dev-packages/e2e-tests/test-applications/default-browser/build.mjs
new file mode 100644
index 000000000000..aeaad894bdbd
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/default-browser/build.mjs
@@ -0,0 +1,49 @@
+import * as path from 'path';
+import * as url from 'url';
+import HtmlWebpackPlugin from 'html-webpack-plugin';
+import TerserPlugin from 'terser-webpack-plugin';
+import webpack from 'webpack';
+
+const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
+
+webpack(
+ {
+ entry: path.join(__dirname, 'src/index.js'),
+ output: {
+ path: path.join(__dirname, 'build'),
+ filename: 'app.js',
+ },
+ optimization: {
+ minimize: true,
+ minimizer: [new TerserPlugin()],
+ },
+ plugins: [
+ new webpack.EnvironmentPlugin(['E2E_TEST_DSN']),
+ new HtmlWebpackPlugin({
+ template: path.join(__dirname, 'public/index.html'),
+ }),
+ ],
+ mode: 'production',
+ },
+ (err, stats) => {
+ if (err) {
+ console.error(err.stack || err);
+ if (err.details) {
+ console.error(err.details);
+ }
+ return;
+ }
+
+ const info = stats.toJson();
+
+ if (stats.hasErrors()) {
+ console.error(info.errors);
+ process.exit(1);
+ }
+
+ if (stats.hasWarnings()) {
+ console.warn(info.warnings);
+ process.exit(1);
+ }
+ },
+);
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/package.json b/dev-packages/e2e-tests/test-applications/default-browser/package.json
new file mode 100644
index 000000000000..d6286c2423b6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/default-browser/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "default-browser-test-app",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@sentry/browser": "latest || *",
+ "@types/node": "16.7.13",
+ "typescript": "4.9.5"
+ },
+ "scripts": {
+ "start": "serve -s build",
+ "build": "node build.mjs",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && npx playwright install && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.44.1",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "webpack": "^5.91.0",
+ "serve": "14.0.1",
+ "terser-webpack-plugin": "^5.3.10",
+ "html-webpack-plugin": "^5.6.0"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/default-browser/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/default-browser/playwright.config.mjs
@@ -0,0 +1,7 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: `pnpm start`,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/public/index.html b/dev-packages/e2e-tests/test-applications/default-browser/public/index.html
new file mode 100644
index 000000000000..35e91be91c84
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/default-browser/public/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Default Browser App
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 000000000000..d3eea216fe84
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/default-browser/src/index.js
@@ -0,0 +1,18 @@
+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',
+});
+
+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' });
+});
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/default-browser/start-event-proxy.mjs
new file mode 100644
index 000000000000..6c84e74d541b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/default-browser/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'default-browser',
+});
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/default-browser/tests/errors.test.ts
new file mode 100644
index 000000000000..e4f2eda9a579
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/default-browser/tests/errors.test.ts
@@ -0,0 +1,58 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+test('captures an error', async ({ page }) => {
+ const errorEventPromise = waitForError('default-browser', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
+ });
+
+ await page.goto('/');
+
+ const exceptionButton = page.locator('id=exception-button');
+ await exceptionButton.click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!');
+
+ expect(errorEvent.transaction).toBe('/');
+
+ expect(errorEvent.request).toEqual({
+ url: 'http://localhost:3030/',
+ headers: expect.any(Object),
+ });
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: expect.any(String),
+ span_id: expect.any(String),
+ });
+});
+
+test('sets correct transactionName', async ({ page }) => {
+ const transactionPromise = waitForTransaction('default-browser', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ const errorEventPromise = waitForError('default-browser', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
+ });
+
+ await page.goto('/');
+ const transactionEvent = await transactionPromise;
+
+ const exceptionButton = page.locator('id=exception-button');
+ await exceptionButton.click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!');
+
+ expect(errorEvent.transaction).toEqual('/');
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: transactionEvent.contexts?.trace?.trace_id,
+ span_id: expect.not.stringContaining(transactionEvent.contexts?.trace?.span_id || ''),
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/default-browser/tests/performance.test.ts
new file mode 100644
index 000000000000..7013fb43ecef
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/default-browser/tests/performance.test.ts
@@ -0,0 +1,118 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('captures a pageload transaction', async ({ page }) => {
+ const transactionPromise = waitForTransaction('default-browser', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto(`/`);
+
+ const pageLoadTransaction = await transactionPromise;
+
+ expect(pageLoadTransaction).toEqual({
+ contexts: {
+ trace: {
+ data: expect.objectContaining({
+ 'sentry.idle_span_finish_reason': 'idleTimeout',
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.browser',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'url',
+ }),
+ op: 'pageload',
+ origin: 'auto.pageload.browser',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ },
+ environment: 'qa',
+ event_id: expect.stringMatching(/[a-f0-9]{32}/),
+ measurements: {
+ 'connection.rtt': {
+ unit: 'millisecond',
+ value: expect.any(Number),
+ },
+ fcp: {
+ unit: 'millisecond',
+ value: expect.any(Number),
+ },
+ fp: {
+ unit: 'millisecond',
+ value: expect.any(Number),
+ },
+ lcp: {
+ unit: 'millisecond',
+ value: expect.any(Number),
+ },
+ ttfb: {
+ unit: 'millisecond',
+ value: expect.any(Number),
+ },
+ 'ttfb.requestTime': {
+ unit: 'millisecond',
+ value: expect.any(Number),
+ },
+ },
+ platform: 'javascript',
+ release: 'e2e-test',
+ request: {
+ headers: {
+ 'User-Agent': expect.any(String),
+ },
+ url: 'http://localhost:3030/',
+ },
+ sdk: {
+ integrations: expect.any(Array),
+ name: 'sentry.javascript.browser',
+ packages: [
+ {
+ name: 'npm:@sentry/browser',
+ version: expect.any(String),
+ },
+ ],
+ version: expect.any(String),
+ },
+ spans: expect.any(Array),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: '/',
+ transaction_info: {
+ source: 'url',
+ },
+ type: 'transaction',
+ });
+});
+
+test('captures a navigation transaction', async ({ page }) => {
+ page.on('console', msg => console.log(msg.text()));
+ const pageLoadTransactionPromise = waitForTransaction('default-browser', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ const navigationTransactionPromise = waitForTransaction('default-browser', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+ await pageLoadTransactionPromise;
+
+ const linkElement = page.locator('id=navigation-link');
+
+ await linkElement.click();
+
+ const navigationTransaction = await navigationTransactionPromise;
+
+ expect(navigationTransaction).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.browser',
+ },
+ },
+ transaction: '/',
+ transaction_info: {
+ source: 'url',
+ },
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/default-browser/tsconfig.json b/dev-packages/e2e-tests/test-applications/default-browser/tsconfig.json
new file mode 100644
index 000000000000..4cc95dc2689a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/default-browser/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "es2018",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react"
+ },
+ "include": ["src", "tests"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
index 39fcedf174da..8fd9f4368977 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
@@ -17,7 +17,7 @@
"@types/node": "18.11.17",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
- "next": "15.0.0-canary.77",
+ "next": "15.0.0-canary.112",
"react": "beta",
"react-dom": "beta",
"typescript": "4.9.5"
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/app.vue b/dev-packages/e2e-tests/test-applications/nuxt-3/app.vue
index 06f3020220dd..23283a522546 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/app.vue
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/app.vue
@@ -3,6 +3,8 @@