Skip to content

Commit

Permalink
fix(nextjs): Use Next.js internal AsyncStorage (#7630)
Browse files Browse the repository at this point in the history
  • Loading branch information
lforst committed Mar 29, 2023
1 parent 30f2c24 commit 2974ff1
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 39 deletions.
8 changes: 7 additions & 1 deletion packages/nextjs/rollup.npm.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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__',
],
},
}),
),
Expand Down
47 changes: 41 additions & 6 deletions packages/nextjs/src/config/loaders/wrappingLoader.ts
Original file line number Diff line number Diff line change
@@ -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' });

Expand All @@ -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,
'..',
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions packages/nextjs/src/config/templates/requestAsyncStorageShim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface RequestAsyncStorage {
getStore: () =>
| {
headers: {
get: Headers['get'];
};
}
| undefined;
}

export const requestAsyncStorage: RequestAsyncStorage = {
getStore: () => {
return undefined;
},
};
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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, {
Expand Down

0 comments on commit 2974ff1

Please sign in to comment.