Skip to content

Commit f36d418

Browse files
author
Luca Forstner
committed
Let's try this
1 parent e0170b3 commit f36d418

File tree

4 files changed

+121
-112
lines changed

4 files changed

+121
-112
lines changed

packages/nextjs/src/config/webpack.ts

Lines changed: 82 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable complexity */
22
/* eslint-disable max-lines */
33
import { getSentryRelease } from '@sentry/node';
4-
import { arrayify, dropUndefinedKeys, escapeStringForRegex, logger, stringMatchesSomePattern } from '@sentry/utils';
4+
import { arrayify, dropUndefinedKeys, escapeStringForRegex, logger } from '@sentry/utils';
55
import { default as SentryWebpackPlugin } from '@sentry/webpack-plugin';
66
import * as chalk from 'chalk';
77
import * as fs from 'fs';
@@ -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)) {
325320
addFilesToExistingEntryPoint(newEntryProperty, entryPointName, filesToInject);
326321
} else {
327322
if (
@@ -453,51 +448,19 @@ function checkWebpackPluginOverrides(
453448
* @param excludeServerRoutes A list of excluded serverside entrypoints provided by the user
454449
* @returns `true` if sentry code should be injected, and `false` otherwise
455450
*/
456-
function shouldAddSentryToEntryPoint(
457-
entryPointName: string,
458-
isServer: boolean,
459-
excludeServerRoutes: Array<string | RegExp> = [],
460-
isDev: boolean,
461-
): boolean {
451+
function shouldAddSentryToEntryPoint(entryPointName: string, runtime: 'node' | 'browser' | 'edge'): boolean {
462452
// 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-
470-
// User-specified pages to skip. (Note: For ease of use, `excludeServerRoutes` is specified in terms of routes,
471-
// which don't have the `pages` prefix.)
472-
if (stringMatchesSomePattern(entryPointRoute, excludeServerRoutes, true)) {
473-
return false;
474-
}
475-
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-
) {
491-
return false;
492-
}
493-
494-
// We want to inject Sentry into all other pages
495-
return true;
496-
} else {
453+
if (runtime === 'node') {
454+
// This expression will implicitly include `pages/_app` which is called for all serverside routes and pages
455+
// regardless whether or not the user has a`_app` file.
456+
return entryPointName.startsWith('pages/');
457+
} else if (runtime === 'browser') {
497458
return (
498-
entryPointName === 'pages/_app' || // entrypoint for `/pages` pages
459+
entryPointName === 'main' || // entrypoint for `/pages` pages
499460
entryPointName === 'main-app' // entrypoint for `/app` pages
500461
);
462+
} else {
463+
return true;
501464
}
502465
}
503466

@@ -526,13 +489,19 @@ export function getWebpackPluginOptions(
526489

527490
const serverInclude = isServerless
528491
? [{ paths: [`${distDirAbsPath}/serverless/`], urlPrefix: `${urlPrefix}/serverless` }]
529-
: [{ paths: [`${distDirAbsPath}/server/pages/`], urlPrefix: `${urlPrefix}/server/pages` }].concat(
492+
: [
493+
{ paths: [`${distDirAbsPath}/server/pages/`], urlPrefix: `${urlPrefix}/server/pages` },
494+
{ paths: [`${distDirAbsPath}/server/app/`], urlPrefix: `${urlPrefix}/server/app` },
495+
].concat(
530496
isWebpack5 ? [{ paths: [`${distDirAbsPath}/server/chunks/`], urlPrefix: `${urlPrefix}/server/chunks` }] : [],
531497
);
532498

533499
const clientInclude = userSentryOptions.widenClientFileUpload
534500
? [{ paths: [`${distDirAbsPath}/static/chunks`], urlPrefix: `${urlPrefix}/static/chunks` }]
535-
: [{ paths: [`${distDirAbsPath}/static/chunks/pages`], urlPrefix: `${urlPrefix}/static/chunks/pages` }];
501+
: [
502+
{ paths: [`${distDirAbsPath}/static/chunks/pages`], urlPrefix: `${urlPrefix}/static/chunks/pages` },
503+
{ paths: [`${distDirAbsPath}/static/chunks/app`], urlPrefix: `${urlPrefix}/static/chunks/app` },
504+
];
536505

537506
const defaultPluginOptions = dropUndefinedKeys({
538507
include: isServer ? serverInclude : clientInclude,
@@ -550,8 +519,7 @@ export function getWebpackPluginOptions(
550519
configFile: hasSentryProperties ? 'sentry.properties' : undefined,
551520
stripPrefix: ['webpack://_N_E/'],
552521
urlPrefix,
553-
entries: (entryPointName: string) =>
554-
shouldAddSentryToEntryPoint(entryPointName, isServer, userSentryOptions.excludeServerRoutes, isDev),
522+
entries: [], // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead.
555523
release: getSentryRelease(buildId),
556524
dryRun: isDev,
557525
});
@@ -675,12 +643,14 @@ function addValueInjectionLoader(
675643
newConfig: WebpackConfigObjectWithModuleRules,
676644
userNextConfig: NextConfigObject,
677645
userSentryOptions: UserSentryOptions,
646+
buildContext: BuildContext,
678647
): void {
679648
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';
680649

681650
const isomorphicValues = {
682651
// `rewritesTunnel` set by the user in Next.js config
683652
__sentryRewritesTunnelPath__: userSentryOptions.tunnelRoute,
653+
SENTRY_RELEASE: { id: getSentryRelease(buildContext.buildId) },
684654
};
685655

686656
const serverValues = {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
13
export default async function () {
4+
// do some request so that next will render this component serverside for each new pageload
5+
await fetch('http://example.com', { cache: 'no-store' });
6+
Sentry.captureException(new Error('I am an Error captured inside a server component'));
27
return <p>I am a server component!</p>;
38
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const assert = require('assert');
2+
3+
const { sleep } = require('../utils/common');
4+
const { getAsync, interceptEventRequest } = require('../utils/server');
5+
6+
module.exports = async ({ url: urlBase, argv }) => {
7+
if (Number(process.env.NEXTJS_VERSION) < 13 || Number(process.env.NODE_MAJOR) < 16) {
8+
// Next.js versions < 13 don't support the app directory and the app dir requires Node v16.8.0 or later.
9+
return;
10+
}
11+
12+
const url = `${urlBase}/servercomponent`;
13+
14+
const capturedRequest = interceptEventRequest(
15+
{
16+
exception: {
17+
values: [
18+
{
19+
type: 'Error',
20+
value: 'I am an Error captured inside a server component',
21+
},
22+
],
23+
},
24+
},
25+
argv,
26+
'servercomponentCapturedException',
27+
);
28+
29+
await getAsync(url);
30+
await sleep(250);
31+
32+
assert.ok(capturedRequest.isDone(), 'Did not intercept expected request');
33+
};

packages/nextjs/test/integration/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"skipLibCheck": true,
1818
"strict": true,
1919
"target": "esnext",
20+
"incremental": true, // automatically set by Next.js 13
2021
"plugins": [
2122
{
2223
"name": "next"

0 commit comments

Comments
 (0)