diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-notfound/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-notfound/page.tsx new file mode 100644 index 000000000000..46d4ddd7f962 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-notfound/page.tsx @@ -0,0 +1,11 @@ +import { notFound } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function PageWithRedirect() { + return

Hello World!

; +} + +export async function generateMetadata() { + notFound(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-redirect/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-redirect/page.tsx new file mode 100644 index 000000000000..f1f37d7a32c6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-redirect/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function PageWithRedirect() { + return

Hello World!

; +} + +export async function generateMetadata() { + redirect('/'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/page.tsx new file mode 100644 index 000000000000..6f4e63ef5748 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

Home

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts index 3828312607ea..b5fe7ee67393 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts @@ -77,3 +77,37 @@ test('Should send a transaction and an error event for a faulty generateViewport expect(await transactionPromise).toBeDefined(); expect(await errorEventPromise).toBeDefined(); }); + +test('Should send a transaction event with correct status for a generateMetadata() function invokation with redirect()', async ({ + page, +}) => { + const testTitle = 'redirect-foobar'; + + const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions/with-redirect)' && + transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle + ); + }); + + await page.goto(`/generation-functions/with-redirect?metadataTitle=${testTitle}`); + + expect((await transactionPromise).contexts?.trace?.status).toBe('ok'); +}); + +test('Should send a transaction event with correct status for a generateMetadata() function invokation with notfound()', async ({ + page, +}) => { + const testTitle = 'notfound-foobar'; + + const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions/with-notfound)' && + transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle + ); + }); + + await page.goto(`/generation-functions/with-notfound?metadataTitle=${testTitle}`); + + expect((await transactionPromise).contexts?.trace?.status).toBe('not_found'); +}); diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index d1765aa2c41e..fe90b6f6ca39 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -6,12 +6,13 @@ import { getCurrentScope, handleCallbackErrors, runWithAsyncContext, - startSpan, + startSpanManual, } from '@sentry/core'; import type { WebFetchHeaders } from '@sentry/types'; import { winterCGHeadersToDict } from '@sentry/utils'; import type { GenerationFunctionContext } from '../common/types'; +import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; import { commonObjectToPropagationContext } from './utils/commonObjectTracing'; /** @@ -61,7 +62,7 @@ export function wrapGenerationFunctionWithSentry a transactionContext.parentSpanId = commonSpanId; } - return startSpan( + return startSpanManual( { op: 'function.nextjs', name: `${componentType}.${generationFunctionIdentifier} (${componentRoute})`, @@ -76,18 +77,31 @@ export function wrapGenerationFunctionWithSentry a }, }, }, - () => { + span => { return handleCallbackErrors( () => originalFunction.apply(thisArg, args), - err => - captureException(err, { - mechanism: { - handled: false, - data: { - function: 'wrapGenerationFunctionWithSentry', + err => { + if (isNotFoundNavigationError(err)) { + // We don't want to report "not-found"s + span?.setStatus('not_found'); + } else if (isRedirectNavigationError(err)) { + // We don't want to report redirects + span?.setStatus('ok'); + } else { + span?.setStatus('internal_error'); + captureException(err, { + mechanism: { + handled: false, + data: { + function: 'wrapGenerationFunctionWithSentry', + }, }, - }, - }), + }); + } + }, + () => { + span?.end(); + }, ); }, );