From fc9bc4e0162a97b310d8adfbb2017693acd83c0a Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 4 Aug 2025 13:17:41 +0200 Subject: [PATCH 1/7] feat(nuxt): Do not inject trace meta-tags on cached HTML pages --- .../rendering-modes/client-side-only-page.vue | 1 + .../rendering-modes/isr-1h-cached-page.vue | 1 + .../pages/rendering-modes/isr-cached-page.vue | 1 + .../rendering-modes/pre-rendered-page.vue | 1 + .../rendering-modes/swr-1h-cached-page.vue | 1 + .../pages/rendering-modes/swr-cached-page.vue | 1 + .../test-applications/nuxt-4/nuxt.config.ts | 9 + .../test-applications/nuxt-4/package.json | 2 +- .../nuxt-4/tests/cached-html.test.ts | 229 ++++++++++++++++++ .../nuxt/src/runtime/plugins/sentry.server.ts | 20 +- 10 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/client-side-only-page.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/isr-1h-cached-page.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/isr-cached-page.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/pre-rendered-page.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/swr-1h-cached-page.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/rendering-modes/swr-cached-page.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/tests/cached-html.test.ts 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 @@ + 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 @@ + 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 @@ + 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 @@ + 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 @@ + 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 @@ + 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..3aab7ba03a84 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/pre-rendered-page': { prerender: true }, + '/rendering-modes/swr-cached-page': { swr: true }, + '/rendering-modes/swr-1h-cached-page': { swr: 3600 }, + '/rendering-modes/isr-cached-page': { isr: true }, + '/rendering-modes/isr-1h-cached-page': { isr: 3600 }, + }, + modules: ['@pinia/nuxt', '@sentry/nuxt/module'], runtimeConfig: { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json index f73e7ff99200..626ec052346e 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json @@ -10,7 +10,7 @@ "start": "node .output/server/index.mjs", "start:import": "node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs", "clean": "npx nuxi cleanup", - "test": "playwright test", + "test": "playwright test -g 'Rendering Modes'", "test:build": "pnpm install && pnpm build", "test:build-canary": "pnpm add nuxt@npm:nuxt-nightly@latest && pnpm add nitropack@npm:nitropack-nightly@latest && pnpm install --force && pnpm build", "test:assert": "pnpm test" diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cached-html.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cached-html.test.ts new file mode 100644 index 000000000000..455ed0b3a1d4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cached-html.test.ts @@ -0,0 +1,229 @@ +import { expect, test } 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 ISR-cached page', async ({ page }) => { + // === 1. Request === + const clientTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction === '/rendering-modes/isr-cached-page'; + }); + + const serverTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /rendering-modes/isr-cached-page') ?? false; + }); + + const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([ + page.goto(`/rendering-modes/isr-cached-page`), + clientTxnEventPromise1, + serverTxnEventPromise1, + expect(page.getByText(`ISR Cached Page`, { 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 === '/rendering-modes/isr-cached-page'; + }); + + const serverTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /rendering-modes/isr-cached-page') ?? false; + }); + + const [_2, clientTxnEvent2, serverTxnEvent2] = await Promise.all([ + page.goto(`/rendering-modes/isr-cached-page`), + clientTxnEventPromise2, + serverTxnEventPromise2, + expect(page.getByText(`ISR Cached Page`, { 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; + + console.log('Server Transaction 1:', serverTxnEvent1TraceId); + console.log('Server Transaction 2:', serverTxnEvent2TraceId); + + 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).not.toBe(serverTxnEvent2TraceId); + expect(serverTxnEvent1TraceId).not.toBe(htmlMetaTraceId2); + }); + }); + + test('exclude tracing meta tags on SWR-cached page', async ({ page }) => { + // === 1. Request === + const clientTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction === '/rendering-modes/swr-cached-page'; + }); + + // Only the 1. request creates a server transaction + const serverTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /rendering-modes/swr-cached-page') ?? false; + }); + + const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([ + page.goto(`/rendering-modes/swr-cached-page`), + clientTxnEventPromise1, + serverTxnEventPromise1, + expect(page.getByText(`SWR Cached Page`, { exact: true })).toBeVisible(), + ]); + + await test.step('No baggage and sentry-trace meta tags are present on first request', async () => { + 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(`/rendering-modes/swr-cached-page`); + + const clientTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction === '/rendering-modes/swr-cached-page'; + }); + + let serverTxnEvent2 = undefined; + const serverTxnEventPromise2 = Promise.race([ + waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /rendering-modes/swr-cached-page') ?? 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(`SWR Cached Page`, { 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 first 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('First 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 (SWR caching leads to multiple usages of the same server trace id) + expect(clientTxnEvent1TraceId).not.toBe(clientTxnEvent2TraceId); + expect(clientTxnEvent1TraceId).not.toBe(serverTxnEvent1TraceId); + }); + }); + + test('exclude tracing meta tags on pre-rendered page', async ({ page }) => { + // === 1. Request === + const clientTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction === '/rendering-modes/pre-rendered-page'; + }); + + // Only the 1. request creates a server transaction + const serverTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /rendering-modes/pre-rendered-page') ?? false; + }); + + const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([ + page.goto(`/rendering-modes/pre-rendered-page`), + clientTxnEventPromise1, + serverTxnEventPromise1, + expect(page.getByText(`Pre-Rendered Page`, { exact: true })).toBeVisible(), + ]); + + await test.step('No baggage and sentry-trace meta tags are present on first request', async () => { + 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(`/rendering-modes/pre-rendered-page`); + + const clientTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction === '/rendering-modes/pre-rendered-page'; + }); + + let serverTxnEvent2 = undefined; + const serverTxnEventPromise2 = Promise.race([ + waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes('GET /rendering-modes/pre-rendered-page') ?? 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(`Pre-Rendered Page`, { 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 first 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('First 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 (pre-rendering 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..114a7c62690e 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 for the page.`, + ); + } }); }); From 41262e5fc5f63260bdf497d54f0834e69598a2f4 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 4 Aug 2025 13:35:55 +0200 Subject: [PATCH 2/7] create testing utility --- .../nuxt-4/tests/cached-html.test.ts | 232 +++++++----------- packages/nuxt/src/module.ts | 2 + vite/vite.config.ts | 2 +- 3 files changed, 93 insertions(+), 143 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cached-html.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cached-html.test.ts index 455ed0b3a1d4..849981f8feab 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cached-html.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cached-html.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { expect, test, type Page } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test.describe('Rendering Modes with Cached HTML', () => { @@ -47,9 +47,6 @@ test.describe('Rendering Modes with Cached HTML', () => { const serverTxnEvent1TraceId = serverTxnEvent1.contexts?.trace?.trace_id; const serverTxnEvent2TraceId = serverTxnEvent2.contexts?.trace?.trace_id; - console.log('Server Transaction 1:', serverTxnEvent1TraceId); - console.log('Server Transaction 2:', serverTxnEvent2TraceId); - await test.step('Test distributed trace from 1. request', () => { expect(baggageMetaTagContent1).toContain(`sentry-trace_id=${serverTxnEvent1TraceId}`); @@ -74,156 +71,107 @@ test.describe('Rendering Modes with Cached HTML', () => { }); test('exclude tracing meta tags on SWR-cached page', async ({ page }) => { - // === 1. Request === - const clientTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction === '/rendering-modes/swr-cached-page'; - }); - - // Only the 1. request creates a server transaction - const serverTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction?.includes('GET /rendering-modes/swr-cached-page') ?? false; - }); - - const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([ - page.goto(`/rendering-modes/swr-cached-page`), - clientTxnEventPromise1, - serverTxnEventPromise1, - expect(page.getByText(`SWR Cached Page`, { exact: true })).toBeVisible(), - ]); - - await test.step('No baggage and sentry-trace meta tags are present on first request', async () => { - 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(`/rendering-modes/swr-cached-page`); - - const clientTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction === '/rendering-modes/swr-cached-page'; - }); - - let serverTxnEvent2 = undefined; - const serverTxnEventPromise2 = Promise.race([ - waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction?.includes('GET /rendering-modes/swr-cached-page') ?? 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(`SWR Cached Page`, { 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 first 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('First 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 (SWR caching leads to multiple usages of the same server trace id) - expect(clientTxnEvent1TraceId).not.toBe(clientTxnEvent2TraceId); - expect(clientTxnEvent1TraceId).not.toBe(serverTxnEvent1TraceId); - }); + await testExcludeTracingMetaTagsOnCachedPage(page, '/rendering-modes/swr-cached-page', 'SWR Cached Page'); }); test('exclude tracing meta tags on pre-rendered page', async ({ page }) => { - // === 1. Request === - const clientTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction === '/rendering-modes/pre-rendered-page'; - }); - - // Only the 1. request creates a server transaction - const serverTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction?.includes('GET /rendering-modes/pre-rendered-page') ?? false; - }); - - const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([ - page.goto(`/rendering-modes/pre-rendered-page`), - clientTxnEventPromise1, - serverTxnEventPromise1, - expect(page.getByText(`Pre-Rendered Page`, { exact: true })).toBeVisible(), - ]); - - await test.step('No baggage and sentry-trace meta tags are present on first request', async () => { - expect(await page.locator('meta[name="baggage"]').count()).toBe(0); - expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(0); - }); + await testExcludeTracingMetaTagsOnCachedPage(page, '/rendering-modes/pre-rendered-page', 'Pre-Rendered Page'); + }); - // === 2. Request === + 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'); + }); +}); - await page.goto(`/rendering-modes/pre-rendered-page`); +/** + * 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; + }); - const clientTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction === '/rendering-modes/pre-rendered-page'; - }); + // Only the 1. request creates a server transaction + const serverTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false; + }); - let serverTxnEvent2 = undefined; - const serverTxnEventPromise2 = Promise.race([ - waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction?.includes('GET /rendering-modes/pre-rendered-page') ?? false; - }), - new Promise((_, reject) => setTimeout(() => reject(new Error('No second server transaction expected')), 2000)), - ]); + const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([ + page.goto(routePath), + clientTxnEventPromise1, + serverTxnEventPromise1, + expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(), + ]); - 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'); - } + // 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); - const [clientTxnEvent2] = await Promise.all([ - clientTxnEventPromise2, - expect(page.getByText(`Pre-Rendered Page`, { exact: true })).toBeVisible(), - ]); + // === 2. Request === - const clientTxnEvent1TraceId = clientTxnEvent1.contexts?.trace?.trace_id; - const clientTxnEvent2TraceId = clientTxnEvent2.contexts?.trace?.trace_id; + await page.goto(routePath); - const serverTxnEvent1TraceId = serverTxnEvent1.contexts?.trace?.trace_id; - const serverTxnEvent2TraceId = serverTxnEvent2?.contexts?.trace?.trace_id; + const clientTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction === routePath; + }); - await test.step('No baggage and sentry-trace meta tags are present on first request', async () => { - expect(await page.locator('meta[name="baggage"]').count()).toBe(0); - expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(0); - }); + 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('First 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('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 (pre-rendering leads to multiple usages of the same server trace id) - expect(clientTxnEvent1TraceId).not.toBe(clientTxnEvent2TraceId); - expect(clientTxnEvent1TraceId).not.toBe(serverTxnEvent1TraceId); - }); + 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/module.ts b/packages/nuxt/src/module.ts index 0e6d92636246..06b65e0bb5f1 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -110,6 +110,8 @@ export default defineNuxtModule({ }; }); + console.log('nuxt.routeRules', nuxt.options.routeRules); + nuxt.hooks.hook('nitro:init', nitro => { if (serverConfigFile?.includes('.server.config')) { if (nitro.options.dev) { 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', }, From 625cd7ae86d168bf465994aa79e1129933f2064f Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 4 Aug 2025 14:01:12 +0200 Subject: [PATCH 3/7] create testing utility for multiple request --- .../test-applications/nuxt-4/nuxt.config.ts | 6 +- ...ml.test.ts => tracing.cached-html.test.ts} | 147 +++++++++++------- 2 files changed, 91 insertions(+), 62 deletions(-) rename dev-packages/e2e-tests/test-applications/nuxt-4/tests/{cached-html.test.ts => tracing.cached-html.test.ts} (51%) 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 3aab7ba03a84..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 @@ -5,11 +5,11 @@ export default defineNuxtConfig({ routeRules: { '/rendering-modes/client-side-only-page': { ssr: false }, - '/rendering-modes/pre-rendered-page': { prerender: true }, - '/rendering-modes/swr-cached-page': { swr: true }, - '/rendering-modes/swr-1h-cached-page': { swr: 3600 }, '/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'], diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cached-html.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.cached-html.test.ts similarity index 51% rename from dev-packages/e2e-tests/test-applications/nuxt-4/tests/cached-html.test.ts rename to dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.cached-html.test.ts index 849981f8feab..1dea68dd77ff 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/cached-html.test.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.cached-html.test.ts @@ -2,86 +2,115 @@ 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 }) => { - // === 1. Request === - const clientTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction === '/rendering-modes/isr-cached-page'; - }); + await testChangingTracingMetaTagsOnISRPage(page, '/rendering-modes/isr-cached-page', 'ISR Cached Page'); + }); - const serverTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction?.includes('GET /rendering-modes/isr-cached-page') ?? false; - }); + 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'); + }); - const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([ - page.goto(`/rendering-modes/isr-cached-page`), - clientTxnEventPromise1, - serverTxnEventPromise1, - expect(page.getByText(`ISR Cached Page`, { exact: true })).toBeVisible(), - ]); + test('exclude tracing meta tags on SWR-cached page', async ({ page }) => { + await testExcludeTracingMetaTagsOnCachedPage(page, '/rendering-modes/swr-cached-page', 'SWR Cached Page'); + }); - 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('-') || []; + 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'); + }); - // === 2. Request === + test('exclude tracing meta tags on pre-rendered page', async ({ page }) => { + await testExcludeTracingMetaTagsOnCachedPage(page, '/rendering-modes/pre-rendered-page', 'Pre-Rendered Page'); + }); +}); - const clientTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction === '/rendering-modes/isr-cached-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 serverTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => { - return txnEvent.transaction?.includes('GET /rendering-modes/isr-cached-page') ?? false; - }); + const serverTxnEventPromise1 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false; + }); - const [_2, clientTxnEvent2, serverTxnEvent2] = await Promise.all([ - page.goto(`/rendering-modes/isr-cached-page`), - clientTxnEventPromise2, - serverTxnEventPromise2, - expect(page.getByText(`ISR Cached Page`, { exact: true })).toBeVisible(), - ]); + const [_1, clientTxnEvent1, serverTxnEvent1] = await Promise.all([ + page.goto(routePath), + clientTxnEventPromise1, + serverTxnEventPromise1, + 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 baggageMetaTagContent1 = await page.locator('meta[name="baggage"]').getAttribute('content'); + const sentryTraceMetaTagContent1 = await page.locator('meta[name="sentry-trace"]').getAttribute('content'); + const [htmlMetaTraceId1] = sentryTraceMetaTagContent1?.split('-') || []; - const serverTxnEvent1TraceId = serverTxnEvent1.contexts?.trace?.trace_id; - const serverTxnEvent2TraceId = serverTxnEvent2.contexts?.trace?.trace_id; + // === 2. Request === - await test.step('Test distributed trace from 1. request', () => { - expect(baggageMetaTagContent1).toContain(`sentry-trace_id=${serverTxnEvent1TraceId}`); + const clientTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction === routePath; + }); - 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); - }); + const serverTxnEventPromise2 = waitForTransaction('nuxt-4', txnEvent => { + return txnEvent.transaction?.includes(`GET ${routePath}`) ?? false; + }); - await test.step('Test distributed trace from 2. request', () => { - expect(baggageMetaTagContent2).toContain(`sentry-trace_id=${serverTxnEvent2TraceId}`); + const [_2, clientTxnEvent2, serverTxnEvent2] = await Promise.all([ + page.goto(routePath), + clientTxnEventPromise2, + serverTxnEventPromise2, + expect(page.getByText(expectedPageText, { exact: true })).toBeVisible(), + ]); - 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); - }); + 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('-') || []; - await test.step('Test that trace IDs from subsequent requests are different', () => { - // Different trace IDs for the server transactions - expect(serverTxnEvent1TraceId).not.toBe(serverTxnEvent2TraceId); - expect(serverTxnEvent1TraceId).not.toBe(htmlMetaTraceId2); - }); - }); + const serverTxnEvent1TraceId = serverTxnEvent1.contexts?.trace?.trace_id; + const serverTxnEvent2TraceId = serverTxnEvent2.contexts?.trace?.trace_id; - test('exclude tracing meta tags on SWR-cached page', async ({ page }) => { - await testExcludeTracingMetaTagsOnCachedPage(page, '/rendering-modes/swr-cached-page', 'SWR Cached Page'); + 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); }); - test('exclude tracing meta tags on pre-rendered page', async ({ page }) => { - await testExcludeTracingMetaTagsOnCachedPage(page, '/rendering-modes/pre-rendered-page', 'Pre-Rendered Page'); + 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); }); - 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'); + 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.) From 944cd97230abfee687f8d5b94024cf295345cfc0 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 4 Aug 2025 14:13:49 +0200 Subject: [PATCH 4/7] add tests for nuxt-3-min --- .../nuxt-3-min/nuxt.config.ts | 12 +- .../rendering-modes/client-side-only-page.vue | 1 + .../rendering-modes/isr-1h-cached-page.vue | 1 + .../pages/rendering-modes/isr-cached-page.vue | 1 + .../rendering-modes/pre-rendered-page.vue | 1 + .../rendering-modes/swr-1h-cached-page.vue | 1 + .../pages/rendering-modes/swr-cached-page.vue | 1 + .../tests/tracing.cached-html.test.ts | 206 ++++++++++++++++++ 8 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/client-side-only-page.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/isr-1h-cached-page.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/isr-cached-page.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/pre-rendered-page.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/swr-1h-cached-page.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/pages/rendering-modes/swr-cached-page.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/tracing.cached-html.test.ts 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 @@ + 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 @@ + 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 @@ + 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 @@ + 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 @@ + 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 @@ + 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); + }); +} From d83f999be0f6daa55454d7af4e3d72e39a99ec66 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 4 Aug 2025 14:17:14 +0200 Subject: [PATCH 5/7] delete log --- packages/nuxt/src/module.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 06b65e0bb5f1..0e6d92636246 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -110,8 +110,6 @@ export default defineNuxtModule({ }; }); - console.log('nuxt.routeRules', nuxt.options.routeRules); - nuxt.hooks.hook('nitro:init', nitro => { if (serverConfigFile?.includes('.server.config')) { if (nitro.options.dev) { From 15f742393d9cea22a8e003412fbd04b57a89c2fc Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 4 Aug 2025 14:30:49 +0200 Subject: [PATCH 6/7] delete test modification --- dev-packages/e2e-tests/test-applications/nuxt-4/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json index 626ec052346e..f73e7ff99200 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json @@ -10,7 +10,7 @@ "start": "node .output/server/index.mjs", "start:import": "node --import ./.output/server/sentry.server.config.mjs .output/server/index.mjs", "clean": "npx nuxi cleanup", - "test": "playwright test -g 'Rendering Modes'", + "test": "playwright test", "test:build": "pnpm install && pnpm build", "test:build-canary": "pnpm add nuxt@npm:nuxt-nightly@latest && pnpm add nitropack@npm:nitropack-nightly@latest && pnpm install --force && pnpm build", "test:assert": "pnpm test" From ce9135f5ab6f69fa1695804295cd0e86b97e3728 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 4 Aug 2025 16:02:33 +0200 Subject: [PATCH 7/7] improve log message --- packages/nuxt/src/runtime/plugins/sentry.server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index 114a7c62690e..b529eafc4ee0 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -33,7 +33,7 @@ export default defineNitroPlugin(nitroApp => { } 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 for the page.`, + `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.`, ); } });