Skip to content

Commit f22366d

Browse files
authored
feat(nextjs): Use Edge SDK for Edge bundles (#6753)
1 parent 9e3390d commit f22366d

File tree

8 files changed

+82
-46
lines changed

8 files changed

+82
-46
lines changed

packages/nextjs/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
},
1212
"main": "build/cjs/index.js",
1313
"module": "build/esm/index.js",
14-
"browser": "build/esm/client/index.js",
1514
"types": "build/types/index.types.d.ts",
1615
"publishConfig": {
1716
"access": "public"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { default as valueInjectionLoader } from './valueInjectionLoader';
22
export { default as prefixLoader } from './prefixLoader';
33
export { default as wrappingLoader } from './wrappingLoader';
4+
export { default as sdkMultiplexerLoader } from './sdkMultiplexerLoader';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { LoaderThis } from './types';
2+
3+
type LoaderOptions = {
4+
importTarget: string;
5+
};
6+
7+
/**
8+
* This loader allows us to multiplex SDKs depending on what is passed to the `importTarget` loader option.
9+
* If this loader encounters a file that contains the string "__SENTRY_SDK_MULTIPLEXER__" it will replace it's entire
10+
* content with an "export all"-statement that points to `importTarget`.
11+
*
12+
* In our case we use this to multiplex different SDKs depending on whether we're bundling browser code, server code,
13+
* or edge-runtime code.
14+
*/
15+
export default function sdkMultiplexerLoader(this: LoaderThis<LoaderOptions>, userCode: string): string {
16+
if (!userCode.includes('__SENTRY_SDK_MULTIPLEXER__')) {
17+
return userCode;
18+
}
19+
20+
// We know one or the other will be defined, depending on the version of webpack being used
21+
const { importTarget } = 'getOptions' in this ? this.getOptions() : this.query;
22+
23+
return `export * from "${importTarget}";`;
24+
}

packages/nextjs/src/config/webpack.ts

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,17 @@ export function constructWebpackConfigFunction(
8585
// Add a loader which will inject code that sets global values
8686
addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions);
8787

88-
if (buildContext.nextRuntime === 'edge') {
89-
// eslint-disable-next-line no-console
90-
console.warn(
91-
'[@sentry/nextjs] You are using edge functions or middleware. Please note that Sentry does not yet support error monitoring for these features.',
92-
);
93-
}
88+
newConfig.module.rules.push({
89+
test: /node_modules\/@sentry\/nextjs/,
90+
use: [
91+
{
92+
loader: path.resolve(__dirname, 'loaders/sdkMultiplexerLoader.js'),
93+
options: {
94+
importTarget: buildContext.nextRuntime === 'edge' ? './edge' : './client',
95+
},
96+
},
97+
],
98+
});
9499

95100
if (isServer) {
96101
if (userSentryOptions.autoInstrumentServerFunctions !== false) {
@@ -301,28 +306,25 @@ async function addSentryToEntryProperty(
301306
// we know is that it won't have gotten *simpler* in form, so we only need to worry about the object and function
302307
// options. See https://webpack.js.org/configuration/entry-context/#entry.
303308

304-
const { isServer, dir: projectDir, dev: isDev } = buildContext;
309+
const { isServer, dir: projectDir, dev: isDev, nextRuntime } = buildContext;
305310

306311
const newEntryProperty =
307312
typeof currentEntryProperty === 'function' ? await currentEntryProperty() : { ...currentEntryProperty };
308313

309314
// `sentry.server.config.js` or `sentry.client.config.js` (or their TS equivalents)
310-
const userConfigFile = isServer ? getUserConfigFile(projectDir, 'server') : getUserConfigFile(projectDir, 'client');
315+
const userConfigFile =
316+
nextRuntime === 'edge'
317+
? getUserConfigFile(projectDir, 'edge')
318+
: isServer
319+
? getUserConfigFile(projectDir, 'server')
320+
: getUserConfigFile(projectDir, 'client');
311321

312322
// we need to turn the filename into a path so webpack can find it
313-
const filesToInject = [`./${userConfigFile}`];
323+
const filesToInject = userConfigFile ? [`./${userConfigFile}`] : [];
314324

315325
// inject into all entry points which might contain user's code
316326
for (const entryPointName in newEntryProperty) {
317-
if (
318-
shouldAddSentryToEntryPoint(
319-
entryPointName,
320-
isServer,
321-
userSentryOptions.excludeServerRoutes,
322-
isDev,
323-
buildContext.nextRuntime === 'edge',
324-
)
325-
) {
327+
if (shouldAddSentryToEntryPoint(entryPointName, isServer, userSentryOptions.excludeServerRoutes, isDev)) {
326328
addFilesToExistingEntryPoint(newEntryProperty, entryPointName, filesToInject);
327329
} else {
328330
if (
@@ -345,10 +347,11 @@ async function addSentryToEntryProperty(
345347
* TypeScript or JavaScript file.
346348
*
347349
* @param projectDir The root directory of the project, where the file should be located
348-
* @param platform Either "server" or "client", so that we know which file to look for
349-
* @returns The name of the relevant file. If no file is found, this method throws an error.
350+
* @param platform Either "server", "client" or "edge", so that we know which file to look for
351+
* @returns The name of the relevant file. If the server or client file is not found, this method throws an error. The
352+
* edge file is optional, if it is not found this function will return `undefined`.
350353
*/
351-
export function getUserConfigFile(projectDir: string, platform: 'server' | 'client'): string {
354+
export function getUserConfigFile(projectDir: string, platform: 'server' | 'client' | 'edge'): string | undefined {
352355
const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`];
353356

354357
for (const filename of possibilities) {
@@ -357,7 +360,16 @@ export function getUserConfigFile(projectDir: string, platform: 'server' | 'clie
357360
}
358361
}
359362

360-
throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`);
363+
// Edge config file is optional
364+
if (platform === 'edge') {
365+
// eslint-disable-next-line no-console
366+
console.warn(
367+
'[@sentry/nextjs] You are using Next.js features that run on the Edge Runtime. Please add a "sentry.edge.config.js" or a "sentry.edge.config.ts" file to your project root in which you initialize the Sentry SDK with "Sentry.init()".',
368+
);
369+
return;
370+
} else {
371+
throw new Error(`Cannot find '${possibilities[0]}' or '${possibilities[1]}' in '${projectDir}'.`);
372+
}
361373
}
362374

363375
/**
@@ -449,11 +461,9 @@ function shouldAddSentryToEntryPoint(
449461
isServer: boolean,
450462
excludeServerRoutes: Array<string | RegExp> = [],
451463
isDev: boolean,
452-
isEdgeRuntime: boolean,
453464
): boolean {
454-
// We don't support the Edge runtime yet
455-
if (isEdgeRuntime) {
456-
return false;
465+
if (entryPointName === 'middleware') {
466+
return true;
457467
}
458468

459469
// On the server side, by default we inject the `Sentry.init()` code into every page (with a few exceptions).
@@ -479,9 +489,6 @@ function shouldAddSentryToEntryPoint(
479489
// versions.)
480490
entryPointRoute === '/_app' ||
481491
entryPointRoute === '/_document' ||
482-
// While middleware was in beta, it could be anywhere (at any level) in the `pages` directory, and would be called
483-
// `_middleware.js`. Until the SDK runs successfully in the lambda edge environment, we have to exclude these.
484-
entryPointName.includes('_middleware') ||
485492
// Newer versions of nextjs are starting to introduce things outside the `pages/` folder (middleware, an `app/`
486493
// directory, etc), but until those features are stable and we know how we want to support them, the safest bet is
487494
// not to inject anywhere but inside `pages/`.
@@ -552,13 +559,7 @@ export function getWebpackPluginOptions(
552559
stripPrefix: ['webpack://_N_E/'],
553560
urlPrefix,
554561
entries: (entryPointName: string) =>
555-
shouldAddSentryToEntryPoint(
556-
entryPointName,
557-
isServer,
558-
userSentryOptions.excludeServerRoutes,
559-
isDev,
560-
buildContext.nextRuntime === 'edge',
561-
),
562+
shouldAddSentryToEntryPoint(entryPointName, isServer, userSentryOptions.excludeServerRoutes, isDev),
562563
release: getSentryRelease(buildId),
563564
dryRun: isDev,
564565
});

packages/nextjs/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export * from './config';
22
export * from './server';
3+
4+
// __SENTRY_SDK_MULTIPLEXER__

packages/nextjs/test/config/fixtures.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99

1010
export const SERVER_SDK_CONFIG_FILE = 'sentry.server.config.js';
1111
export const CLIENT_SDK_CONFIG_FILE = 'sentry.client.config.js';
12+
export const EDGE_SDK_CONFIG_FILE = 'sentry.edge.config.js';
1213

1314
/** Mock next config object */
1415
export const userNextConfig: NextConfigObject = {
@@ -43,7 +44,7 @@ export const serverWebpackConfig: WebpackConfigObject = {
4344
'pages/_error': 'private-next-pages/_error.js',
4445
'pages/_app': 'private-next-pages/_app.js',
4546
'pages/sniffTour': ['./node_modules/smellOVision/index.js', 'private-next-pages/sniffTour.js'],
46-
'pages/api/_middleware': 'private-next-pages/api/_middleware.js',
47+
middleware: 'private-next-pages/middleware.js',
4748
'pages/api/simulator/dogStats/[name]': { import: 'private-next-pages/api/simulator/dogStats/[name].js' },
4849
'pages/simulator/leaderboard': {
4950
import: ['./node_modules/dogPoints/converter.js', 'private-next-pages/simulator/leaderboard.js'],
@@ -84,7 +85,7 @@ export const clientWebpackConfig: WebpackConfigObject = {
8485
* @returns A mock build context for the given target
8586
*/
8687
export function getBuildContext(
87-
buildTarget: 'server' | 'client',
88+
buildTarget: 'server' | 'client' | 'edge',
8889
materializedNextConfig: ExportedNextConfig,
8990
webpackVersion: string = '5.4.15',
9091
): BuildContext {
@@ -101,9 +102,11 @@ export function getBuildContext(
101102
webpack: { version: webpackVersion },
102103
defaultLoaders: true,
103104
totalPages: 2,
104-
isServer: buildTarget === 'server',
105+
isServer: buildTarget === 'server' || buildTarget === 'edge',
106+
nextRuntime: ({ server: 'nodejs', client: undefined, edge: 'edge' } as const)[buildTarget],
105107
};
106108
}
107109

108110
export const serverBuildContext = getBuildContext('server', exportedNextConfig);
109111
export const clientBuildContext = getBuildContext('client', exportedNextConfig);
112+
export const edgeBuildContext = getBuildContext('edge', exportedNextConfig);

packages/nextjs/test/config/mocks.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@ import * as os from 'os';
66
import * as path from 'path';
77
import * as rimraf from 'rimraf';
88

9-
import { CLIENT_SDK_CONFIG_FILE, SERVER_SDK_CONFIG_FILE } from './fixtures';
9+
import { CLIENT_SDK_CONFIG_FILE, EDGE_SDK_CONFIG_FILE, SERVER_SDK_CONFIG_FILE } from './fixtures';
1010

1111
// We use `fs.existsSync()` in `getUserConfigFile()`. When we're not testing `getUserConfigFile()` specifically, all we
1212
// need is for it to give us any valid answer, so make it always find what it's looking for. Since this is a core node
1313
// built-in, though, which jest itself uses, otherwise let it do the normal thing. Storing the real version of the
1414
// function also lets us restore the original when we do want to test `getUserConfigFile()`.
1515
export const realExistsSync = jest.requireActual('fs').existsSync;
1616
export const mockExistsSync = (path: fs.PathLike): ReturnType<typeof realExistsSync> => {
17-
if ((path as string).endsWith(SERVER_SDK_CONFIG_FILE) || (path as string).endsWith(CLIENT_SDK_CONFIG_FILE)) {
17+
if (
18+
(path as string).endsWith(SERVER_SDK_CONFIG_FILE) ||
19+
(path as string).endsWith(CLIENT_SDK_CONFIG_FILE) ||
20+
(path as string).endsWith(EDGE_SDK_CONFIG_FILE)
21+
) {
1822
return true;
1923
}
2024

packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
CLIENT_SDK_CONFIG_FILE,
77
clientBuildContext,
88
clientWebpackConfig,
9+
EDGE_SDK_CONFIG_FILE,
10+
edgeBuildContext,
911
exportedNextConfig,
1012
SERVER_SDK_CONFIG_FILE,
1113
serverBuildContext,
@@ -87,6 +89,7 @@ describe('constructWebpackConfigFunction()', () => {
8789
describe('webpack `entry` property config', () => {
8890
const serverConfigFilePath = `./${SERVER_SDK_CONFIG_FILE}`;
8991
const clientConfigFilePath = `./${CLIENT_SDK_CONFIG_FILE}`;
92+
const edgeConfigFilePath = `./${EDGE_SDK_CONFIG_FILE}`;
9093

9194
it('handles various entrypoint shapes', async () => {
9295
const finalWebpackConfig = await materializeFinalWebpackConfig({
@@ -207,17 +210,16 @@ describe('constructWebpackConfigFunction()', () => {
207210
);
208211
});
209212

210-
it('does not inject user config file into API middleware', async () => {
213+
it('injects user config file into API middleware', async () => {
211214
const finalWebpackConfig = await materializeFinalWebpackConfig({
212215
exportedNextConfig,
213216
incomingWebpackConfig: serverWebpackConfig,
214-
incomingWebpackBuildContext: serverBuildContext,
217+
incomingWebpackBuildContext: edgeBuildContext,
215218
});
216219

217220
expect(finalWebpackConfig.entry).toEqual(
218221
expect.objectContaining({
219-
// no injected file
220-
'pages/api/_middleware': 'private-next-pages/api/_middleware.js',
222+
middleware: [edgeConfigFilePath, 'private-next-pages/middleware.js'],
221223
}),
222224
);
223225
});

0 commit comments

Comments
 (0)