From 6b4401f88b657a6b6dafb20a3f5cf160655efd37 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Thu, 10 Oct 2024 16:39:24 +0900 Subject: [PATCH] fix(nextjs): Detect new locations for request async storage to support Next.js v15.0.0-canary.180 and higher (#13920) --- .../test-applications/nextjs-15/package.json | 2 +- .../templates/routeHandlerWrapperTemplate.ts | 23 +++++++++++++++---- .../serverComponentWrapperTemplate.ts | 23 +++++++++++++++---- packages/nextjs/src/config/webpack.ts | 6 +++++ 4 files changed, 45 insertions(+), 9 deletions(-) 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 8fd9f4368977..54d672523253 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json @@ -17,7 +17,7 @@ "@types/node": "18.11.17", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", - "next": "15.0.0-canary.112", + "next": "15.0.0-canary.182", "react": "beta", "react-dom": "beta", "typescript": "4.9.5" diff --git a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts index 346b2c29a784..9f955f9f4109 100644 --- a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts @@ -2,12 +2,29 @@ import * as Sentry from '@sentry/nextjs'; import type { WebFetchHeaders } from '@sentry/types'; // @ts-expect-error Because we cannot be sure if the RequestAsyncStorage module exists (it is not part of the Next.js public // API) we use a shim if it doesn't exist. The logic for this is in the wrapping loader. -import { requestAsyncStorage } from '__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__'; +import * as origModule from '__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__'; // @ts-expect-error See above import * as routeModule from '__SENTRY_WRAPPING_TARGET_FILE__'; import type { RequestAsyncStorage } from './requestAsyncStorageShim'; +type NextAsyncStorageModule = + | { + workUnitAsyncStorage: RequestAsyncStorage; + } + | { + requestAsyncStorage: RequestAsyncStorage; + }; + +const asyncStorageModule = { ...origModule } as NextAsyncStorageModule; + +const requestAsyncStorage: RequestAsyncStorage | undefined = + 'workUnitAsyncStorage' in asyncStorageModule + ? asyncStorageModule.workUnitAsyncStorage + : 'requestAsyncStorage' in asyncStorageModule + ? asyncStorageModule.requestAsyncStorage + : undefined; + function wrapHandler(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'): T { // Running the instrumentation code during the build phase will mark any function as "dynamic" because we're accessing // the Request object. We do not want to turn handlers dynamic so we skip instrumentation in the build phase. @@ -28,7 +45,7 @@ function wrapHandler(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | ' // We try-catch here just in case the API around `requestAsyncStorage` changes unexpectedly since it is not public API try { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const requestAsyncStore = requestAsyncStorage.getStore() as ReturnType; + const requestAsyncStore = requestAsyncStorage?.getStore() as ReturnType; sentryTraceHeader = requestAsyncStore?.headers.get('sentry-trace') ?? undefined; baggageHeader = requestAsyncStore?.headers.get('baggage') ?? undefined; headers = requestAsyncStore?.headers; @@ -54,8 +71,6 @@ export * from '__SENTRY_WRAPPING_TARGET_FILE__'; // @ts-expect-error This is the file we're wrapping export { default } from '__SENTRY_WRAPPING_TARGET_FILE__'; -declare const requestAsyncStorage: RequestAsyncStorage; - type RouteHandler = (...args: unknown[]) => unknown; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access diff --git a/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts b/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts index 717826e3a081..364affc08058 100644 --- a/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts @@ -2,15 +2,29 @@ import * as Sentry from '@sentry/nextjs'; import type { WebFetchHeaders } from '@sentry/types'; // @ts-expect-error Because we cannot be sure if the RequestAsyncStorage module exists (it is not part of the Next.js public // API) we use a shim if it doesn't exist. The logic for this is in the wrapping loader. -// biome-ignore lint/nursery/noUnusedImports: Biome doesn't understand the shim with variable import path -import { requestAsyncStorage } from '__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__'; +import * as origModule from '__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__'; // @ts-expect-error We use `__SENTRY_WRAPPING_TARGET_FILE__` as a placeholder for the path to the file being wrapped. // biome-ignore lint/nursery/noUnusedImports: Biome doesn't understand the shim with variable import path import * as serverComponentModule from '__SENTRY_WRAPPING_TARGET_FILE__'; import type { RequestAsyncStorage } from './requestAsyncStorageShim'; -declare const requestAsyncStorage: RequestAsyncStorage; +type NextAsyncStorageModule = + | { + workUnitAsyncStorage: RequestAsyncStorage; + } + | { + requestAsyncStorage: RequestAsyncStorage; + }; + +const asyncStorageModule = { ...origModule } as NextAsyncStorageModule; + +const requestAsyncStorage: RequestAsyncStorage | undefined = + 'workUnitAsyncStorage' in asyncStorageModule + ? asyncStorageModule.workUnitAsyncStorage + : 'requestAsyncStorage' in asyncStorageModule + ? asyncStorageModule.requestAsyncStorage + : undefined; declare const serverComponentModule: { default: unknown; @@ -34,7 +48,8 @@ if (typeof serverComponent === 'function') { // We try-catch here just in `requestAsyncStorage` is undefined since it may not be defined try { - const requestAsyncStore = requestAsyncStorage.getStore(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const requestAsyncStore = requestAsyncStorage?.getStore() as ReturnType; sentryTraceHeader = requestAsyncStore?.headers.get('sentry-trace') ?? undefined; baggageHeader = requestAsyncStore?.headers.get('baggage') ?? undefined; headers = requestAsyncStore?.headers; diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 8fbc94b42195..9656a74c4efe 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -644,6 +644,12 @@ const POTENTIAL_REQUEST_ASYNC_STORAGE_LOCATIONS = [ // Introduced in Next.js 13.4.20 // https://github.com/vercel/next.js/blob/e1bc270830f2fc2df3542d4ef4c61b916c802df3/packages/next/src/client/components/request-async-storage.external.ts 'next/dist/client/components/request-async-storage.external.js', + // Introduced in Next.js 15.0.0-canary.180 + // https://github.com/vercel/next.js/blob/541167b9b0fed6af9f36472e632863ffec41f18c/packages/next/src/server/app-render/work-unit-async-storage.external.ts + 'next/dist/server/app-render/work-unit-async-storage.external.js', + // Introduced in Next.js 15.0.0-canary.182 + // https://github.com/vercel/next.js/blob/f35159e5e80138ca7373f57b47edcaae3bcf1728/packages/next/src/client/components/work-unit-async-storage.external.ts + 'next/dist/client/components/work-unit-async-storage.external.js', ]; function getRequestAsyncStorageModuleLocation(