diff --git a/apps/site/app/[locale]/error.tsx b/apps/site/app/[locale]/error.tsx index 0c4bf7f827ec8..afeaeb7932b83 100644 --- a/apps/site/app/[locale]/error.tsx +++ b/apps/site/app/[locale]/error.tsx @@ -4,11 +4,17 @@ import { useTranslations } from 'next-intl'; import Button from '#site/components/Common/Button'; import GlowingBackdropLayout from '#site/layouts/GlowingBackdrop'; +import { SHOW_ERROR_DETAILS } from '#site/next.constants.mjs'; import type { FC } from 'react'; -const ErrorPage: FC<{ error: Error }> = () => { +type ErrorPageProps = { + error: Error & { digest?: string }; +}; + +const ErrorPage: FC = ({ error }) => { const t = useTranslations(); + const hasErrorDetails = Boolean(error.message || error.digest); return ( @@ -22,6 +28,21 @@ const ErrorPage: FC<{ error: Error }> = () => { {t('layouts.error.internalServerError.description')}

+ {SHOW_ERROR_DETAILS && hasErrorDetails && ( +
+ + {t('layouts.error.details')} + + +
+            {error.message}
+            {error.digest
+              ? `${error.message ? '\n' : ''}digest: ${error.digest}`
+              : ''}
+          
+
+ )} +
); diff --git a/apps/site/next.constants.mjs b/apps/site/next.constants.mjs index c90c61711b7c6..b960c103fdb00 100644 --- a/apps/site/next.constants.mjs +++ b/apps/site/next.constants.mjs @@ -14,6 +14,18 @@ export const IS_DEV_ENV = process.env.NODE_ENV === 'development'; */ export const VERCEL_ENV = process.env.VERCEL_ENV || undefined; +/** + * Public-facing Vercel environment, safe to use in client-side code. + */ +export const PUBLIC_VERCEL_ENV = + process.env.NEXT_PUBLIC_VERCEL_ENV || VERCEL_ENV; + +/** + * Error details should only be exposed in local development or Vercel preview + * deployments, never in production. + */ +export const SHOW_ERROR_DETAILS = IS_DEV_ENV || PUBLIC_VERCEL_ENV === 'preview'; + /** * This is used for telling Next.js to do a Static Export Build of the Website * diff --git a/apps/site/tests/errorPage.test.jsx b/apps/site/tests/errorPage.test.jsx new file mode 100644 index 0000000000000..6fd30ef5b0676 --- /dev/null +++ b/apps/site/tests/errorPage.test.jsx @@ -0,0 +1,77 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { render, screen } from '@testing-library/react'; + +describe('ErrorPage', () => { + const setupErrorPage = async (t, showErrorDetails, suffix = '') => { + t.mock.module('#site/components/Common/Button', { + defaultExport: ({ children, href }) => {children}, + }); + + t.mock.module('#site/layouts/GlowingBackdrop', { + defaultExport: ({ children }) =>
{children}
, + }); + + t.mock.module('#site/next.constants.mjs', { + namedExports: { + SHOW_ERROR_DETAILS: showErrorDetails, + }, + }); + + return import(`../app/[locale]/error.tsx${suffix}`); + }; + + it('renders technical details in preview environments', async t => { + const { default: ErrorPage } = await setupErrorPage(t, true); + + render( + + ); + + assert.equal( + screen.getByRole('heading').textContent, + 'layouts.error.internalServerError.title' + ); + assert.equal( + screen.getByRole('link').textContent, + 'layouts.error.backToHome' + ); + assert.equal( + screen.getByText('layouts.error.details').textContent, + 'layouts.error.details' + ); + assert.match( + screen.getByText(/Preview deployment failed/).textContent, + /Preview deployment failed/ + ); + assert.match( + screen.getByText(/digest: abc123/i).textContent, + /digest: abc123/i + ); + }); + + it('hides technical details when the flag is disabled', async t => { + const { default: ErrorPage } = await setupErrorPage( + t, + false, + '?show-error-details-disabled' + ); + + render( + + ); + + assert.equal(screen.queryByText('layouts.error.details'), null); + assert.equal(screen.queryByText(/Production should stay generic/), null); + assert.equal(screen.queryByText(/digest: hidden123/i), null); + }); +}); diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index a794c5f164efc..a267da0a71a79 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -370,6 +370,7 @@ "title": "Internal Server Error", "description": "This page has thrown a non-recoverable error." }, + "details": "Details", "backToHome": "Back to Home" }, "download": {