Skip to content

Commit b35e3d7

Browse files
lobsterkatieLuca Forstner
andauthored
fix(nextjs): Stop excluding withSentryConfig from serverless bundles (#6267)
Co-authored-by: Luca Forstner <[email protected]>
1 parent 99864fa commit b35e3d7

File tree

5 files changed

+105
-35
lines changed

5 files changed

+105
-35
lines changed

packages/nextjs/rollup.npm.config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ export default [
55
makeBaseNPMConfig({
66
// We need to include `instrumentServer.ts` separately because it's only conditionally required, and so rollup
77
// doesn't automatically include it when calculating the module dependency tree.
8-
entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/utils/instrumentServer.ts'],
8+
entrypoints: [
9+
'src/index.server.ts',
10+
'src/index.client.ts',
11+
'src/utils/instrumentServer.ts',
12+
'src/config/webpack.ts',
13+
],
914

1015
// prevent this internal nextjs code from ending up in our built package (this doesn't happen automatially because
1116
// the name doesn't match an SDK dependency)

packages/nextjs/src/config/webpack.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
/* eslint-disable max-lines */
22
import { getSentryRelease } from '@sentry/node';
3-
import { arrayify, dropUndefinedKeys, escapeStringForRegex, logger, stringMatchesSomePattern } from '@sentry/utils';
3+
import {
4+
arrayify,
5+
dropUndefinedKeys,
6+
escapeStringForRegex,
7+
GLOBAL_OBJ,
8+
logger,
9+
stringMatchesSomePattern,
10+
} from '@sentry/utils';
411
import { default as SentryWebpackPlugin } from '@sentry/webpack-plugin';
512
import * as chalk from 'chalk';
613
import * as fs from 'fs';
@@ -27,6 +34,14 @@ export { SentryWebpackPlugin };
2734
// TODO: merge default SentryWebpackPlugin include with their SentryWebpackPlugin include
2835
// TODO: drop merged keys from override check? `includeDefaults` option?
2936

37+
// In order to make sure that build-time code isn't getting bundled in runtime bundles (specifically, in the serverless
38+
// functions into which nextjs converts a user's routes), we exclude this module (and all of its dependencies) from the
39+
// nft file manifests nextjs generates. (See the code dealing with the `TraceEntryPointsPlugin` below.) So that we don't
40+
// crash people, we therefore need to make sure nothing tries to either require this file or import from it. Setting
41+
// this env variable allows us to test whether or not that's happened.
42+
const _global = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryWebpackModuleLoaded?: true };
43+
_global._sentryWebpackModuleLoaded = true;
44+
3045
/**
3146
* Construct the function which will be used as the nextjs config's `webpack` value.
3247
*
@@ -106,14 +121,15 @@ export function constructWebpackConfigFunction(
106121

107122
// Prevent `@vercel/nft` (which nextjs uses to determine which files are needed when packaging up a lambda) from
108123
// including any of our build-time code or dependencies. (Otherwise it'll include files like this one and even the
109-
// entirety of rollup and sucrase.)
124+
// entirety of rollup and sucrase.) Since this file is the root of that dependency tree, it's enough to just
125+
// exclude it and the rest will be excluded as well.
110126
const nftPlugin = newConfig.plugins?.find((plugin: WebpackPluginInstance) => {
111127
const proto = Object.getPrototypeOf(plugin) as WebpackPluginInstance;
112128
return proto.constructor.name === 'TraceEntryPointsPlugin';
113129
}) as WebpackPluginInstance & { excludeFiles: string[] };
114130
if (nftPlugin) {
115131
if (Array.isArray(nftPlugin.excludeFiles)) {
116-
nftPlugin.excludeFiles.push(path.join(__dirname, 'withSentryConfig.js'));
132+
nftPlugin.excludeFiles.push(__filename);
117133
} else {
118134
__DEBUG_BUILD__ &&
119135
logger.warn(
Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import type { ExportedNextConfig, NextConfigFunction, NextConfigObject, SentryWebpackPluginOptions } from './types';
2-
import { constructWebpackConfigFunction } from './webpack';
1+
import { isBuild } from '../utils/isBuild';
2+
import type {
3+
ExportedNextConfig,
4+
NextConfigFunction,
5+
NextConfigObject,
6+
NextConfigObjectWithSentry,
7+
SentryWebpackPluginOptions,
8+
} from './types';
39

410
/**
511
* Add Sentry options to the config to be exported from the user's `next.config.js` file.
@@ -16,38 +22,42 @@ export function withSentryConfig(
1622
// `defaults` in order to pass them along to the user's function
1723
if (typeof exportedUserNextConfig === 'function') {
1824
return function (phase: string, defaults: { defaultConfig: NextConfigObject }): NextConfigObject {
19-
let userNextConfigObject = exportedUserNextConfig(phase, defaults);
25+
const userNextConfigObject = exportedUserNextConfig(phase, defaults);
2026

21-
// Next 12.2.3+ warns about non-canonical properties on `userNextConfig`, so grab and then remove the `sentry`
22-
// property there. Where we actually need it is in the webpack config function we're going to create, so pass it
23-
// to `constructWebpackConfigFunction` so that it will be in the created function's closure.
24-
const { sentry: userSentryOptions } = userNextConfigObject;
25-
delete userNextConfigObject.sentry;
26-
// Remind TS that there's now no `sentry` property
27-
userNextConfigObject = userNextConfigObject as NextConfigObject;
28-
29-
return {
30-
...userNextConfigObject,
31-
webpack: constructWebpackConfigFunction(
32-
userNextConfigObject,
33-
userSentryWebpackPluginOptions,
34-
userSentryOptions,
35-
),
36-
};
27+
return getFinalConfigObject(userNextConfigObject, userSentryWebpackPluginOptions);
3728
};
3829
}
3930

4031
// Otherwise, we can just merge their config with ours and return an object.
32+
return getFinalConfigObject(exportedUserNextConfig, userSentryWebpackPluginOptions);
33+
}
4134

42-
// Prevent nextjs from getting mad about having a non-standard config property in `userNextConfig`. (See note above
43-
// for a more thorough explanation of what we're doing here.)
44-
const { sentry: userSentryOptions } = exportedUserNextConfig;
45-
delete exportedUserNextConfig.sentry;
35+
// Modify the materialized object form of the user's next config by deleting the `sentry` property and wrapping the
36+
// `webpack` property
37+
function getFinalConfigObject(
38+
incomingUserNextConfigObject: NextConfigObjectWithSentry,
39+
userSentryWebpackPluginOptions: Partial<SentryWebpackPluginOptions>,
40+
): NextConfigObject {
41+
// Next 12.2.3+ warns about non-canonical properties on `userNextConfig`, so grab and then remove the `sentry`
42+
// property there. Where we actually need it is in the webpack config function we're going to create, so pass it
43+
// to `constructWebpackConfigFunction` so that it can live in the returned function's closure.
44+
const { sentry: userSentryOptions } = incomingUserNextConfigObject;
45+
delete incomingUserNextConfigObject.sentry;
4646
// Remind TS that there's now no `sentry` property
47-
const userNextConfigObject = exportedUserNextConfig as NextConfigObject;
47+
const userNextConfigObject = incomingUserNextConfigObject as NextConfigObject;
48+
49+
// In order to prevent all of our build-time code from being bundled in people's route-handling serverless functions,
50+
// we exclude `webpack.ts` and all of its dependencies from nextjs's `@vercel/nft` filetracing. We therefore need to
51+
// make sure that we only require it at build time.
52+
if (isBuild()) {
53+
// eslint-disable-next-line @typescript-eslint/no-var-requires
54+
const { constructWebpackConfigFunction } = require('./webpack');
55+
return {
56+
...userNextConfigObject,
57+
webpack: constructWebpackConfigFunction(userNextConfigObject, userSentryWebpackPluginOptions, userSentryOptions),
58+
};
59+
}
4860

49-
return {
50-
...userNextConfigObject,
51-
webpack: constructWebpackConfigFunction(userNextConfigObject, userSentryWebpackPluginOptions, userSentryOptions),
52-
};
61+
// At runtime, we just return the user's config untouched.
62+
return userNextConfigObject;
5363
}

packages/nextjs/test/buildProcess/tests/nft.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ it('excludes build-time SDK dependencies from nft files', () => {
1111

1212
// These are all of the files which control the way we modify the user's app build by changing the webpack config.
1313
// They get mistakenly included by the nft plugin because we export `withSentryConfig` from `index.server.ts`. Because
14-
// the wrappers are referenced from the code we inject at build time, they need to stay.
14+
// the wrappers are referenced from the code we inject at build time, they need to stay. `withSentryConfig.ts` itself
15+
// also needs to stay because nextjs loads `next.config.js` at runtime
1516
const sentryConfigDirEntries = dogNFTjson.files.filter(entry =>
1617
entry.includes('node_modules/@sentry/nextjs/build/cjs/config'),
1718
);
18-
const withSentryConfigEntries = sentryConfigDirEntries.filter(entry => !entry.includes('config/wrappers'));
1919
const sentryWrapperEntries = sentryConfigDirEntries.filter(entry => entry.includes('config/wrappers'));
20+
const excludedSentryConfigEntries = sentryConfigDirEntries.filter(
21+
entry => !sentryWrapperEntries.includes(entry) && !entry.includes('withSentryConfig'),
22+
);
2023

2124
// Sucrase and rollup are dependencies of one of the webpack loaders we add to the config - also not needed at runtime.
2225
const sucraseEntries = dogNFTjson.files.filter(entry => entry.includes('node_modules/sucrase'));
@@ -25,7 +28,7 @@ it('excludes build-time SDK dependencies from nft files', () => {
2528
);
2629

2730
// None of the build-time dependencies should be listed
28-
expect(withSentryConfigEntries.length).toEqual(0);
31+
expect(excludedSentryConfigEntries.length).toEqual(0);
2932
expect(sucraseEntries.length).toEqual(0);
3033
expect(rollupEntries.length).toEqual(0);
3134

packages/nextjs/test/config/index.test.ts renamed to packages/nextjs/test/config/withSentryConfig.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import * as isBuildModule from '../../src/utils/isBuild';
12
import { defaultsObject, exportedNextConfig, runtimePhase, userNextConfig } from './fixtures';
23
import { materializeFinalNextConfig } from './testUtils';
34

5+
const isBuildSpy = jest.spyOn(isBuildModule, 'isBuild').mockReturnValue(true);
6+
47
describe('withSentryConfig', () => {
58
it('includes expected properties', () => {
69
const finalConfig = materializeFinalNextConfig(exportedNextConfig);
@@ -59,4 +62,37 @@ describe('withSentryConfig', () => {
5962
// directly
6063
expect('sentry' in finalConfig).toBe(false);
6164
});
65+
66+
describe('conditional use of `constructWebpackConfigFunction`', () => {
67+
// Note: In these tests, it would be nice to be able to spy on `constructWebpackConfigFunction` to see whether or
68+
// not it's called, but that sets up a catch-22: If you import or require the module to spy on the function, it gets
69+
// cached and the `require` call we care about (inside of `withSentryConfig`) doesn't actually run the module code.
70+
// Alternatively, if we call `jest.resetModules()` after setting up the spy, then the module code *is* run a second
71+
// time, but the spy belongs to the first instance of the module and therefore never registers a call. Thus we have
72+
// to test whether or not the file is required instead.
73+
74+
it('imports from `webpack.ts` if `isBuild` returns true', () => {
75+
jest.isolateModules(() => {
76+
// In case this is still set from elsewhere, reset it
77+
delete (global as any)._sentryWebpackModuleLoaded;
78+
79+
materializeFinalNextConfig(exportedNextConfig);
80+
81+
expect((global as any)._sentryWebpackModuleLoaded).toBe(true);
82+
});
83+
});
84+
85+
it("doesn't import from `webpack.ts` if `isBuild` returns false", () => {
86+
jest.isolateModules(() => {
87+
isBuildSpy.mockReturnValueOnce(false);
88+
89+
// In case this is still set from elsewhere, reset it
90+
delete (global as any)._sentryWebpackModuleLoaded;
91+
92+
materializeFinalNextConfig(exportedNextConfig);
93+
94+
expect((global as any)._sentryWebpackModuleLoaded).toBeUndefined();
95+
});
96+
});
97+
});
6298
});

0 commit comments

Comments
 (0)