Skip to content

Commit 8063fbb

Browse files
authored
ref(nextjs): Inject init code in _app and API routes (#3786)
Change how we include the user's `sentry.server.config.js` and `sentry.client.config.js` code (which is what calls `Sentry.init()`) in server and browser bundles, in order both to fix apps deployed to Vercel and to eliminate some semi-gross hacks included in the current implementation. A great deal more detail in the PR description.
1 parent f964a69 commit 8063fbb

File tree

4 files changed

+76
-161
lines changed

4 files changed

+76
-161
lines changed

packages/nextjs/src/config/utils.ts

Lines changed: 0 additions & 60 deletions
This file was deleted.

packages/nextjs/src/config/webpack.ts

Lines changed: 12 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import * as SentryWebpackPlugin from '@sentry/webpack-plugin';
44

55
import {
66
BuildContext,
7-
EntryPointObject,
87
EntryPointValue,
98
EntryPropertyObject,
109
NextConfigObject,
@@ -13,16 +12,15 @@ import {
1312
WebpackConfigObject,
1413
WebpackEntryProperty,
1514
} from './types';
16-
import { SERVER_SDK_INIT_PATH, storeServerConfigFileLocation } from './utils';
1715

1816
export { SentryWebpackPlugin };
1917

2018
// TODO: merge default SentryWebpackPlugin ignore with their SentryWebpackPlugin ignore or ignoreFile
2119
// TODO: merge default SentryWebpackPlugin include with their SentryWebpackPlugin include
2220
// TODO: drop merged keys from override check? `includeDefaults` option?
2321

24-
const CLIENT_SDK_CONFIG_FILE = './sentry.client.config.js';
25-
const SERVER_SDK_CONFIG_FILE = './sentry.server.config.js';
22+
export const CLIENT_SDK_CONFIG_FILE = './sentry.client.config.js';
23+
export const SERVER_SDK_CONFIG_FILE = './sentry.server.config.js';
2624

2725
const defaultSentryWebpackPluginOptions = dropUndefinedKeys({
2826
url: process.env.SENTRY_URL,
@@ -58,12 +56,6 @@ export function constructWebpackConfigFunction(
5856
const newWebpackFunction = (incomingConfig: WebpackConfigObject, buildContext: BuildContext): WebpackConfigObject => {
5957
let newConfig = { ...incomingConfig };
6058

61-
// if we're building server code, store the webpack output path as an env variable, so we know where to look for the
62-
// webpack-processed version of `sentry.server.config.js` when we need it
63-
if (newConfig.target === 'node') {
64-
storeServerConfigFileLocation(newConfig);
65-
}
66-
6759
// if user has custom webpack config (which always takes the form of a function), run it so we have actual values to
6860
// work with
6961
if ('webpack' in userNextConfig && typeof userNextConfig.webpack === 'function') {
@@ -140,39 +132,11 @@ async function addSentryToEntryProperty(
140132
const newEntryProperty =
141133
typeof currentEntryProperty === 'function' ? await currentEntryProperty() : { ...currentEntryProperty };
142134

143-
// Add a new element to the `entry` array, we force webpack to create a bundle out of the user's
144-
// `sentry.server.config.js` file and output it to `SERVER_INIT_LOCATION`. (See
145-
// https://webpack.js.org/guides/code-splitting/#entry-points.) We do this so that the user's config file is run
146-
// through babel (and any other processors through which next runs the rest of the user-provided code - pages, API
147-
// routes, etc.). Specifically, we need any ESM-style `import` code to get transpiled into ES5, so that we can call
148-
// `require()` on the resulting file when we're instrumenting the sesrver. (We can't use a dynamic import there
149-
// because that then forces the user into a particular TS config.)
150-
151-
// On the server, create a separate bundle, as there's no one entry point depended on by all the others
152-
if (buildContext.isServer) {
153-
// slice off the final `.js` since webpack is going to add it back in for us, and we don't want to end up with
154-
// `.js.js` as the extension
155-
newEntryProperty[SERVER_SDK_INIT_PATH.slice(0, -3)] = SERVER_SDK_CONFIG_FILE;
156-
}
157-
// On the client, it's sufficient to inject it into the `main` JS code, which is included in every browser page.
158-
else {
159-
addFileToExistingEntryPoint(newEntryProperty, 'main', CLIENT_SDK_CONFIG_FILE);
160-
161-
// To work around a bug in nextjs, we need to ensure that the `main.js` entry is empty (otherwise it'll choose that
162-
// over `main` and we'll lose the change we just made). In case some other library has put something into it, copy
163-
// its contents over before emptying it out. See
164-
// https://github.com/getsentry/sentry-javascript/pull/3696#issuecomment-863363803.)
165-
const mainjsValue = newEntryProperty['main.js'];
166-
if (Array.isArray(mainjsValue) && mainjsValue.length > 0) {
167-
const mainValue = newEntryProperty.main;
168-
169-
// copy the `main.js` entries over
170-
newEntryProperty.main = Array.isArray(mainValue)
171-
? [...mainjsValue, ...mainValue]
172-
: { ...(mainValue as EntryPointObject), import: [...mainjsValue, ...(mainValue as EntryPointObject).import] };
173-
174-
// nuke the entries
175-
newEntryProperty['main.js'] = [];
135+
const userConfigFile = buildContext.isServer ? SERVER_SDK_CONFIG_FILE : CLIENT_SDK_CONFIG_FILE;
136+
137+
for (const entryPointName in newEntryProperty) {
138+
if (entryPointName === 'pages/_app' || entryPointName.includes('pages/api')) {
139+
addFileToExistingEntryPoint(newEntryProperty, entryPointName, userConfigFile);
176140
}
177141
}
178142

@@ -195,22 +159,20 @@ function addFileToExistingEntryPoint(
195159
const currentEntryPoint = entryProperty[entryPointName];
196160
let newEntryPoint: EntryPointValue;
197161

198-
// We inject the user's client config file after the existing code so that the config file has access to
199-
// `publicRuntimeConfig`. See https://github.com/getsentry/sentry-javascript/issues/3485
200162
if (typeof currentEntryPoint === 'string') {
201-
newEntryPoint = [currentEntryPoint, filepath];
163+
newEntryPoint = [filepath, currentEntryPoint];
202164
} else if (Array.isArray(currentEntryPoint)) {
203-
newEntryPoint = [...currentEntryPoint, filepath];
165+
newEntryPoint = [filepath, ...currentEntryPoint];
204166
}
205167
// descriptor object (webpack 5+)
206168
else if (typeof currentEntryPoint === 'object' && 'import' in currentEntryPoint) {
207169
const currentImportValue = currentEntryPoint.import;
208-
let newImportValue: string | string[];
170+
let newImportValue;
209171

210172
if (typeof currentImportValue === 'string') {
211-
newImportValue = [currentImportValue, filepath];
173+
newImportValue = [filepath, currentImportValue];
212174
} else {
213-
newImportValue = [...currentImportValue, filepath];
175+
newImportValue = [filepath, ...currentImportValue];
214176
}
215177

216178
newEntryPoint = {

packages/nextjs/src/utils/instrumentServer.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { fill, isString, logger, stripUrlQueryAndFragment } from '@sentry/utils'
55
import * as domain from 'domain';
66
import * as http from 'http';
77
import { default as createNextServer } from 'next';
8-
import * as path from 'path';
98
import * as querystring from 'querystring';
109
import * as url from 'url';
1110

@@ -111,18 +110,6 @@ function makeWrappedHandlerGetter(origHandlerGetter: HandlerGetter): WrappedHand
111110
// Otherwise, it's just a pass-through to the original method.
112111
const wrappedHandlerGetter = async function(this: NextServer): Promise<ReqHandler> {
113112
if (!sdkSetupComplete) {
114-
try {
115-
// `SENTRY_SERVER_INIT_PATH` is set at build time, and points to a webpack-processed version of the user's
116-
// `sentry.server.config.js`. Requiring it starts the SDK.
117-
require(path.resolve(process.env.SENTRY_SERVER_INIT_PATH as string));
118-
} catch (err) {
119-
// Log the error but don't bail - we still want the wrapping to happen, in case the user is doing something weird
120-
// and manually calling `Sentry.init()` somewhere else. We log to console instead of using logger from utils
121-
// because Sentry is not initialized.
122-
// eslint-disable-next-line no-console
123-
console.error(`[Sentry] Could not initialize SDK. Received error:\n${err}`);
124-
}
125-
126113
// stash this in the closure so that `makeWrappedReqHandler` can use it
127114
liveServer = this.server;
128115
const serverPrototype = Object.getPrototypeOf(liveServer);

packages/nextjs/test/config.test.ts

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,14 @@ import {
77
SentryWebpackPluginOptions,
88
WebpackConfigObject,
99
} from '../src/config/types';
10-
import { SENTRY_SERVER_CONFIG_FILE, SERVER_SDK_INIT_PATH } from '../src/config/utils';
11-
import { constructWebpackConfigFunction, SentryWebpackPlugin } from '../src/config/webpack';
12-
13-
// mock `storeServerConfigFileLocation` in order to make it a no-op when necessary
14-
jest.mock('../src/config/utils', () => {
15-
const original = jest.requireActual('../src/config/utils');
16-
return {
17-
...original,
18-
// nuke this so it won't try to look for our dummy paths
19-
storeServerConfigFileLocation: jest.fn(),
20-
};
21-
});
10+
import {
11+
CLIENT_SDK_CONFIG_FILE,
12+
constructWebpackConfigFunction,
13+
SentryWebpackPlugin,
14+
SERVER_SDK_CONFIG_FILE,
15+
} from '../src/config/webpack';
2216

23-
/** mocks of the arguments passed to `withSentryConfig` */
17+
/** Mocks of the arguments passed to `withSentryConfig` */
2418
const userNextConfig = {
2519
publicRuntimeConfig: { location: 'dogpark', activities: ['fetch', 'chasing', 'digging'] },
2620
webpack: (config: WebpackConfigObject, _options: BuildContext) => ({
@@ -35,19 +29,36 @@ const userNextConfig = {
3529
};
3630
const userSentryWebpackPluginConfig = { org: 'squirrelChasers', project: 'simulator', include: './thirdPartyMaps' };
3731

38-
/** mocks of the arguments passed to the result of `withSentryConfig` (when it's a function) */
39-
const runtimePhase = 'puppy-phase-chew-everything-in-sight';
32+
/** Mocks of the arguments passed to the result of `withSentryConfig` (when it's a function). */
33+
const runtimePhase = 'ball-fetching';
4034
const defaultNextConfig = { nappingHoursPerDay: 20, oversizeFeet: true, shouldChaseTail: true };
4135

4236
/** mocks of the arguments passed to `nextConfig.webpack` */
4337
const serverWebpackConfig = {
44-
entry: () => Promise.resolve({ 'pages/api/dogs/[name]': 'private-next-pages/api/dogs/[name].js' }),
38+
entry: () =>
39+
Promise.resolve({
40+
'pages/api/dogs/[name]': 'private-next-pages/api/dogs/[name].js',
41+
'pages/_app': ['./node_modules/smellOVision/index.js', 'private-next-pages/_app.js'],
42+
'pages/api/simulator/dogStats/[name]': { import: 'private-next-pages/api/simulator/dogStats/[name].js' },
43+
'pages/api/simulator/leaderboard': {
44+
import: ['./node_modules/dogPoints/converter.js', 'private-next-pages/api/simulator/leaderboard.js'],
45+
},
46+
'pages/api/tricks/[trickName]': {
47+
import: 'private-next-pages/api/tricks/[trickName].js',
48+
dependOn: 'treats',
49+
},
50+
treats: './node_modules/dogTreats/treatProvider.js',
51+
}),
4552
output: { filename: '[name].js', path: '/Users/Maisey/projects/squirrelChasingSimulator/.next' },
4653
target: 'node',
4754
context: '/Users/Maisey/projects/squirrelChasingSimulator',
4855
};
4956
const clientWebpackConfig = {
50-
entry: () => Promise.resolve({ main: './src/index.ts' }),
57+
entry: () =>
58+
Promise.resolve({
59+
main: './src/index.ts',
60+
'pages/_app': 'next-client-pages-loader?page=%2F_app',
61+
}),
5162
output: { filename: 'static/chunks/[name].js', path: '/Users/Maisey/projects/squirrelChasingSimulator/.next' },
5263
target: 'web',
5364
context: '/Users/Maisey/projects/squirrelChasingSimulator',
@@ -212,7 +223,7 @@ describe('webpack config', () => {
212223
});
213224

214225
describe('webpack `entry` property config', () => {
215-
it('injects correct code when building server bundle', async () => {
226+
it('handles various entrypoint shapes', async () => {
216227
const finalWebpackConfig = await materializeFinalWebpackConfig({
217228
userNextConfig,
218229
incomingWebpackConfig: serverWebpackConfig,
@@ -221,38 +232,53 @@ describe('webpack config', () => {
221232

222233
expect(finalWebpackConfig.entry).toEqual(
223234
expect.objectContaining({
224-
[SERVER_SDK_INIT_PATH.slice(0, -3)]: SENTRY_SERVER_CONFIG_FILE,
235+
// original entry point value is a string
236+
// (was 'private-next-pages/api/dogs/[name].js')
237+
'pages/api/dogs/[name]': [SERVER_SDK_CONFIG_FILE, 'private-next-pages/api/dogs/[name].js'],
238+
239+
// original entry point value is a string array
240+
// (was ['./node_modules/smellOVision/index.js', 'private-next-pages/_app.js'])
241+
'pages/_app': [SERVER_SDK_CONFIG_FILE, './node_modules/smellOVision/index.js', 'private-next-pages/_app.js'],
242+
243+
// original entry point value is an object containing a string `import` value
244+
// (`import` was 'private-next-pages/api/simulator/dogStats/[name].js')
245+
'pages/api/simulator/dogStats/[name]': {
246+
import: [SERVER_SDK_CONFIG_FILE, 'private-next-pages/api/simulator/dogStats/[name].js'],
247+
},
248+
249+
// original entry point value is an object containing a string array `import` value
250+
// (`import` was ['./node_modules/dogPoints/converter.js', 'private-next-pages/api/simulator/leaderboard.js'])
251+
'pages/api/simulator/leaderboard': {
252+
import: [
253+
SERVER_SDK_CONFIG_FILE,
254+
'./node_modules/dogPoints/converter.js',
255+
'private-next-pages/api/simulator/leaderboard.js',
256+
],
257+
},
258+
259+
// original entry point value is an object containg properties besides `import`
260+
// (`dependOn` remains untouched)
261+
'pages/api/tricks/[trickName]': {
262+
import: [SERVER_SDK_CONFIG_FILE, 'private-next-pages/api/tricks/[trickName].js'],
263+
dependOn: 'treats',
264+
},
225265
}),
226266
);
227267
});
228268

229-
it('injects correct code when building client bundle', async () => {
269+
it('does not inject into non-_app, non-API routes', async () => {
230270
const finalWebpackConfig = await materializeFinalWebpackConfig({
231271
userNextConfig,
232272
incomingWebpackConfig: clientWebpackConfig,
233273
incomingWebpackBuildContext: clientBuildContext,
234274
});
235275

236-
expect(finalWebpackConfig.entry).toEqual(
237-
expect.objectContaining({ main: ['./src/index.ts', './sentry.client.config.js'] }),
238-
);
239-
});
240-
241-
// see https://github.com/getsentry/sentry-javascript/pull/3696#issuecomment-863363803
242-
it('handles non-empty `main.js` entry point', async () => {
243-
const finalWebpackConfig = await materializeFinalWebpackConfig({
244-
userNextConfig,
245-
incomingWebpackConfig: {
246-
...clientWebpackConfig,
247-
entry: () => Promise.resolve({ main: './src/index.ts', 'main.js': ['sitLieDownRollOver.config.js'] }),
248-
},
249-
incomingWebpackBuildContext: clientBuildContext,
250-
});
251-
252276
expect(finalWebpackConfig.entry).toEqual(
253277
expect.objectContaining({
254-
main: ['sitLieDownRollOver.config.js', './src/index.ts', './sentry.client.config.js'],
255-
'main.js': [],
278+
// no injected file
279+
main: './src/index.ts',
280+
// was 'next-client-pages-loader?page=%2F_app'
281+
'pages/_app': [CLIENT_SDK_CONFIG_FILE, 'next-client-pages-loader?page=%2F_app'],
256282
}),
257283
);
258284
});

0 commit comments

Comments
 (0)