Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/error_page_with_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
sable: minor
---

added error page making it easier to report errors when they occur in the field
116 changes: 116 additions & 0 deletions src/app/components/DefaultErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Box, Button, Dialog, Icon, Icons, Text, color, config } from 'folds';
import { SplashScreen } from '$components/splash-screen';
import { buildGitHubUrl } from '$features/bug-report/BugReportModal';

type ErrorPageProps = {
error: Error;
};

function createIssueUrl(error: Error): string {
const stacktrace = error.stack || 'No stacktrace available';

const automatedBugReport = `# Automated Bug Report
Error occurred in the application.

## Error Message
\`\`\`
${error.message}
\`\`\`

## Stacktrace
\`\`\`
${stacktrace}
\`\`\``;

return buildGitHubUrl('bug', `Error: ${error.message}`, { context: automatedBugReport });
}

// This component is used as the fallback for the ErrorBoundary in App.tsx, which means it will be rendered whenever an uncaught error is thrown in any of the child components and not handled locally.
// It provides a user-friendly error message and options to report the issue or reload the page.
// Motivation of the design is to encourage users to report issues while also providing them with the necessary information to do so, and to give them an easy way to recover by reloading the page.
// Note: Since this component is rendered in response to an error, it should be as resilient as possible and avoid any complex logic or dependencies that could potentially throw additional errors.
export function ErrorPage({ error }: ErrorPageProps) {
return (
<SplashScreen>
<Box grow="Yes" direction="Column" gap="400" alignItems="Center" justifyContent="Center">
<Dialog
style={{
maxWidth: '600px',
minWidth: '300px',
padding: config.space.S400,
}}
>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Box direction="Column" gap="100">
<Box alignItems="Center" gap="200">
<Icon
size="300"
src={Icons.Warning}
filled
style={{ color: color.Critical.Main }}
/>
<Text size="H2">Oops! Something went wrong</Text>
</Box>
<Text size="T300">
An unexpected error occurred. Please try again. If it continues, report the issue on
our GitHub using the button below, it will include error details to help us
investigate. Thank you for helping improve the app.
</Text>
<Button
variant="Secondary"
onClick={() => window.open(createIssueUrl(error), '_blank', 'noopener noreferrer')}
fill="Solid"
title="Clicking this button will open a new issue on our GitHub repository with the error details pre-filled. Please review the information before submitting."
>
<Text as="span" size="B400">
Report Issue
</Text>
</Button>
<Box
direction="Column"
gap="300"
style={{
padding: config.space.S300,
backgroundColor: color.Surface.Container,
borderRadius: config.radii.R300,
}}
>
<Text size="T300" style={{ color: color.Critical.Main }}>
{error.message}
</Text>
<Box
style={{
padding: config.space.S200,
backgroundColor: color.Surface.Container,
borderRadius: config.radii.R300,
overflow: 'auto',
maxHeight: '200px',
minHeight: '100px',
}}
>
<Text
as="pre"
size="T200"
style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}
>
{error.stack}
</Text>
</Box>
</Box>
</Box>
<Button
variant="Primary"
onClick={() => window.location.reload()}
fill="Solid"
title="clicking this will reload the page and hopefully lead to a functioning app again :)"
>
<Text as="span" size="B400">
Reload Page
</Text>
</Button>
</Box>
</Dialog>
</Box>
</SplashScreen>
);
}
6 changes: 5 additions & 1 deletion src/app/features/bug-report/BugReportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ async function searchSimilarIssues(query: string, signal: AbortSignal): Promise<

// Field IDs match the ids defined in .github/ISSUE_TEMPLATE/bug_report.yml
// and feature_request.yml so GitHub pre-fills each form field directly.
function buildGitHubUrl(type: ReportType, title: string, fields: Record<string, string>): string {
export function buildGitHubUrl(
type: ReportType,
title: string,
fields: Record<string, string>
): string {
const devLabel = IS_RELEASE_TAG ? '' : '-dev';
const buildLabel = BUILD_HASH ? ` (${BUILD_HASH})` : '';
const version = `v${APP_VERSION}${devLabel}${buildLabel}`;
Expand Down
58 changes: 31 additions & 27 deletions src/app/pages/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { OverlayContainerProvider, PopOutContainerProvider, TooltipContainerProv
import { RouterProvider } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ErrorBoundary } from 'react-error-boundary';

import { ClientConfigLoader } from '$components/ClientConfigLoader';
import { ClientConfigProvider } from '$hooks/useClientConfig';
import { ScreenSizeProvider, useScreenSize } from '$hooks/useScreenSize';
import { useCompositionEndTracking } from '$hooks/useComposingCheck';
import { ErrorPage } from '$components/DefaultErrorPage';
import { ConfigConfigError, ConfigConfigLoading } from './ConfigConfig';
import { FeatureCheck } from './FeatureCheck';
import { createRouter } from './Router';
Expand All @@ -21,33 +23,35 @@ function App() {
const portalContainer = document.getElementById('portalContainer') ?? undefined;

return (
<TooltipContainerProvider value={portalContainer}>
<PopOutContainerProvider value={portalContainer}>
<OverlayContainerProvider value={portalContainer}>
<ScreenSizeProvider value={screenSize}>
<FeatureCheck>
<ClientConfigLoader
fallback={() => <ConfigConfigLoading />}
error={(err, retry, ignore) => (
<ConfigConfigError error={err} retry={retry} ignore={ignore} />
)}
>
{(clientConfig) => (
<ClientConfigProvider value={clientConfig}>
<QueryClientProvider client={queryClient}>
<JotaiProvider>
<RouterProvider router={createRouter(clientConfig, screenSize)} />
</JotaiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</ClientConfigProvider>
)}
</ClientConfigLoader>
</FeatureCheck>
</ScreenSizeProvider>
</OverlayContainerProvider>
</PopOutContainerProvider>
</TooltipContainerProvider>
<ErrorBoundary FallbackComponent={ErrorPage}>
<TooltipContainerProvider value={portalContainer}>
<PopOutContainerProvider value={portalContainer}>
<OverlayContainerProvider value={portalContainer}>
<ScreenSizeProvider value={screenSize}>
<FeatureCheck>
<ClientConfigLoader
fallback={() => <ConfigConfigLoading />}
error={(err, retry, ignore) => (
<ConfigConfigError error={err} retry={retry} ignore={ignore} />
)}
>
{(clientConfig) => (
<ClientConfigProvider value={clientConfig}>
<QueryClientProvider client={queryClient}>
<JotaiProvider>
<RouterProvider router={createRouter(clientConfig, screenSize)} />
</JotaiProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</ClientConfigProvider>
)}
</ClientConfigLoader>
</FeatureCheck>
</ScreenSizeProvider>
</OverlayContainerProvider>
</PopOutContainerProvider>
</TooltipContainerProvider>
</ErrorBoundary>
);
}

Expand Down
Loading