Skip to content

Commit 484aad5

Browse files
authored
chore(nextjs): Cosmetic changes ahead of API route fix (#3784)
Nothing substantive here - just a bunch of changes pulled out of an upcoming PR fixing API route monitoring in nextjs, to make that one easier to review. Major themes: - Standardize naming - Split current and new values into separate variables - Remove old code - Check for entry point existence (moot given our minimum supported nextjs version) - Unnecessary stuff which came along when tracing code was copied from `instrumentServer.js`
1 parent e814408 commit 484aad5

File tree

3 files changed

+63
-71
lines changed

3 files changed

+63
-71
lines changed

packages/nextjs/src/config/types.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,14 @@ export type BuildContext = { dev: boolean; isServer: boolean; buildId: string };
5252
// For our purposes, the value for `entry` is either an object, or an async function which returns such an object
5353
export type WebpackEntryProperty = EntryPropertyObject | EntryPropertyFunction;
5454

55-
// Each value in that object is either a string representing a single entry point, an array of such strings, or an
56-
// object containing either of those, along with other configuration options. In that third case, the entry point(s) are
57-
// listed under the key `import`.
5855
export type EntryPropertyObject = {
59-
[key: string]:
60-
| string
61-
| Array<string>
62-
// only in webpack 5
63-
| EntryPointObject;
56+
[key: string]: EntryPointValue;
6457
};
6558

6659
export type EntryPropertyFunction = () => Promise<EntryPropertyObject>;
6760

68-
// An object with options for a single entry point, potentially one of many in the webpack `entry` property
61+
// Each value in that object is either a string representing a single entry point, an array of such strings, or an
62+
// object containing either of those, along with other configuration options. In that third case, the entry point(s) are
63+
// listed under the key `import`.
64+
export type EntryPointValue = string | Array<string> | EntryPointObject;
6965
export type EntryPointObject = { import: string | Array<string> };

packages/nextjs/src/config/webpack.ts

+53-49
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,25 @@ import * as SentryWebpackPlugin from '@sentry/webpack-plugin';
55
import {
66
BuildContext,
77
EntryPointObject,
8+
EntryPointValue,
89
EntryPropertyObject,
910
NextConfigObject,
1011
SentryWebpackPluginOptions,
1112
WebpackConfigFunction,
1213
WebpackConfigObject,
1314
WebpackEntryProperty,
1415
} from './types';
15-
import {
16-
SENTRY_CLIENT_CONFIG_FILE,
17-
SENTRY_SERVER_CONFIG_FILE,
18-
SERVER_SDK_INIT_PATH,
19-
storeServerConfigFileLocation,
20-
} from './utils';
16+
import { SERVER_SDK_INIT_PATH, storeServerConfigFileLocation } from './utils';
2117

2218
export { SentryWebpackPlugin };
2319

2420
// TODO: merge default SentryWebpackPlugin ignore with their SentryWebpackPlugin ignore or ignoreFile
2521
// TODO: merge default SentryWebpackPlugin include with their SentryWebpackPlugin include
2622
// TODO: drop merged keys from override check? `includeDefaults` option?
2723

24+
const CLIENT_SDK_CONFIG_FILE = './sentry.client.config.js';
25+
const SERVER_SDK_CONFIG_FILE = './sentry.server.config.js';
26+
2827
const defaultSentryWebpackPluginOptions = dropUndefinedKeys({
2928
url: process.env.SENTRY_URL,
3029
org: process.env.SENTRY_ORG,
@@ -53,9 +52,11 @@ export function constructWebpackConfigFunction(
5352
userNextConfig: NextConfigObject = {},
5453
userSentryWebpackPluginOptions: Partial<SentryWebpackPluginOptions> = {},
5554
): WebpackConfigFunction {
56-
const newWebpackFunction = (config: WebpackConfigObject, options: BuildContext): WebpackConfigObject => {
57-
// clone to avoid mutability issues
58-
let newConfig = { ...config };
55+
// Will be called by nextjs and passed its default webpack configuration and context data about the build (whether
56+
// we're building server or client, whether we're in dev, what version of webpack we're using, etc). Note that
57+
// `incomingConfig` and `buildContext` are referred to as `config` and `options` in the nextjs docs.
58+
const newWebpackFunction = (incomingConfig: WebpackConfigObject, buildContext: BuildContext): WebpackConfigObject => {
59+
let newConfig = { ...incomingConfig };
5960

6061
// if we're building server code, store the webpack output path as an env variable, so we know where to look for the
6162
// webpack-processed version of `sentry.server.config.js` when we need it
@@ -66,7 +67,7 @@ export function constructWebpackConfigFunction(
6667
// if user has custom webpack config (which always takes the form of a function), run it so we have actual values to
6768
// work with
6869
if ('webpack' in userNextConfig && typeof userNextConfig.webpack === 'function') {
69-
newConfig = userNextConfig.webpack(newConfig, options);
70+
newConfig = userNextConfig.webpack(newConfig, buildContext);
7071
}
7172

7273
// Tell webpack to inject user config files (containing the two `Sentry.init()` calls) into the appropriate output
@@ -78,10 +79,10 @@ export function constructWebpackConfigFunction(
7879
// will call the callback which will call `f` which will call `x.y`... and on and on. Theoretically this could also
7980
// be fixed by using `bind`, but this is way simpler.)
8081
const origEntryProperty = newConfig.entry;
81-
newConfig.entry = async () => addSentryToEntryProperty(origEntryProperty, options.isServer);
82+
newConfig.entry = async () => addSentryToEntryProperty(origEntryProperty, buildContext);
8283

8384
// Enable the Sentry plugin (which uploads source maps to Sentry when not in dev) by default
84-
const enableWebpackPlugin = options.isServer
85+
const enableWebpackPlugin = buildContext.isServer
8586
? !userNextConfig.sentry?.disableServerWebpackPlugin
8687
: !userNextConfig.sentry?.disableClientWebpackPlugin;
8788

@@ -92,7 +93,7 @@ export function constructWebpackConfigFunction(
9293

9394
// Next doesn't let you change this is dev even if you want to - see
9495
// https://github.com/vercel/next.js/blob/master/errors/improper-devtool.md
95-
if (!options.dev) {
96+
if (!buildContext.dev) {
9697
newConfig.devtool = 'source-map';
9798
}
9899

@@ -103,8 +104,8 @@ export function constructWebpackConfigFunction(
103104
// @ts-ignore Our types for the plugin are messed up somehow - TS wants this to be `SentryWebpackPlugin.default`,
104105
// but that's not actually a thing
105106
new SentryWebpackPlugin({
106-
dryRun: options.dev,
107-
release: getSentryRelease(options.buildId),
107+
dryRun: buildContext.dev,
108+
release: getSentryRelease(buildContext.buildId),
108109
...defaultSentryWebpackPluginOptions,
109110
...userSentryWebpackPluginOptions,
110111
}),
@@ -121,27 +122,23 @@ export function constructWebpackConfigFunction(
121122
* Modify the webpack `entry` property so that the code in `sentry.server.config.js` and `sentry.client.config.js` is
122123
* included in the the necessary bundles.
123124
*
124-
* @param origEntryProperty The value of the property before Sentry code has been injected
125-
* @param isServer A boolean provided by nextjs indicating whether we're handling the server bundles or the browser
126-
* bundles
125+
* @param currentEntryProperty The value of the property before Sentry code has been injected
126+
* @param buildContext Object passed by nextjs containing metadata about the build
127127
* @returns The value which the new `entry` property (which will be a function) will return (TODO: this should return
128128
* the function, rather than the function's return value)
129129
*/
130130
async function addSentryToEntryProperty(
131-
origEntryProperty: WebpackEntryProperty,
132-
isServer: boolean,
131+
currentEntryProperty: WebpackEntryProperty,
132+
buildContext: BuildContext,
133133
): Promise<EntryPropertyObject> {
134134
// The `entry` entry in a webpack config can be a string, array of strings, object, or function. By default, nextjs
135135
// sets it to an async function which returns the promise of an object of string arrays. Because we don't know whether
136136
// someone else has come along before us and changed that, we need to check a few things along the way. The one thing
137137
// we know is that it won't have gotten *simpler* in form, so we only need to worry about the object and function
138138
// options. See https://webpack.js.org/configuration/entry-context/#entry.
139139

140-
let newEntryProperty = origEntryProperty;
141-
if (typeof origEntryProperty === 'function') {
142-
newEntryProperty = await origEntryProperty();
143-
}
144-
newEntryProperty = newEntryProperty as EntryPropertyObject;
140+
const newEntryProperty =
141+
typeof currentEntryProperty === 'function' ? await currentEntryProperty() : { ...currentEntryProperty };
145142

146143
// Add a new element to the `entry` array, we force webpack to create a bundle out of the user's
147144
// `sentry.server.config.js` file and output it to `SERVER_INIT_LOCATION`. (See
@@ -152,14 +149,14 @@ async function addSentryToEntryProperty(
152149
// because that then forces the user into a particular TS config.)
153150

154151
// On the server, create a separate bundle, as there's no one entry point depended on by all the others
155-
if (isServer) {
152+
if (buildContext.isServer) {
156153
// 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
157154
// `.js.js` as the extension
158-
newEntryProperty[SERVER_SDK_INIT_PATH.slice(0, -3)] = SENTRY_SERVER_CONFIG_FILE;
155+
newEntryProperty[SERVER_SDK_INIT_PATH.slice(0, -3)] = SERVER_SDK_CONFIG_FILE;
159156
}
160157
// On the client, it's sufficient to inject it into the `main` JS code, which is included in every browser page.
161158
else {
162-
addFileToExistingEntryPoint(newEntryProperty, 'main', SENTRY_CLIENT_CONFIG_FILE);
159+
addFileToExistingEntryPoint(newEntryProperty, 'main', CLIENT_SDK_CONFIG_FILE);
163160

164161
// To work around a bug in nextjs, we need to ensure that the `main.js` entry is empty (otherwise it'll choose that
165162
// over `main` and we'll lose the change we just made). In case some other library has put something into it, copy
@@ -195,37 +192,44 @@ function addFileToExistingEntryPoint(
195192
filepath: string,
196193
): void {
197194
// can be a string, array of strings, or object whose `import` property is one of those two
198-
let injectedInto = entryProperty[entryPointName];
199-
200-
// Sometimes especially for older next.js versions it happens we don't have an entry point
201-
if (!injectedInto) {
202-
// eslint-disable-next-line no-console
203-
console.error(`[Sentry] Can't inject ${filepath}, no entrypoint is defined.`);
204-
return;
205-
}
195+
const currentEntryPoint = entryProperty[entryPointName];
196+
let newEntryPoint: EntryPointValue;
206197

207198
// We inject the user's client config file after the existing code so that the config file has access to
208199
// `publicRuntimeConfig`. See https://github.com/getsentry/sentry-javascript/issues/3485
209-
if (typeof injectedInto === 'string') {
210-
injectedInto = [injectedInto, filepath];
211-
} else if (Array.isArray(injectedInto)) {
212-
injectedInto = [...injectedInto, filepath];
213-
} else {
214-
let importVal: string | string[];
200+
if (typeof currentEntryPoint === 'string') {
201+
newEntryPoint = [currentEntryPoint, filepath];
202+
} else if (Array.isArray(currentEntryPoint)) {
203+
newEntryPoint = [...currentEntryPoint, filepath];
204+
}
205+
// descriptor object (webpack 5+)
206+
else if (typeof currentEntryPoint === 'object' && 'import' in currentEntryPoint) {
207+
const currentImportValue = currentEntryPoint.import;
208+
let newImportValue: string | string[];
215209

216-
if (typeof injectedInto.import === 'string') {
217-
importVal = [injectedInto.import, filepath];
210+
if (typeof currentImportValue === 'string') {
211+
newImportValue = [currentImportValue, filepath];
218212
} else {
219-
importVal = [...injectedInto.import, filepath];
213+
newImportValue = [...currentImportValue, filepath];
220214
}
221215

222-
injectedInto = {
223-
...injectedInto,
224-
import: importVal,
216+
newEntryPoint = {
217+
...currentEntryPoint,
218+
import: newImportValue,
225219
};
220+
} else {
221+
// mimic the logger prefix in order to use `console.warn` (which will always be printed, regardless of SDK settings)
222+
// eslint-disable-next-line no-console
223+
console.error(
224+
'Sentry Logger [Error]:',
225+
`Could not inject SDK initialization code into entry point ${entryPointName}, as it is not a recognized format.\n`,
226+
`Expected: string | Array<string> | { [key:string]: any, import: string | Array<string> }\n`,
227+
`Got: ${currentEntryPoint}`,
228+
);
229+
return;
226230
}
227231

228-
entryProperty[entryPointName] = injectedInto;
232+
entryProperty[entryPointName] = newEntryPoint;
229233
}
230234

231235
/**

packages/nextjs/src/utils/handlers.ts

+5-13
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
2020
if (currentScope) {
2121
currentScope.addEventProcessor(event => addRequestDataToEvent(event, req as NextRequest));
2222

23-
// We only want to record page and API requests
2423
if (hasTracingEnabled()) {
2524
// If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision)
2625
let traceparentData;
@@ -34,20 +33,18 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
3433
let reqPath = stripUrlQueryAndFragment(url);
3534
// Replace with placeholder
3635
if (req.query) {
36+
// TODO get this from next if possible, to avoid accidentally replacing non-dynamic parts of the path if
37+
// they match dynamic parts
3738
for (const [key, value] of Object.entries(req.query)) {
3839
reqPath = reqPath.replace(`${value}`, `[${key}]`);
3940
}
4041
}
41-
42-
// requests for pages will only ever be GET requests, so don't bother to include the method in the transaction
43-
// name; requests to API routes could be GET, POST, PUT, etc, so do include it there
44-
const namePrefix = `${(req.method || 'GET').toUpperCase()} `;
42+
const reqMethod = `${(req.method || 'GET').toUpperCase()} `;
4543

4644
const transaction = startTransaction(
4745
{
48-
name: `${namePrefix}${reqPath}`,
46+
name: `${reqMethod}${reqPath}`,
4947
op: 'http.server',
50-
metadata: { requestPath: reqPath },
5148
...traceparentData,
5249
},
5350
// extra context passed to the `tracesSampler`
@@ -57,7 +54,7 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
5754
}
5855
}
5956

60-
return await handler(req, res); // Call Handler
57+
return await handler(req, res); // Call original handler
6158
} catch (e) {
6259
withScope(scope => {
6360
scope.addEventProcessor(event => {
@@ -74,11 +71,6 @@ export const withSentry = (handler: NextApiHandler): WrappedNextApiHandler => {
7471
if (transaction) {
7572
transaction.setHttpStatus(res.statusCode);
7673

77-
// we'll collect this data in a more targeted way in the event processor we added above,
78-
// `addRequestDataToEvent`
79-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
80-
delete transaction.metadata.requestPath;
81-
8274
transaction.finish();
8375
}
8476
try {

0 commit comments

Comments
 (0)