From 566e046716f472768f38ba08ea8ee5fe59cbb9d1 Mon Sep 17 00:00:00 2001 From: Mengdi Chen Date: Fri, 25 Mar 2022 15:47:07 -0400 Subject: [PATCH] [ReactDevTools] custom view for errors occur in user's code --- .../src/backend/renderer.js | 23 ++++++++- .../src/backend/types.js | 10 ++++ .../react-devtools-shared/src/backendAPI.js | 2 +- .../src/devtools/views/Components/types.js | 1 + .../views/ErrorBoundary/ErrorBoundary.js | 17 ++++++- .../views/ErrorBoundary/UserErrorView.js | 47 +++++++++++++++++++ .../src/{ => errors}/TimeoutError.js | 0 .../src/errors/UserError.js | 21 +++++++++ .../src/inspectedElementMutableSource.js | 14 +++++- 9 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 packages/react-devtools-shared/src/devtools/views/ErrorBoundary/UserErrorView.js rename packages/react-devtools-shared/src/{ => errors}/TimeoutError.js (100%) create mode 100644 packages/react-devtools-shared/src/errors/UserError.js diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 3c18ce4083..10aa4a0dcb 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -60,7 +60,7 @@ import { TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS, TREE_OPERATION_UPDATE_TREE_BASE_DURATION, } from '../constants'; -import {inspectHooksOfFiber} from 'react-debug-tools'; +import {inspectHooksOfFiber, ErrorsNames as DebugToolsErrors} from 'react-debug-tools'; import { patch as patchConsole, registerRenderer as registerRendererWithConsole, @@ -3616,6 +3616,27 @@ export function attach( try { mostRecentlyInspectedElement = inspectElementRaw(id); } catch (error) { + if (error.name === DebugToolsErrors.RENDER_FUNCTION_ERROR) { + let message = 'Error rendering inspected element.'; + let stack; + console.error(message + '\n\n', error); + if (error.cause != null) { + console.error('Original error causing above error: \n\n', error.cause); + if (error.cause instanceof Error) { + message = error.cause.message || message; + stack = error.cause.stack; + } + } + + return { + type: 'user-error', + id, + responseID: requestID, + message, + stack, + }; + } + console.error('Error inspecting element.\n\n', error); return { diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 4d975dbfec..11019d3f0e 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -281,6 +281,7 @@ export type InspectedElement = {| |}; export const InspectElementErrorType = 'error'; +export const InspectElementUserErrorType = 'user-error'; export const InspectElementFullDataType = 'full-data'; export const InspectElementNoChangeType = 'no-change'; export const InspectElementNotFoundType = 'not-found'; @@ -293,6 +294,14 @@ export type InspectElementError = {| stack: string, |}; +export type InspectElementUserError = {| + id: number, + responseID: number, + type: 'user-error', + message: string, + stack: ?string, +|}; + export type InspectElementFullData = {| id: number, responseID: number, @@ -322,6 +331,7 @@ export type InspectElementNotFound = {| export type InspectedElementPayload = | InspectElementError + | InspectElementUserError | InspectElementFullData | InspectElementHydratedPath | InspectElementNoChange diff --git a/packages/react-devtools-shared/src/backendAPI.js b/packages/react-devtools-shared/src/backendAPI.js index 3849899b7d..adf0c5e8b0 100644 --- a/packages/react-devtools-shared/src/backendAPI.js +++ b/packages/react-devtools-shared/src/backendAPI.js @@ -10,7 +10,7 @@ import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration'; import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils'; import Store from 'react-devtools-shared/src/devtools/store'; -import TimeoutError from 'react-devtools-shared/src/TimeoutError'; +import TimeoutError from 'react-devtools-shared/src/errors/TimeoutError'; import type { InspectedElement as InspectedElementBackend, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/types.js b/packages/react-devtools-shared/src/devtools/views/Components/types.js index c6789fa1e9..d6918a0813 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/types.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/types.js @@ -63,6 +63,7 @@ export type OwnersList = {| export type InspectedElementResponseType = | 'error' + | 'user-error' | 'full-data' | 'hydrated-path' | 'no-change' diff --git a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js index f4994e23a9..c770ce1b43 100644 --- a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js +++ b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js @@ -15,8 +15,10 @@ import ErrorView from './ErrorView'; import SearchingGitHubIssues from './SearchingGitHubIssues'; import SuspendingErrorView from './SuspendingErrorView'; import TimeoutView from './TimeoutView'; +import UserErrorView from './UserErrorView'; import UnsupportedBridgeOperationError from 'react-devtools-shared/src/UnsupportedBridgeOperationError'; -import TimeoutError from 'react-devtools-shared/src/TimeoutError'; +import TimeoutError from 'react-devtools-shared/src/errors/TimeoutError'; +import UserError from 'react-devtools-shared/src/errors/UserError'; import {logEvent} from 'react-devtools-shared/src/Logger'; type Props = {| @@ -34,6 +36,7 @@ type State = {| hasError: boolean, isUnsupportedBridgeOperationError: boolean, isTimeout: boolean, + isUserError: boolean, |}; const InitialState: State = { @@ -44,6 +47,7 @@ const InitialState: State = { hasError: false, isUnsupportedBridgeOperationError: false, isTimeout: false, + isUserError: false, }; export default class ErrorBoundary extends Component { @@ -58,6 +62,7 @@ export default class ErrorBoundary extends Component { : null; const isTimeout = error instanceof TimeoutError; + const isUserError = error instanceof UserError; const isUnsupportedBridgeOperationError = error instanceof UnsupportedBridgeOperationError; @@ -77,6 +82,7 @@ export default class ErrorBoundary extends Component { hasError: true, isUnsupportedBridgeOperationError, isTimeout, + isUserError, }; } @@ -111,6 +117,7 @@ export default class ErrorBoundary extends Component { hasError, isUnsupportedBridgeOperationError, isTimeout, + isUserError, } = this.state; if (hasError) { @@ -133,6 +140,14 @@ export default class ErrorBoundary extends Component { errorMessage={errorMessage} /> ); + } else if (isUserError) { + return ( + + ); } else { return ( + {children} +
+
+
+ {errorMessage || 'Error occured in inspected element'} +
+
+
+ This is likely to be caused by implementation of current inspected element. +
+ {!!callStack && ( +
+ The error was thrown {callStack.trim()} +
+ )} +
+ + ); + } + \ No newline at end of file diff --git a/packages/react-devtools-shared/src/TimeoutError.js b/packages/react-devtools-shared/src/errors/TimeoutError.js similarity index 100% rename from packages/react-devtools-shared/src/TimeoutError.js rename to packages/react-devtools-shared/src/errors/TimeoutError.js diff --git a/packages/react-devtools-shared/src/errors/UserError.js b/packages/react-devtools-shared/src/errors/UserError.js new file mode 100644 index 0000000000..752dbf9e88 --- /dev/null +++ b/packages/react-devtools-shared/src/errors/UserError.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + + export default class UserError extends Error { + constructor(message: string) { + super(message); + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, UserError); + } + + this.name = 'UserError'; + } +} diff --git a/packages/react-devtools-shared/src/inspectedElementMutableSource.js b/packages/react-devtools-shared/src/inspectedElementMutableSource.js index b5ea32b960..329f174f15 100644 --- a/packages/react-devtools-shared/src/inspectedElementMutableSource.js +++ b/packages/react-devtools-shared/src/inspectedElementMutableSource.js @@ -8,6 +8,7 @@ */ import LRU from 'lru-cache'; +import {UserHookError} from 'react-debug-tools'; import { convertInspectedElementBackendToFrontend, hydrateHelper, @@ -19,6 +20,7 @@ import type {LRUCache} from 'react-devtools-shared/src/types'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; import type { InspectElementError, + InspectElementUserError, InspectElementFullData, InspectElementHydratedPath, } from 'react-devtools-shared/src/backend/types'; @@ -27,6 +29,7 @@ import type { InspectedElement as InspectedElementFrontend, InspectedElementResponseType, } from 'react-devtools-shared/src/devtools/views/Components/types'; +import UserError from 'react-devtools-shared/src/errors/UserError'; // Maps element ID to inspected data. // We use an LRU for this rather than a WeakMap because of how the "no-change" optimization works. @@ -80,7 +83,7 @@ export function inspectElement({ let inspectedElement; switch (type) { - case 'error': + case 'error': { const {message, stack} = ((data: any): InspectElementError); // The backend's stack (where the error originated) is more meaningful than this stack. @@ -88,6 +91,15 @@ export function inspectElement({ error.stack = stack; throw error; + } + + case 'user-error': { + const {message, stack} = (data: InspectElementUserError); + // Trying to keep useful information from user's side. + const error = new UserError(message); + error.stack = stack || error.stack; + throw error; + } case 'no-change': // This is a no-op for the purposes of our cache.