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": {