From 5c462f1cec04ae2b925879095e6c5d011b37cee1 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 19 Aug 2022 17:14:19 +0200 Subject: [PATCH] feat(nextjs): Instrument server-side `getInitialProps` of `_app`, `_document` and `_error` (#5604) --- .../src/config/loaders/dataFetchersLoader.ts | 29 +++++++---- .../config/templates/proxyLoaderTemplate.ts | 13 +++-- packages/nextjs/src/config/wrappers/index.ts | 5 +- .../wrappers/withSentryGetServerSideProps.ts | 5 +- .../withSentryServerSideAppGetInitialProps.ts | 43 +++++++++++++++ ...SentryServerSideDocumentGetInitialProps.ts | 52 +++++++++++++++++++ ...ithSentryServerSideErrorGetInitialProps.ts | 46 ++++++++++++++++ .../withSentryServerSideGetInitialProps.ts | 10 ++-- .../src/config/wrappers/wrapperUtils.ts | 18 ++++--- packages/nextjs/src/index.server.ts | 3 ++ 10 files changed, 195 insertions(+), 29 deletions(-) create mode 100644 packages/nextjs/src/config/wrappers/withSentryServerSideAppGetInitialProps.ts create mode 100644 packages/nextjs/src/config/wrappers/withSentryServerSideDocumentGetInitialProps.ts create mode 100644 packages/nextjs/src/config/wrappers/withSentryServerSideErrorGetInitialProps.ts diff --git a/packages/nextjs/src/config/loaders/dataFetchersLoader.ts b/packages/nextjs/src/config/loaders/dataFetchersLoader.ts index d1608cbb8415..dc6d2e3a71a4 100644 --- a/packages/nextjs/src/config/loaders/dataFetchersLoader.ts +++ b/packages/nextjs/src/config/loaders/dataFetchersLoader.ts @@ -151,22 +151,33 @@ export default function wrapDataFetchersLoader(this: LoaderThis, if (hasDefaultExport(ast)) { outputFileContent += ` import { default as _sentry_default } from "${this.resourcePath}?sentry-proxy-loader"; - import { withSentryGetInitialProps } from "@sentry/nextjs";`; + import { + withSentryServerSideGetInitialProps, + withSentryServerSideAppGetInitialProps, + withSentryServerSideDocumentGetInitialProps, + withSentryServerSideErrorGetInitialProps, + } from "@sentry/nextjs";`; if (parameterizedRouteName === '/_app') { - // getInitialProps signature is a bit different in _app.js so we need a different wrapper - // Currently a no-op - } else if (parameterizedRouteName === '/_error') { - // getInitialProps behaviour is a bit different in _error.js so we probably want different wrapper - // Currently a no-op + outputFileContent += ` + if (typeof _sentry_default.getInitialProps === 'function') { + _sentry_default.getInitialProps = withSentryServerSideAppGetInitialProps(_sentry_default.getInitialProps); + }`; } else if (parameterizedRouteName === '/_document') { - // getInitialProps signature is a bit different in _document.js so we need a different wrapper - // Currently a no-op + outputFileContent += ` + if (typeof _sentry_default.getInitialProps === 'function') { + _sentry_default.getInitialProps = withSentryServerSideDocumentGetInitialProps(_sentry_default.getInitialProps); + }`; + } else if (parameterizedRouteName === '/_error') { + outputFileContent += ` + if (typeof _sentry_default.getInitialProps === 'function') { + _sentry_default.getInitialProps = withSentryServerSideErrorGetInitialProps(_sentry_default.getInitialProps); + }`; } else { // We enter this branch for any "normal" Next.js page outputFileContent += ` if (typeof _sentry_default.getInitialProps === 'function') { - _sentry_default.getInitialProps = withSentryGetInitialProps(_sentry_default.getInitialProps, '${parameterizedRouteName}'); + _sentry_default.getInitialProps = withSentryServerSideGetInitialProps(_sentry_default.getInitialProps); }`; } diff --git a/packages/nextjs/src/config/templates/proxyLoaderTemplate.ts b/packages/nextjs/src/config/templates/proxyLoaderTemplate.ts index 43630f52daae..9979ae814c49 100644 --- a/packages/nextjs/src/config/templates/proxyLoaderTemplate.ts +++ b/packages/nextjs/src/config/templates/proxyLoaderTemplate.ts @@ -26,11 +26,16 @@ const origGetInitialProps = pageComponent.getInitialProps; const origGetStaticProps = userPageModule.getStaticProps; const origGetServerSideProps = userPageModule.getServerSideProps; +const getInitialPropsWrappers: Record = { + '/_app': Sentry.withSentryServerSideAppGetInitialProps, + '/_document': Sentry.withSentryServerSideDocumentGetInitialProps, + '/_error': Sentry.withSentryServerSideErrorGetInitialProps, +}; + +const getInitialPropsWrapper = getInitialPropsWrappers['__ROUTE__'] || Sentry.withSentryServerSideGetInitialProps; + if (typeof origGetInitialProps === 'function') { - pageComponent.getInitialProps = Sentry.withSentryServerSideGetInitialProps( - origGetInitialProps, - '__ROUTE__', - ) as NextPageComponent['getInitialProps']; + pageComponent.getInitialProps = getInitialPropsWrapper(origGetInitialProps) as NextPageComponent['getInitialProps']; } export const getStaticProps = diff --git a/packages/nextjs/src/config/wrappers/index.ts b/packages/nextjs/src/config/wrappers/index.ts index bd47765ae04f..d675812fbd99 100644 --- a/packages/nextjs/src/config/wrappers/index.ts +++ b/packages/nextjs/src/config/wrappers/index.ts @@ -1,3 +1,6 @@ export { withSentryGetStaticProps } from './withSentryGetStaticProps'; -export { withSentryGetServerSideProps } from './withSentryGetServerSideProps'; export { withSentryServerSideGetInitialProps } from './withSentryServerSideGetInitialProps'; +export { withSentryServerSideAppGetInitialProps } from './withSentryServerSideAppGetInitialProps'; +export { withSentryServerSideDocumentGetInitialProps } from './withSentryServerSideDocumentGetInitialProps'; +export { withSentryServerSideErrorGetInitialProps } from './withSentryServerSideErrorGetInitialProps'; +export { withSentryGetServerSideProps } from './withSentryGetServerSideProps'; diff --git a/packages/nextjs/src/config/wrappers/withSentryGetServerSideProps.ts b/packages/nextjs/src/config/wrappers/withSentryGetServerSideProps.ts index a9d06f17cf4e..49fe55535d0e 100644 --- a/packages/nextjs/src/config/wrappers/withSentryGetServerSideProps.ts +++ b/packages/nextjs/src/config/wrappers/withSentryGetServerSideProps.ts @@ -29,8 +29,9 @@ export function withSentryGetServerSideProps( if (hasTracingEnabled()) { return callTracedServerSideDataFetcher(errorWrappedGetServerSideProps, getServerSidePropsArguments, req, res, { - parameterizedRoute, - functionName: 'getServerSideProps', + dataFetcherRouteName: parameterizedRoute, + requestedRouteName: parameterizedRoute, + dataFetchingMethodName: 'getServerSideProps', }); } else { return errorWrappedGetServerSideProps(...getServerSidePropsArguments); diff --git a/packages/nextjs/src/config/wrappers/withSentryServerSideAppGetInitialProps.ts b/packages/nextjs/src/config/wrappers/withSentryServerSideAppGetInitialProps.ts new file mode 100644 index 000000000000..798ceeae2e8f --- /dev/null +++ b/packages/nextjs/src/config/wrappers/withSentryServerSideAppGetInitialProps.ts @@ -0,0 +1,43 @@ +import { hasTracingEnabled } from '@sentry/tracing'; +import App from 'next/app'; + +import { isBuild } from '../../utils/isBuild'; +import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils'; + +type AppGetInitialProps = typeof App['getInitialProps']; + +/** + * Create a wrapped version of the user's exported `getInitialProps` function in + * a custom app ("_app.js"). + * + * @param origAppGetInitialProps The user's `getInitialProps` function + * @param parameterizedRoute The page's parameterized route + * @returns A wrapped version of the function + */ +export function withSentryServerSideAppGetInitialProps(origAppGetInitialProps: AppGetInitialProps): AppGetInitialProps { + return async function ( + ...appGetInitialPropsArguments: Parameters + ): ReturnType { + if (isBuild()) { + return origAppGetInitialProps(...appGetInitialPropsArguments); + } + + const [context] = appGetInitialPropsArguments; + const { req, res } = context.ctx; + + const errorWrappedAppGetInitialProps = withErrorInstrumentation(origAppGetInitialProps); + + if (hasTracingEnabled()) { + // Since this wrapper is only applied to `getInitialProps` running on the server, we can assert that `req` and + // `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return callTracedServerSideDataFetcher(errorWrappedAppGetInitialProps, appGetInitialPropsArguments, req!, res!, { + dataFetcherRouteName: '/_app', + requestedRouteName: context.ctx.pathname, + dataFetchingMethodName: 'getInitialProps', + }); + } else { + return errorWrappedAppGetInitialProps(...appGetInitialPropsArguments); + } + }; +} diff --git a/packages/nextjs/src/config/wrappers/withSentryServerSideDocumentGetInitialProps.ts b/packages/nextjs/src/config/wrappers/withSentryServerSideDocumentGetInitialProps.ts new file mode 100644 index 000000000000..f914a43a3916 --- /dev/null +++ b/packages/nextjs/src/config/wrappers/withSentryServerSideDocumentGetInitialProps.ts @@ -0,0 +1,52 @@ +import { hasTracingEnabled } from '@sentry/tracing'; +import Document from 'next/document'; + +import { isBuild } from '../../utils/isBuild'; +import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils'; + +type DocumentGetInitialProps = typeof Document.getInitialProps; + +/** + * Create a wrapped version of the user's exported `getInitialProps` function in + * a custom document ("_document.js"). + * + * @param origDocumentGetInitialProps The user's `getInitialProps` function + * @param parameterizedRoute The page's parameterized route + * @returns A wrapped version of the function + */ +export function withSentryServerSideDocumentGetInitialProps( + origDocumentGetInitialProps: DocumentGetInitialProps, +): DocumentGetInitialProps { + return async function ( + ...documentGetInitialPropsArguments: Parameters + ): ReturnType { + if (isBuild()) { + return origDocumentGetInitialProps(...documentGetInitialPropsArguments); + } + + const [context] = documentGetInitialPropsArguments; + const { req, res } = context; + + const errorWrappedGetInitialProps = withErrorInstrumentation(origDocumentGetInitialProps); + + if (hasTracingEnabled()) { + // Since this wrapper is only applied to `getInitialProps` running on the server, we can assert that `req` and + // `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object + return callTracedServerSideDataFetcher( + errorWrappedGetInitialProps, + documentGetInitialPropsArguments, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + req!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + res!, + { + dataFetcherRouteName: '/_document', + requestedRouteName: context.pathname, + dataFetchingMethodName: 'getInitialProps', + }, + ); + } else { + return errorWrappedGetInitialProps(...documentGetInitialPropsArguments); + } + }; +} diff --git a/packages/nextjs/src/config/wrappers/withSentryServerSideErrorGetInitialProps.ts b/packages/nextjs/src/config/wrappers/withSentryServerSideErrorGetInitialProps.ts new file mode 100644 index 000000000000..379b18ad05f8 --- /dev/null +++ b/packages/nextjs/src/config/wrappers/withSentryServerSideErrorGetInitialProps.ts @@ -0,0 +1,46 @@ +import { hasTracingEnabled } from '@sentry/tracing'; +import { NextPageContext } from 'next'; +import { ErrorProps } from 'next/error'; + +import { isBuild } from '../../utils/isBuild'; +import { callTracedServerSideDataFetcher, withErrorInstrumentation } from './wrapperUtils'; + +type ErrorGetInitialProps = (context: NextPageContext) => Promise; + +/** + * Create a wrapped version of the user's exported `getInitialProps` function in + * a custom error page ("_error.js"). + * + * @param origErrorGetInitialProps The user's `getInitialProps` function + * @param parameterizedRoute The page's parameterized route + * @returns A wrapped version of the function + */ +export function withSentryServerSideErrorGetInitialProps( + origErrorGetInitialProps: ErrorGetInitialProps, +): ErrorGetInitialProps { + return async function ( + ...errorGetInitialPropsArguments: Parameters + ): ReturnType { + if (isBuild()) { + return origErrorGetInitialProps(...errorGetInitialPropsArguments); + } + + const [context] = errorGetInitialPropsArguments; + const { req, res } = context; + + const errorWrappedGetInitialProps = withErrorInstrumentation(origErrorGetInitialProps); + + if (hasTracingEnabled()) { + // Since this wrapper is only applied to `getInitialProps` running on the server, we can assert that `req` and + // `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return callTracedServerSideDataFetcher(errorWrappedGetInitialProps, errorGetInitialPropsArguments, req!, res!, { + dataFetcherRouteName: '/_error', + requestedRouteName: context.pathname, + dataFetchingMethodName: 'getInitialProps', + }); + } else { + return errorWrappedGetInitialProps(...errorGetInitialPropsArguments); + } + }; +} diff --git a/packages/nextjs/src/config/wrappers/withSentryServerSideGetInitialProps.ts b/packages/nextjs/src/config/wrappers/withSentryServerSideGetInitialProps.ts index 19be4b8e9a6f..458609a9646b 100644 --- a/packages/nextjs/src/config/wrappers/withSentryServerSideGetInitialProps.ts +++ b/packages/nextjs/src/config/wrappers/withSentryServerSideGetInitialProps.ts @@ -13,10 +13,7 @@ type GetInitialProps = Required['getInitialProps']; * @param parameterizedRoute The page's parameterized route * @returns A wrapped version of the function */ -export function withSentryServerSideGetInitialProps( - origGetInitialProps: GetInitialProps, - parameterizedRoute: string, -): GetInitialProps { +export function withSentryServerSideGetInitialProps(origGetInitialProps: GetInitialProps): GetInitialProps { return async function ( ...getInitialPropsArguments: Parameters ): Promise> { @@ -34,8 +31,9 @@ export function withSentryServerSideGetInitialProps( // `res` are always defined: https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return callTracedServerSideDataFetcher(errorWrappedGetInitialProps, getInitialPropsArguments, req!, res!, { - parameterizedRoute, - functionName: 'getInitialProps', + dataFetcherRouteName: context.pathname, + requestedRouteName: context.pathname, + dataFetchingMethodName: 'getInitialProps', }); } else { return errorWrappedGetInitialProps(...getInitialPropsArguments); diff --git a/packages/nextjs/src/config/wrappers/wrapperUtils.ts b/packages/nextjs/src/config/wrappers/wrapperUtils.ts index 115d663350cc..0042143b15f3 100644 --- a/packages/nextjs/src/config/wrappers/wrapperUtils.ts +++ b/packages/nextjs/src/config/wrappers/wrapperUtils.ts @@ -73,8 +73,12 @@ export function callTracedServerSideDataFetcher Pr req: IncomingMessage, res: ServerResponse, options: { - parameterizedRoute: string; - functionName: string; + /** Parameterized route of the request - will be used for naming the transaction. */ + requestedRouteName: string; + /** Name of the route the data fetcher was defined in - will be used for describing the data fetcher's span. */ + dataFetcherRouteName: string; + /** Name of the data fetching method - will be used for describing the data fetcher's span. */ + dataFetchingMethodName: string; }, ): Promise> { return domain.create().bind(async () => { @@ -84,8 +88,8 @@ export function callTracedServerSideDataFetcher Pr // TODO: Extract trace data from `req` object (trace and baggage headers) and attach it to transaction const newTransaction = startTransaction({ - op: 'nextjs.data', - name: options.parameterizedRoute, + op: 'nextjs.data.server', + name: options.requestedRouteName, metadata: { source: 'route', }, @@ -97,8 +101,8 @@ export function callTracedServerSideDataFetcher Pr } const dataFetcherSpan = requestTransaction.startChild({ - op: 'nextjs.data', - description: `${options.functionName} (${options.parameterizedRoute})`, + op: 'nextjs.data.server', + description: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`, }); const currentScope = getCurrentHub().getScope(); @@ -158,7 +162,7 @@ export async function callDataFetcherTraced Promis // Capture the route, since pre-loading, revalidation, etc might mean that this span may happen during another // route's transaction const span = transaction.startChild({ - op: 'nextjs.data', + op: 'nextjs.data.server', description: `${dataFetchingMethodName} (${parameterizedRoute})`, }); diff --git a/packages/nextjs/src/index.server.ts b/packages/nextjs/src/index.server.ts index 4fbe95bb8ee6..ee4a4e17c356 100644 --- a/packages/nextjs/src/index.server.ts +++ b/packages/nextjs/src/index.server.ts @@ -129,6 +129,9 @@ export { withSentryGetServerSideProps, withSentryGetStaticProps, withSentryServerSideGetInitialProps, + withSentryServerSideAppGetInitialProps, + withSentryServerSideDocumentGetInitialProps, + withSentryServerSideErrorGetInitialProps, } from './config/wrappers'; export { withSentry } from './utils/withSentry';