diff --git a/packages/nextjs/rollup.npm.config.js b/packages/nextjs/rollup.npm.config.js index db1fa9c1fde1..5ef6e1ac96a6 100644 --- a/packages/nextjs/rollup.npm.config.js +++ b/packages/nextjs/rollup.npm.config.js @@ -14,7 +14,11 @@ export default [ ), ...makeNPMConfigVariants( makeBaseNPMConfig({ - entrypoints: ['src/config/templates/pageWrapperTemplate.ts', 'src/config/templates/apiWrapperTemplate.ts'], + entrypoints: [ + 'src/config/templates/pageWrapperTemplate.ts', + 'src/config/templates/apiWrapperTemplate.ts', + 'src/config/templates/middlewareWrapperTemplate.ts', + ], packageSpecificConfig: { output: { @@ -29,7 +33,7 @@ export default [ // make it so Rollup calms down about the fact that we're combining default and named exports exports: 'named', }, - external: ['@sentry/nextjs', '__SENTRY_WRAPPING_TARGET__'], + external: ['@sentry/nextjs', '__SENTRY_WRAPPING_TARGET_FILE__'], }, }), ), diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts index f092216bbf1d..8178ace4c096 100644 --- a/packages/nextjs/src/config/loaders/wrappingLoader.ts +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -12,17 +12,19 @@ const apiWrapperTemplateCode = fs.readFileSync(apiWrapperTemplatePath, { encodin const pageWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'pageWrapperTemplate.js'); const pageWrapperTemplateCode = fs.readFileSync(pageWrapperTemplatePath, { encoding: 'utf8' }); +const middlewareWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'middlewareWrapperTemplate.js'); +const middlewareWrapperTemplateCode = fs.readFileSync(middlewareWrapperTemplatePath, { encoding: 'utf8' }); + // Just a simple placeholder to make referencing module consistent const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module'; // Needs to end in .cjs in order for the `commonjs` plugin to pick it up -const WRAPPING_TARGET_MODULE_NAME = '__SENTRY_WRAPPING_TARGET__.cjs'; +const WRAPPING_TARGET_MODULE_NAME = '__SENTRY_WRAPPING_TARGET_FILE__.cjs'; type LoaderOptions = { pagesDir: string; pageExtensionRegex: string; excludeServerRoutes: Array; - isEdgeRuntime: boolean; }; /** @@ -40,14 +42,8 @@ export default function wrappingLoader( pagesDir, pageExtensionRegex, excludeServerRoutes = [], - isEdgeRuntime, } = 'getOptions' in this ? this.getOptions() : this.query; - // We currently don't support the edge runtime - if (isEdgeRuntime) { - return userCode; - } - this.async(); // Get the parameterized route name from this page's filepath @@ -71,13 +67,23 @@ export default function wrappingLoader( return; } - let templateCode = parameterizedRoute.startsWith('/api') ? apiWrapperTemplateCode : pageWrapperTemplateCode; + const middlewareJsPath = path.join(pagesDir, '..', 'middleware.js'); + const middlewareTsPath = path.join(pagesDir, '..', 'middleware.js'); + + let templateCode: string; + if (parameterizedRoute.startsWith('/api')) { + templateCode = apiWrapperTemplateCode; + } else if (this.resourcePath === middlewareJsPath || this.resourcePath === middlewareTsPath) { + templateCode = middlewareWrapperTemplateCode; + } else { + templateCode = pageWrapperTemplateCode; + } // Inject the route and the path to the file we're wrapping into the template templateCode = templateCode.replace(/__ROUTE__/g, parameterizedRoute.replace(/\\/g, '\\\\')); // Replace the import path of the wrapping target in the template with a path that the `wrapUserCode` function will understand. - templateCode = templateCode.replace(/__SENTRY_WRAPPING_TARGET__/g, WRAPPING_TARGET_MODULE_NAME); + templateCode = templateCode.replace(/__SENTRY_WRAPPING_TARGET_FILE__/g, WRAPPING_TARGET_MODULE_NAME); // Run the proxy module code through Rollup, in order to split the `export * from ''` out into // individual exports (which nextjs seems to require). diff --git a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts index 2f8dd2184301..969d433f126f 100644 --- a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts @@ -1,14 +1,14 @@ -/** +/* * This file is a template for the code which will be substituted when our webpack loader handles API files in the * `pages/` directory. * - * We use `__RESOURCE_PATH__` as a placeholder for the path to the file being wrapped. Because it's not a real package, + * We use `__SENTRY_WRAPPING_TARGET_FILE__` as a placeholder for the path to the file being wrapped. Because it's not a real package, * this causes both TS and ESLint to complain, hence the pragma comments below. */ // @ts-ignore See above // eslint-disable-next-line import/no-unresolved -import * as origModule from '__SENTRY_WRAPPING_TARGET__'; +import * as origModule from '__SENTRY_WRAPPING_TARGET_FILE__'; // eslint-disable-next-line import/no-extraneous-dependencies import * as Sentry from '@sentry/nextjs'; import type { PageConfig } from 'next'; @@ -60,4 +60,4 @@ export default userProvidedHandler ? Sentry.withSentryAPI(userProvidedHandler, ' // not include anything whose name matchs something we've explicitly exported above. // @ts-ignore See above // eslint-disable-next-line import/no-unresolved -export * from '__SENTRY_WRAPPING_TARGET__'; +export * from '__SENTRY_WRAPPING_TARGET_FILE__'; diff --git a/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts b/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts new file mode 100644 index 000000000000..373f63646933 --- /dev/null +++ b/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts @@ -0,0 +1,50 @@ +/* + * This file is a template for the code which will be substituted when our webpack loader handles middleware files. + * + * We use `__SENTRY_WRAPPING_TARGET_FILE__` as a placeholder for the path to the file being wrapped. Because it's not a real package, + * this causes both TS and ESLint to complain, hence the pragma comments below. + */ + +// @ts-ignore See above +// eslint-disable-next-line import/no-unresolved +import * as origModule from '__SENTRY_WRAPPING_TARGET_FILE__'; +// eslint-disable-next-line import/no-extraneous-dependencies +import * as Sentry from '@sentry/nextjs'; + +import type { EdgeRouteHandler } from '../../edge/types'; + +type NextApiModule = + | { + // ESM export + default?: EdgeRouteHandler; + middleware?: EdgeRouteHandler; + } + // CJS export + | EdgeRouteHandler; + +const userApiModule = origModule as NextApiModule; + +// Default to undefined. It's possible for Next.js users to not define any exports/handlers in an API route. If that is +// the case Next.js wil crash during runtime but the Sentry SDK should definitely not crash so we need tohandle it. +let userProvidedNamedHandler: EdgeRouteHandler | undefined = undefined; +let userProvidedDefaultHandler: EdgeRouteHandler | undefined = undefined; + +if ('middleware' in userApiModule && typeof userApiModule.middleware === 'function') { + // Handle when user defines via named ESM export: `export { middleware };` + userProvidedNamedHandler = userApiModule.middleware; +} else if ('default' in userApiModule && typeof userApiModule.default === 'function') { + // Handle when user defines via ESM export: `export default myFunction;` + userProvidedDefaultHandler = userApiModule.default; +} else if (typeof userApiModule === 'function') { + // Handle when user defines via CJS export: "module.exports = myFunction;" + userProvidedDefaultHandler = userApiModule; +} + +export const middleware = userProvidedNamedHandler ? Sentry.withSentryMiddleware(userProvidedNamedHandler) : undefined; +export default userProvidedDefaultHandler ? Sentry.withSentryMiddleware(userProvidedDefaultHandler) : undefined; + +// Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to +// not include anything whose name matchs something we've explicitly exported above. +// @ts-ignore See above +// eslint-disable-next-line import/no-unresolved +export * from '__SENTRY_WRAPPING_TARGET_FILE__'; diff --git a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts index e3b6b4e7e296..955e920552d1 100644 --- a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts @@ -1,14 +1,14 @@ -/** +/* * This file is a template for the code which will be substituted when our webpack loader handles non-API files in the * `pages/` directory. * - * We use `__RESOURCE_PATH__` as a placeholder for the path to the file being wrapped. Because it's not a real package, + * We use `__SENTRY_WRAPPING_TARGET_FILE__` as a placeholder for the path to the file being wrapped. Because it's not a real package, * this causes both TS and ESLint to complain, hence the pragma comments below. */ // @ts-ignore See above // eslint-disable-next-line import/no-unresolved -import * as wrapee from '__SENTRY_WRAPPING_TARGET__'; +import * as wrapee from '__SENTRY_WRAPPING_TARGET_FILE__'; // eslint-disable-next-line import/no-extraneous-dependencies import * as Sentry from '@sentry/nextjs'; import type { GetServerSideProps, GetStaticProps, NextPage as NextPageComponent } from 'next'; @@ -54,4 +54,4 @@ export default pageComponent; // not include anything whose name matchs something we've explicitly exported above. // @ts-ignore See above // eslint-disable-next-line import/no-unresolved -export * from '__SENTRY_WRAPPING_TARGET__'; +export * from '__SENTRY_WRAPPING_TARGET_FILE__'; diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index b79243627432..dd7a18f2f821 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -22,6 +22,7 @@ export type ExportedNextConfig = NextConfigObjectWithSentry | NextConfigFunction export type NextConfigObjectWithSentry = NextConfigObject & { sentry?: UserSentryOptions; }; + export type NextConfigFunctionWithSentry = ( phase: string, defaults: { defaultConfig: NextConfigObject }, @@ -60,39 +61,67 @@ export type NextConfigObject = { }; export type UserSentryOptions = { - // Override the SDK's default decision about whether or not to enable to the webpack plugin. Note that `false` forces - // the plugin to be enabled, even in situations where it's not recommended. + /** + * Override the SDK's default decision about whether or not to enable to the Sentry webpack plugin for server files. + * Note that `false` forces the plugin to be enabled, even in situations where it's not recommended. + */ disableServerWebpackPlugin?: boolean; + + /** + * Override the SDK's default decision about whether or not to enable to the Sentry webpack plugin for client files. + * Note that `false` forces the plugin to be enabled, even in situations where it's not recommended. + */ disableClientWebpackPlugin?: boolean; - // Use `hidden-source-map` for webpack `devtool` option, which strips the `sourceMappingURL` from the bottom of built - // JS files + /** + * Use `hidden-source-map` for webpack `devtool` option, which strips the `sourceMappingURL` from the bottom of built + * JS files. + */ hideSourceMaps?: boolean; - // Force webpack to apply the same transpilation rules to the SDK code as apply to user code. Helpful when targeting - // older browsers which don't support ES6 (or ES6+ features like object spread). + /** + * Instructs webpack to apply the same transpilation rules to the SDK code as apply to user code. Helpful when + * targeting older browsers which don't support ES6 (or ES6+ features like object spread). + */ transpileClientSDK?: boolean; - // Upload files from `/static/chunks` rather than `/static/chunks/pages`. Usually files outside of - // `pages/` only contain third-party code, but in cases where they contain user code, restricting the webpack - // plugin's upload breaks sourcemaps for those user-code-containing files, because it keeps them from being - // uploaded. At the same time, we don't want to widen the scope if we don't have to, because we're guaranteed to end - // up uploading too many files, which is why this defaults to `false`. + /** + * Instructs the Sentry webpack plugin to upload source files from `/static/chunks` rather than + * `/static/chunks/pages`. Usually files outside of `pages/` only contain third-party code, but in cases + * where they contain user code, restricting the webpack plugin's upload breaks sourcemaps for those + * user-code-containing files, because it keeps them from being uploaded. Defaults to `false`. + */ + // We don't want to widen the scope if we don't have to, because we're guaranteed to end up uploading too many files, + // which is why this defaults to`false`. widenClientFileUpload?: boolean; - // Automatically instrument Next.js data fetching methods and Next.js API routes + /** + * Automatically instrument Next.js data fetching methods and Next.js API routes with error and performance monitoring. + * Defaults to `true`. + */ autoInstrumentServerFunctions?: boolean; - // Exclude certain serverside API routes or pages from being instrumented with Sentry. This option takes an array of - // strings or regular expressions. - // - // NOTE: Pages should be specified as routes (`/animals` or `/api/animals/[animalType]/habitat`), not filepaths - // (`pages/animals/index.js` or `.\src\pages\api\animals\[animalType]\habitat.tsx`), and strings must be be a full, - // exact match. + /** + * Automatically instrument Next.js middleware with error and performance monitoring. Defaults to `true`. + */ + autoInstrumentMiddleware?: boolean; + + /** + * Exclude certain serverside API routes or pages from being instrumented with Sentry. This option takes an array of + * strings or regular expressions. + * + * NOTE: Pages should be specified as routes (`/animals` or `/api/animals/[animalType]/habitat`), not filepaths + * (`pages/animals/index.js` or `.\src\pages\api\animals\[animalType]\habitat.tsx`), and strings must be be a full, + * exact match. + */ excludeServerRoutes?: Array; - // Tunnel Sentry requests through this route on the Next.js server, to circumvent ad-blockers blocking Sentry events from being sent. - // This option should be a path (for example: '/error-monitoring'). + /** + * Tunnel Sentry requests through this route on the Next.js server, to circumvent ad-blockers blocking Sentry events + * from being sent. This option should be a path (for example: '/error-monitoring'). + * + * NOTE: This feature only works with Next.js 11+ + */ tunnelRoute?: string; }; @@ -164,7 +193,7 @@ export type EntryPointObject = { import: string | Array }; */ export type WebpackModuleRule = { - test?: string | RegExp; + test?: string | RegExp | ((resourcePath: string) => boolean); include?: Array | RegExp; exclude?: (filepath: string) => boolean; use?: ModuleRuleUseProperty | Array; diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index b5a8d07db2fd..0772f3b0b683 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -99,23 +99,61 @@ export function constructWebpackConfigFunction( if (isServer) { if (userSentryOptions.autoInstrumentServerFunctions !== false) { - const pagesDir = newConfig.resolve?.alias?.['private-next-pages'] as string; + let pagesDirPath: string; + if ( + fs.existsSync(path.join(projectDir, 'pages')) && + fs.lstatSync(path.join(projectDir, 'pages')).isDirectory() + ) { + pagesDirPath = path.join(projectDir, 'pages'); + } else { + pagesDirPath = path.join(projectDir, 'src', 'pages'); + } + + const middlewareJsPath = path.join(pagesDirPath, '..', 'middleware.js'); + const middlewareTsPath = path.join(pagesDirPath, '..', 'middleware.ts'); // Default page extensions per https://github.com/vercel/next.js/blob/f1dbc9260d48c7995f6c52f8fbcc65f08e627992/packages/next/server/config-shared.ts#L161 const pageExtensions = userNextConfig.pageExtensions || ['tsx', 'ts', 'jsx', 'js']; + const dotPrefixedPageExtensions = pageExtensions.map(ext => `.${ext}`); const pageExtensionRegex = pageExtensions.map(escapeStringForRegex).join('|'); // It is very important that we insert our loader at the beginning of the array because we expect any sort of transformations/transpilations (e.g. TS -> JS) to already have happened. newConfig.module.rules.unshift({ - test: new RegExp(`^${escapeStringForRegex(pagesDir)}.*\\.(${pageExtensionRegex})$`), + test: resourcePath => { + // We generally want to apply the loader to all API routes, pages and to the middleware file. + + // `resourcePath` may be an absolute path or a path relative to the context of the webpack config + let absoluteResourcePath: string; + if (path.isAbsolute(resourcePath)) { + absoluteResourcePath = resourcePath; + } else { + absoluteResourcePath = path.join(projectDir, resourcePath); + } + const normalizedAbsoluteResourcePath = path.normalize(absoluteResourcePath); + + if ( + // Match everything inside pages/ with the appropriate file extension + normalizedAbsoluteResourcePath.startsWith(pagesDirPath) && + dotPrefixedPageExtensions.some(ext => normalizedAbsoluteResourcePath.endsWith(ext)) + ) { + return true; + } else if ( + // Match middleware.js and middleware.ts + normalizedAbsoluteResourcePath === middlewareJsPath || + normalizedAbsoluteResourcePath === middlewareTsPath + ) { + return userSentryOptions.autoInstrumentMiddleware ?? true; + } else { + return false; + } + }, use: [ { loader: path.resolve(__dirname, 'loaders/wrappingLoader.js'), options: { - pagesDir, + pagesDir: pagesDirPath, pageExtensionRegex, excludeServerRoutes: userSentryOptions.excludeServerRoutes, - isEdgeRuntime: buildContext.nextRuntime === 'edge', }, }, ], diff --git a/packages/nextjs/src/edge/types.ts b/packages/nextjs/src/edge/types.ts index 81bc6796d6e0..71f96ec1946b 100644 --- a/packages/nextjs/src/edge/types.ts +++ b/packages/nextjs/src/edge/types.ts @@ -1,5 +1,5 @@ // We cannot make any assumptions about what users define as their handler except maybe that it is a function export interface EdgeRouteHandler { // eslint-disable-next-line @typescript-eslint/no-explicit-any - (req: any): any | Promise; + (...args: any[]): any; }