mirror of
https://github.com/outline/outline.git
synced 2026-01-06 02:59:54 -06:00
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:
@@ -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,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -68,6 +68,17 @@ class Storage {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all values from storage.
|
||||
*/
|
||||
public clear() {
|
||||
try {
|
||||
this.interface.clear();
|
||||
} catch (_err) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user