Skip to content

Commit ca19f34

Browse files
authored
feat(nextjs): Support new async APIs (headers(), params, searchParams) (#13828)
Changes in Next.js vercel/next.js#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 #13805 Resolves #13779 Resolves #13780
1 parent 2ee1687 commit ca19f34

File tree

8 files changed

+60
-31
lines changed

8 files changed

+60
-31
lines changed

dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx

+12-14
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,31 @@ export default function Page() {
77
return <p>Hello World!</p>;
88
}
99

10-
export async function generateMetadata({
11-
searchParams,
12-
}: {
13-
searchParams: { [key: string]: string | string[] | undefined };
14-
}) {
10+
export async function generateMetadata({ searchParams }: { searchParams: any }) {
11+
// 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
12+
const normalizedSearchParams = await searchParams;
13+
1514
Sentry.setTag('my-isolated-tag', true);
1615
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
1716

18-
if (searchParams['shouldThrowInGenerateMetadata']) {
17+
if (normalizedSearchParams['shouldThrowInGenerateMetadata']) {
1918
throw new Error('generateMetadata Error');
2019
}
2120

2221
return {
23-
title: searchParams['metadataTitle'] ?? 'not set',
22+
title: normalizedSearchParams['metadataTitle'] ?? 'not set',
2423
};
2524
}
2625

27-
export function generateViewport({
28-
searchParams,
29-
}: {
30-
searchParams: { [key: string]: string | undefined };
31-
}) {
32-
if (searchParams['shouldThrowInGenerateViewport']) {
26+
export async function generateViewport({ searchParams }: { searchParams: any }) {
27+
// 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
28+
const normalizedSearchParams = await searchParams;
29+
30+
if (normalizedSearchParams['shouldThrowInGenerateViewport']) {
3331
throw new Error('generateViewport Error');
3432
}
3533

3634
return {
37-
themeColor: searchParams['viewportThemeColor'] ?? 'black',
35+
themeColor: normalizedSearchParams['viewportThemeColor'] ?? 'black',
3836
};
3937
}

dev-packages/e2e-tests/test-applications/nextjs-14/app/propagation/utils.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export function makeHttpRequest(url: string) {
2626
});
2727
}
2828

29-
export function checkHandler() {
30-
const headerList = headers();
29+
export async function checkHandler() {
30+
const headerList = await headers();
3131

3232
const headerObj: Record<string, unknown> = {};
3333
headerList.forEach((value, key) => {

dev-packages/e2e-tests/test-applications/nextjs-15/app/ppr-error/[param]/page.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import * as Sentry from '@sentry/nextjs';
33
export default async function Page({
44
searchParams,
55
}: {
6-
searchParams: { id?: string };
6+
searchParams: any;
77
}) {
8+
// 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
9+
const normalizedSearchParams = await searchParams;
10+
811
try {
9-
console.log(searchParams.id); // Accessing a field on searchParams will throw the PPR error
12+
console.log(normalizedSearchParams.id); // Accessing a field on searchParams will throw the PPR error
1013
} catch (e) {
1114
Sentry.captureException(e); // This error should not be reported
1215
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for any async event processors to run

dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[...parameters]/page.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import { use } from 'react';
12
import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools';
23

3-
export default function Page({ params }: { params: Record<string, string> }) {
4+
export default function Page({ params }: any) {
5+
// 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
6+
const normalizedParams = 'then' in params ? use(params) : params;
7+
48
return (
59
<div style={{ border: '1px solid lightgrey', padding: '12px' }}>
610
<h2>Page (/client-component/[...parameters])</h2>
7-
<p>Params: {JSON.stringify(params['parameters'])}</p>
11+
<p>Params: {JSON.stringify(normalizedParams['parameters'])}</p>
812
<ClientErrorDebugTools />
913
</div>
1014
);

dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/client-component/parameter/[parameter]/page.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import { use } from 'react';
12
import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools';
23

3-
export default function Page({ params }: { params: Record<string, string> }) {
4+
export default function Page({ params }: any) {
5+
// 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
6+
const normalizedParams = 'then' in params ? use(params) : params;
7+
48
return (
59
<div style={{ border: '1px solid lightgrey', padding: '12px' }}>
610
<h2>Page (/client-component/[parameter])</h2>
7-
<p>Parameter: {JSON.stringify(params['parameter'])}</p>
11+
<p>Parameter: {JSON.stringify(normalizedParams['parameter'])}</p>
812
<ClientErrorDebugTools />
913
</div>
1014
);

dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[...parameters]/page.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
import { use } from 'react';
12
import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools';
23

34
export const dynamic = 'force-dynamic';
45

5-
export default async function Page({ params }: { params: Record<string, string> }) {
6+
export default function Page({ params }: any) {
7+
// 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
8+
const normalizedParams = 'then' in params ? use(params) : params;
9+
610
return (
711
<div style={{ border: '1px solid lightgrey', padding: '12px' }}>
812
<h2>Page (/server-component/[...parameters])</h2>
9-
<p>Params: {JSON.stringify(params['parameters'])}</p>
13+
<p>Params: {JSON.stringify(normalizedParams['parameters'])}</p>
1014
<ClientErrorDebugTools />
1115
</div>
1216
);

dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/parameter/[parameter]/page.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
import { use } from 'react';
12
import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools';
23

34
export const dynamic = 'force-dynamic';
45

5-
export default async function Page({ params }: { params: Record<string, string> }) {
6+
export default function Page({ params }: any) {
7+
// 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
8+
const normalizedParams = 'then' in params ? use(params) : params;
9+
610
return (
711
<div style={{ border: '1px solid lightgrey', padding: '12px' }}>
812
<h2>Page (/server-component/[parameter])</h2>
9-
<p>Parameter: {JSON.stringify(params['parameter'])}</p>
13+
<p>Parameter: {JSON.stringify(normalizedParams['parameter'])}</p>
1014
<ClientErrorDebugTools />
1115
</div>
1216
);

packages/nextjs/src/common/withServerActionInstrumentation.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,18 @@ import { vercelWaitUntil } from './utils/vercelWaitUntil';
1515

1616
interface Options {
1717
formData?: FormData;
18-
headers?: Headers;
18+
19+
/**
20+
* Headers as returned from `headers()`.
21+
*
22+
* Currently accepts both a plain `Headers` object and `Promise<ReadonlyHeaders>` to be compatible with async APIs introduced in Next.js 15: https://github.com/vercel/next.js/pull/68812
23+
*/
24+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
25+
headers?: Headers | Promise<any>;
26+
27+
/**
28+
* Whether the server action response should be included in any events captured within the server action.
29+
*/
1930
recordResponse?: boolean;
2031
}
2132

@@ -55,16 +66,17 @@ async function withServerActionInstrumentationImplementation<A extends (...args:
5566
callback: A,
5667
): Promise<ReturnType<A>> {
5768
return escapeNextjsTracing(() => {
58-
return withIsolationScope(isolationScope => {
69+
return withIsolationScope(async isolationScope => {
5970
const sendDefaultPii = getClient()?.getOptions().sendDefaultPii;
6071

6172
let sentryTraceHeader;
6273
let baggageHeader;
6374
const fullHeadersObject: Record<string, string> = {};
6475
try {
65-
sentryTraceHeader = options.headers?.get('sentry-trace') ?? undefined;
66-
baggageHeader = options.headers?.get('baggage');
67-
options.headers?.forEach((value, key) => {
76+
const awaitedHeaders: Headers = await options.headers;
77+
sentryTraceHeader = awaitedHeaders?.get('sentry-trace') ?? undefined;
78+
baggageHeader = awaitedHeaders?.get('baggage');
79+
awaitedHeaders?.forEach((value, key) => {
6880
fullHeadersObject[key] = value;
6981
});
7082
} catch (e) {

0 commit comments

Comments
 (0)