diff --git a/packages/nextjs/src/client/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/client/wrapAppGetInitialPropsWithSentry.ts index 205650d41dac..41a24d57c5c2 100644 --- a/packages/nextjs/src/client/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/client/wrapAppGetInitialPropsWithSentry.ts @@ -7,9 +7,11 @@ type AppGetInitialProps = typeof App['getInitialProps']; * so we are consistent with the serverside implementation. */ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetInitialProps): AppGetInitialProps { - return async function (this: unknown, ...args: Parameters): ReturnType { - return await origAppGetInitialProps.apply(this, args); - }; + return new Proxy(origAppGetInitialProps, { + apply: async (wrappingTarget, thisArg, args: Parameters) => { + return await wrappingTarget.apply(thisArg, args); + }, + }); } /** diff --git a/packages/nextjs/src/client/wrapDocumentGetInitialPropsWithSentry.ts b/packages/nextjs/src/client/wrapDocumentGetInitialPropsWithSentry.ts index c68c2a266df1..0af40a1f3f84 100644 --- a/packages/nextjs/src/client/wrapDocumentGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/client/wrapDocumentGetInitialPropsWithSentry.ts @@ -9,12 +9,11 @@ type DocumentGetInitialProps = typeof Document.getInitialProps; export function wrapDocumentGetInitialPropsWithSentry( origDocumentGetInitialProps: DocumentGetInitialProps, ): DocumentGetInitialProps { - return async function ( - this: unknown, - ...args: Parameters - ): ReturnType { - return await origDocumentGetInitialProps.apply(this, args); - }; + return new Proxy(origDocumentGetInitialProps, { + apply: async (wrappingTarget, thisArg, args: Parameters) => { + return await wrappingTarget.apply(thisArg, args); + }, + }); } /** diff --git a/packages/nextjs/src/client/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/client/wrapErrorGetInitialPropsWithSentry.ts index e018fb47246d..605efa58eff9 100644 --- a/packages/nextjs/src/client/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/client/wrapErrorGetInitialPropsWithSentry.ts @@ -10,9 +10,11 @@ type ErrorGetInitialProps = (context: NextPageContext) => Promise; export function wrapErrorGetInitialPropsWithSentry( origErrorGetInitialProps: ErrorGetInitialProps, ): ErrorGetInitialProps { - return async function (this: unknown, ...args: Parameters): ReturnType { - return await origErrorGetInitialProps.apply(this, args); - }; + return new Proxy(origErrorGetInitialProps, { + apply: async (wrappingTarget, thisArg, args: Parameters) => { + return await wrappingTarget.apply(thisArg, args); + }, + }); } /** diff --git a/packages/nextjs/src/client/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/client/wrapGetInitialPropsWithSentry.ts index f29561a4f333..1fbbd8707063 100644 --- a/packages/nextjs/src/client/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/client/wrapGetInitialPropsWithSentry.ts @@ -7,9 +7,11 @@ type GetInitialProps = Required['getInitialProps']; * so we are consistent with the serverside implementation. */ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialProps): GetInitialProps { - return async function (this: unknown, ...args: Parameters): Promise> { - return origGetInitialProps.apply(this, args); - }; + return new Proxy(origGetInitialProps, { + apply: async (wrappingTarget, thisArg, args: Parameters) => { + return await wrappingTarget.apply(thisArg, args); + }, + }); } /** diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index 7e87c6a5e607..ef228abc40e9 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -10,22 +10,24 @@ export function wrapApiHandlerWithSentry( handler: H, parameterizedRoute: string, ): (...params: Parameters) => Promise> { - return async function (this: unknown, ...args: Parameters): Promise> { - const req = args[0]; + return new Proxy(handler, { + apply: async (wrappingTarget, thisArg, args: Parameters) => { + const req = args[0]; - const activeSpan = !!getCurrentHub().getScope()?.getSpan(); + const activeSpan = !!getCurrentHub().getScope()?.getSpan(); - const wrappedHandler = withEdgeWrapping(handler, { - spanDescription: - activeSpan || !(req instanceof Request) - ? `handler (${parameterizedRoute})` - : `${req.method} ${parameterizedRoute}`, - spanOp: activeSpan ? 'function' : 'http.server', - mechanismFunctionName: 'wrapApiHandlerWithSentry', - }); + const wrappedHandler = withEdgeWrapping(wrappingTarget, { + spanDescription: + activeSpan || !(req instanceof Request) + ? `handler (${parameterizedRoute})` + : `${req.method} ${parameterizedRoute}`, + spanOp: activeSpan ? 'function' : 'http.server', + mechanismFunctionName: 'wrapApiHandlerWithSentry', + }); - return await wrappedHandler.apply(this, args); - }; + return await wrappedHandler.apply(thisArg, args); + }, + }); } /** diff --git a/packages/nextjs/src/edge/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/edge/wrapMiddlewareWithSentry.ts index cb535c41b28d..18c16f1a4198 100644 --- a/packages/nextjs/src/edge/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/edge/wrapMiddlewareWithSentry.ts @@ -10,9 +10,13 @@ import { withEdgeWrapping } from './utils/edgeWrapperUtils'; export function wrapMiddlewareWithSentry( middleware: H, ): (...params: Parameters) => Promise> { - return withEdgeWrapping(middleware, { - spanDescription: 'middleware', - spanOp: 'middleware.nextjs', - mechanismFunctionName: 'withSentryMiddleware', + return new Proxy(middleware, { + apply: async (wrappingTarget, thisArg, args: Parameters) => { + return withEdgeWrapping(wrappingTarget, { + spanDescription: 'middleware', + spanOp: 'middleware.nextjs', + mechanismFunctionName: 'withSentryMiddleware', + }).apply(thisArg, args); + }, }); } diff --git a/packages/nextjs/src/server/utils/nextLogger.ts b/packages/nextjs/src/server/utils/nextLogger.ts deleted file mode 100644 index 14d701d6edb7..000000000000 --- a/packages/nextjs/src/server/utils/nextLogger.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable no-console */ -import * as chalk from 'chalk'; - -// This is nextjs's own logging formatting, vendored since it's not exported. See -// https://github.com/vercel/next.js/blob/c3ceeb03abb1b262032bd96457e224497d3bbcef/packages/next/build/output/log.ts#L3-L11 -// and -// https://github.com/vercel/next.js/blob/de7aa2d6e486c40b8be95a1327639cbed75a8782/packages/next/lib/eslint/runLintCheck.ts#L321-L323. - -const prefixes = { - wait: `${chalk.cyan('wait')} -`, - error: `${chalk.red('error')} -`, - warn: `${chalk.yellow('warn')} -`, - ready: `${chalk.green('ready')} -`, - info: `${chalk.cyan('info')} -`, - event: `${chalk.magenta('event')} -`, - trace: `${chalk.magenta('trace')} -`, -}; - -export const formatAsCode = (str: string): string => chalk.bold.cyan(str); - -export const nextLogger: { - [key: string]: (...message: unknown[]) => void; -} = { - wait: (...message) => console.log(prefixes.wait, ...message), - error: (...message) => console.error(prefixes.error, ...message), - warn: (...message) => console.warn(prefixes.warn, ...message), - ready: (...message) => console.log(prefixes.ready, ...message), - info: (...message) => console.log(prefixes.info, ...message), - event: (...message) => console.log(prefixes.event, ...message), - trace: (...message) => console.log(prefixes.trace, ...message), -}; diff --git a/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts index 897b362cc550..6ba3d1851acf 100644 --- a/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts @@ -11,8 +11,7 @@ import { } from '@sentry/utils'; import * as domain from 'domain'; -import type { AugmentedNextApiRequest, AugmentedNextApiResponse, NextApiHandler, WrappedNextApiHandler } from './types'; -import { formatAsCode, nextLogger } from './utils/nextLogger'; +import type { AugmentedNextApiRequest, AugmentedNextApiResponse, NextApiHandler } from './types'; import { platformSupportsStreaming } from './utils/platformSupportsStreaming'; import { autoEndTransactionOnResponseEnd, finishTransaction, flushQueue } from './utils/responseEnd'; @@ -20,35 +19,18 @@ import { autoEndTransactionOnResponseEnd, finishTransaction, flushQueue } from ' * Wrap the given API route handler for tracing and error capturing. Thin wrapper around `withSentry`, which only * applies it if it hasn't already been applied. * - * @param maybeWrappedHandler The handler exported from the user's API page route file, which may or may not already be + * @param apiHandler The handler exported from the user's API page route file, which may or may not already be * wrapped with `withSentry` * @param parameterizedRoute The page's route, passed in via the proxy loader * @returns The wrapped handler */ -export function wrapApiHandlerWithSentry( - maybeWrappedHandler: NextApiHandler | WrappedNextApiHandler, - parameterizedRoute: string, -): WrappedNextApiHandler { - // Log a warning if the user is still manually wrapping their route in `withSentry`. Doesn't work in cases where - // there's been an intermediate wrapper (like `withSentryAPI(someOtherWrapper(withSentry(handler)))`) but should catch - // most cases. Only runs once per route. (Note: Such double-wrapping isn't harmful, but we'll eventually deprecate and remove `withSentry`, so - // best to get people to stop using it.) - if (maybeWrappedHandler.name === 'sentryWrappedHandler') { - const [_sentryNextjs_, _autoWrapOption_, _withSentry_, _route_] = [ - '@sentry/nextjs', - 'autoInstrumentServerFunctions', - 'withSentry', - parameterizedRoute, - ].map(phrase => formatAsCode(phrase)); - - nextLogger.info( - `${_sentryNextjs_} is running with the ${_autoWrapOption_} flag set, which means API routes no longer need to ` + - `be manually wrapped with ${_withSentry_}. Detected manual wrapping in ${_route_}.`, - ); - } - - // eslint-disable-next-line deprecation/deprecation - return withSentry(maybeWrappedHandler, parameterizedRoute); +export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameterizedRoute: string): NextApiHandler { + return new Proxy(apiHandler, { + apply: async (wrappingTarget, thisArg, args: Parameters) => { + // eslint-disable-next-line deprecation/deprecation + return withSentry(wrappingTarget, parameterizedRoute).apply(thisArg, args); + }, + }); } /** @@ -59,175 +41,178 @@ export const withSentryAPI = wrapApiHandlerWithSentry; /** * Legacy function for manually wrapping API route handlers, now used as the innards of `wrapApiHandlerWithSentry`. * - * @param origHandler The user's original API route handler + * @param apiHandler The user's original API route handler * @param parameterizedRoute The route whose handler is being wrapped. Meant for internal use only. * @returns A wrapped version of the handler * * @deprecated Use `wrapApiWithSentry()` instead */ -export function withSentry(origHandler: NextApiHandler, parameterizedRoute?: string): WrappedNextApiHandler { - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - return async function sentryWrappedHandler(req: AugmentedNextApiRequest, res: AugmentedNextApiResponse) { - // We're now auto-wrapping API route handlers using `wrapApiHandlerWithSentry` (which uses `withSentry` under the hood), but - // users still may have their routes manually wrapped with `withSentry`. This check makes `sentryWrappedHandler` - // idempotent so that those cases don't break anything. - if (req.__withSentry_applied__) { - return origHandler(req, res); - } - req.__withSentry_applied__ = true; - - // use a domain in order to prevent scope bleed between requests - const local = domain.create(); - local.add(req); - local.add(res); - - // `local.bind` causes everything to run inside a domain, just like `local.run` does, but it also lets the callback - // return a value. In our case, all any of the codepaths return is a promise of `void`, but nextjs still counts on - // getting that before it will finish the response. - // eslint-disable-next-line complexity - const boundHandler = local.bind(async () => { - let transaction: Transaction | undefined; - const hub = getCurrentHub(); - const currentScope = hub.getScope(); - const options = hub.getClient()?.getOptions(); - - if (currentScope) { - currentScope.setSDKProcessingMetadata({ request: req }); - - if (hasTracingEnabled(options) && options?.instrumenter === 'sentry') { - // If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision) - let traceparentData; - if (req.headers && isString(req.headers['sentry-trace'])) { - traceparentData = extractTraceparentData(req.headers['sentry-trace']); - __DEBUG_BUILD__ && logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`); - } +export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: string): NextApiHandler { + return new Proxy(apiHandler, { + apply: async (wrappingTarget, thisArg, args: [AugmentedNextApiRequest, AugmentedNextApiResponse]) => { + const [req, res] = args; + + // We're now auto-wrapping API route handlers using `wrapApiHandlerWithSentry` (which uses `withSentry` under the hood), but + // users still may have their routes manually wrapped with `withSentry`. This check makes `sentryWrappedHandler` + // idempotent so that those cases don't break anything. + if (req.__withSentry_applied__) { + return wrappingTarget.apply(thisArg, args); + } + req.__withSentry_applied__ = true; + + // use a domain in order to prevent scope bleed between requests + const local = domain.create(); + local.add(req); + local.add(res); + + // `local.bind` causes everything to run inside a domain, just like `local.run` does, but it also lets the callback + // return a value. In our case, all any of the codepaths return is a promise of `void`, but nextjs still counts on + // getting that before it will finish the response. + // eslint-disable-next-line complexity + const boundHandler = local.bind(async () => { + let transaction: Transaction | undefined; + const hub = getCurrentHub(); + const currentScope = hub.getScope(); + const options = hub.getClient()?.getOptions(); - const baggageHeader = req.headers && req.headers.baggage; - const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader); - - // prefer the parameterized route, if we have it (which we will if we've auto-wrapped the route handler) - let reqPath = parameterizedRoute; - - // If not, fake it by just replacing parameter values with their names, hoping that none of them match either - // each other or any hard-coded parts of the path - if (!reqPath) { - const url = `${req.url}`; - // pull off query string, if any - reqPath = stripUrlQueryAndFragment(url); - // Replace with placeholder - if (req.query) { - for (const [key, value] of Object.entries(req.query)) { - reqPath = reqPath.replace(`${value}`, `[${key}]`); - } + if (currentScope) { + currentScope.setSDKProcessingMetadata({ request: req }); + + if (hasTracingEnabled(options) && options?.instrumenter === 'sentry') { + // If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision) + let traceparentData; + if (req.headers && isString(req.headers['sentry-trace'])) { + traceparentData = extractTraceparentData(req.headers['sentry-trace']); + __DEBUG_BUILD__ && logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`); } - } - const reqMethod = `${(req.method || 'GET').toUpperCase()} `; - - transaction = startTransaction( - { - name: `${reqMethod}${reqPath}`, - op: 'http.server', - ...traceparentData, - metadata: { - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, - source: 'route', - request: req, - }, - }, - // extra context passed to the `tracesSampler` - { request: req }, - ); - currentScope.setSpan(transaction); - if (platformSupportsStreaming() && !origHandler.__sentry_test_doesnt_support_streaming__) { - autoEndTransactionOnResponseEnd(transaction, res); - } else { - // If we're not on a platform that supports streaming, we're blocking res.end() until the queue is flushed. - // res.json() and res.send() will implicitly call res.end(), so it is enough to wrap res.end(). - - // eslint-disable-next-line @typescript-eslint/unbound-method - const origResEnd = res.end; - res.end = async function (this: unknown, ...args: unknown[]) { - if (transaction) { - await finishTransaction(transaction, res); - await flushQueue(); + const baggageHeader = req.headers && req.headers.baggage; + const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader); + + // prefer the parameterized route, if we have it (which we will if we've auto-wrapped the route handler) + let reqPath = parameterizedRoute; + + // If not, fake it by just replacing parameter values with their names, hoping that none of them match either + // each other or any hard-coded parts of the path + if (!reqPath) { + const url = `${req.url}`; + // pull off query string, if any + reqPath = stripUrlQueryAndFragment(url); + // Replace with placeholder + if (req.query) { + for (const [key, value] of Object.entries(req.query)) { + reqPath = reqPath.replace(`${value}`, `[${key}]`); + } } + } - origResEnd.apply(this, args); - }; + const reqMethod = `${(req.method || 'GET').toUpperCase()} `; + + transaction = startTransaction( + { + name: `${reqMethod}${reqPath}`, + op: 'http.server', + ...traceparentData, + metadata: { + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + source: 'route', + request: req, + }, + }, + // extra context passed to the `tracesSampler` + { request: req }, + ); + currentScope.setSpan(transaction); + if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { + autoEndTransactionOnResponseEnd(transaction, res); + } else { + // If we're not on a platform that supports streaming, we're blocking res.end() until the queue is flushed. + // res.json() and res.send() will implicitly call res.end(), so it is enough to wrap res.end(). + + // eslint-disable-next-line @typescript-eslint/unbound-method + const origResEnd = res.end; + res.end = async function (this: unknown, ...args: unknown[]) { + if (transaction) { + await finishTransaction(transaction, res); + await flushQueue(); + } + + origResEnd.apply(this, args); + }; + } } } - } - try { - const handlerResult = await origHandler(req, res); - - if ( - process.env.NODE_ENV === 'development' && - !process.env.SENTRY_IGNORE_API_RESOLUTION_ERROR && - !res.finished - // This can only happen (not always) when the user is using `withSentry` manually, which we're deprecating. - // Warning suppression on Next.JS is only necessary in that case. - ) { - // eslint-disable-next-line no-console - console.warn( - `[sentry] If Next.js logs a warning "API resolved without sending a response", it's a false positive, which may happen when you use \`withSentry\` manually to wrap your routes. - To suppress this warning, set \`SENTRY_IGNORE_API_RESOLUTION_ERROR\` to 1 in your env. - To suppress the nextjs warning, use the \`externalResolver\` API route option (see https://nextjs.org/docs/api-routes/api-middlewares#custom-config for details).`, - ); - } - - return handlerResult; - } catch (e) { - // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can - // store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced - // to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a - // way to prevent it from actually being reported twice.) - const objectifiedErr = objectify(e); + try { + const handlerResult = await wrappingTarget.apply(thisArg, args); + + if ( + process.env.NODE_ENV === 'development' && + !process.env.SENTRY_IGNORE_API_RESOLUTION_ERROR && + !res.finished + // This can only happen (not always) when the user is using `withSentry` manually, which we're deprecating. + // Warning suppression on Next.JS is only necessary in that case. + ) { + // eslint-disable-next-line no-console + console.warn( + `[sentry] If Next.js logs a warning "API resolved without sending a response", it's a false positive, which may happen when you use \`withSentry\` manually to wrap your routes. + To suppress this warning, set \`SENTRY_IGNORE_API_RESOLUTION_ERROR\` to 1 in your env. + To suppress the nextjs warning, use the \`externalResolver\` API route option (see https://nextjs.org/docs/api-routes/api-middlewares#custom-config for details).`, + ); + } - if (currentScope) { - currentScope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'instrument', - handled: true, - data: { - wrapped_handler: origHandler.name, - function: 'withSentry', - }, + return handlerResult; + } catch (e) { + // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can + // store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced + // to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a + // way to prevent it from actually being reported twice.) + const objectifiedErr = objectify(e); + + if (currentScope) { + currentScope.addEventProcessor(event => { + addExceptionMechanism(event, { + type: 'instrument', + handled: true, + data: { + wrapped_handler: wrappingTarget.name, + function: 'withSentry', + }, + }); + return event; }); - return event; - }); - captureException(objectifiedErr); - } + captureException(objectifiedErr); + } - // Because we're going to finish and send the transaction before passing the error onto nextjs, it won't yet - // have had a chance to set the status to 500, so unless we do it ourselves now, we'll incorrectly report that - // the transaction was error-free - res.statusCode = 500; - res.statusMessage = 'Internal Server Error'; - - // Make sure we have a chance to finish the transaction and flush events to Sentry before the handler errors - // out. (Apps which are deployed on Vercel run their API routes in lambdas, and those lambdas will shut down the - // moment they detect an error, so it's important to get this done before rethrowing the error. Apps not - // deployed serverlessly will run into this cleanup code again in `res.end(), but the transaction will already - // be finished and the queue will already be empty, so effectively it'll just no-op.) - if (platformSupportsStreaming() && !origHandler.__sentry_test_doesnt_support_streaming__) { - void finishTransaction(transaction, res); - } else { - await finishTransaction(transaction, res); - await flushQueue(); - } + // Because we're going to finish and send the transaction before passing the error onto nextjs, it won't yet + // have had a chance to set the status to 500, so unless we do it ourselves now, we'll incorrectly report that + // the transaction was error-free + res.statusCode = 500; + res.statusMessage = 'Internal Server Error'; + + // Make sure we have a chance to finish the transaction and flush events to Sentry before the handler errors + // out. (Apps which are deployed on Vercel run their API routes in lambdas, and those lambdas will shut down the + // moment they detect an error, so it's important to get this done before rethrowing the error. Apps not + // deployed serverlessly will run into this cleanup code again in `res.end(), but the transaction will already + // be finished and the queue will already be empty, so effectively it'll just no-op.) + if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { + void finishTransaction(transaction, res); + } else { + await finishTransaction(transaction, res); + await flushQueue(); + } - // We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it - // would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark - // the error as already having been captured.) - throw objectifiedErr; - } - }); + // We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it + // would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark + // the error as already having been captured.) + throw objectifiedErr; + } + }); - // Since API route handlers are all async, nextjs always awaits the return value (meaning it's fine for us to return - // a promise here rather than a real result, and it saves us the overhead of an `await` call.) - return boundHandler(); - }; + // Since API route handlers are all async, nextjs always awaits the return value (meaning it's fine for us to return + // a promise here rather than a real result, and it saves us the overhead of an `await` call.) + return boundHandler(); + }, + }); } diff --git a/packages/nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts index 09b9d7070e9f..953f75142b32 100644 --- a/packages/nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts @@ -21,58 +21,60 @@ type AppGetInitialProps = typeof App['getInitialProps']; * @returns A wrapped version of the function */ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetInitialProps): AppGetInitialProps { - return async function (this: unknown, ...args: Parameters): ReturnType { - if (isBuild()) { - return origAppGetInitialProps.apply(this, args); - } + return new Proxy(origAppGetInitialProps, { + apply: async (wrappingTarget, thisArg, args: Parameters) => { + if (isBuild()) { + return wrappingTarget.apply(thisArg, args); + } - const [context] = args; - const { req, res } = context.ctx; + const [context] = args; + const { req, res } = context.ctx; - const errorWrappedAppGetInitialProps = withErrorInstrumentation(origAppGetInitialProps); - const options = getCurrentHub().getClient()?.getOptions(); + const errorWrappedAppGetInitialProps = withErrorInstrumentation(wrappingTarget); + const options = getCurrentHub().getClient()?.getOptions(); - // Generally we can assume that `req` and `res` are always defined on the server: - // https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object - // This does not seem to be the case in dev mode. Because we have no clean way of associating the the data fetcher - // span with each other when there are no req or res objects, we simply do not trace them at all here. - if (hasTracingEnabled() && req && res && options?.instrumenter === 'sentry') { - const tracedGetInitialProps = withTracedServerSideDataFetcher(errorWrappedAppGetInitialProps, req, res, { - dataFetcherRouteName: '/_app', - requestedRouteName: context.ctx.pathname, - dataFetchingMethodName: 'getInitialProps', - }); + // Generally we can assume that `req` and `res` are always defined on the server: + // https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object + // This does not seem to be the case in dev mode. Because we have no clean way of associating the the data fetcher + // span with each other when there are no req or res objects, we simply do not trace them at all here. + if (hasTracingEnabled() && req && res && options?.instrumenter === 'sentry') { + const tracedGetInitialProps = withTracedServerSideDataFetcher(errorWrappedAppGetInitialProps, req, res, { + dataFetcherRouteName: '/_app', + requestedRouteName: context.ctx.pathname, + dataFetchingMethodName: 'getInitialProps', + }); - const appGetInitialProps: { - pageProps: { - _sentryTraceData?: string; - _sentryBaggage?: string; - }; - } = await tracedGetInitialProps.apply(this, args); + const appGetInitialProps: { + pageProps: { + _sentryTraceData?: string; + _sentryBaggage?: string; + }; + } = await tracedGetInitialProps.apply(thisArg, args); - const requestTransaction = getTransactionFromRequest(req); + const requestTransaction = getTransactionFromRequest(req); - // Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call - // `App.getInitialProps(appContext)` in their custom `_app` pages which is required as per - // https://nextjs.org/docs/advanced-features/custom-app - resulting in missing `pageProps`. - // For this reason, we just handle the case where `pageProps` doesn't exist explicitly. - if (!appGetInitialProps.pageProps) { - appGetInitialProps.pageProps = {}; - } + // Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call + // `App.getInitialProps(appContext)` in their custom `_app` pages which is required as per + // https://nextjs.org/docs/advanced-features/custom-app - resulting in missing `pageProps`. + // For this reason, we just handle the case where `pageProps` doesn't exist explicitly. + if (!appGetInitialProps.pageProps) { + appGetInitialProps.pageProps = {}; + } - if (requestTransaction) { - appGetInitialProps.pageProps._sentryTraceData = requestTransaction.toTraceparent(); + if (requestTransaction) { + appGetInitialProps.pageProps._sentryTraceData = requestTransaction.toTraceparent(); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); - appGetInitialProps.pageProps._sentryBaggage = - dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - } + const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + appGetInitialProps.pageProps._sentryBaggage = + dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + } - return appGetInitialProps; - } else { - return errorWrappedAppGetInitialProps.apply(this, args); - } - }; + return appGetInitialProps; + } else { + return errorWrappedAppGetInitialProps.apply(thisArg, args); + } + }, + }); } /** diff --git a/packages/nextjs/src/server/wrapDocumentGetInitialPropsWithSentry.ts b/packages/nextjs/src/server/wrapDocumentGetInitialPropsWithSentry.ts index 614516af1e47..a3d1aa46eaea 100644 --- a/packages/nextjs/src/server/wrapDocumentGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/server/wrapDocumentGetInitialPropsWithSentry.ts @@ -18,36 +18,35 @@ type DocumentGetInitialProps = typeof Document.getInitialProps; export function wrapDocumentGetInitialPropsWithSentry( origDocumentGetInitialProps: DocumentGetInitialProps, ): DocumentGetInitialProps { - return async function ( - this: unknown, - ...args: Parameters - ): ReturnType { - if (isBuild()) { - return origDocumentGetInitialProps.apply(this, args); - } - - const [context] = args; - const { req, res } = context; - - const errorWrappedGetInitialProps = withErrorInstrumentation(origDocumentGetInitialProps); - const options = getCurrentHub().getClient()?.getOptions(); - - // Generally we can assume that `req` and `res` are always defined on the server: - // https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object - // This does not seem to be the case in dev mode. Because we have no clean way of associating the the data fetcher - // span with each other when there are no req or res objects, we simply do not trace them at all here. - if (hasTracingEnabled() && req && res && options?.instrumenter === 'sentry') { - const tracedGetInitialProps = withTracedServerSideDataFetcher(errorWrappedGetInitialProps, req, res, { - dataFetcherRouteName: '/_document', - requestedRouteName: context.pathname, - dataFetchingMethodName: 'getInitialProps', - }); - - return await tracedGetInitialProps.apply(this, args); - } else { - return errorWrappedGetInitialProps.apply(this, args); - } - }; + return new Proxy(origDocumentGetInitialProps, { + apply: async (wrappingTarget, thisArg, args: Parameters) => { + if (isBuild()) { + return wrappingTarget.apply(thisArg, args); + } + + const [context] = args; + const { req, res } = context; + + const errorWrappedGetInitialProps = withErrorInstrumentation(wrappingTarget); + const options = getCurrentHub().getClient()?.getOptions(); + + // Generally we can assume that `req` and `res` are always defined on the server: + // https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object + // This does not seem to be the case in dev mode. Because we have no clean way of associating the the data fetcher + // span with each other when there are no req or res objects, we simply do not trace them at all here. + if (hasTracingEnabled() && req && res && options?.instrumenter === 'sentry') { + const tracedGetInitialProps = withTracedServerSideDataFetcher(errorWrappedGetInitialProps, req, res, { + dataFetcherRouteName: '/_document', + requestedRouteName: context.pathname, + dataFetchingMethodName: 'getInitialProps', + }); + + return await tracedGetInitialProps.apply(thisArg, args); + } else { + return errorWrappedGetInitialProps.apply(thisArg, args); + } + }, + }); } /** diff --git a/packages/nextjs/src/server/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/server/wrapErrorGetInitialPropsWithSentry.ts index 5c1987b26416..bf01ec8e4e84 100644 --- a/packages/nextjs/src/server/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/server/wrapErrorGetInitialPropsWithSentry.ts @@ -24,46 +24,48 @@ type ErrorGetInitialProps = (context: NextPageContext) => Promise; export function wrapErrorGetInitialPropsWithSentry( origErrorGetInitialProps: ErrorGetInitialProps, ): ErrorGetInitialProps { - return async function (this: unknown, ...args: Parameters): ReturnType { - if (isBuild()) { - return origErrorGetInitialProps.apply(this, args); - } + return new Proxy(origErrorGetInitialProps, { + apply: async (wrappingTarget, thisArg, args: Parameters) => { + if (isBuild()) { + return wrappingTarget.apply(thisArg, args); + } - const [context] = args; - const { req, res } = context; + const [context] = args; + const { req, res } = context; - const errorWrappedGetInitialProps = withErrorInstrumentation(origErrorGetInitialProps); - const options = getCurrentHub().getClient()?.getOptions(); + const errorWrappedGetInitialProps = withErrorInstrumentation(wrappingTarget); + const options = getCurrentHub().getClient()?.getOptions(); - // Generally we can assume that `req` and `res` are always defined on the server: - // https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object - // This does not seem to be the case in dev mode. Because we have no clean way of associating the the data fetcher - // span with each other when there are no req or res objects, we simply do not trace them at all here. - if (hasTracingEnabled() && req && res && options?.instrumenter === 'sentry') { - const tracedGetInitialProps = withTracedServerSideDataFetcher(errorWrappedGetInitialProps, req, res, { - dataFetcherRouteName: '/_error', - requestedRouteName: context.pathname, - dataFetchingMethodName: 'getInitialProps', - }); + // Generally we can assume that `req` and `res` are always defined on the server: + // https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object + // This does not seem to be the case in dev mode. Because we have no clean way of associating the the data fetcher + // span with each other when there are no req or res objects, we simply do not trace them at all here. + if (hasTracingEnabled() && req && res && options?.instrumenter === 'sentry') { + const tracedGetInitialProps = withTracedServerSideDataFetcher(errorWrappedGetInitialProps, req, res, { + dataFetcherRouteName: '/_error', + requestedRouteName: context.pathname, + dataFetchingMethodName: 'getInitialProps', + }); - const errorGetInitialProps: ErrorProps & { - _sentryTraceData?: string; - _sentryBaggage?: string; - } = await tracedGetInitialProps.apply(this, args); + const errorGetInitialProps: ErrorProps & { + _sentryTraceData?: string; + _sentryBaggage?: string; + } = await tracedGetInitialProps.apply(thisArg, args); - const requestTransaction = getTransactionFromRequest(req); - if (requestTransaction) { - errorGetInitialProps._sentryTraceData = requestTransaction.toTraceparent(); + const requestTransaction = getTransactionFromRequest(req); + if (requestTransaction) { + errorGetInitialProps._sentryTraceData = requestTransaction.toTraceparent(); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); - errorGetInitialProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - } + const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + errorGetInitialProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + } - return errorGetInitialProps; - } else { - return errorWrappedGetInitialProps.apply(this, args); - } - }; + return errorGetInitialProps; + } else { + return errorWrappedGetInitialProps.apply(thisArg, args); + } + }, + }); } /** diff --git a/packages/nextjs/src/server/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/server/wrapGetInitialPropsWithSentry.ts index 72521f10c4f2..c180dfe3b9be 100644 --- a/packages/nextjs/src/server/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/server/wrapGetInitialPropsWithSentry.ts @@ -20,46 +20,48 @@ type GetInitialProps = Required['getInitialProps']; * @returns A wrapped version of the function */ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialProps): GetInitialProps { - return async function (this: unknown, ...args: Parameters): Promise> { - if (isBuild()) { - return origGetInitialProps.apply(this, args); - } + return new Proxy(origGetInitialProps, { + apply: async (wrappingTarget, thisArg, args: Parameters) => { + if (isBuild()) { + return wrappingTarget.apply(thisArg, args); + } - const [context] = args; - const { req, res } = context; + const [context] = args; + const { req, res } = context; - const errorWrappedGetInitialProps = withErrorInstrumentation(origGetInitialProps); - const options = getCurrentHub().getClient()?.getOptions(); + const errorWrappedGetInitialProps = withErrorInstrumentation(wrappingTarget); + const options = getCurrentHub().getClient()?.getOptions(); - // Generally we can assume that `req` and `res` are always defined on the server: - // https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object - // This does not seem to be the case in dev mode. Because we have no clean way of associating the the data fetcher - // span with each other when there are no req or res objects, we simply do not trace them at all here. - if (hasTracingEnabled() && req && res && options?.instrumenter === 'sentry') { - const tracedGetInitialProps = withTracedServerSideDataFetcher(errorWrappedGetInitialProps, req, res, { - dataFetcherRouteName: context.pathname, - requestedRouteName: context.pathname, - dataFetchingMethodName: 'getInitialProps', - }); + // Generally we can assume that `req` and `res` are always defined on the server: + // https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object + // This does not seem to be the case in dev mode. Because we have no clean way of associating the the data fetcher + // span with each other when there are no req or res objects, we simply do not trace them at all here. + if (hasTracingEnabled() && req && res && options?.instrumenter === 'sentry') { + const tracedGetInitialProps = withTracedServerSideDataFetcher(errorWrappedGetInitialProps, req, res, { + dataFetcherRouteName: context.pathname, + requestedRouteName: context.pathname, + dataFetchingMethodName: 'getInitialProps', + }); - const initialProps: { - _sentryTraceData?: string; - _sentryBaggage?: string; - } = await tracedGetInitialProps.apply(this, args); + const initialProps: { + _sentryTraceData?: string; + _sentryBaggage?: string; + } = await tracedGetInitialProps.apply(thisArg, args); - const requestTransaction = getTransactionFromRequest(req); - if (requestTransaction) { - initialProps._sentryTraceData = requestTransaction.toTraceparent(); + const requestTransaction = getTransactionFromRequest(req); + if (requestTransaction) { + initialProps._sentryTraceData = requestTransaction.toTraceparent(); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); - initialProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - } + const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + initialProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + } - return initialProps; - } else { - return errorWrappedGetInitialProps.apply(this, args); - } - }; + return initialProps; + } else { + return errorWrappedGetInitialProps.apply(thisArg, args); + } + }, + }); } /** diff --git a/packages/nextjs/src/server/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/server/wrapGetServerSidePropsWithSentry.ts index 691b890e4723..e305a72686d5 100644 --- a/packages/nextjs/src/server/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/server/wrapGetServerSidePropsWithSentry.ts @@ -21,43 +21,45 @@ export function wrapGetServerSidePropsWithSentry( origGetServerSideProps: GetServerSideProps, parameterizedRoute: string, ): GetServerSideProps { - return async function (this: unknown, ...args: Parameters): ReturnType { - if (isBuild()) { - return origGetServerSideProps.apply(this, args); - } + return new Proxy(origGetServerSideProps, { + apply: async (wrappingTarget, thisArg, args: Parameters) => { + if (isBuild()) { + return wrappingTarget.apply(thisArg, args); + } - const [context] = args; - const { req, res } = context; + const [context] = args; + const { req, res } = context; - const errorWrappedGetServerSideProps = withErrorInstrumentation(origGetServerSideProps); - const options = getCurrentHub().getClient()?.getOptions(); + const errorWrappedGetServerSideProps = withErrorInstrumentation(wrappingTarget); + const options = getCurrentHub().getClient()?.getOptions(); - if (hasTracingEnabled() && options?.instrumenter === 'sentry') { - const tracedGetServerSideProps = withTracedServerSideDataFetcher(errorWrappedGetServerSideProps, req, res, { - dataFetcherRouteName: parameterizedRoute, - requestedRouteName: parameterizedRoute, - dataFetchingMethodName: 'getServerSideProps', - }); + if (hasTracingEnabled() && options?.instrumenter === 'sentry') { + const tracedGetServerSideProps = withTracedServerSideDataFetcher(errorWrappedGetServerSideProps, req, res, { + dataFetcherRouteName: parameterizedRoute, + requestedRouteName: parameterizedRoute, + dataFetchingMethodName: 'getServerSideProps', + }); - const serverSideProps = await (tracedGetServerSideProps.apply(this, args) as ReturnType< - typeof tracedGetServerSideProps - >); + const serverSideProps = await (tracedGetServerSideProps.apply(thisArg, args) as ReturnType< + typeof tracedGetServerSideProps + >); - if ('props' in serverSideProps) { - const requestTransaction = getTransactionFromRequest(req); - if (requestTransaction) { - serverSideProps.props._sentryTraceData = requestTransaction.toTraceparent(); + if ('props' in serverSideProps) { + const requestTransaction = getTransactionFromRequest(req); + if (requestTransaction) { + serverSideProps.props._sentryTraceData = requestTransaction.toTraceparent(); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); - serverSideProps.props._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + serverSideProps.props._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + } } - } - return serverSideProps; - } else { - return errorWrappedGetServerSideProps.apply(this, args); - } - }; + return serverSideProps; + } else { + return errorWrappedGetServerSideProps.apply(thisArg, args); + } + }, + }); } /** diff --git a/packages/nextjs/src/server/wrapGetStaticPropsWithSentry.ts b/packages/nextjs/src/server/wrapGetStaticPropsWithSentry.ts index 380ea38949fc..e21978580ea6 100644 --- a/packages/nextjs/src/server/wrapGetStaticPropsWithSentry.ts +++ b/packages/nextjs/src/server/wrapGetStaticPropsWithSentry.ts @@ -15,28 +15,28 @@ type Props = { [key: string]: unknown }; * @returns A wrapped version of the function */ export function wrapGetStaticPropsWithSentry( - origGetStaticProps: GetStaticProps, + origGetStaticPropsa: GetStaticProps, parameterizedRoute: string, ): GetStaticProps { - return async function ( - ...getStaticPropsArguments: Parameters> - ): ReturnType> { - if (isBuild()) { - return origGetStaticProps(...getStaticPropsArguments); - } + return new Proxy(origGetStaticPropsa, { + apply: async (wrappingTarget, thisArg, args: Parameters>) => { + if (isBuild()) { + return wrappingTarget.apply(thisArg, args); + } - const errorWrappedGetStaticProps = withErrorInstrumentation(origGetStaticProps); - const options = getCurrentHub().getClient()?.getOptions(); + const errorWrappedGetStaticProps = withErrorInstrumentation(wrappingTarget); + const options = getCurrentHub().getClient()?.getOptions(); - if (hasTracingEnabled() && options?.instrumenter === 'sentry') { - return callDataFetcherTraced(errorWrappedGetStaticProps, getStaticPropsArguments, { - parameterizedRoute, - dataFetchingMethodName: 'getStaticProps', - }); - } + if (hasTracingEnabled() && options?.instrumenter === 'sentry') { + return callDataFetcherTraced(errorWrappedGetStaticProps, args, { + parameterizedRoute, + dataFetchingMethodName: 'getStaticProps', + }); + } - return errorWrappedGetStaticProps(...getStaticPropsArguments); - }; + return errorWrappedGetStaticProps.apply(thisArg, args); + }, + }); } /** diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts index e622df357a61..dfc06c9bcf7e 100644 --- a/packages/nextjs/test/config/withSentry.test.ts +++ b/packages/nextjs/test/config/withSentry.test.ts @@ -1,10 +1,10 @@ import * as hub from '@sentry/core'; import * as Sentry from '@sentry/node'; import type { Client, ClientOptions } from '@sentry/types'; -import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'; +import type { NextApiRequest, NextApiResponse } from 'next'; import { withSentry } from '../../src/server'; -import type { AugmentedNextApiResponse, WrappedNextApiHandler } from '../../src/server/types'; +import type { AugmentedNextApiResponse, NextApiHandler } from '../../src/server/types'; const FLUSH_DURATION = 200; @@ -23,7 +23,7 @@ async function sleep(ms: number): Promise { * @param req * @param res */ -async function callWrappedHandler(wrappedHandler: WrappedNextApiHandler, req: NextApiRequest, res: NextApiResponse) { +async function callWrappedHandler(wrappedHandler: NextApiHandler, req: NextApiRequest, res: NextApiResponse) { await wrappedHandler(req, res); // we know we need to wait at least this long for `flush()` to finish