From e8ea26fb03d610ea3061bb73eda17b46c48b859b Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 24 Jul 2025 18:57:35 +0200 Subject: [PATCH 1/4] await params --- .../nextjs/src/common/utils/wrapperUtils.ts | 37 +++++++++++++++++++ .../wrapGenerationFunctionWithSentry.ts | 7 ++-- .../common/wrapServerComponentWithSentry.ts | 9 ++--- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index 529acab7f96e..61f81bfdfdc5 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -6,6 +6,7 @@ import { getRootSpan, getTraceData, httpRequestToRequestData, + isThenable, } from '@sentry/core'; import type { IncomingMessage, ServerResponse } from 'http'; import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../span-attributes-with-logic-attached'; @@ -102,3 +103,39 @@ export async function callDataFetcherTraced Promis throw e; } } + +/** + * Extracts the params and searchParams from the props object. + * + * Depending on the next version, params and searchParams may be a promise. + */ +export async function safeExtractParamsAndSearchParamsFromProps(props: unknown): Promise<{ + params: Record | undefined; + searchParams: Record | undefined; +}> { + let params = + props && typeof props === 'object' && 'params' in props + ? (props.params as Record | Promise> | undefined) + : undefined; + if (isThenable(params)) { + try { + params = await params; + } catch (e) { + params = undefined; + } + } + + let searchParams = + props && typeof props === 'object' && 'searchParams' in props + ? (props.searchParams as Record | Promise> | undefined) + : undefined; + if (isThenable(searchParams)) { + try { + searchParams = await searchParams; + } catch (e) { + searchParams = undefined; + } + } + + return { params, searchParams }; +} diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index aad64e0f4ea4..3c503af9dce4 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -24,6 +24,7 @@ import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavi import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; import { getSanitizedRequestUrl } from './utils/urls'; +import { safeExtractParamsAndSearchParamsFromProps } from './utils/wrapperUtils'; /** * Wraps a generation function (e.g. generateMetadata) with Sentry error and performance instrumentation. */ @@ -34,7 +35,7 @@ export function wrapGenerationFunctionWithSentry a ): F { const { requestAsyncStorage, componentRoute, componentType, generationFunctionIdentifier } = context; return new Proxy(generationFunction, { - apply: (originalFunction, thisArg, args) => { + apply: async (originalFunction, thisArg, args) => { const requestTraceId = getActiveSpan()?.spanContext().traceId; let headers: WebFetchHeaders | undefined = undefined; // We try-catch here just in case anything goes wrong with the async storage here goes wrong since it is Next.js internal API @@ -65,9 +66,7 @@ export function wrapGenerationFunctionWithSentry a let data: Record | undefined = undefined; if (getClient()?.getOptions().sendDefaultPii) { const props: unknown = args[0]; - const params = props && typeof props === 'object' && 'params' in props ? props.params : undefined; - const searchParams = - props && typeof props === 'object' && 'searchParams' in props ? props.searchParams : undefined; + const { params, searchParams } = await safeExtractParamsAndSearchParamsFromProps(props); data = { params, searchParams }; } diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 16f6728deda1..13aa599e8d81 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -26,6 +26,7 @@ import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-l import { flushSafelyWithTimeout } from './utils/responseEnd'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; import { getSanitizedRequestUrl } from './utils/urls'; +import { safeExtractParamsAndSearchParamsFromProps } from './utils/wrapperUtils'; /** * Wraps an `app` directory server component with Sentry error instrumentation. @@ -40,7 +41,7 @@ export function wrapServerComponentWithSentry any> // Next.js will turn them into synchronous functions and it will transform any `await`s into instances of the `use` // hook. 🤯 return new Proxy(appDirComponent, { - apply: (originalFunction, thisArg, args) => { + apply: async (originalFunction, thisArg, args) => { const requestTraceId = getActiveSpan()?.spanContext().traceId; const isolationScope = commonObjectToIsolationScope(context.headers); @@ -64,10 +65,8 @@ export function wrapServerComponentWithSentry any> if (getClient()?.getOptions().sendDefaultPii) { const props: unknown = args[0]; - params = - props && typeof props === 'object' && 'params' in props - ? (props.params as Record) - : undefined; + const { params: paramsFromProps } = await safeExtractParamsAndSearchParamsFromProps(props); + params = paramsFromProps; } isolationScope.setSDKProcessingMetadata({ From d6d4ca65b8f4a8da3c1ea33273c1aeca005d42db Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 25 Jul 2025 11:27:49 +0200 Subject: [PATCH 2/4] no more promises --- .../nextjs/src/common/utils/wrapperUtils.ts | 18 +++++------------- .../common/wrapGenerationFunctionWithSentry.ts | 6 +++--- .../common/wrapServerComponentWithSentry.ts | 6 +++--- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index 61f81bfdfdc5..23b960c857cf 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -107,22 +107,18 @@ export async function callDataFetcherTraced Promis /** * Extracts the params and searchParams from the props object. * - * Depending on the next version, params and searchParams may be a promise. + * Depending on the next version, params and searchParams may be a promise which we do not want to resolve in this function. */ -export async function safeExtractParamsAndSearchParamsFromProps(props: unknown): Promise<{ +export function maybeExtractSynchronousParamsAndSearchParams(props: unknown): { params: Record | undefined; searchParams: Record | undefined; -}> { +} { let params = props && typeof props === 'object' && 'params' in props ? (props.params as Record | Promise> | undefined) : undefined; if (isThenable(params)) { - try { - params = await params; - } catch (e) { - params = undefined; - } + params = undefined; } let searchParams = @@ -130,11 +126,7 @@ export async function safeExtractParamsAndSearchParamsFromProps(props: unknown): ? (props.searchParams as Record | Promise> | undefined) : undefined; if (isThenable(searchParams)) { - try { - searchParams = await searchParams; - } catch (e) { - searchParams = undefined; - } + searchParams = undefined; } return { params, searchParams }; diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 3c503af9dce4..2067ebccc245 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -24,7 +24,7 @@ import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavi import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; import { getSanitizedRequestUrl } from './utils/urls'; -import { safeExtractParamsAndSearchParamsFromProps } from './utils/wrapperUtils'; +import { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils'; /** * Wraps a generation function (e.g. generateMetadata) with Sentry error and performance instrumentation. */ @@ -35,7 +35,7 @@ export function wrapGenerationFunctionWithSentry a ): F { const { requestAsyncStorage, componentRoute, componentType, generationFunctionIdentifier } = context; return new Proxy(generationFunction, { - apply: async (originalFunction, thisArg, args) => { + apply: (originalFunction, thisArg, args) => { const requestTraceId = getActiveSpan()?.spanContext().traceId; let headers: WebFetchHeaders | undefined = undefined; // We try-catch here just in case anything goes wrong with the async storage here goes wrong since it is Next.js internal API @@ -66,7 +66,7 @@ export function wrapGenerationFunctionWithSentry a let data: Record | undefined = undefined; if (getClient()?.getOptions().sendDefaultPii) { const props: unknown = args[0]; - const { params, searchParams } = await safeExtractParamsAndSearchParamsFromProps(props); + const { params, searchParams } = maybeExtractSynchronousParamsAndSearchParams(props); data = { params, searchParams }; } diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 13aa599e8d81..50bfd2d6ab0f 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -26,7 +26,7 @@ import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-l import { flushSafelyWithTimeout } from './utils/responseEnd'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; import { getSanitizedRequestUrl } from './utils/urls'; -import { safeExtractParamsAndSearchParamsFromProps } from './utils/wrapperUtils'; +import { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils'; /** * Wraps an `app` directory server component with Sentry error instrumentation. @@ -41,7 +41,7 @@ export function wrapServerComponentWithSentry any> // Next.js will turn them into synchronous functions and it will transform any `await`s into instances of the `use` // hook. 🤯 return new Proxy(appDirComponent, { - apply: async (originalFunction, thisArg, args) => { + apply: (originalFunction, thisArg, args) => { const requestTraceId = getActiveSpan()?.spanContext().traceId; const isolationScope = commonObjectToIsolationScope(context.headers); @@ -65,7 +65,7 @@ export function wrapServerComponentWithSentry any> if (getClient()?.getOptions().sendDefaultPii) { const props: unknown = args[0]; - const { params: paramsFromProps } = await safeExtractParamsAndSearchParamsFromProps(props); + const { params: paramsFromProps } = maybeExtractSynchronousParamsAndSearchParams(props); params = paramsFromProps; } From 22b71dc1250128ce4560459327d06c4c80fcb9b9 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 25 Jul 2025 13:16:40 +0200 Subject: [PATCH 3/4] test dev-server output --- .../test-applications/nextjs-15/.gitignore | 2 ++ .../test-applications/nextjs-15/app/page.tsx | 3 +++ .../test-applications/nextjs-15/package.json | 2 +- .../nextjs-15/playwright.config.mjs | 4 ++-- .../nextjs-15/tests/async-params.test.ts | 16 ++++++++++++++++ 5 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/app/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/tests/async-params.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore index ebdbfc025b6a..0c60c8eeaee8 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore @@ -44,3 +44,5 @@ next-env.d.ts test-results event-dumps + +.tmp_dev_server_logs diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/page.tsx new file mode 100644 index 000000000000..04618df0d754 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

Next 15 test app

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json index 8216f06f7be6..063f36d3b164 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", - "clean": "npx rimraf node_modules pnpm-lock.yaml", + "clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test", "test:dev-turbo": "TEST_ENV=dev-turbopack playwright test", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs index f2aa01e3e3c8..e1be6810f4dc 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs @@ -7,11 +7,11 @@ if (!testEnv) { const getStartCommand = () => { if (testEnv === 'dev-turbopack') { - return 'pnpm next dev -p 3030 --turbopack'; + return 'pnpm next dev -p 3030 --turbopack 2>&1 | tee .tmp_dev_server_logs'; } if (testEnv === 'development') { - return 'pnpm next dev -p 3030'; + return 'pnpm next dev -p 3030 2>&1 | tee .tmp_dev_server_logs'; } if (testEnv === 'production') { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/async-params.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/async-params.test.ts new file mode 100644 index 000000000000..d1f60cc0c5af --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/async-params.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from '@playwright/test'; +import fs from 'fs'; + +test.only('should not print warning for async params', async ({ page }) => { + test.skip( + process.env.TEST_ENV !== 'development' && process.env.TEST_ENV !== 'dev-turbopack', + 'should be skipped for non-dev mode', + ); + await page.goto('/'); + + // If the server exits with code 1, the test will fail (see instrumentation.ts) + const devStdout = fs.readFileSync('.tmp_dev_server_logs', 'utf-8'); + expect(devStdout).not.toContain('`params` should be awaited before using its properties.'); + + await expect(page.getByText('Next 15 test app')).toBeVisible(); +}); From 44fe6099518c6878a3ad9d32326dfd4aff7a2dad Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 25 Jul 2025 13:24:02 +0200 Subject: [PATCH 4/4] old man thanks cursor --- .../test-applications/nextjs-15/tests/async-params.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/async-params.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/async-params.test.ts index d1f60cc0c5af..c8b35ea491ef 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/async-params.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/async-params.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import fs from 'fs'; -test.only('should not print warning for async params', async ({ page }) => { +test('should not print warning for async params', async ({ page }) => { test.skip( process.env.TEST_ENV !== 'development' && process.env.TEST_ENV !== 'dev-turbopack', 'should be skipped for non-dev mode',