diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts
index 0fcccd560af9..77393582e048 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/nuxt.config.ts
@@ -1,9 +1,17 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
modules: ['@sentry/nuxt/module'],
- imports: {
- autoImport: false,
+ imports: { autoImport: false },
+
+ routeRules: {
+ '/rendering-modes/client-side-only-page': { ssr: false },
+ '/rendering-modes/isr-cached-page': { isr: true },
+ '/rendering-modes/isr-1h-cached-page': { isr: 3600 },
+ '/rendering-modes/swr-cached-page': { swr: true },
+ '/rendering-modes/swr-1h-cached-page': { swr: 3600 },
+ '/rendering-modes/pre-rendered-page': { prerender: true },
},
+
runtimeConfig: {
public: {
sentry: {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/client-side-only-page.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/client-side-only-page.vue
new file mode 100644
index 000000000000..fb41b62b3308
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/client-side-only-page.vue
@@ -0,0 +1 @@
+Client Side Only Page
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/isr-1h-cached-page.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/isr-1h-cached-page.vue
new file mode 100644
index 000000000000..e702eca86715
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/isr-1h-cached-page.vue
@@ -0,0 +1 @@
+ISR 1h Cached Page
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/isr-cached-page.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/isr-cached-page.vue
new file mode 100644
index 000000000000..780adc07de53
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/isr-cached-page.vue
@@ -0,0 +1 @@
+ISR Cached Page
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/pre-rendered-page.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/pre-rendered-page.vue
new file mode 100644
index 000000000000..25b423a4c442
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/pre-rendered-page.vue
@@ -0,0 +1 @@
+Pre-Rendered Page
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/swr-1h-cached-page.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/swr-1h-cached-page.vue
new file mode 100644
index 000000000000..24918924f4a9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/swr-1h-cached-page.vue
@@ -0,0 +1 @@
+SWR 1h Cached Page
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/swr-cached-page.vue b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/swr-cached-page.vue
new file mode 100644
index 000000000000..d0d8e7241968
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/swr-cached-page.vue
@@ -0,0 +1 @@
+SWR Cached Page
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.cached-html.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.cached-html.test.ts
new file mode 100644
index 000000000000..4c31667feed0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.cached-html.test.ts
@@ -0,0 +1,206 @@
+import { expect, test, type Page } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test.describe('Rendering Modes with Cached HTML', () => {
+ test('changes tracing meta tags with multiple requests on Client-Side only page', async ({ page }) => {
+ await testChangingTracingMetaTagsOnISRPage(page, '/rendering-modes/client-side-only-page', 'Client Side Only Page');
+ });
+
+ test('changes tracing meta tags with multiple requests on ISR-cached page', async ({ page }) => {
+ await testChangingTracingMetaTagsOnISRPage(page, '/rendering-modes/isr-cached-page', 'ISR Cached Page');
+ });
+
+ test('changes tracing meta tags with multiple requests on 1h ISR-cached page', async ({ page }) => {
+ await testChangingTracingMetaTagsOnISRPage(page, '/rendering-modes/isr-1h-cached-page', 'ISR 1h Cached Page');
+ });
+
+ test('exclude tracing meta tags on SWR-cached page', async ({ page }) => {
+ await testExcludeTracingMetaTagsOnCachedPage(page, '/rendering-modes/swr-cached-page', 'SWR Cached Page');
+ });
+
+ test('exclude tracing meta tags on SWR 1h cached page', async ({ page }) => {
+ await testExcludeTracingMetaTagsOnCachedPage(page, '/rendering-modes/swr-1h-cached-page', 'SWR 1h Cached Page');
+ });
+
+ test('exclude tracing meta tags on pre-rendered page', async ({ page }) => {
+ await testExcludeTracingMetaTagsOnCachedPage(page, '/rendering-modes/pre-rendered-page', 'Pre-Rendered Page');
+ });
+});
+
+/**
+ * Tests that tracing meta-tags change with multiple requests on ISR-cached pages
+ * This utility handles the common pattern of:
+ * 1. Making two requests to an ISR-cached page
+ * 2. Verifying tracing meta-tags are present and change between requests
+ * 3. Verifying distributed tracing works correctly for both requests
+ * 4. Verifying trace IDs are different between requests
+ *
+ * @param page - Playwright page object
+ * @param routePath - The route path to test (e.g., '/rendering-modes/isr-cached-page')
+ * @param expectedPageText - The text to verify is visible on the page (e.g., 'ISR Cached Page')
+ */
+export async function testChangingTracingMetaTagsOnISRPage(
+ page: Page,
+ routePath: string,
+ expectedPageText: string,
+): Promise {
+ // === 1. Request ===
+ const clientTxnEventPromise1 = waitForTransaction('nuxt-3-min', txnEvent => {
+ return txnEvent.transaction === routePath;
+ });
+
+ const serverTxnEventPromise1 = waitForTransaction('nuxt-3-min', txnEvent => {
+ return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false;
+ });
+
+ const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([
+ page.goto(routePath),
+ clientTxnEventPromise1,
+ serverTxnEventPromise1,
+ expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(),
+ ]);
+
+ const baggageMetaTagContent1 = await page.locator('meta[name="baggage"]').getAttribute('content');
+ const sentryTraceMetaTagContent1 = await page.locator('meta[name="sentry-trace"]').getAttribute('content');
+ const [htmlMetaTraceId1] = sentryTraceMetaTagContent1?.split('-') || [];
+
+ // === 2. Request ===
+
+ const clientTxnEventPromise2 = waitForTransaction('nuxt-3-min', txnEvent => {
+ return txnEvent.transaction === routePath;
+ });
+
+ const serverTxnEventPromise2 = waitForTransaction('nuxt-3-min', txnEvent => {
+ return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false;
+ });
+
+ const [_2, clientTxnEvent2, serverTxnEvent2] = await Promise.all([
+ page.goto(routePath),
+ clientTxnEventPromise2,
+ serverTxnEventPromise2,
+ expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(),
+ ]);
+
+ const baggageMetaTagContent2 = await page.locator('meta[name="baggage"]').getAttribute('content');
+ const sentryTraceMetaTagContent2 = await page.locator('meta[name="sentry-trace"]').getAttribute('content');
+ const [htmlMetaTraceId2] = sentryTraceMetaTagContent2?.split('-') || [];
+
+ const serverTxnEvent1TraceId = serverTxnEvent1.contexts?.trace?.trace_id;
+ const serverTxnEvent2TraceId = serverTxnEvent2.contexts?.trace?.trace_id;
+
+ await test.step('Test distributed trace from 1. request', () => {
+ expect(baggageMetaTagContent1).toContain(`sentry-trace_id=${serverTxnEvent1TraceId}`);
+
+ expect(clientTxnEvent1.contexts?.trace?.trace_id).toBe(serverTxnEvent1TraceId);
+ expect(clientTxnEvent1.contexts?.trace?.parent_span_id).toBe(serverTxnEvent1.contexts?.trace?.span_id);
+ expect(serverTxnEvent1.contexts?.trace?.trace_id).toBe(htmlMetaTraceId1);
+ });
+
+ await test.step('Test distributed trace from 2. request', () => {
+ expect(baggageMetaTagContent2).toContain(`sentry-trace_id=${serverTxnEvent2TraceId}`);
+
+ expect(clientTxnEvent2.contexts?.trace?.trace_id).toBe(serverTxnEvent2TraceId);
+ expect(clientTxnEvent2.contexts?.trace?.parent_span_id).toBe(serverTxnEvent2.contexts?.trace?.span_id);
+ expect(serverTxnEvent2.contexts?.trace?.trace_id).toBe(htmlMetaTraceId2);
+ });
+
+ await test.step('Test that trace IDs from subsequent requests are different', () => {
+ // Different trace IDs for the server transactions
+ expect(serverTxnEvent1TraceId).toBeDefined();
+ expect(serverTxnEvent1TraceId).not.toBe(serverTxnEvent2TraceId);
+ expect(serverTxnEvent1TraceId).not.toBe(htmlMetaTraceId2);
+ });
+}
+
+/**
+ * Tests that tracing meta-tags are excluded on cached pages (SWR, pre-rendered, etc.)
+ * This utility handles the common pattern of:
+ * 1. Making two requests to a cached page
+ * 2. Verifying no tracing meta-tags are present
+ * 3. Verifying only the first request creates a server transaction
+ * 4. Verifying traces are not distributed
+ *
+ * @param page - Playwright page object
+ * @param routePath - The route path to test (e.g., '/rendering-modes/swr-cached-page')
+ * @param expectedPageText - The text to verify is visible on the page (e.g., 'SWR Cached Page')
+ * @returns Object containing transaction events for additional custom assertions
+ */
+export async function testExcludeTracingMetaTagsOnCachedPage(
+ page: Page,
+ routePath: string,
+ expectedPageText: string,
+): Promise {
+ // === 1. Request ===
+ const clientTxnEventPromise1 = waitForTransaction('nuxt-3-min', txnEvent => {
+ return txnEvent.transaction === routePath;
+ });
+
+ // Only the 1. request creates a server transaction
+ const serverTxnEventPromise1 = waitForTransaction('nuxt-3-min', txnEvent => {
+ return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false;
+ });
+
+ const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([
+ page.goto(routePath),
+ clientTxnEventPromise1,
+ serverTxnEventPromise1,
+ expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(),
+ ]);
+
+ // Verify no baggage and sentry-trace meta-tags are present on first request
+ expect(await page.locator('meta[name="baggage"]').count()).toBe(0);
+ expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(0);
+
+ // === 2. Request ===
+
+ await page.goto(routePath);
+
+ const clientTxnEventPromise2 = waitForTransaction('nuxt-3-min', txnEvent => {
+ return txnEvent.transaction === routePath;
+ });
+
+ let serverTxnEvent2 = undefined;
+ const serverTxnEventPromise2 = Promise.race([
+ waitForTransaction('nuxt-3-min', txnEvent => {
+ return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false;
+ }),
+ new Promise((_, reject) => setTimeout(() => reject(new Error('No second server transaction expected')), 2000)),
+ ]);
+
+ try {
+ serverTxnEvent2 = await serverTxnEventPromise2;
+ throw new Error('Second server transaction should not have been sent');
+ } catch (error) {
+ expect(error.message).toBe('No second server transaction expected');
+ }
+
+ const [clientTxnEvent2] = await Promise.all([
+ clientTxnEventPromise2,
+ expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(),
+ ]);
+
+ const clientTxnEvent1TraceId = clientTxnEvent1.contexts?.trace?.trace_id;
+ const clientTxnEvent2TraceId = clientTxnEvent2.contexts?.trace?.trace_id;
+
+ const serverTxnEvent1TraceId = serverTxnEvent1.contexts?.trace?.trace_id;
+ const serverTxnEvent2TraceId = serverTxnEvent2?.contexts?.trace?.trace_id;
+
+ await test.step('No baggage and sentry-trace meta-tags are present on second request', async () => {
+ expect(await page.locator('meta[name="baggage"]').count()).toBe(0);
+ expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(0);
+ });
+
+ await test.step('1. Server Transaction and all Client Transactions are defined', () => {
+ expect(serverTxnEvent1TraceId).toBeDefined();
+ expect(clientTxnEvent1TraceId).toBeDefined();
+ expect(clientTxnEvent2TraceId).toBeDefined();
+ expect(serverTxnEvent2).toBeUndefined();
+ expect(serverTxnEvent2TraceId).toBeUndefined();
+ });
+
+ await test.step('Trace is not distributed', () => {
+ // Cannot create distributed trace as HTML Meta Tags are not added (caching leads to multiple usages of the same server trace id)
+ expect(clientTxnEvent1TraceId).not.toBe(clientTxnEvent2TraceId);
+ expect(clientTxnEvent1TraceId).not.toBe(serverTxnEvent1TraceId);
+ });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/client-side-only-page.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/client-side-only-page.vue
new file mode 100644
index 000000000000..fb41b62b3308
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/client-side-only-page.vue
@@ -0,0 +1 @@
+Client Side Only Page
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/isr-1h-cached-page.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/isr-1h-cached-page.vue
new file mode 100644
index 000000000000..e702eca86715
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/isr-1h-cached-page.vue
@@ -0,0 +1 @@
+ISR 1h Cached Page
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/isr-cached-page.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/isr-cached-page.vue
new file mode 100644
index 000000000000..780adc07de53
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/isr-cached-page.vue
@@ -0,0 +1 @@
+ISR Cached Page
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/pre-rendered-page.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/pre-rendered-page.vue
new file mode 100644
index 000000000000..25b423a4c442
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/pre-rendered-page.vue
@@ -0,0 +1 @@
+Pre-Rendered Page
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/swr-1h-cached-page.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/swr-1h-cached-page.vue
new file mode 100644
index 000000000000..24918924f4a9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/swr-1h-cached-page.vue
@@ -0,0 +1 @@
+SWR 1h Cached Page
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/swr-cached-page.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/swr-cached-page.vue
new file mode 100644
index 000000000000..d0d8e7241968
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/swr-cached-page.vue
@@ -0,0 +1 @@
+SWR Cached Page
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
index 741d2d20706c..d0ae045f1e9d 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
@@ -3,6 +3,15 @@ export default defineNuxtConfig({
compatibilityDate: '2025-06-06',
imports: { autoImport: false },
+ routeRules: {
+ '/rendering-modes/client-side-only-page': { ssr: false },
+ '/rendering-modes/isr-cached-page': { isr: true },
+ '/rendering-modes/isr-1h-cached-page': { isr: 3600 },
+ '/rendering-modes/swr-cached-page': { swr: true },
+ '/rendering-modes/swr-1h-cached-page': { swr: 3600 },
+ '/rendering-modes/pre-rendered-page': { prerender: true },
+ },
+
modules: ['@pinia/nuxt', '@sentry/nuxt/module'],
runtimeConfig: {
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.cached-html.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.cached-html.test.ts
new file mode 100644
index 000000000000..1dea68dd77ff
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.cached-html.test.ts
@@ -0,0 +1,206 @@
+import { expect, test, type Page } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test.describe('Rendering Modes with Cached HTML', () => {
+ test('changes tracing meta tags with multiple requests on Client-Side only page', async ({ page }) => {
+ await testChangingTracingMetaTagsOnISRPage(page, '/rendering-modes/client-side-only-page', 'Client Side Only Page');
+ });
+
+ test('changes tracing meta tags with multiple requests on ISR-cached page', async ({ page }) => {
+ await testChangingTracingMetaTagsOnISRPage(page, '/rendering-modes/isr-cached-page', 'ISR Cached Page');
+ });
+
+ test('changes tracing meta tags with multiple requests on 1h ISR-cached page', async ({ page }) => {
+ await testChangingTracingMetaTagsOnISRPage(page, '/rendering-modes/isr-1h-cached-page', 'ISR 1h Cached Page');
+ });
+
+ test('exclude tracing meta tags on SWR-cached page', async ({ page }) => {
+ await testExcludeTracingMetaTagsOnCachedPage(page, '/rendering-modes/swr-cached-page', 'SWR Cached Page');
+ });
+
+ test('exclude tracing meta tags on SWR 1h cached page', async ({ page }) => {
+ await testExcludeTracingMetaTagsOnCachedPage(page, '/rendering-modes/swr-1h-cached-page', 'SWR 1h Cached Page');
+ });
+
+ test('exclude tracing meta tags on pre-rendered page', async ({ page }) => {
+ await testExcludeTracingMetaTagsOnCachedPage(page, '/rendering-modes/pre-rendered-page', 'Pre-Rendered Page');
+ });
+});
+
+/**
+ * Tests that tracing meta-tags change with multiple requests on ISR-cached pages
+ * This utility handles the common pattern of:
+ * 1. Making two requests to an ISR-cached page
+ * 2. Verifying tracing meta-tags are present and change between requests
+ * 3. Verifying distributed tracing works correctly for both requests
+ * 4. Verifying trace IDs are different between requests
+ *
+ * @param page - Playwright page object
+ * @param routePath - The route path to test (e.g., '/rendering-modes/isr-cached-page')
+ * @param expectedPageText - The text to verify is visible on the page (e.g., 'ISR Cached Page')
+ */
+export async function testChangingTracingMetaTagsOnISRPage(
+ page: Page,
+ routePath: string,
+ expectedPageText: string,
+): Promise {
+ // === 1. Request ===
+ const clientTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => {
+ return txnEvent.transaction === routePath;
+ });
+
+ const serverTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => {
+ return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false;
+ });
+
+ const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([
+ page.goto(routePath),
+ clientTxnEventPromise1,
+ serverTxnEventPromise1,
+ expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(),
+ ]);
+
+ const baggageMetaTagContent1 = await page.locator('meta[name="baggage"]').getAttribute('content');
+ const sentryTraceMetaTagContent1 = await page.locator('meta[name="sentry-trace"]').getAttribute('content');
+ const [htmlMetaTraceId1] = sentryTraceMetaTagContent1?.split('-') || [];
+
+ // === 2. Request ===
+
+ const clientTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => {
+ return txnEvent.transaction === routePath;
+ });
+
+ const serverTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => {
+ return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false;
+ });
+
+ const [_2, clientTxnEvent2, serverTxnEvent2] = await Promise.all([
+ page.goto(routePath),
+ clientTxnEventPromise2,
+ serverTxnEventPromise2,
+ expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(),
+ ]);
+
+ const baggageMetaTagContent2 = await page.locator('meta[name="baggage"]').getAttribute('content');
+ const sentryTraceMetaTagContent2 = await page.locator('meta[name="sentry-trace"]').getAttribute('content');
+ const [htmlMetaTraceId2] = sentryTraceMetaTagContent2?.split('-') || [];
+
+ const serverTxnEvent1TraceId = serverTxnEvent1.contexts?.trace?.trace_id;
+ const serverTxnEvent2TraceId = serverTxnEvent2.contexts?.trace?.trace_id;
+
+ await test.step('Test distributed trace from 1. request', () => {
+ expect(baggageMetaTagContent1).toContain(`sentry-trace_id=${serverTxnEvent1TraceId}`);
+
+ expect(clientTxnEvent1.contexts?.trace?.trace_id).toBe(serverTxnEvent1TraceId);
+ expect(clientTxnEvent1.contexts?.trace?.parent_span_id).toBe(serverTxnEvent1.contexts?.trace?.span_id);
+ expect(serverTxnEvent1.contexts?.trace?.trace_id).toBe(htmlMetaTraceId1);
+ });
+
+ await test.step('Test distributed trace from 2. request', () => {
+ expect(baggageMetaTagContent2).toContain(`sentry-trace_id=${serverTxnEvent2TraceId}`);
+
+ expect(clientTxnEvent2.contexts?.trace?.trace_id).toBe(serverTxnEvent2TraceId);
+ expect(clientTxnEvent2.contexts?.trace?.parent_span_id).toBe(serverTxnEvent2.contexts?.trace?.span_id);
+ expect(serverTxnEvent2.contexts?.trace?.trace_id).toBe(htmlMetaTraceId2);
+ });
+
+ await test.step('Test that trace IDs from subsequent requests are different', () => {
+ // Different trace IDs for the server transactions
+ expect(serverTxnEvent1TraceId).toBeDefined();
+ expect(serverTxnEvent1TraceId).not.toBe(serverTxnEvent2TraceId);
+ expect(serverTxnEvent1TraceId).not.toBe(htmlMetaTraceId2);
+ });
+}
+
+/**
+ * Tests that tracing meta-tags are excluded on cached pages (SWR, pre-rendered, etc.)
+ * This utility handles the common pattern of:
+ * 1. Making two requests to a cached page
+ * 2. Verifying no tracing meta-tags are present
+ * 3. Verifying only the first request creates a server transaction
+ * 4. Verifying traces are not distributed
+ *
+ * @param page - Playwright page object
+ * @param routePath - The route path to test (e.g., '/rendering-modes/swr-cached-page')
+ * @param expectedPageText - The text to verify is visible on the page (e.g., 'SWR Cached Page')
+ * @returns Object containing transaction events for additional custom assertions
+ */
+export async function testExcludeTracingMetaTagsOnCachedPage(
+ page: Page,
+ routePath: string,
+ expectedPageText: string,
+): Promise {
+ // === 1. Request ===
+ const clientTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => {
+ return txnEvent.transaction === routePath;
+ });
+
+ // Only the 1. request creates a server transaction
+ const serverTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => {
+ return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false;
+ });
+
+ const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([
+ page.goto(routePath),
+ clientTxnEventPromise1,
+ serverTxnEventPromise1,
+ expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(),
+ ]);
+
+ // Verify no baggage and sentry-trace meta-tags are present on first request
+ expect(await page.locator('meta[name="baggage"]').count()).toBe(0);
+ expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(0);
+
+ // === 2. Request ===
+
+ await page.goto(routePath);
+
+ const clientTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => {
+ return txnEvent.transaction === routePath;
+ });
+
+ let serverTxnEvent2 = undefined;
+ const serverTxnEventPromise2 = Promise.race([
+ waitForTransaction('nuxt-4', txnEvent => {
+ return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false;
+ }),
+ new Promise((_, reject) => setTimeout(() => reject(new Error('No second server transaction expected')), 2000)),
+ ]);
+
+ try {
+ serverTxnEvent2 = await serverTxnEventPromise2;
+ throw new Error('Second server transaction should not have been sent');
+ } catch (error) {
+ expect(error.message).toBe('No second server transaction expected');
+ }
+
+ const [clientTxnEvent2] = await Promise.all([
+ clientTxnEventPromise2,
+ expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(),
+ ]);
+
+ const clientTxnEvent1TraceId = clientTxnEvent1.contexts?.trace?.trace_id;
+ const clientTxnEvent2TraceId = clientTxnEvent2.contexts?.trace?.trace_id;
+
+ const serverTxnEvent1TraceId = serverTxnEvent1.contexts?.trace?.trace_id;
+ const serverTxnEvent2TraceId = serverTxnEvent2?.contexts?.trace?.trace_id;
+
+ await test.step('No baggage and sentry-trace meta-tags are present on second request', async () => {
+ expect(await page.locator('meta[name="baggage"]').count()).toBe(0);
+ expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(0);
+ });
+
+ await test.step('1. Server Transaction and all Client Transactions are defined', () => {
+ expect(serverTxnEvent1TraceId).toBeDefined();
+ expect(clientTxnEvent1TraceId).toBeDefined();
+ expect(clientTxnEvent2TraceId).toBeDefined();
+ expect(serverTxnEvent2).toBeUndefined();
+ expect(serverTxnEvent2TraceId).toBeUndefined();
+ });
+
+ await test.step('Trace is not distributed', () => {
+ // Cannot create distributed trace as HTML Meta Tags are not added (caching leads to multiple usages of the same server trace id)
+ expect(clientTxnEvent1TraceId).not.toBe(clientTxnEvent2TraceId);
+ expect(clientTxnEvent1TraceId).not.toBe(serverTxnEvent1TraceId);
+ });
+}
diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts
index c76f7ffce5bf..b529eafc4ee0 100644
--- a/packages/nuxt/src/runtime/plugins/sentry.server.ts
+++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts
@@ -5,8 +5,7 @@ import {
getIsolationScope,
withIsolationScope,
} from '@sentry/core';
-// eslint-disable-next-line import/no-extraneous-dependencies
-import { type EventHandler } from 'h3';
+import type { EventHandler, H3Event } from 'h3';
// eslint-disable-next-line import/no-extraneous-dependencies
import { defineNitroPlugin } from 'nitropack/runtime';
import type { NuxtRenderHTMLContext } from 'nuxt/app';
@@ -22,8 +21,21 @@ export default defineNitroPlugin(nitroApp => {
nitroApp.hooks.hook('error', sentryCaptureErrorHook);
// @ts-expect-error - 'render:html' is a valid hook name in the Nuxt context
- nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext) => {
- addSentryTracingMetaTags(html.head);
+ nitroApp.hooks.hook('render:html', (html: NuxtRenderHTMLContext, { event }: { event: H3Event }) => {
+ const headers = event.node.res?.getHeaders() || {};
+
+ const isPreRenderedPage = Object.keys(headers).includes('x-nitro-prerender');
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ const isSWRCachedPage = event?.context?.cache?.options.swr as boolean | undefined;
+
+ if (!isPreRenderedPage && !isSWRCachedPage) {
+ addSentryTracingMetaTags(html.head);
+ } else {
+ const reason = isPreRenderedPage ? 'the page was pre-rendered' : 'SWR caching is enabled for the route';
+ debug.log(
+ `Not adding Sentry tracing meta tags to HTML for ${event.path} because ${reason}. This will disable distributed tracing and prevent connecting multiple client page loads to the same server request.`,
+ );
+ }
});
});
diff --git a/vite/vite.config.ts b/vite/vite.config.ts
index 53823d2b9451..62f89a570f52 100644
--- a/vite/vite.config.ts
+++ b/vite/vite.config.ts
@@ -19,7 +19,7 @@ export default defineConfig({
'vite.config.*',
],
},
- reporters: ['default', ...(process.env.CI ? [['junit', { classnameTemplate: '{filepath}' }]] : [])],
+ reporters: process.env.CI ? ['default', ['junit', { classnameTemplate: '{filepath}' }]] : ['default'],
outputFile: {
junit: 'vitest.junit.xml',
},