diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-class.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-class.tsx new file mode 100644 index 000000000000..86ce68c1c034 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-class.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +export default class Page extends React.Component { + render() { + throw new Error('Pages SSR Error Class'); + return
Hello world!
; + } +} + +export function getServerSideProps() { + return { + props: { + foo: 'bar', + }, + }; +} diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-fc.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-fc.tsx new file mode 100644 index 000000000000..6342caec47ca --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/pages/pages-router/ssr-error-fc.tsx @@ -0,0 +1,12 @@ +export default function Page() { + throw new Error('Pages SSR Error FC'); + return
Hello world!
; +} + +export function getServerSideProps() { + return { + props: { + foo: 'bar', + }, + }; +} diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts new file mode 100644 index 000000000000..2c1eb9729fdd --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test'; +import { waitForError } from '../event-proxy-server'; + +test('Will capture error for SSR rendering error (Class Component)', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error Class'; + }); + + await page.goto('/pages-router/ssr-error-class'); + + const errorEvent = await errorEventPromise; + expect(errorEvent).toBeDefined(); +}); + +test('Will capture error for SSR rendering error (Functional Component)', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error FC'; + }); + + await page.goto('/pages-router/ssr-error-fc'); + + const errorEvent = await errorEventPromise; + expect(errorEvent).toBeDefined(); +}); 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;