tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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;