From c9ba1169f4aa55a374281abbb2bf64a12115241d Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 3 Nov 2026 10:36:54 +1100 Subject: [PATCH] [UI] Copy error button (#13168) Adds button to copy UI error information --- src/frontend/lib/components/Boundary.tsx | 77 +++++++++++++++++-- .../AccountSettings/SecurityContent.tsx | 22 +----- 2 files changed, 74 insertions(+), 25 deletions(-) diff --git a/src/frontend/lib/components/Boundary.tsx b/src/frontend/lib/components/Boundary.tsx index 045eec46d8..116bd0a775 100644 --- a/src/frontend/lib/components/Boundary.tsx +++ b/src/frontend/lib/components/Boundary.tsx @@ -1,13 +1,57 @@ import { t } from '@lingui/core/macro'; -import { Alert, Stack, Text } from '@mantine/core'; +import { Alert, Group, Stack, Text } from '@mantine/core'; import { ErrorBoundary, type FallbackRender } from '@sentry/react'; import { IconExclamationCircle, IconInfoCircle } from '@tabler/icons-react'; import { type ReactNode, useCallback, useState } from 'react'; +import { CopyButton } from './CopyButton'; + +/** + * Construct a plain-text error report, suitable for copying into a bug report + */ +export function formatErrorReport({ + label, + error, + componentStack +}: { + label: string; + error: unknown; + componentStack?: string; +}): string { + const lines: string[] = [ + `Error rendering component: ${label}`, + `Timestamp: ${new Date().toISOString()}`, + `URL: ${window.location.href}`, + `User Agent: ${navigator.userAgent}`, + '' + ]; + + if (error instanceof Error) { + lines.push(`${error.name}: ${error.message}`); + if (error.stack) { + lines.push('', 'Stack trace:', error.stack); + } + } else { + lines.push(String(error)); + } + + if (componentStack) { + lines.push('', 'Component stack:', componentStack.trim()); + } + + return lines.join('\n'); +} export function DefaultFallback({ title, - error -}: Readonly<{ title: string; error: string | null }>): ReactNode { + error, + details +}: Readonly<{ + title: string; + error: string | null; + details?: string | null; +}>): ReactNode { + const copyValue = details ?? error; + return ( <> {error && ( } title={t`Error Details`}> - {error} + + {error} + {copyValue && ( + + + + )} + )} @@ -43,20 +100,28 @@ export function Boundary({ fallback?: React.ReactElement | FallbackRender; }>): ReactNode { const [errorMessage, setErrorMessage] = useState(null); + const [errorDetails, setErrorDetails] = useState(null); const onError = useCallback( (error: unknown, componentStack: string | undefined, eventId: string) => { console.error(`ERR: Error rendering component: ${label}`); console.error(error); setErrorMessage(error instanceof Error ? error.message : String(error)); + setErrorDetails(formatErrorReport({ label, error, componentStack })); }, - [] + [label] ); return ( + fallback ?? ( + + ) } onError={onError} > diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx index ba208eb739..94ba4f8041 100644 --- a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx @@ -1,4 +1,4 @@ -import { DefaultFallback } from '@lib/components/Boundary'; +import { Boundary } from '@lib/components/Boundary'; import { StylishText } from '@lib/components/StylishText'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { apiUrl } from '@lib/functions/Api'; @@ -22,7 +22,6 @@ import { TextInput } from '@mantine/core'; import { hideNotification, showNotification } from '@mantine/notifications'; -import { ErrorBoundary } from '@sentry/react'; import { IconAlertCircle, IconAt, @@ -45,16 +44,6 @@ export function SecurityContent() { const user = useUserState(); - const [errorMessage, setErrorMessage] = useState(null); - - const onError = useCallback( - (error: unknown, componentStack: string | undefined, eventId: string) => { - console.error(`ERR: Error rendering component: ${error}`); - setErrorMessage(error instanceof Error ? error.message : String(error)); - }, - [] - ); - return ( @@ -97,14 +86,9 @@ export function SecurityContent() { {t`Access Tokens`} - - } - onError={onError} - > + - + {user.isSuperuser() && (