Skip to content

Commit b16c34a

Browse files
lobsterkatieHazAT
authored andcommitted
inject SDK init earlier - API transactions working
(cherry picked from commit 8cffa76)
1 parent 65ebb16 commit b16c34a

File tree

2 files changed

+53
-41
lines changed

2 files changed

+53
-41
lines changed

packages/nextjs/src/utils/config.ts

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,36 @@ import { logger } from '@sentry/utils';
33
import defaultWebpackPlugin, { SentryCliPluginOptions } from '@sentry/webpack-plugin';
44
import * as SentryWebpackPlugin from '@sentry/webpack-plugin';
55

6+
const SENTRY_CLIENT_CONFIG_FILE = './sentry.client.config.js';
7+
const SENTRY_SERVER_CONFIG_FILE = './sentry.server.config.js';
8+
// this is where the transpiled/bundled version of `USER_SERVER_CONFIG_FILE` will end up
9+
export const SERVER_INIT_OUTPUT_LOCATION = 'sentry/serverInit';
10+
611
// eslint-disable-next-line @typescript-eslint/no-explicit-any
712
type PlainObject<T = any> = { [key: string]: T };
813

9-
// Man are these types hard to name well. "Entry" = an item in some collection of items, but in our case, one of the
10-
// things we're worried about here is property (entry) in an object called... entry. So henceforth, the specific
11-
// property we're modifying is going to be known as an EntryProperty.
12-
1314
// The function which is ultimately going to be exported from `next.config.js` under the name `webpack`
1415
type WebpackExport = (config: WebpackConfig, options: WebpackOptions) => WebpackConfig;
1516

1617
// The two arguments passed to the exported `webpack` function, as well as the thing it returns
17-
type WebpackConfig = { devtool: string; plugins: PlainObject[]; entry: EntryProperty };
18+
type WebpackConfig = { devtool: string; plugins: PlainObject[]; entry: EntryProperty; output: { path: string } };
1819
type WebpackOptions = { dev: boolean; isServer: boolean; buildId: string };
1920

2021
// For our purposes, the value for `entry` is either an object, or a function which returns such an object
2122
type EntryProperty = (() => Promise<EntryPropertyObject>) | EntryPropertyObject;
22-
2323
// Each value in that object is either a string representing a single entry point, an array of such strings, or an
2424
// object containing either of those, along with other configuration options. In that third case, the entry point(s) are
2525
// listed under the key `import`.
26-
type EntryPropertyObject = PlainObject<string | Array<string> | EntryPointObject>;
26+
type EntryPropertyObject = PlainObject<string> | PlainObject<Array<string>> | PlainObject<EntryPointObject>;
2727
type EntryPointObject = { import: string | Array<string> };
2828

29-
const sentryClientConfig = './sentry.client.config.js';
30-
const sentryServerConfig = './sentry.server.config.js';
31-
32-
/** Add a file (`injectee`) to a given element (`injectionPoint`) of the `entry` property */
29+
/**
30+
* Add a file to a specific element of the given `entry` webpack config property.
31+
*
32+
* @param entryProperty The existing `entry` config object
33+
* @param injectionPoint The key where the file should be injected
34+
* @param injectee The path to the injected file
35+
*/
3336
const _injectFile = (entryProperty: EntryPropertyObject, injectionPoint: string, injectee: string): void => {
3437
// can be a string, array of strings, or object whose `import` property is one of those two
3538
let injectedInto = entryProperty[injectionPoint];
@@ -41,21 +44,19 @@ const _injectFile = (entryProperty: EntryPropertyObject, injectionPoint: string,
4144
return;
4245
}
4346

44-
// In case we inject our client config, we need to add it after the frontend code
45-
// otherwise the runtime config isn't loaded. See: https://github.com/getsentry/sentry-javascript/issues/3485
46-
const isClient = injectee === sentryClientConfig;
47-
47+
// We inject the user's client config file after the existing code so that the config file has access to
48+
// `publicRuntimeConfig`. See https://github.com/getsentry/sentry-javascript/issues/3485
4849
if (typeof injectedInto === 'string') {
49-
injectedInto = isClient ? [injectedInto, injectee] : [injectee, injectedInto];
50+
injectedInto = [injectedInto, injectee];
5051
} else if (Array.isArray(injectedInto)) {
51-
injectedInto = isClient ? [...injectedInto, injectee] : [injectee, ...injectedInto];
52+
injectedInto = [...injectedInto, injectee];
5253
} else {
53-
let importVal: string | string[] | EntryPointObject;
54+
let importVal: string | string[];
55+
5456
if (typeof injectedInto.import === 'string') {
55-
importVal = isClient ? [injectedInto.import, injectee] : [injectee, injectedInto.import];
57+
importVal = [injectedInto.import, injectee];
5658
} else {
57-
// If it's not a string, the inner value is an array
58-
importVal = isClient ? [...injectedInto.import, injectee] : [injectee, ...injectedInto.import];
59+
importVal = [...injectedInto.import, injectee];
5960
}
6061

6162
injectedInto = {
@@ -68,8 +69,6 @@ const _injectFile = (entryProperty: EntryPropertyObject, injectionPoint: string,
6869
};
6970

7071
const injectSentry = async (origEntryProperty: EntryProperty, isServer: boolean): Promise<EntryProperty> => {
71-
// Out of the box, nextjs uses the `() => Promise<EntryPropertyObject>)` flavor of EntryProperty, where the returned
72-
// object has string arrays for values.
7372
// The `entry` entry in a webpack config can be a string, array of strings, object, or function. By default, nextjs
7473
// sets it to an async function which returns the promise of an object of string arrays. Because we don't know whether
7574
// someone else has come along before us and changed that, we need to check a few things along the way. The one thing
@@ -80,19 +79,19 @@ const injectSentry = async (origEntryProperty: EntryProperty, isServer: boolean)
8079
newEntryProperty = await origEntryProperty();
8180
}
8281
newEntryProperty = newEntryProperty as EntryPropertyObject;
83-
// On the server, we need to inject the SDK into both into the base page (`_document`) and into individual API routes
84-
// (which have no common base).
82+
// Add a new element to the `entry` array, we force webpack to create a bundle out of the user's
83+
// `sentry.server.config.js` file and output it to `SERVER_INIT_LOCATION`. (See
84+
// https://webpack.js.org/guides/code-splitting/#entry-points.) We do this so that the user's config file is run
85+
// through babel (and any other processors through which next runs the rest of the user-provided code - pages, API
86+
// routes, etc.). Specifically, we need any ESM-style `import` code to get transpiled into ES5, so that we can call
87+
// `require()` on the resulting file when we're instrumenting the sesrver. (We can't use a dynamic import there
88+
// because that then forces the user into a particular TS config.)
8589
if (isServer) {
86-
Object.keys(newEntryProperty).forEach(key => {
87-
if (key === 'pages/_document' || key.includes('pages/api')) {
88-
// for some reason, because we're now in a function, we have to cast again
89-
_injectFile(newEntryProperty as EntryPropertyObject, key, sentryServerConfig);
90-
}
91-
});
90+
newEntryProperty[SERVER_INIT_OUTPUT_LOCATION] = SENTRY_SERVER_CONFIG_FILE;
9291
}
9392
// On the client, it's sufficient to inject it into the `main` JS code, which is included in every browser page.
9493
else {
95-
_injectFile(newEntryProperty, 'main', sentryClientConfig);
94+
_injectFile(newEntryProperty, 'main', SENTRY_CLIENT_CONFIG_FILE);
9695
}
9796
// TODO: hack made necessary because the async-ness of this function turns our object back into a promise, meaning the
9897
// internal `next` code which should do this doesn't
@@ -109,11 +108,18 @@ type NextConfigExports = {
109108
webpack?: WebpackExport;
110109
};
111110

111+
/**
112+
* Add Sentry options to the config to be exported from the user's `next.config.js` file.
113+
*
114+
* @param providedExports The existing config to be exported ,prior to adding Sentry
115+
* @param providedSentryWebpackPluginOptions Configuration for SentryWebpackPlugin
116+
* @returns The modified config to be exported
117+
*/
112118
export function withSentryConfig(
113119
providedExports: NextConfigExports = {},
114-
providedWebpackPluginOptions: Partial<SentryCliPluginOptions> = {},
120+
providedSentryWebpackPluginOptions: Partial<SentryCliPluginOptions> = {},
115121
): NextConfigExports {
116-
const defaultWebpackPluginOptions = {
122+
const defaultSentryWebpackPluginOptions = {
117123
url: process.env.SENTRY_URL,
118124
org: process.env.SENTRY_ORG,
119125
project: process.env.SENTRY_PROJECT,
@@ -122,17 +128,17 @@ export function withSentryConfig(
122128
stripPrefix: ['webpack://_N_E/'],
123129
urlPrefix: `~/_next`,
124130
include: '.next/',
125-
ignore: ['node_modules', 'webpack.config.js'],
131+
ignore: ['.next/cache', 'server/ssr-module-cache.js', 'static/*/_ssgManifest.js', 'static/*/_buildManifest.js'],
126132
};
127133

128134
// warn if any of the default options for the webpack plugin are getting overridden
129-
const webpackPluginOptionOverrides = Object.keys(defaultWebpackPluginOptions)
135+
const sentryWebpackPluginOptionOverrides = Object.keys(defaultSentryWebpackPluginOptions)
130136
.concat('dryrun')
131-
.filter(key => key in Object.keys(providedWebpackPluginOptions));
132-
if (webpackPluginOptionOverrides.length > 0) {
137+
.filter(key => key in Object.keys(providedSentryWebpackPluginOptions));
138+
if (sentryWebpackPluginOptionOverrides.length > 0) {
133139
logger.warn(
134140
'[Sentry] You are overriding the following automatically-set SentryWebpackPlugin config options:\n' +
135-
`\t${webpackPluginOptionOverrides.toString()},\n` +
141+
`\t${sentryWebpackPluginOptionOverrides.toString()},\n` +
136142
"which has the possibility of breaking source map upload and application. This is only a good idea if you know what you're doing.",
137143
);
138144
}
@@ -161,8 +167,8 @@ export function withSentryConfig(
161167
new ((SentryWebpackPlugin as unknown) as typeof defaultWebpackPlugin)({
162168
dryRun: options.dev,
163169
release: getSentryRelease(options.buildId),
164-
...defaultWebpackPluginOptions,
165-
...providedWebpackPluginOptions,
170+
...defaultSentryWebpackPluginOptions,
171+
...providedSentryWebpackPluginOptions,
166172
}),
167173
);
168174

packages/nextjs/src/utils/instrumentServer.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ type WrappedReqHandler = ReqHandler;
4545
// TODO is it necessary for this to be an object?
4646
const closure: PlainObject = {};
4747

48+
let sdkInitialized = false;
49+
4850
/**
4951
* Do the monkeypatching and wrapping necessary to catch errors in page routes. Along the way, as a bonus, grab (and
5052
* return) the path of the project root, for use in `RewriteFrames`.
@@ -58,6 +60,10 @@ export function instrumentServer(): string {
5860
// wrap this getter because it runs before the request handler runs, which gives us a chance to wrap the logger before
5961
// it's called for the first time
6062
fill(nextServerPrototype, 'getServerRequestHandler', makeWrappedHandlerGetter);
63+
if (!sdkInitialized) {
64+
require(path.join(process.cwd(), 'sentry.server.config.js'));
65+
sdkInitialized = true;
66+
}
6167

6268
return closure.projectRootDir;
6369
}

0 commit comments

Comments
 (0)