Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nextjs): Support new async APIs (headers(), params, searchParams) #13828

Merged
merged 8 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading