Skip to content

Commit 3ab107b

Browse files
authored
fix(nextjs): Add missing changes from #3462 (#3476)
1 parent b7382a2 commit 3ab107b

File tree

4 files changed

+100
-10
lines changed

4 files changed

+100
-10
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
"packages/integrations",
3131
"packages/minimal",
3232
"packages/nextjs",
33-
"packages/next-plugin-sentry",
3433
"packages/node",
3534
"packages/react",
3635
"packages/serverless",

packages/nextjs/src/index.server.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { configureScope, init as nodeInit } from '@sentry/node';
22

3+
import { instrumentServer } from './utils/instrumentServer';
34
import { MetadataBuilder } from './utils/metadataBuilder';
45
import { NextjsOptions } from './utils/nextjsOptions';
56
import { defaultRewriteFrames, getFinalServerIntegrations } from './utils/serverIntegrations';
@@ -28,3 +29,6 @@ export function init(options: NextjsOptions): void {
2829

2930
export { withSentryConfig } from './utils/config';
3031
export { withSentry } from './utils/handlers';
32+
33+
// TODO capture project root (which this returns) for RewriteFrames?
34+
instrumentServer();

packages/nextjs/src/utils/config.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ type PlainObject<T = any> = { [key: string]: T };
88

99
// Man are these types hard to name well. "Entry" = an item in some collection of items, but in our case, one of the
1010
// 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.
11+
// property we're modifying is going to be known as an EntryProperty.
1212

1313
// The function which is ultimately going to be exported from `next.config.js` under the name `webpack`
1414
type WebpackExport = (config: WebpackConfig, options: WebpackOptions) => WebpackConfig;
15-
// type WebpackExport = (config: WebpackConfig, options: WebpackOptions) => Promise<WebpackConfig>;
1615

1716
// The two arguments passed to the exported `webpack` function, as well as the thing it returns
1817
type WebpackConfig = { devtool: string; plugins: PlainObject[]; entry: EntryProperty };
18+
// TODO use real webpack types
1919
type WebpackOptions = { dev: boolean; isServer: boolean };
2020

2121
// For our purposes, the value for `entry` is either an object, or a function which returns such an object
@@ -27,7 +27,6 @@ type EntryProperty = (() => Promise<EntryPropertyObject>) | EntryPropertyObject;
2727
type EntryPropertyObject = PlainObject<string | Array<string> | EntryPointObject>;
2828
type EntryPointObject = { import: string | Array<string> };
2929

30-
// const injectSentry = async (origEntryProperty: EntryProperty, isServer: boolean): Promise<EntryPropertyObject> => {
3130
const injectSentry = async (origEntryProperty: EntryProperty, isServer: boolean): Promise<EntryProperty> => {
3231
// Out of the box, nextjs uses the `() => Promise<EntryPropertyObject>)` flavor of EntryProperty, where the returned
3332
// object has string arrays for values. But because we don't know whether someone else has come along before us and
@@ -77,7 +76,8 @@ const injectSentry = async (origEntryProperty: EntryProperty, isServer: boolean)
7776

7877
newEntryProperty[injectionPoint] = injectedInto;
7978

80-
// TODO: hack made necessary because promises are currently kicking my butt
79+
// TODO: hack made necessary because the async-ness of this function turns our object back into a promise, meaning the
80+
// internal `next` code which should do this doesn't
8181
if ('main.js' in newEntryProperty) {
8282
delete newEntryProperty['main.js'];
8383
}
@@ -115,19 +115,17 @@ export function withSentryConfig(
115115
.map(key => key in Object.keys(providedWebpackPluginOptions));
116116
if (webpackPluginOptionOverrides.length > 0) {
117117
logger.warn(
118-
'[next-plugin-sentry] You are overriding the following automatically-set SentryWebpackPlugin config options:\n' +
118+
'[Sentry] You are overriding the following automatically-set SentryWebpackPlugin config options:\n' +
119119
`\t${webpackPluginOptionOverrides.toString()},\n` +
120120
"which has the possibility of breaking source map upload and application. This is only a good idea if you know what you're doing.",
121121
);
122122
}
123123

124-
// const newWebpackExport = async (config: WebpackConfig, options: WebpackOptions): Promise<WebpackConfig> => {
125124
const newWebpackExport = (config: WebpackConfig, options: WebpackOptions): WebpackConfig => {
126125
let newConfig = config;
127126

128127
if (typeof providedExports.webpack === 'function') {
129128
newConfig = providedExports.webpack(config, options);
130-
// newConfig = await providedExports.webpack(config, options);
131129
}
132130

133131
// Ensure quality source maps in production. (Source maps aren't uploaded in dev, and besides, Next doesn't let you
@@ -140,8 +138,6 @@ export function withSentryConfig(
140138
// Inject user config files (`sentry.client.confg.js` and `sentry.server.config.js`), which is where `Sentry.init()`
141139
// is called. By adding them here, we ensure that they're bundled by webpack as part of both server code and client code.
142140
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);
145141

146142
// Add the Sentry plugin, which uploads source maps to Sentry when not in dev
147143
newConfig.plugins.push(
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { fill } from '@sentry/utils';
2+
import * as http from 'http';
3+
import { default as createNextServer } from 'next';
4+
import * as url from 'url';
5+
6+
import * as Sentry from '../index.server';
7+
8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+
type PlainObject<T = any> = { [key: string]: T };
10+
11+
interface NextServer {
12+
server: Server;
13+
}
14+
15+
interface Server {
16+
dir: string;
17+
}
18+
19+
type HandlerGetter = () => Promise<ReqHandler>;
20+
type ReqHandler = (
21+
req: http.IncomingMessage,
22+
res: http.ServerResponse,
23+
parsedUrl?: url.UrlWithParsedQuery,
24+
) => Promise<void>;
25+
type ErrorLogger = (err: Error) => void;
26+
27+
// these aliases are purely to make the function signatures more easily understandable
28+
type WrappedHandlerGetter = HandlerGetter;
29+
type WrappedErrorLogger = ErrorLogger;
30+
31+
// TODO is it necessary for this to be an object?
32+
const closure: PlainObject = {};
33+
34+
/**
35+
* Do the monkeypatching and wrapping necessary to catch errors in page routes. Along the way, as a bonus, grab (and
36+
* return) the path of the project root, for use in `RewriteFrames`.
37+
*
38+
* @returns The absolute path of the project root directory
39+
*
40+
*/
41+
export function instrumentServer(): string {
42+
const nextServerPrototype = Object.getPrototypeOf(createNextServer({}));
43+
44+
// wrap this getter because it runs before the request handler runs, which gives us a chance to wrap the logger before
45+
// it's called for the first time
46+
fill(nextServerPrototype, 'getServerRequestHandler', makeWrappedHandlerGetter);
47+
48+
return closure.projectRootDir;
49+
}
50+
51+
/**
52+
* Create a wrapped version of Nextjs's `NextServer.getServerRequestHandler` method, as a way to access the running
53+
* `Server` instance and monkeypatch its prototype.
54+
*
55+
* @param origHandlerGetter Nextjs's `NextServer.getServerRequestHandler` method
56+
* @returns A wrapped version of the same method, to monkeypatch in at server startup
57+
*/
58+
function makeWrappedHandlerGetter(origHandlerGetter: HandlerGetter): WrappedHandlerGetter {
59+
// We wrap this purely in order to be able to grab data and do further monkeypatching the first time it runs.
60+
// Otherwise, it's just a pass-through to the original method.
61+
const wrappedHandlerGetter = async function(this: NextServer): Promise<ReqHandler> {
62+
if (!closure.wrappingComplete) {
63+
closure.projectRootDir = this.server.dir;
64+
65+
const serverPrototype = Object.getPrototypeOf(this.server);
66+
67+
// wrap the logger so we can capture errors in page-level functions like `getServerSideProps`
68+
fill(serverPrototype, 'logError', makeWrappedErrorLogger);
69+
70+
closure.wrappingComplete = true;
71+
}
72+
73+
return origHandlerGetter.call(this);
74+
};
75+
76+
return wrappedHandlerGetter;
77+
}
78+
79+
/**
80+
* Wrap the error logger used by the server to capture exceptions which arise from functions like `getServerSideProps`.
81+
*
82+
* @param origErrorLogger The original logger from the `Server` class
83+
* @returns A wrapped version of that logger
84+
*/
85+
function makeWrappedErrorLogger(origErrorLogger: ErrorLogger): WrappedErrorLogger {
86+
return (err: Error): void => {
87+
// TODO add context data here
88+
Sentry.captureException(err);
89+
return origErrorLogger(err);
90+
};
91+
}

0 commit comments

Comments
 (0)