Skip to content

Commit 6ebfd4d

Browse files
author
Luca Forstner
committed
Merge branch 'lforst-inject-sentry-in-server-app-dir' into 7.34.0-beta
2 parents d737b15 + ffc8aa0 commit 6ebfd4d

File tree

7 files changed

+142
-110
lines changed

7 files changed

+142
-110
lines changed

packages/nextjs/src/config/webpack.ts

Lines changed: 89 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import type {
2222
WebpackModuleRule,
2323
} from './types';
2424

25-
export { SentryWebpackPlugin };
26-
2725
// TODO: merge default SentryWebpackPlugin ignore with their SentryWebpackPlugin ignore or ignoreFile
2826
// TODO: merge default SentryWebpackPlugin include with their SentryWebpackPlugin include
2927
// TODO: drop merged keys from override check? `includeDefaults` option?
@@ -53,6 +51,7 @@ export function constructWebpackConfigFunction(
5351
buildContext: BuildContext,
5452
): WebpackConfigObject {
5553
const { isServer, dev: isDev, dir: projectDir } = buildContext;
54+
const runtime = isServer ? (buildContext.nextRuntime === 'edge' ? 'edge' : 'node') : 'browser';
5655

5756
let rawNewConfig = { ...incomingConfig };
5857

@@ -67,82 +66,77 @@ export function constructWebpackConfigFunction(
6766
const newConfig = setUpModuleRules(rawNewConfig);
6867

6968
// Add a loader which will inject code that sets global values
70-
addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions);
69+
addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions, buildContext);
7170

7271
newConfig.module.rules.push({
7372
test: /node_modules[/\\]@sentry[/\\]nextjs/,
7473
use: [
7574
{
7675
loader: path.resolve(__dirname, 'loaders', 'sdkMultiplexerLoader.js'),
7776
options: {
78-
importTarget: buildContext.nextRuntime === 'edge' ? './edge' : './client',
77+
importTarget: { browser: './client', node: './server', edge: './edge' }[runtime],
7978
},
8079
},
8180
],
8281
});
8382

84-
if (isServer) {
85-
if (userSentryOptions.autoInstrumentServerFunctions !== false) {
86-
let pagesDirPath: string;
87-
if (
88-
fs.existsSync(path.join(projectDir, 'pages')) &&
89-
fs.lstatSync(path.join(projectDir, 'pages')).isDirectory()
90-
) {
91-
pagesDirPath = path.join(projectDir, 'pages');
92-
} else {
93-
pagesDirPath = path.join(projectDir, 'src', 'pages');
94-
}
83+
if (isServer && userSentryOptions.autoInstrumentServerFunctions !== false) {
84+
let pagesDirPath: string;
85+
if (fs.existsSync(path.join(projectDir, 'pages')) && fs.lstatSync(path.join(projectDir, 'pages')).isDirectory()) {
86+
pagesDirPath = path.join(projectDir, 'pages');
87+
} else {
88+
pagesDirPath = path.join(projectDir, 'src', 'pages');
89+
}
9590

96-
const middlewareJsPath = path.join(pagesDirPath, '..', 'middleware.js');
97-
const middlewareTsPath = path.join(pagesDirPath, '..', 'middleware.ts');
98-
99-
// Default page extensions per https://github.com/vercel/next.js/blob/f1dbc9260d48c7995f6c52f8fbcc65f08e627992/packages/next/server/config-shared.ts#L161
100-
const pageExtensions = userNextConfig.pageExtensions || ['tsx', 'ts', 'jsx', 'js'];
101-
const dotPrefixedPageExtensions = pageExtensions.map(ext => `.${ext}`);
102-
const pageExtensionRegex = pageExtensions.map(escapeStringForRegex).join('|');
103-
104-
// It is very important that we insert our loader at the beginning of the array because we expect any sort of transformations/transpilations (e.g. TS -> JS) to already have happened.
105-
newConfig.module.rules.unshift({
106-
test: resourcePath => {
107-
// We generally want to apply the loader to all API routes, pages and to the middleware file.
108-
109-
// `resourcePath` may be an absolute path or a path relative to the context of the webpack config
110-
let absoluteResourcePath: string;
111-
if (path.isAbsolute(resourcePath)) {
112-
absoluteResourcePath = resourcePath;
113-
} else {
114-
absoluteResourcePath = path.join(projectDir, resourcePath);
115-
}
116-
const normalizedAbsoluteResourcePath = path.normalize(absoluteResourcePath);
117-
118-
if (
119-
// Match everything inside pages/ with the appropriate file extension
120-
normalizedAbsoluteResourcePath.startsWith(pagesDirPath) &&
121-
dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext))
122-
) {
123-
return true;
124-
} else if (
125-
// Match middleware.js and middleware.ts
126-
normalizedAbsoluteResourcePath === middlewareJsPath ||
127-
normalizedAbsoluteResourcePath === middlewareTsPath
128-
) {
129-
return userSentryOptions.autoInstrumentMiddleware ?? true;
130-
} else {
131-
return false;
132-
}
133-
},
134-
use: [
135-
{
136-
loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
137-
options: {
138-
pagesDir: pagesDirPath,
139-
pageExtensionRegex,
140-
excludeServerRoutes: userSentryOptions.excludeServerRoutes,
141-
},
91+
const middlewareJsPath = path.join(pagesDirPath, '..', 'middleware.js');
92+
const middlewareTsPath = path.join(pagesDirPath, '..', 'middleware.ts');
93+
94+
// Default page extensions per https://github.com/vercel/next.js/blob/f1dbc9260d48c7995f6c52f8fbcc65f08e627992/packages/next/server/config-shared.ts#L161
95+
const pageExtensions = userNextConfig.pageExtensions || ['tsx', 'ts', 'jsx', 'js'];
96+
const dotPrefixedPageExtensions = pageExtensions.map(ext => `.${ext}`);
97+
const pageExtensionRegex = pageExtensions.map(escapeStringForRegex).join('|');
98+
99+
// It is very important that we insert our loader at the beginning of the array because we expect any sort of transformations/transpilations (e.g. TS -> JS) to already have happened.
100+
newConfig.module.rules.unshift({
101+
test: resourcePath => {
102+
// We generally want to apply the loader to all API routes, pages and to the middleware file.
103+
104+
// `resourcePath` may be an absolute path or a path relative to the context of the webpack config
105+
let absoluteResourcePath: string;
106+
if (path.isAbsolute(resourcePath)) {
107+
absoluteResourcePath = resourcePath;
108+
} else {
109+
absoluteResourcePath = path.join(projectDir, resourcePath);
110+
}
111+
const normalizedAbsoluteResourcePath = path.normalize(absoluteResourcePath);
112+
113+
if (
114+
// Match everything inside pages/ with the appropriate file extension
115+
normalizedAbsoluteResourcePath.startsWith(pagesDirPath) &&
116+
dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext))
117+
) {
118+
return true;
119+
} else if (
120+
// Match middleware.js and middleware.ts
121+
normalizedAbsoluteResourcePath === middlewareJsPath ||
122+
normalizedAbsoluteResourcePath === middlewareTsPath
123+
) {
124+
return userSentryOptions.autoInstrumentMiddleware ?? true;
125+
} else {
126+
return false;
127+
}
128+
},
129+
use: [
130+
{
131+
loader: path.resolve(__dirname, 'loaders', 'wrappingLoader.js'),
132+
options: {
133+
pagesDir: pagesDirPath,
134+
pageExtensionRegex,
135+
excludeServerRoutes: userSentryOptions.excludeServerRoutes,
142136
},
143-
],
144-
});
145-
}
137+
},
138+
],
139+
});
146140
}
147141

148142
// The SDK uses syntax (ES6 and ES6+ features like object spread) which isn't supported by older browsers. For users
@@ -303,7 +297,8 @@ async function addSentryToEntryProperty(
303297
// we know is that it won't have gotten *simpler* in form, so we only need to worry about the object and function
304298
// options. See https://webpack.js.org/configuration/entry-context/#entry.
305299

306-
const { isServer, dir: projectDir, dev: isDev, nextRuntime } = buildContext;
300+
const { isServer, dir: projectDir, nextRuntime } = buildContext;
301+
const runtime = isServer ? (buildContext.nextRuntime === 'edge' ? 'edge' : 'node') : 'browser';
307302

308303
const newEntryProperty =
309304
typeof currentEntryProperty === 'function' ? await currentEntryProperty() : { ...currentEntryProperty };
@@ -321,7 +316,7 @@ async function addSentryToEntryProperty(
321316

322317
// inject into all entry points which might contain user's code
323318
for (const entryPointName in newEntryProperty) {
324-
if (shouldAddSentryToEntryPoint(entryPointName, isServer, userSentryOptions.excludeServerRoutes, isDev)) {
319+
if (shouldAddSentryToEntryPoint(entryPointName, runtime, userSentryOptions.excludeServerRoutes ?? [])) {
325320
addFilesToExistingEntryPoint(newEntryProperty, entryPointName, filesToInject);
326321
} else {
327322
if (
@@ -455,49 +450,35 @@ function checkWebpackPluginOverrides(
455450
*/
456451
function shouldAddSentryToEntryPoint(
457452
entryPointName: string,
458-
isServer: boolean,
459-
excludeServerRoutes: Array<string | RegExp> = [],
460-
isDev: boolean,
453+
runtime: 'node' | 'browser' | 'edge',
454+
excludeServerRoutes: Array<string | RegExp>,
461455
): boolean {
462456
// On the server side, by default we inject the `Sentry.init()` code into every page (with a few exceptions).
463-
if (isServer) {
464-
if (entryPointName === 'middleware') {
465-
return true;
466-
}
467-
468-
const entryPointRoute = entryPointName.replace(/^pages/, '');
469-
457+
if (runtime === 'node') {
470458
// User-specified pages to skip. (Note: For ease of use, `excludeServerRoutes` is specified in terms of routes,
471459
// which don't have the `pages` prefix.)
460+
const entryPointRoute = entryPointName.replace(/^pages/, '');
472461
if (stringMatchesSomePattern(entryPointRoute, excludeServerRoutes, true)) {
473462
return false;
474463
}
475464

476-
// In dev mode, page routes aren't considered entrypoints so we inject the init call in the `/_app` entrypoint which
477-
// always exists, even if the user didn't add a `_app` page themselves
478-
if (isDev) {
479-
return entryPointRoute === '/_app';
480-
}
481-
482-
if (
483-
// All non-API pages contain both of these components, and we don't want to inject more than once, so as long as
484-
// we're doing the individual pages, it's fine to skip these. (Note: Even if a given user doesn't have either or
485-
// both of these in their `pages/` folder, they'll exist as entrypoints because nextjs will supply default
486-
// versions.)
487-
entryPointRoute === '/_app' ||
488-
entryPointRoute === '/_document' ||
489-
!entryPointName.startsWith('pages/')
490-
) {
465+
// This expression will implicitly include `pages/_app` which is called for all serverside routes and pages
466+
// regardless whether or not the user has a`_app` file.
467+
return entryPointName.startsWith('pages/');
468+
} else if (runtime === 'browser') {
469+
return (
470+
entryPointName === 'main' || // entrypoint for `/pages` pages
471+
entryPointName === 'main-app' // entrypoint for `/app` pages
472+
);
473+
} else {
474+
// User-specified pages to skip. (Note: For ease of use, `excludeServerRoutes` is specified in terms of routes,
475+
// which don't have the `pages` prefix.)
476+
const entryPointRoute = entryPointName.replace(/^pages/, '');
477+
if (stringMatchesSomePattern(entryPointRoute, excludeServerRoutes, true)) {
491478
return false;
492479
}
493480

494-
// We want to inject Sentry into all other pages
495481
return true;
496-
} else {
497-
return (
498-
entryPointName === 'pages/_app' || // entrypoint for `/pages` pages
499-
entryPointName === 'main-app' // entrypoint for `/app` pages
500-
);
501482
}
502483
}
503484

@@ -526,13 +507,19 @@ export function getWebpackPluginOptions(
526507

527508
const serverInclude = isServerless
528509
? [{ paths: [`${distDirAbsPath}/serverless/`], urlPrefix: `${urlPrefix}/serverless` }]
529-
: [{ paths: [`${distDirAbsPath}/server/pages/`], urlPrefix: `${urlPrefix}/server/pages` }].concat(
510+
: [
511+
{ paths: [`${distDirAbsPath}/server/pages/`], urlPrefix: `${urlPrefix}/server/pages` },
512+
{ paths: [`${distDirAbsPath}/server/app/`], urlPrefix: `${urlPrefix}/server/app` },
513+
].concat(
530514
isWebpack5 ? [{ paths: [`${distDirAbsPath}/server/chunks/`], urlPrefix: `${urlPrefix}/server/chunks` }] : [],
531515
);
532516

533517
const clientInclude = userSentryOptions.widenClientFileUpload
534518
? [{ paths: [`${distDirAbsPath}/static/chunks`], urlPrefix: `${urlPrefix}/static/chunks` }]
535-
: [{ paths: [`${distDirAbsPath}/static/chunks/pages`], urlPrefix: `${urlPrefix}/static/chunks/pages` }];
519+
: [
520+
{ paths: [`${distDirAbsPath}/static/chunks/pages`], urlPrefix: `${urlPrefix}/static/chunks/pages` },
521+
{ paths: [`${distDirAbsPath}/static/chunks/app`], urlPrefix: `${urlPrefix}/static/chunks/app` },
522+
];
536523

537524
const defaultPluginOptions = dropUndefinedKeys({
538525
include: isServer ? serverInclude : clientInclude,
@@ -550,8 +537,7 @@ export function getWebpackPluginOptions(
550537
configFile: hasSentryProperties ? 'sentry.properties' : undefined,
551538
stripPrefix: ['webpack://_N_E/'],
552539
urlPrefix,
553-
entries: (entryPointName: string) =>
554-
shouldAddSentryToEntryPoint(entryPointName, isServer, userSentryOptions.excludeServerRoutes, isDev),
540+
entries: [], // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead.
555541
release: getSentryRelease(buildId),
556542
dryRun: isDev,
557543
});
@@ -675,12 +661,14 @@ function addValueInjectionLoader(
675661
newConfig: WebpackConfigObjectWithModuleRules,
676662
userNextConfig: NextConfigObject,
677663
userSentryOptions: UserSentryOptions,
664+
buildContext: BuildContext,
678665
): void {
679666
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';
680667

681668
const isomorphicValues = {
682669
// `rewritesTunnel` set by the user in Next.js config
683670
__sentryRewritesTunnelPath__: userSentryOptions.tunnelRoute,
671+
SENTRY_RELEASE: { id: getSentryRelease(buildContext.buildId) },
684672
};
685673

686674
const serverValues = {

packages/nextjs/test/config/testUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { default as SentryWebpackPlugin } from '@sentry/webpack-plugin';
12
import type { WebpackPluginInstance } from 'webpack';
23

34
import type {
@@ -9,7 +10,6 @@ import type {
910
WebpackConfigObject,
1011
WebpackConfigObjectWithModuleRules,
1112
} from '../../src/config/types';
12-
import type { SentryWebpackPlugin } from '../../src/config/webpack';
1313
import { constructWebpackConfigFunction } from '../../src/config/webpack';
1414
import { withSentryConfig } from '../../src/config/withSentryConfig';
1515
import { defaultRuntimePhase, defaultsObject } from './fixtures';

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// mock helper functions not tested directly in this file
22
import '../mocks';
33

4-
import { SentryWebpackPlugin } from '../../../src/config/webpack';
4+
import { default as SentryWebpackPlugin } from '@sentry/webpack-plugin';
5+
56
import {
67
CLIENT_SDK_CONFIG_FILE,
78
clientBuildContext,
@@ -138,7 +139,7 @@ describe('constructWebpackConfigFunction()', () => {
138139
);
139140
});
140141

141-
it('injects user config file into `_app` in client bundle but not in server bundle', async () => {
142+
it('injects user config file into `_app` in server bundle but not in client bundle', async () => {
142143
const finalServerWebpackConfig = await materializeFinalWebpackConfig({
143144
exportedNextConfig,
144145
incomingWebpackConfig: serverWebpackConfig,
@@ -152,12 +153,12 @@ describe('constructWebpackConfigFunction()', () => {
152153

153154
expect(finalServerWebpackConfig.entry).toEqual(
154155
expect.objectContaining({
155-
'pages/_app': expect.not.arrayContaining([serverConfigFilePath]),
156+
'pages/_app': expect.arrayContaining([serverConfigFilePath]),
156157
}),
157158
);
158159
expect(finalClientWebpackConfig.entry).toEqual(
159160
expect.objectContaining({
160-
'pages/_app': expect.arrayContaining([clientConfigFilePath]),
161+
'pages/_app': expect.not.arrayContaining([clientConfigFilePath]),
161162
}),
162163
);
163164
});
@@ -232,9 +233,9 @@ describe('constructWebpackConfigFunction()', () => {
232233
});
233234

234235
expect(finalWebpackConfig.entry).toEqual({
235-
main: './src/index.ts',
236+
main: ['./sentry.client.config.js', './src/index.ts'],
236237
// only _app has config file injected
237-
'pages/_app': [clientConfigFilePath, 'next-client-pages-loader?page=%2F_app'],
238+
'pages/_app': 'next-client-pages-loader?page=%2F_app',
238239
'pages/_error': 'next-client-pages-loader?page=%2F_error',
239240
'pages/sniffTour': ['./node_modules/smellOVision/index.js', 'private-next-pages/sniffTour.js'],
240241
'pages/simulator/leaderboard': {

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import { default as SentryWebpackPlugin } from '@sentry/webpack-plugin';
12
import * as fs from 'fs';
23
import * as os from 'os';
34
import * as path from 'path';
45

56
import type { BuildContext, ExportedNextConfig } from '../../../src/config/types';
6-
import { getUserConfigFile, getWebpackPluginOptions, SentryWebpackPlugin } from '../../../src/config/webpack';
7+
import { getUserConfigFile, getWebpackPluginOptions } from '../../../src/config/webpack';
78
import {
89
clientBuildContext,
910
clientWebpackConfig,
@@ -36,7 +37,7 @@ describe('Sentry webpack plugin config', () => {
3637
authToken: 'dogsarebadatkeepingsecrets', // picked up from env
3738
stripPrefix: ['webpack://_N_E/'], // default
3839
urlPrefix: '~/_next', // default
39-
entries: expect.any(Function), // default, tested separately elsewhere
40+
entries: [],
4041
release: 'doGsaREgReaT', // picked up from env
4142
dryRun: false, // based on buildContext.dev being false
4243
}),
@@ -78,6 +79,7 @@ describe('Sentry webpack plugin config', () => {
7879

7980
expect(sentryWebpackPluginInstance.options.include).toEqual([
8081
{ paths: [`${clientBuildContext.dir}/.next/static/chunks/pages`], urlPrefix: '~/_next/static/chunks/pages' },
82+
{ paths: [`${clientBuildContext.dir}/.next/static/chunks/app`], urlPrefix: '~/_next/static/chunks/app' },
8183
]);
8284
});
8385

@@ -141,6 +143,7 @@ describe('Sentry webpack plugin config', () => {
141143

142144
expect(sentryWebpackPluginInstance.options.include).toEqual([
143145
{ paths: [`${serverBuildContextWebpack4.dir}/.next/server/pages/`], urlPrefix: '~/_next/server/pages' },
146+
{ paths: [`${serverBuildContextWebpack4.dir}/.next/server/app/`], urlPrefix: '~/_next/server/app' },
144147
]);
145148
});
146149

@@ -158,6 +161,7 @@ describe('Sentry webpack plugin config', () => {
158161

159162
expect(sentryWebpackPluginInstance.options.include).toEqual([
160163
{ paths: [`${serverBuildContext.dir}/.next/server/pages/`], urlPrefix: '~/_next/server/pages' },
164+
{ paths: [`${serverBuildContext.dir}/.next/server/app/`], urlPrefix: '~/_next/server/app' },
161165
{ paths: [`${serverBuildContext.dir}/.next/server/chunks/`], urlPrefix: '~/_next/server/chunks' },
162166
]);
163167
});

0 commit comments

Comments
 (0)