From 199b9888691973eb05257168449ea76a0113bf1a Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Wed, 5 May 2021 09:12:38 -0700 Subject: [PATCH 1/2] add babel packages --- packages/nextjs/package.json | 3 ++ yarn.lock | 102 ++++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index f18c44c4c620..8c17ce1e047f 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -17,12 +17,15 @@ "access": "public" }, "dependencies": { + "@babel/core": "7.x", + "@babel/plugin-transform-modules-commonjs": "^7.13.8", "@sentry/core": "6.3.5", "@sentry/integrations": "6.3.5", "@sentry/node": "6.3.5", "@sentry/react": "6.3.5", "@sentry/utils": "6.3.5", "@sentry/webpack-plugin": "1.15.0", + "babel-loader": "^8.2.2", "tslib": "^1.9.3" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index be6f65b33f87..094eea25e2eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -42,6 +42,32 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.12.tgz#a8a5ccac19c200f9dd49624cac6e19d7be1236a1" integrity sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ== +"@babel/compat-data@^7.13.15": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.0.tgz#a901128bce2ad02565df95e6ecbf195cf9465919" + integrity sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q== + +"@babel/core@7.x": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.0.tgz#47299ff3ec8d111b493f1a9d04bf88c04e728d88" + integrity sha512-8YqpRig5NmIHlMLw09zMlPTvUVMILjqCOtVgu+TVNWEBvy9b5I3RRyhqnrV4hjgEK7n8P9OqvkWJAFmEL6Wwfw== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/generator" "^7.14.0" + "@babel/helper-compilation-targets" "^7.13.16" + "@babel/helper-module-transforms" "^7.14.0" + "@babel/helpers" "^7.14.0" + "@babel/parser" "^7.14.0" + "@babel/template" "^7.12.13" + "@babel/traverse" "^7.14.0" + "@babel/types" "^7.14.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + source-map "^0.5.0" + "@babel/core@^7.1.0", "@babel/core@^7.1.6", "@babel/core@^7.12.0", "@babel/core@^7.12.10", "@babel/core@^7.12.3", "@babel/core@^7.3.4": version "7.13.14" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.14.tgz#8e46ebbaca460a63497c797e574038ab04ae6d06" @@ -72,6 +98,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.14.0": + version "7.14.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.1.tgz#1f99331babd65700183628da186f36f63d615c93" + integrity sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ== + dependencies: + "@babel/types" "^7.14.1" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab" @@ -97,6 +132,16 @@ browserslist "^4.14.5" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.13.16": + version "7.13.16" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.16.tgz#6e91dccf15e3f43e5556dffe32d860109887563c" + integrity sha512-3gmkYIrpqsLlieFwjkGgLaSHmhnvlAYzZLlYVjlW+QwI+1zE17kGxuJGmIqDQdYp56XdmGeD+Bswx0UTyG18xA== + dependencies: + "@babel/compat-data" "^7.13.15" + "@babel/helper-validator-option" "^7.12.17" + browserslist "^4.14.5" + semver "^6.3.0" + "@babel/helper-create-class-features-plugin@^7.13.0", "@babel/helper-create-class-features-plugin@^7.5.5", "@babel/helper-create-class-features-plugin@^7.8.3": version "7.13.11" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz#30d30a005bca2c953f5653fc25091a492177f4f6" @@ -189,6 +234,20 @@ "@babel/traverse" "^7.13.13" "@babel/types" "^7.13.14" +"@babel/helper-module-transforms@^7.14.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz#8fcf78be220156f22633ee204ea81f73f826a8ad" + integrity sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw== + dependencies: + "@babel/helper-module-imports" "^7.13.12" + "@babel/helper-replace-supers" "^7.13.12" + "@babel/helper-simple-access" "^7.13.12" + "@babel/helper-split-export-declaration" "^7.12.13" + "@babel/helper-validator-identifier" "^7.14.0" + "@babel/template" "^7.12.13" + "@babel/traverse" "^7.14.0" + "@babel/types" "^7.14.0" + "@babel/helper-optimise-call-expression@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz#5c02d171b4c8615b1e7163f888c1c81c30a2aaea" @@ -246,6 +305,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== +"@babel/helper-validator-identifier@^7.14.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288" + integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A== + "@babel/helper-validator-option@^7.12.17": version "7.12.17" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" @@ -270,6 +334,15 @@ "@babel/traverse" "^7.13.0" "@babel/types" "^7.13.0" +"@babel/helpers@^7.14.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.0.tgz#ea9b6be9478a13d6f961dbb5f36bf75e2f3b8f62" + integrity sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg== + dependencies: + "@babel/template" "^7.12.13" + "@babel/traverse" "^7.14.0" + "@babel/types" "^7.14.0" + "@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": version "7.13.10" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" @@ -284,6 +357,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.13.tgz#42f03862f4aed50461e543270916b47dd501f0df" integrity sha512-OhsyMrqygfk5v8HmWwOzlYjJrtLaFhF34MrfG/Z73DgYCI6ojNUTUp2TYbtnjo8PegeJp12eamsNettCQjKjVw== +"@babel/parser@^7.14.0": + version "7.14.1" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.1.tgz#1bd644b5db3f5797c4479d89ec1817fe02b84c47" + integrity sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q== + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.13.12": version "7.13.12" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.13.12.tgz#a3484d84d0b549f3fc916b99ee4783f26fabad2a" @@ -957,6 +1035,20 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.14.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.0.tgz#cea0dc8ae7e2b1dec65f512f39f3483e8cc95aef" + integrity sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/generator" "^7.14.0" + "@babel/helper-function-name" "^7.12.13" + "@babel/helper-split-export-declaration" "^7.12.13" + "@babel/parser" "^7.14.0" + "@babel/types" "^7.14.0" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c" @@ -975,6 +1067,14 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.14.0", "@babel/types@^7.14.1": + version "7.14.1" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.1.tgz#095bd12f1c08ab63eff6e8f7745fa7c9cc15a9db" + integrity sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA== + dependencies: + "@babel/helper-validator-identifier" "^7.14.0" + to-fast-properties "^2.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -4600,7 +4700,7 @@ babel-jest@^24.9.0: chalk "^2.4.2" slash "^2.0.0" -babel-loader@^8.0.6: +babel-loader@^8.0.6, babel-loader@^8.2.2: version "8.2.2" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.2.tgz#9363ce84c10c9a40e6c753748e1441b60c8a0b81" integrity sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g== From de5b4d82eae39c8e3b174a65b7532c798804b874 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Wed, 5 May 2021 09:04:42 -0700 Subject: [PATCH 2/2] wrap API routes automatically using webpack --- .../nextjs/src/utils/api-wrapping-loader.ts | 104 ++++++++++++++++++ packages/nextjs/src/utils/config.ts | 48 +++++++- 2 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 packages/nextjs/src/utils/api-wrapping-loader.ts diff --git a/packages/nextjs/src/utils/api-wrapping-loader.ts b/packages/nextjs/src/utils/api-wrapping-loader.ts new file mode 100644 index 000000000000..6f717f1edb3c --- /dev/null +++ b/packages/nextjs/src/utils/api-wrapping-loader.ts @@ -0,0 +1,104 @@ +// there's apparently no way to get at this with `import` +// TODO - really? +// eslint-disable-next-line @typescript-eslint/no-var-requires +const Module = require('module'); + +import { NextApiHandler } from 'next'; + +import * as Sentry from '../index.server'; + +type ModuleObject = { + _compile: (code: string, filename: string) => void; + exports: { default: unknown }; +}; + +type LoaderContext = { resource: string; loaders: Loader[] }; +type Loader = { options: LoaderOptions; path: string }; +type LoaderOptions = { sdkPath: string }; + +type WrappedNextApiHandler = NextApiHandler; // purely for ease of reading + +/** + * Replace the API route handler in the given code with a wrapped version. + * + * @param this Context data passed to the loader + * @param rawInput The stringified code we're modifying + * @returns Modified stringified code + */ +export default function load(this: LoaderContext, rawInput: string): string { + const options = getOptions(this.loaders) as LoaderOptions; + + // Wherever this is running, it can't seem to resolve the Sentry SDK when referred to by + // name (which it will have to do below when it compiles the stringified code into an actual module). Fortunately, + // we're able to do so from within our config file, so we just pass the absolute path through in `options` + const origCode = rawInput.replace('@sentry/nextjs', options.sdkPath); + + // `module.parent` comes back as `null` rather than `undefined` when there is no parent, but the `Module` constructor + // below needs `undefined` instead. Because reasons. (We need to ignore the deprecation warning because `parent` may + // be deprecated, but the `Module` constructor still uses it as of 16.1.0. See + // https://github.com/nodejs/node/blob/26e318a321a872bc0f41e60706bb49381684afb2/lib/internal/modules/cjs/loader.js#L168.) + // eslint-disable-next-line deprecation/deprecation + const parent = module.parent || undefined; + // It's unclear what this does for us, if anything + const filename = 'lookIMadeAModule'; + + // Compile the stringified code into an actual Module object so we can grab its default export (the route handler) for + // wrapping + const routeModule = new Module(filename, parent) as ModuleObject; + routeModule._compile(origCode, filename); + const origHandler = routeModule.exports.default; + + if (typeof origHandler !== 'function') { + // eslint-disable-next-line no-console + console.warn(`[Sentry] Could not wrap ${this.resource} for error handling. Default export is not a function.`); + return rawInput; + } + + // Wrap the route handler in a try/catch to catch any errors which it generates + const newHandler = makeWrappedRequestHandler(origHandler as NextApiHandler); + + // Ultimately we have to return a string, and we need the wrapped handler to take the place of the original one (as + // the default export) so literally substitute it in + let newCode = origCode.replace(origHandler.toString(), newHandler.toString()); + + // The new function we just subbed in is, character for character, the code written below as the return value of + // `makeWrappedRequestHandler`, which means we have to define `origHandler`, since its code has now been replaced + newCode = `${newCode}\n\nconst origHandler = ${origHandler.toString()}`; + + return newCode; +} + +/** Extract the options for this loader out of the array of loaders in scope */ +function getOptions(loaders: Loader[]): LoaderOptions | undefined { + for (const loader of loaders) { + if (loader.path.includes('nextjs/dist/utils/api-wrapping-loader')) { + return loader.options; + } + } + // we shouldn't ever get here - one of the given loaders should be this loader + return undefined; +} + +/** Wrap the given request handler for error-catching purposes */ +function makeWrappedRequestHandler(origHandler: NextApiHandler): WrappedNextApiHandler { + // TODO are there any overloads we need to worry about? + return async (req, res) => { + try { + return await origHandler(req, res); + } catch (err) { + if (Sentry !== undefined) { + Sentry.withScope(scope => { + scope.addEventProcessor(event => Sentry.Handlers.parseRequest(event, req)); + Sentry.captureException(err); + }); + Sentry.flush(2000).catch(() => { + // Never throws + }); + } else { + // eslint-disable-next-line no-console + console.warn('[Sentry] SDK is disabled. Please make sure to initialize Sentry in `sentry.server.config.js`.'); + } + throw err; + } + }; +} diff --git a/packages/nextjs/src/utils/config.ts b/packages/nextjs/src/utils/config.ts index a9c0e6ae3bc4..b9e1172acadf 100644 --- a/packages/nextjs/src/utils/config.ts +++ b/packages/nextjs/src/utils/config.ts @@ -2,6 +2,7 @@ import { getSentryRelease } from '@sentry/node'; import { logger } from '@sentry/utils'; import defaultWebpackPlugin, { SentryCliPluginOptions } from '@sentry/webpack-plugin'; import * as SentryWebpackPlugin from '@sentry/webpack-plugin'; +import * as path from 'path'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type PlainObject = { [key: string]: T }; @@ -14,7 +15,9 @@ type PlainObject = { [key: string]: T }; type WebpackExport = (config: WebpackConfig, options: WebpackOptions) => WebpackConfig; // The two arguments passed to the exported `webpack` function, as well as the thing it returns -type WebpackConfig = { devtool: string; plugins: PlainObject[]; entry: EntryProperty }; +// TODO use real webpack types? +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type WebpackConfig = { devtool: string; plugins: PlainObject[]; entry: EntryProperty; module: { rules: any[] } }; type WebpackOptions = { dev: boolean; isServer: boolean; buildId: string }; // For our purposes, the value for `entry` is either an object, or a function which returns such an object @@ -41,8 +44,9 @@ const _injectFile = (entryProperty: EntryPropertyObject, injectionPoint: string, return; } - // In case we inject our client config, we need to add it after the frontend code - // otherwise the runtime config isn't loaded. See: https://github.com/getsentry/sentry-javascript/issues/3485 + // We need to inject the user-provided config file after the frontend code so that it has access to the runtime config + // (which won't have loaded yet otherwise). See: https://github.com/getsentry/sentry-javascript/issues/3485. On the + // server side, we inject beforehand so that we can catch any errors in the code that follows. const isClient = injectee === sentryClientConfig; if (typeof injectedInto === 'string') { @@ -68,8 +72,6 @@ const _injectFile = (entryProperty: EntryPropertyObject, injectionPoint: string, }; const injectSentry = async (origEntryProperty: EntryProperty, isServer: boolean): Promise => { - // Out of the box, nextjs uses the `() => Promise)` flavor of EntryProperty, where the returned - // object has string arrays for values. // The `entry` entry in a webpack config can be a string, array of strings, object, or function. By default, nextjs // sets it to an async function which returns the promise of an object of string arrays. Because we don't know whether // someone else has come along before us and changed that, we need to check a few things along the way. The one thing @@ -151,6 +153,42 @@ export function withSentryConfig( newConfig.devtool = 'source-map'; } + // Wherever the webpack process ultimately runs, it's not somewhere where modules resolve successfully, so get the + // absolute paths here, where resolution does work, and pass those into the config instead of the module names + const sdkResolvedMain = require.resolve('@sentry/nextjs'); + const loaderPath = path.join(path.dirname(sdkResolvedMain), 'utils/api-wrapping-loader.js'); + const babelLoaderResolvedMain = require.resolve('babel-loader'); + const babelPluginResolvedMain = require.resolve('@babel/plugin-transform-modules-commonjs'); + + newConfig.module = { + ...newConfig.module, + rules: [ + ...newConfig.module.rules, + { + test: /pages\/api\/.*/, + use: [ + { + // `sdkResolvedMain` is the path to `dist/index.server.js` + loader: loaderPath, + options: { + // pass the path into the loader so it can be used there as well (it's another place where modules don't + // resolve well) + sdkPath: sdkResolvedMain, + }, + }, + { + loader: babelLoaderResolvedMain, + options: { + // this is here to turn any `import`s into `requires`, so that our loader can load the string as a + // module (loaders apply top to bottom, so this happens before ours) + plugins: [babelPluginResolvedMain], + }, + }, + ], + }, + ], + }; + // Inject user config files (`sentry.client.confg.js` and `sentry.server.config.js`), which is where `Sentry.init()` // is called. By adding them here, we ensure that they're bundled by webpack as part of both server code and client code. newConfig.entry = (injectSentry(newConfig.entry, options.isServer) as unknown) as EntryProperty;