AppErrorBoundary.js (7539B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 // React deps 8 const { 9 Component, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 12 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 13 const { div, h1, h2, h3, p, a, button } = dom; 14 15 // Localized strings for (devtools/client/locales/en-US/components.properties) 16 loader.lazyGetter(this, "L10N", function () { 17 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); 18 return new LocalizationHelper( 19 "devtools/client/locales/components.properties" 20 ); 21 }); 22 23 loader.lazyGetter(this, "FILE_BUG_BUTTON", function () { 24 return L10N.getStr("appErrorBoundary.fileBugButton"); 25 }); 26 27 loader.lazyGetter(this, "RELOAD_PAGE_INFO", function () { 28 return L10N.getStr("appErrorBoundary.reloadPanelInfo"); 29 }); 30 31 // File a bug for the selected component specifically 32 // Add format=__default__ to make sure users without EDITBUGS permission still 33 // use the regular UI to create bugs, including the prefilled description. 34 const bugLink = 35 "https://bugzilla.mozilla.org/enter_bug.cgi?format=__default__&blocked=devtools-toolbox-crash&product=DevTools&component="; 36 37 /** 38 * Error boundary that wraps around the a given component. 39 */ 40 class AppErrorBoundary extends Component { 41 static get propTypes() { 42 return { 43 children: PropTypes.any.isRequired, 44 panel: PropTypes.any.isRequired, 45 componentName: PropTypes.string.isRequired, 46 openLink: PropTypes.func, 47 }; 48 } 49 50 constructor(props) { 51 super(props); 52 53 this.state = { 54 errorMsg: "No error", 55 errorStack: null, 56 errorInfo: null, 57 }; 58 } 59 60 /** 61 * Map the `info` object to a render. 62 * Currently, `info` usually just contains something similar to the 63 * following object (which is provided to componentDidCatch): 64 * componentStack: {"\n in (component) \n in (other component)..."} 65 */ 66 renderErrorInfo(info = {}) { 67 if (Object.keys(info).length) { 68 return Object.keys(info) 69 .filter(key => info[key]) 70 .map((obj, outerIdx) => { 71 switch (obj) { 72 case "componentStack": { 73 const traceParts = info[obj] 74 .split("\n") 75 .map((part, idx) => p({ key: `strace${idx}` }, part)); 76 return div( 77 { key: `st-div-${outerIdx}`, className: "stack-trace-section" }, 78 h3({}, "React Component Stack"), 79 traceParts 80 ); 81 } 82 case "clientPacket": 83 case "serverPacket": { 84 // Only serverPacket has a stack. 85 const stack = info[obj].stack; 86 const traceParts = stack 87 ? stack 88 .split("\n") 89 .map((part, idx) => p({ key: `strace${idx}` }, part)) 90 : null; 91 return div( 92 { className: "stack-trace-section" }, 93 h3( 94 {}, 95 obj == "clientPacket" ? "Client packet" : "Server packet" 96 ), 97 // Display the packet as JSON, while removing the artifical `stack` attribute from it 98 p( 99 {}, 100 JSON.stringify({ ...info[obj], stack: undefined }, null, 2) 101 ), 102 stack ? h3({}, "Server stack") : null, 103 traceParts 104 ); 105 } 106 } 107 return null; 108 }); 109 } 110 111 return p({}, "undefined errorInfo"); 112 } 113 114 renderStackTrace(stacktrace = "") { 115 const re = /:\d+:\d+/g; 116 const traces = stacktrace 117 .replace(re, "$&,") 118 .split(",") 119 .map((trace, index) => { 120 return p({ key: `rst-${index}` }, trace); 121 }); 122 123 return div( 124 { className: "stack-trace-section" }, 125 h3({}, "Stacktrace"), 126 traces 127 ); 128 } 129 130 renderServerPacket(packet) { 131 const traceParts = packet.stack 132 .split("\n") 133 .map((part, idx) => p({ key: `strace${idx}` }, part)); 134 return [ 135 div( 136 { className: "stack-trace-section" }, 137 h3({}, "Server packet"), 138 p({}, JSON.stringify(packet, null, 2)) 139 ), 140 div( 141 { className: "stack-trace-section" }, 142 h3({}, "Server Stack"), 143 traceParts 144 ), 145 ]; 146 } 147 148 // Return a valid object, even if we don't receive one 149 getValidInfo(infoObj) { 150 if (!infoObj.componentStack) { 151 try { 152 return { componentStack: JSON.stringify(infoObj) }; 153 } catch (err) { 154 return { componentStack: `Unknown Error: ${err}` }; 155 } 156 } 157 return infoObj; 158 } 159 160 // Called when a child component throws an error. 161 componentDidCatch(error, info) { 162 const validInfo = this.getValidInfo(info); 163 this.setState({ 164 errorMsg: error.toString(), 165 errorStack: error.stack, 166 errorInfo: validInfo, 167 }); 168 } 169 170 getBugLink() { 171 const { componentStack, clientPacket, serverPacket } = this.state.errorInfo; 172 173 let msg = 174 "## Steps to reproduce:\n\n" + 175 "If possible, please share specific steps to reproduce the error.\n" + 176 "Otherwise add any additional information useful to investigate the issue.\n\n"; 177 178 msg += `## Error in ${this.props.panel}: \n${this.state.errorMsg}\n\n`; 179 180 if (componentStack) { 181 msg += `## React Component Stack:${componentStack}\n\n`; 182 } 183 184 if (clientPacket) { 185 msg += `## Client Packet:\n\`\`\`\n${JSON.stringify(clientPacket, null, 2)}\n\`\`\`\n\n`; 186 } 187 188 if (serverPacket) { 189 // Display the packet as JSON, while removing the artifical `stack` attribute from it 190 msg += `## Server Packet:\n\`\`\`\n${JSON.stringify({ ...serverPacket, stack: undefined }, null, 2)}\n\`\`\`\n\n`; 191 msg += `## Server Stack:\n\`\`\`\n${serverPacket.stack}\n\`\`\`\n\n`; 192 } 193 194 msg += `## Stacktrace: \n\`\`\`\n${this.state.errorStack}\n\`\`\``; 195 196 return `${bugLink}${this.props.componentName}&comment=${encodeURIComponent( 197 msg 198 )}`; 199 } 200 201 render() { 202 if (this.state.errorInfo !== null) { 203 // "The (componentDesc) has crashed" 204 const errorDescription = L10N.getFormatStr( 205 "appErrorBoundary.description", 206 this.props.panel 207 ); 208 209 const href = this.getBugLink(); 210 211 return div( 212 { 213 className: `app-error-panel`, 214 }, 215 h1({ className: "error-panel-header" }, errorDescription), 216 a( 217 { 218 className: "error-panel-file-button", 219 href, 220 target: "_blank", 221 onClick: this.props.openLink 222 ? e => this.props.openLink(href, e) 223 : null, 224 }, 225 FILE_BUG_BUTTON 226 ), 227 this.state.toolbox 228 ? button({ 229 className: "devtools-tabbar-button error-panel-close", 230 onClick: () => { 231 this.state.toolbox.closeToolbox(); 232 }, 233 }) 234 : null, 235 h2({ className: "error-panel-error" }, this.state.errorMsg), 236 div({}, this.renderErrorInfo(this.state.errorInfo)), 237 div({}, this.renderStackTrace(this.state.errorStack)), 238 p({ className: "error-panel-reload-info" }, RELOAD_PAGE_INFO) 239 ); 240 } 241 return this.props.children; 242 } 243 } 244 245 module.exports = AppErrorBoundary;