From ca19f344998b1cbee8655534cc3cd3fe55ff6db2 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 30 Sep 2024 14:17:31 +0200 Subject: [PATCH] feat(nextjs): Support new async APIs (`headers()`, `params`, `searchParams`) (#13828) Changes in Next.js https://github.com/vercel/next.js/pull/68812 This PR is mostly just adjusting our E2E tests so they don't fail while building. Additionally, we had to update the `withServerActionInstrumentation` API in a semver-minor way so you can pass a promise to the `headers` option. The `ReadonlyHeaders` type isn't exposed in all Next.js versions so for now I typed it as `any`. Resolves https://github.com/getsentry/sentry-javascript/issues/13805 Resolves https://github.com/getsentry/sentry-javascript/issues/13779 Resolves https://github.com/getsentry/sentry-javascript/issues/13780 --- .../app/generation-functions/page.tsx | 26 +++++++++---------- .../nextjs-14/app/propagation/utils.ts | 4 +-- .../nextjs-15/app/ppr-error/[param]/page.tsx | 7 +++-- .../parameter/[...parameters]/page.tsx | 8 ++++-- .../parameter/[parameter]/page.tsx | 8 ++++-- .../parameter/[...parameters]/page.tsx | 8 ++++-- .../parameter/[parameter]/page.tsx | 8 ++++-- .../common/withServerActionInstrumentation.ts | 22 ++++++++++++---- 8 files changed, 60 insertions(+), 31 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx index 92bee1dbac4b..0d8f1841ea9d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx @@ -7,33 +7,31 @@ export default function Page() { return

Hello World!

; } -export async function generateMetadata({ - searchParams, -}: { - searchParams: { [key: string]: string | string[] | undefined }; -}) { +export async function generateMetadata({ searchParams }: { searchParams: any }) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedSearchParams = await searchParams; + Sentry.setTag('my-isolated-tag', true); Sentry.setTag('my-global-scope-isolated-tag', getDefaultIsolationScope().getScopeData().tags['my-isolated-tag']); // We set this tag to be able to assert that the previously set tag has not leaked into the global isolation scope - if (searchParams['shouldThrowInGenerateMetadata']) { + if (normalizedSearchParams['shouldThrowInGenerateMetadata']) { throw new Error('generateMetadata Error'); } return { - title: searchParams['metadataTitle'] ?? 'not set', + title: normalizedSearchParams['metadataTitle'] ?? 'not set', }; } -export function generateViewport({ - searchParams, -}: { - searchParams: { [key: string]: string | undefined }; -}) { - if (searchParams['shouldThrowInGenerateViewport']) { +export async function generateViewport({ searchParams }: { searchParams: any }) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedSearchParams = await searchParams; + + if (normalizedSearchParams['shouldThrowInGenerateViewport']) { throw new Error('generateViewport Error'); } return { - themeColor: searchParams['viewportThemeColor'] ?? 'black', + themeColor: normalizedSearchParams['viewportThemeColor'] ?? 'black', }; } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/utils.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/utils.ts index a065c53ee4c9..249efabe58f3 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/utils.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/utils.ts @@ -26,8 +26,8 @@ export function makeHttpRequest(url: string) { }); } -export function checkHandler() { - const headerList = headers(); +export async function checkHandler() { + const headerList = await headers(); const headerObj: Record = {}; headerList.forEach((value, key) => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx index ec2b2b1232c7..c67513e0e4fd 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx @@ -3,10 +3,13 @@ import * as Sentry from '@sentry/nextjs'; export default async function Page({ searchParams, }: { - searchParams: { id?: string }; + searchParams: any; }) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedSearchParams = await searchParams; + try { - console.log(searchParams.id); // Accessing a field on searchParams will throw the PPR error + console.log(normalizedSearchParams.id); // Accessing a field on searchParams will throw the PPR error } catch (e) { Sentry.captureException(e); // This error should not be reported await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for any async event processors to run diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[...parameters]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[...parameters]/page.tsx index 31fa4ee21be5..f09f06875c3a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[...parameters]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[...parameters]/page.tsx @@ -1,10 +1,14 @@ +import { use } from 'react'; import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools'; -export default function Page({ params }: { params: Record }) { +export default function Page({ params }: any) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedParams = 'then' in params ? use(params) : params; + return (

Page (/client-component/[...parameters])

-

Params: {JSON.stringify(params['parameters'])}

+

Params: {JSON.stringify(normalizedParams['parameters'])}

); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[parameter]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[parameter]/page.tsx index 2b9c28b922ac..514a0833c998 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[parameter]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[parameter]/page.tsx @@ -1,10 +1,14 @@ +import { use } from 'react'; import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools'; -export default function Page({ params }: { params: Record }) { +export default function Page({ params }: any) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedParams = 'then' in params ? use(params) : params; + return (

Page (/client-component/[parameter])

-

Parameter: {JSON.stringify(params['parameter'])}

+

Parameter: {JSON.stringify(normalizedParams['parameter'])}

); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[...parameters]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[...parameters]/page.tsx index 5d9d6c8262c5..63d0e7b53f0b 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[...parameters]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[...parameters]/page.tsx @@ -1,12 +1,16 @@ +import { use } from 'react'; import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools'; export const dynamic = 'force-dynamic'; -export default async function Page({ params }: { params: Record }) { +export default function Page({ params }: any) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedParams = 'then' in params ? use(params) : params; + return (

Page (/server-component/[...parameters])

-

Params: {JSON.stringify(params['parameters'])}

+

Params: {JSON.stringify(normalizedParams['parameters'])}

); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[parameter]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[parameter]/page.tsx index f88fe1cd4a06..98ecb7352ad2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[parameter]/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[parameter]/page.tsx @@ -1,12 +1,16 @@ +import { use } from 'react'; import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools'; export const dynamic = 'force-dynamic'; -export default async function Page({ params }: { params: Record }) { +export default function Page({ params }: any) { + // We need to dynamically check for this because Next.js made the API async for Next.js 15 and we use this test in canary tests + const normalizedParams = 'then' in params ? use(params) : params; + return (

Page (/server-component/[parameter])

-

Parameter: {JSON.stringify(params['parameter'])}

+

Parameter: {JSON.stringify(normalizedParams['parameter'])}

); diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 14c701638ee5..c1633d8fab1b 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -15,7 +15,18 @@ import { vercelWaitUntil } from './utils/vercelWaitUntil'; interface Options { formData?: FormData; - headers?: Headers; + + /** + * Headers as returned from `headers()`. + * + * Currently accepts both a plain `Headers` object and `Promise` to be compatible with async APIs introduced in Next.js 15: https://github.com/vercel/next.js/pull/68812 + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + headers?: Headers | Promise; + + /** + * Whether the server action response should be included in any events captured within the server action. + */ recordResponse?: boolean; } @@ -55,16 +66,17 @@ async function withServerActionInstrumentationImplementation> { return escapeNextjsTracing(() => { - return withIsolationScope(isolationScope => { + return withIsolationScope(async isolationScope => { const sendDefaultPii = getClient()?.getOptions().sendDefaultPii; let sentryTraceHeader; let baggageHeader; const fullHeadersObject: Record = {}; try { - sentryTraceHeader = options.headers?.get('sentry-trace') ?? undefined; - baggageHeader = options.headers?.get('baggage'); - options.headers?.forEach((value, key) => { + const awaitedHeaders: Headers = await options.headers; + sentryTraceHeader = awaitedHeaders?.get('sentry-trace') ?? undefined; + baggageHeader = awaitedHeaders?.get('baggage'); + awaitedHeaders?.forEach((value, key) => { fullHeadersObject[key] = value; }); } catch (e) {