diff options
Diffstat (limited to 'devtools/client/shared/components/AppErrorBoundary.js')
-rw-r--r-- | devtools/client/shared/components/AppErrorBoundary.js | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/devtools/client/shared/components/AppErrorBoundary.js b/devtools/client/shared/components/AppErrorBoundary.js new file mode 100644 index 0000000000..66f839f8a9 --- /dev/null +++ b/devtools/client/shared/components/AppErrorBoundary.js @@ -0,0 +1,161 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// React deps +const { + Component, +} = require("resource://devtools/client/shared/vendor/react.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const { div, h1, h2, h3, p, a } = dom; + +// Localized strings for (devtools/client/locales/en-US/components.properties) +loader.lazyGetter(this, "L10N", function () { + const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); + return new LocalizationHelper( + "devtools/client/locales/components.properties" + ); +}); + +loader.lazyGetter(this, "FILE_BUG_BUTTON", function () { + return L10N.getStr("appErrorBoundary.fileBugButton"); +}); + +loader.lazyGetter(this, "RELOAD_PAGE_INFO", function () { + return L10N.getStr("appErrorBoundary.reloadPanelInfo"); +}); + +// File a bug for the selected component specifically +const bugLink = + "https://bugzilla.mozilla.org/enter_bug.cgi?product=DevTools&component="; + +/** + * Error boundary that wraps around the a given component. + */ +class AppErrorBoundary extends Component { + static get propTypes() { + return { + children: PropTypes.any.isRequired, + panel: PropTypes.any.isRequired, + componentName: PropTypes.string.isRequired, + }; + } + + constructor(props) { + super(props); + + this.state = { + errorMsg: "No error", + errorStack: null, + errorInfo: null, + }; + } + + /** + * Map the `info` object to a render. + * Currently, `info` usually just contains something similar to the + * following object (which is provided to componentDidCatch): + * componentStack: {"\n in (component) \n in (other component)..."} + */ + renderErrorInfo(info = {}) { + if (Object.keys(info).length) { + return Object.keys(info).map((obj, outerIdx) => { + const traceParts = info[obj] + .split("\n") + .map((part, idx) => p({ key: `strace${idx}` }, part)); + return div( + { key: `st-div-${outerIdx}`, className: "stack-trace-section" }, + h3({}, "React Component Stack"), + p({ key: `st-p-${outerIdx}` }, obj.toString()), + traceParts + ); + }); + } + + return p({}, "undefined errorInfo"); + } + + renderStackTrace(stacktrace = "") { + const re = /:\d+:\d+/g; + const traces = stacktrace + .replace(re, "$&,") + .split(",") + .map((trace, index) => { + return p({ key: `rst-${index}` }, trace); + }); + + return div( + { className: "stack-trace-section" }, + h3({}, "Stacktrace"), + traces + ); + } + + // Return a valid object, even if we don't receive one + getValidInfo(infoObj) { + if (!infoObj.componentStack) { + try { + return { componentStack: JSON.stringify(infoObj) }; + } catch (err) { + return { componentStack: `Unknown Error: ${err}` }; + } + } + return infoObj; + } + + // Called when a child component throws an error. + componentDidCatch(error, info) { + const validInfo = this.getValidInfo(info); + this.setState({ + errorMsg: error.toString(), + errorStack: error.stack, + errorInfo: validInfo, + }); + } + + getBugLink() { + const compStack = this.getValidInfo(this.state.errorInfo).componentStack; + const errorMsg = this.state.errorMsg; + const errorStack = this.state.errorStack; + const msg = `Error: \n${errorMsg}\n\nReact Component Stack: ${compStack}\n\nStacktrace: \n${errorStack}`; + return `${bugLink}${this.props.componentName}&comment=${encodeURIComponent( + msg + )}`; + } + + render() { + if (this.state.errorInfo !== null) { + // "The (componentDesc) has crashed" + const errorDescription = L10N.getFormatStr( + "appErrorBoundary.description", + this.props.panel + ); + return div( + { + className: `app-error-panel`, + }, + h1({ className: "error-panel-header" }, errorDescription), + a( + { + className: "error-panel-file-button", + href: this.getBugLink(), + onClick: () => { + window.open(this.getBugLink(), "_blank"); + }, + }, + FILE_BUG_BUTTON + ), + h2({ className: "error-panel-error" }, this.state.errorMsg), + div({}, this.renderErrorInfo(this.state.errorInfo)), + div({}, this.renderStackTrace(this.state.errorStack)), + p({ className: "error-panel-reload-info" }, RELOAD_PAGE_INFO) + ); + } + return this.props.children; + } +} + +module.exports = AppErrorBoundary; |