diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json index 72acea9f33b6..a487d61a144b 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json @@ -6,7 +6,7 @@ "build": "nuxt build", "dev": "nuxt dev", "generate": "nuxt generate", - "preview": "nuxt preview", + "preview": "NODE_OPTIONS='--import ./public/instrument.server.mjs' nuxt preview", "clean": "npx nuxi cleanup", "test": "playwright test", "test:build": "pnpm install && npx playwright install && pnpm build", diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/public/instrument.server.mjs b/dev-packages/e2e-tests/test-applications/nuxt-3/public/instrument.server.mjs new file mode 100644 index 000000000000..729b2296c683 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/public/instrument.server.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/nuxt'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, // Capture 100% of the transactions + tunnel: 'http://localhost:3031/', // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.server.test.ts new file mode 100644 index 000000000000..5b78e235e564 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.server.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; + +test('sends a server action transaction on pageload', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => { + return transactionEvent.transaction.includes('GET /test-param/'); + }); + + await page.goto('/test-param/1234'); + + const transaction = await transactionPromise; + + expect(transaction.contexts.trace).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.http', + }), + }), + ); +}); + +test('does not send transactions for build asset folder "_nuxt"', async ({ page }) => { + let buildAssetFolderOccurred = false; + + waitForTransaction('nuxt-3', transactionEvent => { + if (transactionEvent.transaction?.match(/^GET \/_nuxt\//)) { + buildAssetFolderOccurred = true; + } + return false; // expects to return a boolean (but not relevant here) + }); + + const transactionEventPromise = waitForTransaction('nuxt-3', transactionEvent => { + return transactionEvent.transaction.includes('GET /test-param/'); + }); + + await page.goto('/test-param/1234'); + + const transactionEvent = await transactionEventPromise; + + expect(buildAssetFolderOccurred).toBe(false); + + // todo: url not yet parametrized + expect(transactionEvent.transaction).toBe('GET /test-param/1234'); +}); diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 4faae8e6ef1a..f14cc23ab8cd 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -1,6 +1,6 @@ -import { applySdkMetadata } from '@sentry/core'; +import { applySdkMetadata, getGlobalScope } from '@sentry/core'; import { init as initNode } from '@sentry/node'; -import type { Client } from '@sentry/types'; +import type { Client, EventProcessor } from '@sentry/types'; import type { SentryNuxtOptions } from '../common/types'; /** @@ -15,5 +15,30 @@ export function init(options: SentryNuxtOptions): Client | undefined { applySdkMetadata(sentryOptions, 'nuxt', ['nuxt', 'node']); - return initNode(sentryOptions); + const client = initNode(sentryOptions); + + getGlobalScope().addEventProcessor( + Object.assign( + (event => { + if (event.type === 'transaction') { + // Filter out transactions for Nuxt build assets + // This regex matches the default path to the nuxt-generated build assets (`_nuxt`). + // 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); + return null; + } + + return event; + } else { + return event; + } + }) satisfies EventProcessor, + { id: 'NuxtLowQualityTransactionsFilter' }, + ), + ); + + return client; } diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts new file mode 100644 index 000000000000..8d84dc8b15c8 --- /dev/null +++ b/packages/nuxt/test/server/sdk.test.ts @@ -0,0 +1,42 @@ +import * as SentryNode from '@sentry/node'; +import { SDK_VERSION } from '@sentry/node'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { init } from '../../src/server'; + +const nodeInit = vi.spyOn(SentryNode, 'init'); + +describe('Nuxt Server SDK', () => { + describe('init', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('Adds Nuxt metadata to the SDK options', () => { + expect(nodeInit).not.toHaveBeenCalled(); + + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }); + + const expectedMetadata = { + _metadata: { + sdk: { + name: 'sentry.javascript.nuxt', + version: SDK_VERSION, + packages: [ + { name: 'npm:@sentry/nuxt', version: SDK_VERSION }, + { name: 'npm:@sentry/node', version: SDK_VERSION }, + ], + }, + }, + }; + + expect(nodeInit).toHaveBeenCalledTimes(1); + expect(nodeInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + }); + + it('returns client from init', () => { + expect(init({})).not.toBeUndefined(); + }); + }); +});