240 lines
7.1 KiB
JavaScript
240 lines
7.1 KiB
JavaScript
/* 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.mjs");
|
|
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
|
|
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
|
|
const { div, h1, h2, h3, p, a, button } = 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,
|
|
openLink: PropTypes.func,
|
|
};
|
|
}
|
|
|
|
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)
|
|
.filter(key => info[key])
|
|
.map((obj, outerIdx) => {
|
|
switch (obj) {
|
|
case "componentStack": {
|
|
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"),
|
|
traceParts
|
|
);
|
|
}
|
|
case "clientPacket":
|
|
case "serverPacket": {
|
|
// Only serverPacket has a stack.
|
|
const stack = info[obj].stack;
|
|
const traceParts = stack
|
|
? stack
|
|
.split("\n")
|
|
.map((part, idx) => p({ key: `strace${idx}` }, part))
|
|
: null;
|
|
return div(
|
|
{ className: "stack-trace-section" },
|
|
h3(
|
|
{},
|
|
obj == "clientPacket" ? "Client packet" : "Server packet"
|
|
),
|
|
// Display the packet as JSON, while removing the artifical `stack` attribute from it
|
|
p(
|
|
{},
|
|
JSON.stringify({ ...info[obj], stack: undefined }, null, 2)
|
|
),
|
|
stack ? h3({}, "Server stack") : null,
|
|
traceParts
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
}
|
|
|
|
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
|
|
);
|
|
}
|
|
|
|
renderServerPacket(packet) {
|
|
const traceParts = packet.stack
|
|
.split("\n")
|
|
.map((part, idx) => p({ key: `strace${idx}` }, part));
|
|
return [
|
|
div(
|
|
{ className: "stack-trace-section" },
|
|
h3({}, "Server packet"),
|
|
p({}, JSON.stringify(packet, null, 2))
|
|
),
|
|
div(
|
|
{ className: "stack-trace-section" },
|
|
h3({}, "Server Stack"),
|
|
traceParts
|
|
),
|
|
];
|
|
}
|
|
|
|
// 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 { componentStack, clientPacket, serverPacket } = this.state.errorInfo;
|
|
|
|
let msg = `## Error in ${this.props.panel}: \n${this.state.errorMsg}\n\n`;
|
|
|
|
if (componentStack) {
|
|
msg += `## React Component Stack:${componentStack}\n\n`;
|
|
}
|
|
|
|
if (clientPacket) {
|
|
msg += `## Client Packet:\n\`\`\`\n${JSON.stringify(clientPacket, null, 2)}\n\`\`\`\n\n`;
|
|
}
|
|
|
|
if (serverPacket) {
|
|
// Display the packet as JSON, while removing the artifical `stack` attribute from it
|
|
msg += `## Server Packet:\n\`\`\`\n${JSON.stringify({ ...serverPacket, stack: undefined }, null, 2)}\n\`\`\`\n\n`;
|
|
msg += `## Server Stack:\n\`\`\`\n${serverPacket.stack}\n\`\`\`\n\n`;
|
|
}
|
|
|
|
msg += `## Stacktrace: \n\`\`\`\n${this.state.errorStack}\n\`\`\``;
|
|
|
|
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
|
|
);
|
|
|
|
const href = this.getBugLink();
|
|
|
|
return div(
|
|
{
|
|
className: `app-error-panel`,
|
|
},
|
|
h1({ className: "error-panel-header" }, errorDescription),
|
|
a(
|
|
{
|
|
className: "error-panel-file-button",
|
|
href,
|
|
target: "_blank",
|
|
onClick: this.props.openLink
|
|
? e => this.props.openLink(href, e)
|
|
: null,
|
|
},
|
|
FILE_BUG_BUTTON
|
|
),
|
|
this.state.toolbox
|
|
? button({
|
|
className: "devtools-tabbar-button error-panel-close",
|
|
onClick: () => {
|
|
this.state.toolbox.closeToolbox();
|
|
},
|
|
})
|
|
: null,
|
|
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;
|