Improved error boundary with option to clear cache on repeated errors… (#9891)

* Improved error boundary with option to clear cache on repeated errors and more detail for reporting

* fix

* db
This commit is contained in:
Tom Moor
2025-08-10 15:24:12 -04:00
committed by GitHub
parent c3f93a3e9d
commit 013a7a6d39
4 changed files with 117 additions and 15 deletions

View File

@@ -1,3 +1,4 @@
import Storage from "@shared/utils/Storage";
import copy from "copy-to-clipboard";
import {
BeakerIcon,
@@ -127,6 +128,17 @@ export const clearIndexedDB = createAction({
},
});
export const clearStorage = createAction({
name: ({ t }) => t("Clear local storage"),
icon: <TrashIcon />,
keywords: "cache clear localstorage",
section: DeveloperSection,
perform: ({ t }) => {
Storage.clear();
toast.success(t("Local storage cleared"));
},
});
export const createTestUsers = createAction({
name: "Create 10 test users",
icon: <UserIcon />,
@@ -201,6 +213,7 @@ export const developer = createAction({
createToast,
createTestUsers,
clearIndexedDB,
clearStorage,
startTyping,
],
});

View File

@@ -13,6 +13,9 @@ import Text from "~/components/Text";
import env from "~/env";
import Logger from "~/utils/Logger";
import isCloudHosted from "~/utils/isCloudHosted";
import Storage from "@shared/utils/Storage";
import { deleteAllDatabases } from "~/utils/developer";
import Flex from "./Flex";
type Props = WithTranslation & {
/** Whether to reload the page if a chunk fails to load. */
@@ -23,6 +26,9 @@ type Props = WithTranslation & {
component?: React.ComponentType | string;
};
const ERROR_TRACKING_KEY = "error-boundary-tracking";
const ERROR_TRACKING_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
@observer
class ErrorBoundary extends React.Component<Props> {
@observable
@@ -31,6 +37,13 @@ class ErrorBoundary extends React.Component<Props> {
@observable
showDetails = false;
@observable
isRepeatedError = false;
componentDidMount() {
this.checkForPreviousErrors();
}
componentDidCatch(error: Error) {
this.error = error;
@@ -46,9 +59,47 @@ class ErrorBoundary extends React.Component<Props> {
return;
}
this.trackError();
Logger.error("ErrorBoundary", error);
}
private checkForPreviousErrors = () => {
try {
const stored = Storage.get(ERROR_TRACKING_KEY);
if (!stored) {
return;
}
const errors: number[] = JSON.parse(stored);
const cutoff = Date.now() - ERROR_TRACKING_WINDOW_MS;
const recentErrors = errors.filter((timestamp) => timestamp > cutoff);
this.isRepeatedError = recentErrors.length > 0;
} catch (err) {
Logger.warn("Failed to parse stored errors for error boundary", { err });
}
};
private trackError = () => {
try {
const stored = Storage.get(ERROR_TRACKING_KEY);
const errors: number[] = stored ? JSON.parse(stored) : [];
const cutoff = Date.now() - ERROR_TRACKING_WINDOW_MS;
// Filter out old errors and add current one
const updatedErrors = [
...errors.filter((timestamp) => timestamp > cutoff),
Date.now(),
];
Storage.set(ERROR_TRACKING_KEY, JSON.stringify(updatedErrors));
this.isRepeatedError = updatedErrors.length > 1;
} catch (err) {
Logger.warn("Failed to track error in error boundary", { err });
}
};
handleReload = () => {
window.location.reload();
};
@@ -61,6 +112,12 @@ class ErrorBoundary extends React.Component<Props> {
window.open(isCloudHosted ? UrlHelper.contact : UrlHelper.github);
};
handleClearCache = async () => {
await deleteAllDatabases();
Storage.clear();
window.location.reload();
};
render() {
const { t, component: Component = CenteredContent, showTitle } = this.props;
@@ -107,29 +164,46 @@ class ErrorBoundary extends React.Component<Props> {
</Heading>
</>
)}
<Text as="p" type="secondary">
<Trans
defaults="Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch."
values={{
notified: isReported
? ` ${t("our engineers have been notified")}`
: undefined,
}}
/>
</Text>
{this.showDetails && <Pre>{error.toString()}</Pre>}
<p>
<Button onClick={this.handleReload}>{t("Reload")}</Button>{" "}
{this.isRepeatedError ? (
<Text as="p" type="secondary">
<Trans>
An error has occurred multiple times recently. If it continues
please try clearing the cache or using a different browser.
</Trans>
</Text>
) : (
<Text as="p" type="secondary">
<Trans
defaults="Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch."
values={{
notified: isReported
? ` ${t("our engineers have been notified")}`
: undefined,
}}
/>
</Text>
)}
{this.showDetails && <Pre>{error.stack}</Pre>}
<Flex gap={8} wrap>
{this.isRepeatedError && (
<Button onClick={this.handleClearCache}>
<Trans>Clear cache + reload</Trans>
</Button>
)}
<Button onClick={this.handleReload} neutral={this.isRepeatedError}>
{t("Reload")}
</Button>
{this.showDetails ? (
<Button onClick={this.handleReportBug} neutral>
<Trans>Report a bug</Trans>
<Trans>Report a bug</Trans>
</Button>
) : (
<Button onClick={this.handleShowDetails} neutral>
<Trans>Show detail</Trans>
</Button>
)}
</p>
</Flex>
</Component>
);
}

View File

@@ -40,6 +40,8 @@
"Copy ID": "Copy ID",
"Clear IndexedDB cache": "Clear IndexedDB cache",
"IndexedDB cache cleared": "IndexedDB cache cleared",
"Clear local storage": "Clear local storage",
"Local storage cleared": "Local storage cleared",
"Toggle debug logging": "Toggle debug logging",
"Debug logging enabled": "Debug logging enabled",
"Debug logging disabled": "Debug logging disabled",
@@ -257,8 +259,10 @@
"Sorry, part of the application failed to load. This may be because it was updated since you opened the tab or because of a failed network request. Please try reloading.": "Sorry, part of the application failed to load. This may be because it was updated since you opened the tab or because of a failed network request. Please try reloading.",
"Reload": "Reload",
"Something Unexpected Happened": "Something Unexpected Happened",
"An error has occurred multiple times recently. If it continues please try clearing the cache or using a different browser.": "An error has occurred multiple times recently. If it continues please try clearing the cache or using a different browser.",
"Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch.": "Sorry, an unrecoverable error occurred{{notified}}. Please try reloading the page, it may have been a temporary glitch.",
"our engineers have been notified": "our engineers have been notified",
"Clear cache + reload": "Clear cache + reload",
"Show detail": "Show detail",
"{{userName}} archived": "{{userName}} archived",
"{{userName}} restored": "{{userName}} restored",

View File

@@ -68,6 +68,17 @@ class Storage {
// Ignore errors
}
}
/**
* Clear all values from storage.
*/
public clear() {
try {
this.interface.clear();
} catch (_err) {
// Ignore errors
}
}
}
/**