Skip to content

Commit 8ef39cc

Browse files
authored
fix(nextjs): Stop SentryWebpackPlugin from uploading unnecessary files (#3845)
Though sourcemap uploading using `SentryWebpackPlugin` is generally quite fast, even when there are a lot of files[1], there are situations in which it can slow things down a lot[2]. [1] #3769 (comment) [2] vercel/next.js#26588 (comment) There are a number of reasons for that, but one of them is that, in the current state, `@sentry/nextjs`'s use of the plugin is pretty indiscriminate - it just uploads anything and everything in the `.next` folder (which is where nextjs puts all of its built files), during both client and server builds. The lack of specificity leads us to upload files we don't need, and the fact that we don't distinguish between client and server builds means that whichever one happens second not only uploads its own unnecessary files, it also uploads all of the files from the other build (which have already been uploaded), both necessary and unnecessary. More details in #3769. This change makes it so that we're much more specific in terms of what we upload, in order to fix both the specificity and the duplication problems. Notes: - There's discussion in the linked issue about which non-user sources/maps to upload (things like code from webpack, nextjs itself, etc). In the end, I chose to restrict it to user code (almost - see below), for two reasons. First, non-user code is unlikely to change much (or often) between releases, so if third-party code were included, we'd be uploading lots and lots of copies of the same files. Second, though it's occasionally helpful to be able to see such code in the stacktrace, the vast majority of the time the problem lies in user code. For both reasons, then, including third-party files didn't feel worth it , either in terms of time spent uploading them or space spent storing them. (I say it's "almost" restricted to user code because, among other files, the server build generates bundles which are labeled only by numbers (`1226.js` and such), some of which are user code and some of which are third-party code, and - in this PR at least - I didn't include a way to filter out the latter while making sure to still include the former. This is potentially still a worthwhile thing to do, but the fact that it would mean actually examining the contents of files (rather than just their paths) makes it a more complicated change, which will have to wait for a future PR.) - In that issue, the topic of HMR (hot module reloading) files also came up. For the moment I also chose to skip those (even though they contain user code), as a) the plugin isn't meant for use in a dev environment, where every change triggers a new build, potentially leading to hundreds of sets of release files being uploaded, and b) we'd face a similar issue of needing to examine more than just the path in order to know what to upload (to avoid uploading all previous iterations of the code at each rebuild). - Another small issue I fixed, while I was in the relevant code, was to prevent the webpack plugin from injecting the release into bundles which don't need it (like third-party code, or any bundle with no `Sentry.init()` call). Since this lines up exactly with the files into which we need to inject `sentry.server.config.js` or `sentry.client.config.js`, I pulled it out into a function, `shouldAddSentryToEntryPoint()`. Fixes #3769 Fixes vercel/next.js#26588
1 parent e4ffd37 commit 8ef39cc

File tree

5 files changed

+173
-46
lines changed

5 files changed

+173
-46
lines changed

packages/nextjs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"@sentry/react": "6.10.0",
2424
"@sentry/tracing": "6.10.0",
2525
"@sentry/utils": "6.10.0",
26-
"@sentry/webpack-plugin": "1.15.1",
26+
"@sentry/webpack-plugin": "1.17.1",
2727
"tslib": "^1.9.3"
2828
},
2929
"devDependencies": {

packages/nextjs/src/config/types.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
export { SentryCliPluginOptions as SentryWebpackPluginOptions } from '@sentry/webpack-plugin';
1+
import { SentryCliPluginOptions } from '@sentry/webpack-plugin';
2+
3+
export type SentryWebpackPluginOptions = SentryCliPluginOptions;
4+
export type SentryWebpackPlugin = { options: SentryWebpackPluginOptions };
25

36
/**
47
* Overall Nextjs config
@@ -9,6 +12,8 @@ export type ExportedNextConfig = NextConfigObject | NextConfigFunction;
912
export type NextConfigObject = {
1013
// custom webpack options
1114
webpack?: WebpackConfigFunction;
15+
// whether to build serverless functions for all pages, not just API routes
16+
target?: 'server' | 'experimental-serverless-trace';
1217
sentry?: {
1318
disableServerWebpackPlugin?: boolean;
1419
disableClientWebpackPlugin?: boolean;
@@ -43,7 +48,13 @@ export type WebpackConfigObject = {
4348
};
4449

4550
// Information about the current build environment
46-
export type BuildContext = { dev: boolean; isServer: boolean; buildId: string; dir: string };
51+
export type BuildContext = {
52+
dev: boolean;
53+
isServer: boolean;
54+
buildId: string;
55+
dir: string;
56+
config: Partial<NextConfigObject>;
57+
};
4758

4859
/**
4960
* Webpack `entry` config

packages/nextjs/src/config/webpack.ts

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,6 @@ export { SentryWebpackPlugin };
2121
// TODO: merge default SentryWebpackPlugin include with their SentryWebpackPlugin include
2222
// TODO: drop merged keys from override check? `includeDefaults` option?
2323

24-
const defaultSentryWebpackPluginOptions = dropUndefinedKeys({
25-
url: process.env.SENTRY_URL,
26-
org: process.env.SENTRY_ORG,
27-
project: process.env.SENTRY_PROJECT,
28-
authToken: process.env.SENTRY_AUTH_TOKEN,
29-
configFile: 'sentry.properties',
30-
stripPrefix: ['webpack://_N_E/'],
31-
urlPrefix: `~/_next`,
32-
include: '.next/',
33-
ignore: ['.next/cache', 'server/ssr-module-cache.js', 'static/*/_ssgManifest.js', 'static/*/_buildManifest.js'],
34-
});
35-
3624
/**
3725
* Construct the function which will be used as the nextjs config's `webpack` value.
3826
*
@@ -88,18 +76,11 @@ export function constructWebpackConfigFunction(
8876
newConfig.devtool = 'source-map';
8977
}
9078

91-
checkWebpackPluginOverrides(userSentryWebpackPluginOptions);
92-
9379
newConfig.plugins = newConfig.plugins || [];
9480
newConfig.plugins.push(
9581
// @ts-ignore Our types for the plugin are messed up somehow - TS wants this to be `SentryWebpackPlugin.default`,
9682
// but that's not actually a thing
97-
new SentryWebpackPlugin({
98-
dryRun: buildContext.dev,
99-
release: getSentryRelease(buildContext.buildId),
100-
...defaultSentryWebpackPluginOptions,
101-
...userSentryWebpackPluginOptions,
102-
}),
83+
new SentryWebpackPlugin(getWebpackPluginOptions(buildContext, userSentryWebpackPluginOptions)),
10384
);
10485
}
10586

@@ -136,7 +117,7 @@ async function addSentryToEntryProperty(
136117
: getUserConfigFile(buildContext.dir, 'client');
137118

138119
for (const entryPointName in newEntryProperty) {
139-
if (entryPointName === 'pages/_app' || entryPointName.includes('pages/api')) {
120+
if (shouldAddSentryToEntryPoint(entryPointName)) {
140121
// we need to turn the filename into a path so webpack can find it
141122
addFileToExistingEntryPoint(newEntryProperty, entryPointName, `./${userConfigFile}`);
142123
}
@@ -221,13 +202,15 @@ function addFileToExistingEntryPoint(
221202
* our default options are getting overridden. (Note: If any of our default values is undefined, it won't be included in
222203
* the warning.)
223204
*
224-
* @param userSentryWebpackPluginOptions The user's SentryWebpackPlugin options
205+
* @param defaultOptions Default SentryWebpackPlugin options
206+
* @param userOptions The user's SentryWebpackPlugin options
225207
*/
226-
function checkWebpackPluginOverrides(userSentryWebpackPluginOptions: Partial<SentryWebpackPluginOptions>): void {
208+
function checkWebpackPluginOverrides(
209+
defaultOptions: SentryWebpackPluginOptions,
210+
userOptions: Partial<SentryWebpackPluginOptions>,
211+
): void {
227212
// warn if any of the default options for the webpack plugin are getting overridden
228-
const sentryWebpackPluginOptionOverrides = Object.keys(defaultSentryWebpackPluginOptions)
229-
.concat('dryrun')
230-
.filter(key => key in userSentryWebpackPluginOptions);
213+
const sentryWebpackPluginOptionOverrides = Object.keys(defaultOptions).filter(key => key in userOptions);
231214
if (sentryWebpackPluginOptionOverrides.length > 0) {
232215
logger.warn(
233216
'[Sentry] You are overriding the following automatically-set SentryWebpackPlugin config options:\n' +
@@ -236,3 +219,58 @@ function checkWebpackPluginOverrides(userSentryWebpackPluginOptions: Partial<Sen
236219
);
237220
}
238221
}
222+
223+
/**
224+
* Determine if this is an entry point into which both `Sentry.init()` code and the release value should be injected
225+
*
226+
* @param entryPointName The name of the entry point in question
227+
* @returns `true` if sentry code should be injected, and `false` otherwise
228+
*/
229+
function shouldAddSentryToEntryPoint(entryPointName: string): boolean {
230+
return entryPointName === 'pages/_app' || entryPointName.includes('pages/api');
231+
}
232+
233+
/**
234+
* Combine default and user-provided SentryWebpackPlugin options, accounting for whether we're building server files or
235+
* client files.
236+
*
237+
* @param buildContext Nexjs-provided data about the current build
238+
* @param userPluginOptions User-provided SentryWebpackPlugin options
239+
* @returns Final set of combined options
240+
*/
241+
function getWebpackPluginOptions(
242+
buildContext: BuildContext,
243+
userPluginOptions: Partial<SentryWebpackPluginOptions>,
244+
): SentryWebpackPluginOptions {
245+
const { isServer, dir: projectDir, buildId, dev: isDev, config: nextConfig } = buildContext;
246+
247+
const isServerless = nextConfig.target === 'experimental-serverless-trace';
248+
const hasSentryProperties = fs.existsSync(path.resolve(projectDir, 'sentry.properties'));
249+
250+
const serverInclude = isServerless
251+
? [{ paths: ['.next/serverless/'], urlPrefix: '~/_next/serverless' }]
252+
: [
253+
{ paths: ['.next/server/chunks/'], urlPrefix: '~/_next/server/chunks' },
254+
{ paths: ['.next/server/pages/'], urlPrefix: '~/_next/server/pages' },
255+
];
256+
const clientInclude = [{ paths: ['.next/static/chunks/pages'], urlPrefix: '~/_next/static/chunks/pages' }];
257+
258+
const defaultPluginOptions = dropUndefinedKeys({
259+
include: isServer ? serverInclude : clientInclude,
260+
ignore: [],
261+
url: process.env.SENTRY_URL,
262+
org: process.env.SENTRY_ORG,
263+
project: process.env.SENTRY_PROJECT,
264+
authToken: process.env.SENTRY_AUTH_TOKEN,
265+
configFile: hasSentryProperties ? 'sentry.properties' : undefined,
266+
stripPrefix: ['webpack://_N_E/'],
267+
urlPrefix: `~/_next`,
268+
entries: shouldAddSentryToEntryPoint,
269+
release: getSentryRelease(buildId),
270+
dryRun: isDev,
271+
});
272+
273+
checkWebpackPluginOverrides(defaultPluginOptions, userPluginOptions);
274+
275+
return { ...defaultPluginOptions, ...userPluginOptions };
276+
}

packages/nextjs/test/config.test.ts

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
EntryPropertyFunction,
1010
ExportedNextConfig,
1111
NextConfigObject,
12+
SentryWebpackPlugin as SentryWebpackPluginType,
1213
SentryWebpackPluginOptions,
1314
WebpackConfigObject,
1415
} from '../src/config/types';
@@ -44,7 +45,9 @@ const userNextConfig = {
4445
}),
4546
}),
4647
};
47-
const userSentryWebpackPluginConfig = { org: 'squirrelChasers', project: 'simulator', include: './thirdPartyMaps' };
48+
const userSentryWebpackPluginConfig = { org: 'squirrelChasers', project: 'simulator' };
49+
process.env.SENTRY_AUTH_TOKEN = 'dogsarebadatkeepingsecrets';
50+
process.env.SENTRY_RELEASE = 'doGsaREgReaT';
4851

4952
/** Mocks of the arguments passed to the result of `withSentryConfig` (when it's a function). */
5053
const runtimePhase = 'ball-fetching';
@@ -80,10 +83,12 @@ const clientWebpackConfig = {
8083
target: 'web',
8184
context: '/Users/Maisey/projects/squirrelChasingSimulator',
8285
};
86+
8387
const baseBuildContext = {
8488
dev: false,
85-
buildId: 'doGsaREgReaT',
89+
buildId: 'sItStAyLiEdOwN',
8690
dir: '/Users/Maisey/projects/squirrelChasingSimulator',
91+
config: { target: 'server' as const },
8792
};
8893
const serverBuildContext = { isServer: true, ...baseBuildContext };
8994
const clientBuildContext = { isServer: false, ...baseBuildContext };
@@ -100,7 +105,7 @@ const clientBuildContext = { isServer: false, ...baseBuildContext };
100105
*/
101106
function materializeFinalNextConfig(
102107
userNextConfig: ExportedNextConfig,
103-
userSentryWebpackPluginConfig?: SentryWebpackPluginOptions,
108+
userSentryWebpackPluginConfig?: Partial<SentryWebpackPluginOptions>,
104109
): NextConfigObject {
105110
const sentrifiedConfig = withSentryConfig(userNextConfig, userSentryWebpackPluginConfig);
106111
let finalConfigValues = sentrifiedConfig;
@@ -131,7 +136,7 @@ function materializeFinalNextConfig(
131136
*/
132137
async function materializeFinalWebpackConfig(options: {
133138
userNextConfig: ExportedNextConfig;
134-
userSentryWebpackPluginConfig?: SentryWebpackPluginOptions;
139+
userSentryWebpackPluginConfig?: Partial<SentryWebpackPluginOptions>;
135140
incomingWebpackConfig: WebpackConfigObject;
136141
incomingWebpackBuildContext: BuildContext;
137142
}): Promise<WebpackConfigObject> {
@@ -311,12 +316,40 @@ describe('webpack config', () => {
311316
});
312317

313318
describe('Sentry webpack plugin config', () => {
314-
it('includes expected properties', () => {
315-
// TODO
319+
it('includes expected properties', async () => {
320+
// also, can pull from either env or user config (see notes on specific properties below)
321+
const finalWebpackConfig = await materializeFinalWebpackConfig({
322+
userNextConfig,
323+
userSentryWebpackPluginConfig,
324+
incomingWebpackConfig: serverWebpackConfig,
325+
incomingWebpackBuildContext: serverBuildContext,
326+
});
327+
328+
expect(finalWebpackConfig.plugins?.[0].options).toEqual(
329+
expect.objectContaining({
330+
include: expect.any(Array), // default, tested separately elsewhere
331+
ignore: [], // default
332+
org: 'squirrelChasers', // from user webpack plugin config
333+
project: 'simulator', // from user webpack plugin config
334+
authToken: 'dogsarebadatkeepingsecrets', // picked up from env
335+
stripPrefix: ['webpack://_N_E/'], // default
336+
urlPrefix: `~/_next`, // default
337+
entries: expect.any(Function), // default, tested separately elsewhere
338+
release: 'doGsaREgReaT', // picked up from env
339+
dryRun: false, // based on buildContext.dev being false
340+
}),
341+
);
316342
});
317343

318-
it('preserves unrelated plugin config options', () => {
319-
// TODO
344+
it('preserves unrelated plugin config options', async () => {
345+
const finalWebpackConfig = await materializeFinalWebpackConfig({
346+
userNextConfig,
347+
userSentryWebpackPluginConfig: { ...userSentryWebpackPluginConfig, debug: true },
348+
incomingWebpackConfig: serverWebpackConfig,
349+
incomingWebpackBuildContext: serverBuildContext,
350+
});
351+
352+
expect((finalWebpackConfig.plugins?.[0].options as SentryWebpackPluginOptions).debug).toEqual(true);
320353
});
321354

322355
it('warns when overriding certain default values', () => {
@@ -327,6 +360,51 @@ describe('Sentry webpack plugin config', () => {
327360
// do we even want to do this?
328361
});
329362

363+
describe('Sentry webpack plugin `include` option', () => {
364+
it('has the correct value when building client bundles', async () => {
365+
const finalWebpackConfig = await materializeFinalWebpackConfig({
366+
userNextConfig,
367+
incomingWebpackConfig: clientWebpackConfig,
368+
incomingWebpackBuildContext: clientBuildContext,
369+
});
370+
371+
const sentryWebpackPlugin = finalWebpackConfig.plugins?.[0] as SentryWebpackPluginType;
372+
373+
expect(sentryWebpackPlugin.options?.include).toEqual([
374+
{ paths: ['.next/static/chunks/pages'], urlPrefix: '~/_next/static/chunks/pages' },
375+
]);
376+
});
377+
378+
it('has the correct value when building serverless server bundles', async () => {
379+
const finalWebpackConfig = await materializeFinalWebpackConfig({
380+
userNextConfig,
381+
incomingWebpackConfig: serverWebpackConfig,
382+
incomingWebpackBuildContext: { ...serverBuildContext, config: { target: 'experimental-serverless-trace' } },
383+
});
384+
385+
const sentryWebpackPlugin = finalWebpackConfig.plugins?.[0] as SentryWebpackPluginType;
386+
387+
expect(sentryWebpackPlugin.options?.include).toEqual([
388+
{ paths: ['.next/serverless/'], urlPrefix: '~/_next/serverless' },
389+
]);
390+
});
391+
392+
it('has the correct value when building serverful server bundles', async () => {
393+
const finalWebpackConfig = await materializeFinalWebpackConfig({
394+
userNextConfig,
395+
incomingWebpackConfig: serverWebpackConfig,
396+
incomingWebpackBuildContext: serverBuildContext,
397+
});
398+
399+
const sentryWebpackPlugin = finalWebpackConfig.plugins?.[0] as SentryWebpackPluginType;
400+
401+
expect(sentryWebpackPlugin.options?.include).toEqual([
402+
{ paths: ['.next/server/chunks/'], urlPrefix: '~/_next/server/chunks' },
403+
{ paths: ['.next/server/pages/'], urlPrefix: '~/_next/server/pages' },
404+
]);
405+
});
406+
});
407+
330408
it('allows SentryWebpackPlugin to be turned off for client code (independent of server code)', () => {
331409
const clientFinalNextConfig = materializeFinalNextConfig({
332410
...userNextConfig,

yarn.lock

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2624,10 +2624,10 @@
26242624
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
26252625
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
26262626

2627-
"@sentry/cli@^1.64.1":
2628-
version "1.66.0"
2629-
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.66.0.tgz#0526f1bc1c0570ce72ed817190af92f3b63a2e9a"
2630-
integrity sha512-2pZ+JHnvKqwyJWcGkKg/gCM/zURYronAnruBNllI+rH2g5IL0N90deMmjB1xcqXS66J222+MPTtWrGEK1Vl0/w==
2627+
"@sentry/cli@^1.68.0":
2628+
version "1.68.0"
2629+
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.68.0.tgz#2ced8fac67ee01e746a45e8ee45a518d4526937e"
2630+
integrity sha512-zc7+cxKDqpHLREGJKRH6KwE8fZW8bnczg3OLibJ0czleXoWPdAuOK1Xm1BTMcOnaXfg3VKAh0rI7S1PTdj+SrQ==
26312631
dependencies:
26322632
https-proxy-agent "^5.0.0"
26332633
mkdirp "^0.5.5"
@@ -2636,12 +2636,12 @@
26362636
progress "^2.0.3"
26372637
proxy-from-env "^1.1.0"
26382638

2639-
"@sentry/webpack-plugin@1.15.1":
2640-
version "1.15.1"
2641-
resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.15.1.tgz#deb014fce8c1b51811100f25ec9050dd03addd9b"
2642-
integrity sha512-/Z06MJDXyWcN2+CbeDTMDwVzySjgZWQajOke773TvpkgqdtkeT1eYBsA+pfsje+ZE1prEgrZdlH/L9HdM1odnQ==
2639+
"@sentry/webpack-plugin@1.17.1":
2640+
version "1.17.1"
2641+
resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.17.1.tgz#1b3ebbe9991e4d77125ace2b24594059a088268a"
2642+
integrity sha512-L47a0hxano4a+9jbvQSBzHCT1Ph8fYAvGGUvFg8qc69yXS9si5lXRNIH/pavN6mqJjhQjAcEsEp+vxgvT4xZDQ==
26432643
dependencies:
2644-
"@sentry/cli" "^1.64.1"
2644+
"@sentry/cli" "^1.68.0"
26452645

26462646
"@simple-dom/interface@^1.4.0":
26472647
version "1.4.0"

0 commit comments

Comments
 (0)