Skip to content

Commit 2974ff1

Browse files
author
Luca Forstner
authored
fix(nextjs): Use Next.js internal AsyncStorage (#7630)
1 parent 30f2c24 commit 2974ff1

File tree

4 files changed

+83
-39
lines changed

4 files changed

+83
-39
lines changed

packages/nextjs/rollup.npm.config.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default [
2828
'src/config/templates/apiWrapperTemplate.ts',
2929
'src/config/templates/middlewareWrapperTemplate.ts',
3030
'src/config/templates/serverComponentWrapperTemplate.ts',
31+
'src/config/templates/requestAsyncStorageShim.ts',
3132
],
3233

3334
packageSpecificConfig: {
@@ -43,7 +44,12 @@ export default [
4344
// make it so Rollup calms down about the fact that we're combining default and named exports
4445
exports: 'named',
4546
},
46-
external: ['@sentry/nextjs', 'next/headers', '__SENTRY_WRAPPING_TARGET_FILE__'],
47+
external: [
48+
'@sentry/nextjs',
49+
'next/dist/client/components/request-async-storagee',
50+
'__SENTRY_WRAPPING_TARGET_FILE__',
51+
'__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__',
52+
],
4753
},
4854
}),
4955
),

packages/nextjs/src/config/loaders/wrappingLoader.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
import commonjs from '@rollup/plugin-commonjs';
22
import { stringMatchesSomePattern } from '@sentry/utils';
3+
import * as chalk from 'chalk';
34
import * as fs from 'fs';
45
import * as path from 'path';
56
import { rollup } from 'rollup';
67

78
import type { LoaderThis } from './types';
89

10+
// Just a simple placeholder to make referencing module consistent
11+
const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module';
12+
13+
// Needs to end in .cjs in order for the `commonjs` plugin to pick it up
14+
const WRAPPING_TARGET_MODULE_NAME = '__SENTRY_WRAPPING_TARGET_FILE__.cjs';
15+
16+
// Non-public API. Can be found here: https://github.com/vercel/next.js/blob/46151dd68b417e7850146d00354f89930d10b43b/packages/next/src/client/components/request-async-storage.ts
17+
const NEXTJS_REQUEST_ASYNC_STORAGE_MODULE_PATH = 'next/dist/client/components/request-async-storagee';
18+
919
const apiWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'apiWrapperTemplate.js');
1020
const apiWrapperTemplateCode = fs.readFileSync(apiWrapperTemplatePath, { encoding: 'utf8' });
1121

@@ -15,6 +25,10 @@ const pageWrapperTemplateCode = fs.readFileSync(pageWrapperTemplatePath, { encod
1525
const middlewareWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'middlewareWrapperTemplate.js');
1626
const middlewareWrapperTemplateCode = fs.readFileSync(middlewareWrapperTemplatePath, { encoding: 'utf8' });
1727

28+
const requestAsyncStorageShimPath = path.resolve(__dirname, '..', 'templates', 'requestAsyncStorageShim.js');
29+
const requestAsyncStorageModuleExists = moduleExists(NEXTJS_REQUEST_ASYNC_STORAGE_MODULE_PATH);
30+
let showedMissingAsyncStorageModuleWarning = false;
31+
1832
const serverComponentWrapperTemplatePath = path.resolve(
1933
__dirname,
2034
'..',
@@ -23,12 +37,6 @@ const serverComponentWrapperTemplatePath = path.resolve(
2337
);
2438
const serverComponentWrapperTemplateCode = fs.readFileSync(serverComponentWrapperTemplatePath, { encoding: 'utf8' });
2539

26-
// Just a simple placeholder to make referencing module consistent
27-
const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module';
28-
29-
// Needs to end in .cjs in order for the `commonjs` plugin to pick it up
30-
const WRAPPING_TARGET_MODULE_NAME = '__SENTRY_WRAPPING_TARGET_FILE__.cjs';
31-
3240
type LoaderOptions = {
3341
pagesDir: string;
3442
appDir: string;
@@ -37,6 +45,15 @@ type LoaderOptions = {
3745
wrappingTargetKind: 'page' | 'api-route' | 'middleware' | 'server-component';
3846
};
3947

48+
function moduleExists(id: string): boolean {
49+
try {
50+
require.resolve(id);
51+
return true;
52+
} catch (e) {
53+
return false;
54+
}
55+
}
56+
4057
/**
4158
* Replace the loaded file with a wrapped version the original file. In the wrapped version, the original file is loaded,
4259
* any data-fetching functions (`getInitialProps`, `getStaticProps`, and `getServerSideProps`) or API routes it contains
@@ -126,6 +143,24 @@ export default function wrappingLoader(
126143

127144
templateCode = serverComponentWrapperTemplateCode;
128145

146+
if (requestAsyncStorageModuleExists) {
147+
templateCode = templateCode.replace(
148+
/__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__/g,
149+
NEXTJS_REQUEST_ASYNC_STORAGE_MODULE_PATH,
150+
);
151+
} else {
152+
if (!showedMissingAsyncStorageModuleWarning) {
153+
// eslint-disable-next-line no-console
154+
console.warn(
155+
`${chalk.yellow('warn')} - The Sentry SDK could not access the ${chalk.bold.cyan(
156+
'RequestAsyncStorage',
157+
)} module. Certain features may not work. There is nothing you can do to fix this yourself, but future SDK updates may resolve this.\n`,
158+
);
159+
showedMissingAsyncStorageModuleWarning = true;
160+
}
161+
templateCode = templateCode.replace(/__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__/g, requestAsyncStorageShimPath);
162+
}
163+
129164
templateCode = templateCode.replace(/__ROUTE__/g, parameterizedPagesRoute.replace(/\\/g, '\\\\'));
130165

131166
const componentTypeMatch = path.posix
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export interface RequestAsyncStorage {
2+
getStore: () =>
3+
| {
4+
headers: {
5+
get: Headers['get'];
6+
};
7+
}
8+
| undefined;
9+
}
10+
11+
export const requestAsyncStorage: RequestAsyncStorage = {
12+
getStore: () => {
13+
return undefined;
14+
},
15+
};

packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts

Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
1-
/*
2-
* This file is a template for the code which will be substituted when our webpack loader handles non-API files in the
3-
* `pages/` directory.
4-
*
5-
* We use `__SENTRY_WRAPPING_TARGET_FILE__` as a placeholder for the path to the file being wrapped. Because it's not a real package,
6-
* this causes both TS and ESLint to complain, hence the pragma comments below.
7-
*/
8-
9-
// @ts-ignore See above
1+
// @ts-ignore Because we cannot be sure if the RequestAsyncStorage module exists (it is not part of the Next.js public
2+
// API) we use a shim if it doesn't exist. The logic for this is in the wrapping loader.
103
// eslint-disable-next-line import/no-unresolved
11-
import * as wrapee from '__SENTRY_WRAPPING_TARGET_FILE__';
4+
import { requestAsyncStorage } from '__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__';
5+
// @ts-ignore We use `__SENTRY_WRAPPING_TARGET_FILE__` as a placeholder for the path to the file being wrapped.
6+
// eslint-disable-next-line import/no-unresolved
7+
import * as serverComponentModule from '__SENTRY_WRAPPING_TARGET_FILE__';
128
// eslint-disable-next-line import/no-extraneous-dependencies
139
import * as Sentry from '@sentry/nextjs';
14-
// @ts-ignore This template is only used with the app directory so we know that this dependency exists.
15-
// eslint-disable-next-line import/no-unresolved
16-
import { headers } from 'next/headers';
1710

18-
declare function headers(): { get: (header: string) => string | undefined };
11+
import type { RequestAsyncStorage } from './requestAsyncStorageShim';
1912

20-
type ServerComponentModule = {
13+
declare const requestAsyncStorage: RequestAsyncStorage;
14+
15+
declare const serverComponentModule: {
2116
default: unknown;
2217
};
2318

24-
const serverComponentModule = wrapee as ServerComponentModule;
25-
2619
const serverComponent = serverComponentModule.default;
2720

2821
let wrappedServerComponent;
@@ -32,21 +25,16 @@ if (typeof serverComponent === 'function') {
3225
// is technically a userfile so it gets the loader magic applied.
3326
wrappedServerComponent = new Proxy(serverComponent, {
3427
apply: (originalFunction, thisArg, args) => {
35-
let sentryTraceHeader: string | undefined = undefined;
36-
let baggageHeader: string | undefined = undefined;
37-
38-
// If we call the headers function inside the build phase, Next.js will automatically mark the server component as
39-
// dynamic(SSR) which we do not want in case the users have a static component.
40-
if (process.env.NEXT_PHASE !== 'phase-production-build') {
41-
// try/catch because calling headers() when a previously statically generated page is being revalidated causes a
42-
// runtime error in next.js as switching a page from static to dynamic during runtime is not allowed
43-
try {
44-
const headersList = headers();
45-
sentryTraceHeader = headersList.get('sentry-trace');
46-
baggageHeader = headersList.get('baggage');
47-
} catch {
48-
/** empty */
49-
}
28+
let sentryTraceHeader: string | undefined | null = undefined;
29+
let baggageHeader: string | undefined | null = undefined;
30+
31+
// We try-catch here just in case the API around `requestAsyncStorage` changes unexpectedly since it is not public API
32+
try {
33+
const requestAsyncStore = requestAsyncStorage.getStore();
34+
sentryTraceHeader = requestAsyncStore?.headers.get('sentry-trace');
35+
baggageHeader = requestAsyncStore?.headers.get('baggage');
36+
} catch (e) {
37+
/** empty */
5038
}
5139

5240
return Sentry.wrapServerComponentWithSentry(originalFunction, {

0 commit comments

Comments
 (0)