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..c8b35ea491ef --- /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('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(); +}); diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index 529acab7f96e..23b960c857cf 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,31 @@ 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 which we do not want to resolve in this function. + */ +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)) { + params = undefined; + } + + let searchParams = + props && typeof props === 'object' && 'searchParams' in props + ? (props.searchParams as Record | Promise> | undefined) + : undefined; + if (isThenable(searchParams)) { + searchParams = undefined; + } + + return { params, searchParams }; +} diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index aad64e0f4ea4..2067ebccc245 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 { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils'; /** * Wraps a generation function (e.g. generateMetadata) with Sentry error and performance instrumentation. */ @@ -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 } = maybeExtractSynchronousParamsAndSearchParams(props); data = { params, searchParams }; } diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 16f6728deda1..50bfd2d6ab0f 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 { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils'; /** * Wraps an `app` directory server component with Sentry error instrumentation. @@ -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 } = maybeExtractSynchronousParamsAndSearchParams(props); + params = paramsFromProps; } isolationScope.setSDKProcessingMetadata({