feat: report detail recoverable error (#6412)

* feat: report detail recoverable error

* chore: changelog

* fix: optimize code

* Update runClientApp.tsx
This commit is contained in:
ClarkXia 2023-07-25 17:12:10 +08:00 committed by GitHub
parent a96af97bd1
commit d33f3e652a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 60 additions and 13 deletions

View File

@ -0,0 +1,5 @@
---
'@ice/runtime': patch
---
feat: report detail recoverable error

View File

@ -0,0 +1,39 @@
import type { ErrorStack } from './types.js';
interface ErrorOptions {
ignoreRuntimeWarning?: boolean;
}
function isRuntimeWarning(error: unknown) {
return error instanceof Error ? [
'This Suspense boundary received an update before it finished hydrating.',
].some((message) => error?.message?.includes(message)) : false;
}
export const defaultOnRecoverableError = typeof reportError === 'function'
? reportError
: function (error: unknown) {
console['error'](error);
};
const reportRecoverableError = (error: unknown, errorStack: ErrorStack, options?: ErrorOptions) => {
const ignoreError = options?.ignoreRuntimeWarning && isRuntimeWarning(error);
if (!ignoreError) {
if (process.env.NODE_ENV === 'production') {
// Report error stack in production by default.
if (errorStack?.componentStack && error instanceof Error) {
const detailError = new Error(error.message);
detailError.name = error.name;
detailError.stack = `${error.name}: ${error.message}${errorStack.componentStack}`;
defaultOnRecoverableError(detailError);
return;
}
}
// Fallback to default error handler.
defaultOnRecoverableError(error);
}
};
export default reportRecoverableError;

View File

@ -4,6 +4,7 @@ import { createHashHistory, createBrowserHistory, createMemoryHistory } from '@r
import type { History } from '@remix-run/router';
import type {
AppContext, WindowContext, AppExport, RouteItem, RuntimeModules, AppConfig, AssetsManifest, ClientAppRouterProps,
ErrorStack,
} from './types.js';
import { createHistory as createHistorySingle } from './singleRouter.js';
import { setHistory } from './history.js';
@ -19,6 +20,7 @@ import ClientRouter from './ClientRouter.js';
import addLeadingSlash from './utils/addLeadingSlash.js';
import { AppContextProvider } from './AppContext.js';
import { deprecatedHistory } from './utils/deprecatedHistory.js';
import reportRecoverableError from './reportRecoverableError.js';
export interface RunClientAppOptions {
app: AppExport;
@ -32,6 +34,7 @@ export interface RunClientAppOptions {
dataLoaderDecorator?: Function;
}
export default async function runClientApp(options: RunClientAppOptions) {
const {
app,
@ -105,20 +108,14 @@ export default async function runClientApp(options: RunClientAppOptions) {
const needHydrate = hydrate && !downgrade && !documentOnly;
if (needHydrate) {
const defaultOnRecoverableError = typeof reportError === 'function' ? reportError
: function (error: unknown) {
console['error'](error);
};
runtime.setRender((container, element) => {
const hydrateOptions = revalidate
? {
onRecoverableError(error: unknown) {
// Ignore this error caused by router.revalidate
if ((error as Error)?.message?.indexOf('This Suspense boundary received an update before it finished hydrating.') == -1) {
defaultOnRecoverableError(error);
}
},
} : {};
const hydrateOptions: ReactDOM.HydrationOptions = {
// @ts-ignore react-dom do not define the type of second argument of onRecoverableError.
onRecoverableError: appConfig?.app?.onRecoverableError ||
((error: unknown, errorInfo: ErrorStack) => {
reportRecoverableError(error, errorInfo, { ignoreRuntimeWarning: revalidate });
}),
};
return ReactDOM.hydrateRoot(container, element, hydrateOptions);
});
}

View File

@ -14,8 +14,14 @@ type App = Partial<{
rootId: string;
strict: boolean;
errorBoundary: boolean;
onRecoverableError: (error: unknown, errorInfo: ErrorStack) => void;
} & Record<AppLifecycle, VoidFunction>>;
export interface ErrorStack {
componentStack?: string;
digest?: string;
}
export type AppData = any;
export type RouteData = any;