summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/AppErrorBoundary.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/client/shared/components/AppErrorBoundary.js
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/shared/components/AppErrorBoundary.js')
-rw-r--r--devtools/client/shared/components/AppErrorBoundary.js163
1 files changed, 163 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..de29b3fc6f
--- /dev/null
+++ b/devtools/client/shared/components/AppErrorBoundary.js
@@ -0,0 +1,163 @@
+/* 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
+// Add format=__default__ to make sure users without EDITBUGS permission still
+// use the regular UI to create bugs, including the prefilled description.
+const bugLink =
+ "https://bugzilla.mozilla.org/enter_bug.cgi?format=__default__&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;