Skip to content

Commit

Permalink
feat(nextjs): Support new async APIs (headers(), params, `searchP…
Browse files Browse the repository at this point in the history
…arams`) (#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
  • Loading branch information
lforst authored Sep 30, 2024
1 parent 2ee1687 commit ca19f34
Show file tree
Hide file tree
Showing 8 changed files with 60 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,31 @@ export default function Page() {
return <p>Hello World!</p>;
}

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',
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = {};
headerList.forEach((value, key) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { use } from 'react';
import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools';

export default function Page({ params }: { params: Record<string, string> }) {
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 (
<div style={{ border: '1px solid lightgrey', padding: '12px' }}>
<h2>Page (/client-component/[...parameters])</h2>
<p>Params: {JSON.stringify(params['parameters'])}</p>
<p>Params: {JSON.stringify(normalizedParams['parameters'])}</p>
<ClientErrorDebugTools />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { use } from 'react';
import { ClientErrorDebugTools } from '../../../../components/client-error-debug-tools';

export default function Page({ params }: { params: Record<string, string> }) {
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 (
<div style={{ border: '1px solid lightgrey', padding: '12px' }}>
<h2>Page (/client-component/[parameter])</h2>
<p>Parameter: {JSON.stringify(params['parameter'])}</p>
<p>Parameter: {JSON.stringify(normalizedParams['parameter'])}</p>
<ClientErrorDebugTools />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, string> }) {
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 (
<div style={{ border: '1px solid lightgrey', padding: '12px' }}>
<h2>Page (/server-component/[...parameters])</h2>
<p>Params: {JSON.stringify(params['parameters'])}</p>
<p>Params: {JSON.stringify(normalizedParams['parameters'])}</p>
<ClientErrorDebugTools />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, string> }) {
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 (
<div style={{ border: '1px solid lightgrey', padding: '12px' }}>
<h2>Page (/server-component/[parameter])</h2>
<p>Parameter: {JSON.stringify(params['parameter'])}</p>
<p>Parameter: {JSON.stringify(normalizedParams['parameter'])}</p>
<ClientErrorDebugTools />
</div>
);
Expand Down
22 changes: 17 additions & 5 deletions packages/nextjs/src/common/withServerActionInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReadonlyHeaders>` 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<any>;

/**
* Whether the server action response should be included in any events captured within the server action.
*/
recordResponse?: boolean;
}

Expand Down Expand Up @@ -55,16 +66,17 @@ async function withServerActionInstrumentationImplementation<A extends (...args:
callback: A,
): Promise<ReturnType<A>> {
return escapeNextjsTracing(() => {
return withIsolationScope(isolationScope => {
return withIsolationScope(async isolationScope => {
const sendDefaultPii = getClient()?.getOptions().sendDefaultPii;

let sentryTraceHeader;
let baggageHeader;
const fullHeadersObject: Record<string, string> = {};
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) {
Expand Down

0 comments on commit ca19f34

Please sign in to comment.