Skip to content

Commit ea9ee84

Browse files
authored
fix(nextjs): Handle async params in url extraction (#17162)
1 parent b0909cd commit ea9ee84

File tree

8 files changed

+58
-10
lines changed

8 files changed

+58
-10
lines changed

dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ next-env.d.ts
4444

4545
test-results
4646
event-dumps
47+
48+
.tmp_dev_server_logs
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>Next 15 test app</p>;
3+
}

dev-packages/e2e-tests/test-applications/nextjs-15/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"scripts": {
66
"build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)",
7-
"clean": "npx rimraf node_modules pnpm-lock.yaml",
7+
"clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs",
88
"test:prod": "TEST_ENV=production playwright test",
99
"test:dev": "TEST_ENV=development playwright test",
1010
"test:dev-turbo": "TEST_ENV=dev-turbopack playwright test",

dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ if (!testEnv) {
77

88
const getStartCommand = () => {
99
if (testEnv === 'dev-turbopack') {
10-
return 'pnpm next dev -p 3030 --turbopack';
10+
return 'pnpm next dev -p 3030 --turbopack 2>&1 | tee .tmp_dev_server_logs';
1111
}
1212

1313
if (testEnv === 'development') {
14-
return 'pnpm next dev -p 3030';
14+
return 'pnpm next dev -p 3030 2>&1 | tee .tmp_dev_server_logs';
1515
}
1616

1717
if (testEnv === 'production') {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { expect, test } from '@playwright/test';
2+
import fs from 'fs';
3+
4+
test('should not print warning for async params', async ({ page }) => {
5+
test.skip(
6+
process.env.TEST_ENV !== 'development' && process.env.TEST_ENV !== 'dev-turbopack',
7+
'should be skipped for non-dev mode',
8+
);
9+
await page.goto('/');
10+
11+
// If the server exits with code 1, the test will fail (see instrumentation.ts)
12+
const devStdout = fs.readFileSync('.tmp_dev_server_logs', 'utf-8');
13+
expect(devStdout).not.toContain('`params` should be awaited before using its properties.');
14+
15+
await expect(page.getByText('Next 15 test app')).toBeVisible();
16+
});

packages/nextjs/src/common/utils/wrapperUtils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getRootSpan,
77
getTraceData,
88
httpRequestToRequestData,
9+
isThenable,
910
} from '@sentry/core';
1011
import type { IncomingMessage, ServerResponse } from 'http';
1112
import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../span-attributes-with-logic-attached';
@@ -102,3 +103,31 @@ export async function callDataFetcherTraced<F extends (...args: any[]) => Promis
102103
throw e;
103104
}
104105
}
106+
107+
/**
108+
* Extracts the params and searchParams from the props object.
109+
*
110+
* Depending on the next version, params and searchParams may be a promise which we do not want to resolve in this function.
111+
*/
112+
export function maybeExtractSynchronousParamsAndSearchParams(props: unknown): {
113+
params: Record<string, string> | undefined;
114+
searchParams: Record<string, string> | undefined;
115+
} {
116+
let params =
117+
props && typeof props === 'object' && 'params' in props
118+
? (props.params as Record<string, string> | Promise<Record<string, string>> | undefined)
119+
: undefined;
120+
if (isThenable(params)) {
121+
params = undefined;
122+
}
123+
124+
let searchParams =
125+
props && typeof props === 'object' && 'searchParams' in props
126+
? (props.searchParams as Record<string, string> | Promise<Record<string, string>> | undefined)
127+
: undefined;
128+
if (isThenable(searchParams)) {
129+
searchParams = undefined;
130+
}
131+
132+
return { params, searchParams };
133+
}

packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavi
2424
import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached';
2525
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
2626
import { getSanitizedRequestUrl } from './utils/urls';
27+
import { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils';
2728
/**
2829
* Wraps a generation function (e.g. generateMetadata) with Sentry error and performance instrumentation.
2930
*/
@@ -65,9 +66,7 @@ export function wrapGenerationFunctionWithSentry<F extends (...args: any[]) => a
6566
let data: Record<string, unknown> | undefined = undefined;
6667
if (getClient()?.getOptions().sendDefaultPii) {
6768
const props: unknown = args[0];
68-
const params = props && typeof props === 'object' && 'params' in props ? props.params : undefined;
69-
const searchParams =
70-
props && typeof props === 'object' && 'searchParams' in props ? props.searchParams : undefined;
69+
const { params, searchParams } = maybeExtractSynchronousParamsAndSearchParams(props);
7170
data = { params, searchParams };
7271
}
7372

packages/nextjs/src/common/wrapServerComponentWithSentry.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-l
2626
import { flushSafelyWithTimeout } from './utils/responseEnd';
2727
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
2828
import { getSanitizedRequestUrl } from './utils/urls';
29+
import { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils';
2930

3031
/**
3132
* Wraps an `app` directory server component with Sentry error instrumentation.
@@ -64,10 +65,8 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any>
6465

6566
if (getClient()?.getOptions().sendDefaultPii) {
6667
const props: unknown = args[0];
67-
params =
68-
props && typeof props === 'object' && 'params' in props
69-
? (props.params as Record<string, string>)
70-
: undefined;
68+
const { params: paramsFromProps } = maybeExtractSynchronousParamsAndSearchParams(props);
69+
params = paramsFromProps;
7170
}
7271

7372
isolationScope.setSDKProcessingMetadata({

0 commit comments

Comments
 (0)