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() && (