From 91652f4da2e6a00bf80389e20f0429b3e1c7cdac Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 25 Oct 2023 13:22:13 +0000 Subject: [PATCH] feat(nextjs): Instrument SSR page components --- packages/nextjs/src/common/index.ts | 2 + .../src/common/wrapPageComponentWithSentry.ts | 66 +++++++++++++++++++ .../config/templates/pageWrapperTemplate.ts | 2 +- packages/nextjs/src/index.types.ts | 5 ++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 packages/nextjs/src/common/wrapPageComponentWithSentry.ts diff --git a/packages/nextjs/src/common/index.ts b/packages/nextjs/src/common/index.ts index 3aeef33af760..2f166b3e4b59 100644 --- a/packages/nextjs/src/common/index.ts +++ b/packages/nextjs/src/common/index.ts @@ -41,3 +41,5 @@ export { wrapRouteHandlerWithSentry } from './wrapRouteHandlerWithSentry'; export { wrapApiHandlerWithSentryVercelCrons } from './wrapApiHandlerWithSentryVercelCrons'; export { wrapMiddlewareWithSentry } from './wrapMiddlewareWithSentry'; + +export { wrapPageComponentWithSentry } from './wrapPageComponentWithSentry'; diff --git a/packages/nextjs/src/common/wrapPageComponentWithSentry.ts b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts new file mode 100644 index 000000000000..d67dd2a544c0 --- /dev/null +++ b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts @@ -0,0 +1,66 @@ +import { captureException } from '@sentry/core'; +import { addExceptionMechanism } from '@sentry/utils'; + +interface FunctionComponent { + (...args: unknown[]): unknown; +} + +interface ClassComponent { + new (...args: unknown[]): { + render(...args: unknown[]): unknown; + }; +} + +function isReactClassComponent(target: unknown): target is ClassComponent { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return typeof target === 'function' && target?.prototype?.isReactComponent; +} + +/** + * Wraps a page component with Sentry error instrumentation. + */ +export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | ClassComponent): unknown { + if (isReactClassComponent(pageComponent)) { + return class SentryWrappedPageComponent extends pageComponent { + public render(...args: unknown[]): unknown { + try { + return super.render(...args); + } catch (e) { + captureException(e, scope => { + scope.addEventProcessor(event => { + addExceptionMechanism(event, { + handled: false, + }); + return event; + }); + + return scope; + }); + throw e; + } + } + }; + } else if (typeof pageComponent === 'function') { + return new Proxy(pageComponent, { + apply(target, thisArg, argArray) { + try { + return target.apply(thisArg, argArray); + } catch (e) { + captureException(e, scope => { + scope.addEventProcessor(event => { + addExceptionMechanism(event, { + handled: false, + }); + return event; + }); + + return scope; + }); + throw e; + } + }, + }); + } else { + return pageComponent; + } +} diff --git a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts index 16cce1a6cc39..c383503f42cf 100644 --- a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts @@ -49,7 +49,7 @@ export const getServerSideProps = ? Sentry.wrapGetServerSidePropsWithSentry(origGetServerSideProps, '__ROUTE__') : undefined; -export default pageComponent; +export default pageComponent ? Sentry.wrapPageComponentWithSentry(pageComponent as unknown) : pageComponent; // 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. diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 479760633b54..804c8f2b3e35 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -186,3 +186,8 @@ export declare function wrapApiHandlerWithSentryVercelCrons(WrappingTarget: C): C;