Skip to content

Commit a5131a4

Browse files
lobsterkatieHazAT
andauthored
feat(nextjs): Inject user sentry config files at app startup via webpack (#3463)
Co-authored-by: Daniel Griesser <daniel.griesser.86@gmail.com>
1 parent b805f9c commit a5131a4

File tree

5 files changed

+690
-381
lines changed

5 files changed

+690
-381
lines changed

packages/nextjs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@sentry/types": "6.3.1",
2929
"@types/webpack": "^5.28.0",
3030
"eslint": "7.20.0",
31+
"next": "^10.1.3",
3132
"rimraf": "3.0.2"
3233
},
3334
"scripts": {

packages/nextjs/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ export function init(options: NextjsOptions): void {
2727
}
2828

2929
export { withSentryConfig } from './utils/config';
30+
export { withSentry } from './utils/handlers';

packages/nextjs/src/utils/config.ts

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

6-
type WebpackConfig = { devtool: string; plugins: Array<{ [key: string]: any }> };
6+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7+
type PlainObject<T = any> = { [key: string]: T };
8+
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+
// proptery we're modifying is going to be known as an EntryProperty, or EP for short.
12+
13+
// The function which is ultimately going to be exported from `next.config.js` under the name `webpack`
14+
type WebpackExport = (config: WebpackConfig, options: WebpackOptions) => WebpackConfig;
15+
// type WebpackExport = (config: WebpackConfig, options: WebpackOptions) => Promise<WebpackConfig>;
16+
17+
// The two arguments passed to the exported `webpack` function, as well as the thing it returns
18+
type WebpackConfig = { devtool: string; plugins: PlainObject[]; entry: EntryProperty };
19+
type WebpackOptions = { dev: boolean; isServer: boolean };
20+
21+
// For our purposes, the value for `entry` is either an object, or a function which returns such an object
22+
type EntryProperty = (() => Promise<EntryPropertyObject>) | EntryPropertyObject;
23+
24+
// Each value in that object is either a string representing a single entry point, an array of such strings, or an
25+
// object containing either of those, along with other configuration options. In that third case, the entry point(s) are
26+
// listed under the key `import`.
27+
type EntryPropertyObject = PlainObject<string | Array<string> | EntryPointObject>;
28+
type EntryPointObject = { import: string | Array<string> };
29+
30+
// const injectSentry = async (origEntryProperty: EntryProperty, isServer: boolean): Promise<EntryPropertyObject> => {
31+
const injectSentry = async (origEntryProperty: EntryProperty, isServer: boolean): Promise<EntryProperty> => {
32+
// Out of the box, nextjs uses the `() => Promise<EntryPropertyObject>)` flavor of EntryProperty, where the returned
33+
// object has string arrays for values. But because we don't know whether someone else has come along before us and
34+
// changed that, we need to check a few things along the way.
35+
36+
// The `entry` entry in a webpack config can be a string, array of strings, object, or function. By default, nextjs
37+
// sets it to an async function which returns the promise of an object of string arrays. Because we don't know whether
38+
// someone else has come along before us and changed that, we need to check a few things along the way. The one thing
39+
// we know is that it won't have gotten *simpler* in form, so we only need to worry about the object and function
40+
// options. See https://webpack.js.org/configuration/entry-context/#entry.
41+
42+
let newEntryProperty = origEntryProperty;
43+
44+
if (typeof origEntryProperty === 'function') {
45+
newEntryProperty = await origEntryProperty();
46+
}
47+
48+
newEntryProperty = newEntryProperty as EntryPropertyObject;
49+
50+
// according to vercel, we only need to inject Sentry in one spot for server and one spot for client, and because
51+
// those are used as bases, it will apply everywhere
52+
const injectionPoint = isServer ? 'pages/_document' : 'main';
53+
const injectee = isServer ? './sentry.server.config.js' : './sentry.client.config.js';
54+
55+
// can be a string, array of strings, or object whose `import` property is one of those two
56+
let injectedInto = newEntryProperty[injectionPoint];
57+
58+
// whatever the format, add in the sentry file
59+
injectedInto =
60+
typeof injectedInto === 'string'
61+
? // string case
62+
[injectee, injectedInto]
63+
: // not a string, must be an array or object
64+
Array.isArray(injectedInto)
65+
? // array case
66+
[injectee, ...injectedInto]
67+
: // object case
68+
{
69+
...injectedInto,
70+
import:
71+
typeof injectedInto.import === 'string'
72+
? // string case for inner property
73+
[injectee, injectedInto.import]
74+
: // array case for inner property
75+
[injectee, ...injectedInto.import],
76+
};
77+
78+
newEntryProperty[injectionPoint] = injectedInto;
79+
80+
// TODO: hack made necessary because promises are currently kicking my butt
81+
if ('main.js' in newEntryProperty) {
82+
delete newEntryProperty['main.js'];
83+
}
84+
85+
return newEntryProperty;
86+
};
87+
788
type NextConfigExports = {
889
experimental?: { plugins: boolean };
990
plugins?: string[];
1091
productionBrowserSourceMaps?: boolean;
11-
webpack?: (config: WebpackConfig, { dev }: { dev: boolean }) => WebpackConfig;
92+
webpack?: WebpackExport;
1293
};
1394

1495
export function withSentryConfig(
@@ -27,6 +108,8 @@ export function withSentryConfig(
27108
include: '.next/',
28109
ignore: ['node_modules', 'webpack.config.js'],
29110
};
111+
112+
// warn if any of the default options for the webpack plugin are getting overridden
30113
const webpackPluginOptionOverrides = Object.keys(defaultWebpackPluginOptions)
31114
.concat('dryrun')
32115
.map(key => key in Object.keys(providedWebpackPluginOptions));
@@ -38,32 +121,44 @@ export function withSentryConfig(
38121
);
39122
}
40123

124+
// const newWebpackExport = async (config: WebpackConfig, options: WebpackOptions): Promise<WebpackConfig> => {
125+
const newWebpackExport = (config: WebpackConfig, options: WebpackOptions): WebpackConfig => {
126+
let newConfig = config;
127+
128+
if (typeof providedExports.webpack === 'function') {
129+
newConfig = providedExports.webpack(config, options);
130+
// newConfig = await providedExports.webpack(config, options);
131+
}
132+
133+
// Ensure quality source maps in production. (Source maps aren't uploaded in dev, and besides, Next doesn't let you
134+
// change this is dev even if you want to - see
135+
// https://github.com/vercel/next.js/blob/master/errors/improper-devtool.md.)
136+
if (!options.dev) {
137+
newConfig.devtool = 'source-map';
138+
}
139+
140+
// Inject user config files (`sentry.client.confg.js` and `sentry.server.config.js`), which is where `Sentry.init()`
141+
// is called. By adding them here, we ensure that they're bundled by webpack as part of both server code and client code.
142+
newConfig.entry = (injectSentry(newConfig.entry, options.isServer) as unknown) as EntryProperty;
143+
// newConfig.entry = await injectSentry(newConfig.entry, options.isServer);
144+
// newConfig.entry = async () => injectSentry(newConfig.entry, options.isServer);
145+
146+
// Add the Sentry plugin, which uploads source maps to Sentry when not in dev
147+
newConfig.plugins.push(
148+
// TODO it's not clear how to do this better, but there *must* be a better way
149+
new ((SentryWebpackPlugin as unknown) as typeof defaultWebpackPlugin)({
150+
dryRun: options.dev,
151+
...defaultWebpackPluginOptions,
152+
...providedWebpackPluginOptions,
153+
}),
154+
);
155+
156+
return newConfig;
157+
};
158+
41159
return {
42160
...providedExports,
43161
productionBrowserSourceMaps: true,
44-
webpack: (originalConfig, options) => {
45-
let config = originalConfig;
46-
47-
if (typeof providedExports.webpack === 'function') {
48-
config = providedExports.webpack(originalConfig, options);
49-
}
50-
51-
if (!options.dev) {
52-
// Ensure quality source maps in production. (Source maps aren't uploaded in dev, and besides, Next doesn't let
53-
// you change this is dev even if you want to - see
54-
// https://github.com/vercel/next.js/blob/master/errors/improper-devtool.md.)
55-
config.devtool = 'source-map';
56-
}
57-
config.plugins.push(
58-
// TODO it's not clear how to do this better, but there *must* be a better way
59-
new ((SentryWebpackPlugin as unknown) as typeof defaultWebpackPlugin)({
60-
dryRun: options.dev,
61-
...defaultWebpackPluginOptions,
62-
...providedWebpackPluginOptions,
63-
}),
64-
);
65-
66-
return config;
67-
},
162+
webpack: newWebpackExport,
68163
};
69164
}

packages/nextjs/src/utils/handlers.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { captureException, flush } from '@sentry/node';
2+
import { NextApiRequest, NextApiResponse } from 'next';
3+
4+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
5+
export const withSentry = (handler: (req: NextApiRequest, res: NextApiResponse) => Promise<void>) => {
6+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
7+
return async (req: NextApiRequest, res: NextApiResponse) => {
8+
try {
9+
// TODO: Start Transaction
10+
// TODO: Extract data from req
11+
return await handler(req, res); // Call Handler
12+
// TODO: Finish Transaction
13+
} catch (e) {
14+
captureException(e);
15+
await flush(2000);
16+
throw e;
17+
}
18+
};
19+
};

0 commit comments

Comments
 (0)