mirror of https://github.com/alibaba/ice.git
				
				
				
			feat: report detail recoverable error (#6412)
* feat: report detail recoverable error * chore: changelog * fix: optimize code * Update runClientApp.tsx
This commit is contained in:
		
							parent
							
								
									a96af97bd1
								
							
						
					
					
						commit
						d33f3e652a
					
				|  | @ -0,0 +1,5 @@ | |||
| --- | ||||
| '@ice/runtime': patch | ||||
| --- | ||||
| 
 | ||||
| feat: report detail recoverable error | ||||
|  | @ -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; | ||||
|  | @ -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); | ||||
|     }); | ||||
|   } | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue