diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts index f270a5ad9b48..d1094993131d 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts @@ -8,6 +8,9 @@ const nuxtConfigOptions: ConfigOptions = { }, }; +/* Make sure to import from '@nuxt/test-utils/playwright' in the tests + * Like this: import { expect, test } from '@nuxt/test-utils/playwright' */ + const config = getPlaywrightConfig({ startCommand: `pnpm preview`, use: { ...nuxtConfigOptions }, diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.client.test.ts new file mode 100644 index 000000000000..66c8c9dfce2d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.client.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@nuxt/test-utils/playwright'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload root span with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3', async transactionEvent => { + return transactionEvent.transaction === '/test-param/:param()'; + }); + + await page.goto(`/test-param/1234`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + 'params.param': '1234', + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + transaction: '/test-param/:param()', + transaction_info: { + source: 'route', + }, + }); +}); diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index f14cc23ab8cd..deadea3c54df 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -1,6 +1,8 @@ import { applySdkMetadata, getGlobalScope } from '@sentry/core'; import { init as initNode } from '@sentry/node'; import type { Client, EventProcessor } from '@sentry/types'; +import { logger } from '@sentry/utils'; +import { DEBUG_BUILD } from '../common/debug-build'; import type { SentryNuxtOptions } from '../common/types'; /** @@ -26,8 +28,8 @@ export function init(options: SentryNuxtOptions): Client | undefined { // todo: the buildAssetDir could be changed in the nuxt config - change this to a more generic solution if (event.transaction?.match(/^GET \/_nuxt\//)) { options.debug && - // eslint-disable-next-line no-console - console.log('[Sentry] NuxtLowQualityTransactionsFilter filtered transaction: ', event.transaction); + DEBUG_BUILD && + logger.log('NuxtLowQualityTransactionsFilter filtered transaction: ', event.transaction); return null; } diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index e54c71eb550f..8e8bf32ac172 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -50,18 +50,28 @@ export function instrumentVueRouter( }, startNavigationSpanFn: (context: StartSpanOptions) => void, ): void { + let isFirstPageLoad = true; + router.onError(error => captureException(error, { mechanism: { handled: false } })); router.beforeEach((to, from, next) => { - // According to docs we could use `from === VueRouter.START_LOCATION` but I couldnt get it working for Vue 2 + // According to docs we could use `from === VueRouter.START_LOCATION` but I couldn't get it working for Vue 2 // https://router.vuejs.org/api/#router-start-location // https://next.router.vuejs.org/api/#start-location + // Additionally, Nuxt does not provide the possibility to check for `from.matched.length === 0` (this is never 0). + // Therefore, a flag was added to track the page-load: isFirstPageLoad // from.name: // - Vue 2: null // - Vue 3: undefined + // - Nuxt: undefined // hence only '==' instead of '===', because `undefined == null` evaluates to `true` - const isPageLoadNavigation = from.name == null && from.matched.length === 0; + const isPageLoadNavigation = + (from.name == null && from.matched.length === 0) || (from.name === undefined && isFirstPageLoad); + + if (isFirstPageLoad) { + isFirstPageLoad = false; + } const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts index dc69d7ae0fd9..1da5097b11e0 100644 --- a/packages/vue/test/router.test.ts +++ b/packages/vue/test/router.test.ts @@ -114,6 +114,7 @@ describe('instrumentVueRouter()', () => { const from = testRoutes[fromKey]!; const to = testRoutes[toKey]!; + beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(to, from, mockNext); expect(mockStartSpan).toHaveBeenCalledTimes(1); @@ -127,7 +128,7 @@ describe('instrumentVueRouter()', () => { op: 'navigation', }); - expect(mockNext).toHaveBeenCalledTimes(1); + expect(mockNext).toHaveBeenCalledTimes(2); }, ); @@ -192,6 +193,7 @@ describe('instrumentVueRouter()', () => { const from = testRoutes.normalRoute1!; const to = testRoutes.namedRoute!; + beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(to, from, mockNext); // first startTx call happens when the instrumentation is initialized (for pageloads) @@ -219,6 +221,7 @@ describe('instrumentVueRouter()', () => { const from = testRoutes.normalRoute1!; const to = testRoutes.namedRoute!; + beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(to, from, mockNext); // first startTx call happens when the instrumentation is initialized (for pageloads) @@ -373,6 +376,7 @@ describe('instrumentVueRouter()', () => { expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0]![0]!; + beforeEachCallback(testRoutes['normalRoute1']!, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(testRoutes['normalRoute2']!, testRoutes['normalRoute1']!, mockNext); expect(mockStartSpan).toHaveBeenCalledTimes(expectedCallsAmount); @@ -391,6 +395,7 @@ describe('instrumentVueRouter()', () => { const from = testRoutes.normalRoute1!; const to = testRoutes.namedRoute!; + beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(to, from, undefined); // first startTx call happens when the instrumentation is initialized (for pageloads)