diff --git a/packages/nextjs/rollup.npm.config.js b/packages/nextjs/rollup.npm.config.js index 19e349f70f8f..b573988d3759 100644 --- a/packages/nextjs/rollup.npm.config.js +++ b/packages/nextjs/rollup.npm.config.js @@ -28,6 +28,7 @@ export default [ 'src/config/templates/apiWrapperTemplate.ts', 'src/config/templates/middlewareWrapperTemplate.ts', 'src/config/templates/serverComponentWrapperTemplate.ts', + 'src/config/templates/requestAsyncStorageShim.ts', ], packageSpecificConfig: { @@ -43,7 +44,12 @@ export default [ // make it so Rollup calms down about the fact that we're combining default and named exports exports: 'named', }, - external: ['@sentry/nextjs', 'next/headers', '__SENTRY_WRAPPING_TARGET_FILE__'], + external: [ + '@sentry/nextjs', + 'next/dist/client/components/request-async-storagee', + '__SENTRY_WRAPPING_TARGET_FILE__', + '__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__', + ], }, }), ), diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts index fb3e76be72f0..e76af7126a27 100644 --- a/packages/nextjs/src/config/loaders/wrappingLoader.ts +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -1,11 +1,21 @@ import commonjs from '@rollup/plugin-commonjs'; import { stringMatchesSomePattern } from '@sentry/utils'; +import * as chalk from 'chalk'; import * as fs from 'fs'; import * as path from 'path'; import { rollup } from 'rollup'; import type { LoaderThis } from './types'; +// Just a simple placeholder to make referencing module consistent +const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module'; + +// Needs to end in .cjs in order for the `commonjs` plugin to pick it up +const WRAPPING_TARGET_MODULE_NAME = '__SENTRY_WRAPPING_TARGET_FILE__.cjs'; + +// Non-public API. Can be found here: https://github.com/vercel/next.js/blob/46151dd68b417e7850146d00354f89930d10b43b/packages/next/src/client/components/request-async-storage.ts +const NEXTJS_REQUEST_ASYNC_STORAGE_MODULE_PATH = 'next/dist/client/components/request-async-storagee'; + const apiWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'apiWrapperTemplate.js'); const apiWrapperTemplateCode = fs.readFileSync(apiWrapperTemplatePath, { encoding: 'utf8' }); @@ -15,6 +25,10 @@ const pageWrapperTemplateCode = fs.readFileSync(pageWrapperTemplatePath, { encod const middlewareWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'middlewareWrapperTemplate.js'); const middlewareWrapperTemplateCode = fs.readFileSync(middlewareWrapperTemplatePath, { encoding: 'utf8' }); +const requestAsyncStorageShimPath = path.resolve(__dirname, '..', 'templates', 'requestAsyncStorageShim.js'); +const requestAsyncStorageModuleExists = moduleExists(NEXTJS_REQUEST_ASYNC_STORAGE_MODULE_PATH); +let showedMissingAsyncStorageModuleWarning = false; + const serverComponentWrapperTemplatePath = path.resolve( __dirname, '..', @@ -23,12 +37,6 @@ const serverComponentWrapperTemplatePath = path.resolve( ); const serverComponentWrapperTemplateCode = fs.readFileSync(serverComponentWrapperTemplatePath, { encoding: 'utf8' }); -// Just a simple placeholder to make referencing module consistent -const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module'; - -// Needs to end in .cjs in order for the `commonjs` plugin to pick it up -const WRAPPING_TARGET_MODULE_NAME = '__SENTRY_WRAPPING_TARGET_FILE__.cjs'; - type LoaderOptions = { pagesDir: string; appDir: string; @@ -37,6 +45,15 @@ type LoaderOptions = { wrappingTargetKind: 'page' | 'api-route' | 'middleware' | 'server-component'; }; +function moduleExists(id: string): boolean { + try { + require.resolve(id); + return true; + } catch (e) { + return false; + } +} + /** * Replace the loaded file with a wrapped version the original file. In the wrapped version, the original file is loaded, * any data-fetching functions (`getInitialProps`, `getStaticProps`, and `getServerSideProps`) or API routes it contains @@ -126,6 +143,24 @@ export default function wrappingLoader( templateCode = serverComponentWrapperTemplateCode; + if (requestAsyncStorageModuleExists) { + templateCode = templateCode.replace( + /__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__/g, + NEXTJS_REQUEST_ASYNC_STORAGE_MODULE_PATH, + ); + } else { + if (!showedMissingAsyncStorageModuleWarning) { + // eslint-disable-next-line no-console + console.warn( + `${chalk.yellow('warn')} - The Sentry SDK could not access the ${chalk.bold.cyan( + 'RequestAsyncStorage', + )} module. Certain features may not work. There is nothing you can do to fix this yourself, but future SDK updates may resolve this.\n`, + ); + showedMissingAsyncStorageModuleWarning = true; + } + templateCode = templateCode.replace(/__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__/g, requestAsyncStorageShimPath); + } + templateCode = templateCode.replace(/__ROUTE__/g, parameterizedPagesRoute.replace(/\\/g, '\\\\')); const componentTypeMatch = path.posix diff --git a/packages/nextjs/src/config/templates/requestAsyncStorageShim.ts b/packages/nextjs/src/config/templates/requestAsyncStorageShim.ts new file mode 100644 index 000000000000..bc0e23000815 --- /dev/null +++ b/packages/nextjs/src/config/templates/requestAsyncStorageShim.ts @@ -0,0 +1,15 @@ +export interface RequestAsyncStorage { + getStore: () => + | { + headers: { + get: Headers['get']; + }; + } + | undefined; +} + +export const requestAsyncStorage: RequestAsyncStorage = { + getStore: () => { + return undefined; + }, +}; diff --git a/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts b/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts index 7a8e755a147a..3f5ef8349c30 100644 --- a/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts @@ -1,28 +1,21 @@ -/* - * This file is a template for the code which will be substituted when our webpack loader handles non-API files in the - * `pages/` directory. - * - * We use `__SENTRY_WRAPPING_TARGET_FILE__` as a placeholder for the path to the file being wrapped. Because it's not a real package, - * this causes both TS and ESLint to complain, hence the pragma comments below. - */ - -// @ts-ignore See above +// @ts-ignore 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. // eslint-disable-next-line import/no-unresolved -import * as wrapee from '__SENTRY_WRAPPING_TARGET_FILE__'; +import { requestAsyncStorage } from '__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__'; +// @ts-ignore We use `__SENTRY_WRAPPING_TARGET_FILE__` as a placeholder for the path to the file being wrapped. +// eslint-disable-next-line import/no-unresolved +import * as serverComponentModule from '__SENTRY_WRAPPING_TARGET_FILE__'; // eslint-disable-next-line import/no-extraneous-dependencies import * as Sentry from '@sentry/nextjs'; -// @ts-ignore This template is only used with the app directory so we know that this dependency exists. -// eslint-disable-next-line import/no-unresolved -import { headers } from 'next/headers'; -declare function headers(): { get: (header: string) => string | undefined }; +import type { RequestAsyncStorage } from './requestAsyncStorageShim'; -type ServerComponentModule = { +declare const requestAsyncStorage: RequestAsyncStorage; + +declare const serverComponentModule: { default: unknown; }; -const serverComponentModule = wrapee as ServerComponentModule; - const serverComponent = serverComponentModule.default; let wrappedServerComponent; @@ -32,21 +25,16 @@ if (typeof serverComponent === 'function') { // is technically a userfile so it gets the loader magic applied. wrappedServerComponent = new Proxy(serverComponent, { apply: (originalFunction, thisArg, args) => { - let sentryTraceHeader: string | undefined = undefined; - let baggageHeader: string | undefined = undefined; - - // If we call the headers function inside the build phase, Next.js will automatically mark the server component as - // dynamic(SSR) which we do not want in case the users have a static component. - if (process.env.NEXT_PHASE !== 'phase-production-build') { - // try/catch because calling headers() when a previously statically generated page is being revalidated causes a - // runtime error in next.js as switching a page from static to dynamic during runtime is not allowed - try { - const headersList = headers(); - sentryTraceHeader = headersList.get('sentry-trace'); - baggageHeader = headersList.get('baggage'); - } catch { - /** empty */ - } + let sentryTraceHeader: string | undefined | null = undefined; + let baggageHeader: string | undefined | null = undefined; + + // We try-catch here just in case the API around `requestAsyncStorage` changes unexpectedly since it is not public API + try { + const requestAsyncStore = requestAsyncStorage.getStore(); + sentryTraceHeader = requestAsyncStore?.headers.get('sentry-trace'); + baggageHeader = requestAsyncStore?.headers.get('baggage'); + } catch (e) { + /** empty */ } return Sentry.wrapServerComponentWithSentry(originalFunction, {