瀏覽代碼

improve error handling & map stack trace (#809)

* improve error handling & map stack trace

* log error message and tweak

* support logging multiple errors

* move dynamic import inside try/catch
David Luzar 5 年之前
父節點
當前提交
80a49e9611
共有 3 個文件被更改,包括 118 次插入14 次删除
  1. 47 0
      package-lock.json
  2. 2 1
      package.json
  3. 69 13
      src/index.tsx

+ 47 - 0
package-lock.json

@@ -4761,6 +4761,14 @@
         "is-arrayish": "^0.2.1"
       }
     },
+    "error-stack-parser": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz",
+      "integrity": "sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==",
+      "requires": {
+        "stackframe": "^1.1.1"
+      }
+    },
     "es-abstract": {
       "version": "1.17.4",
       "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz",
@@ -14120,11 +14128,50 @@
       "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
       "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w=="
     },
+    "stack-generator": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.5.tgz",
+      "integrity": "sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q==",
+      "requires": {
+        "stackframe": "^1.1.1"
+      }
+    },
     "stack-utils": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz",
       "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA=="
     },
+    "stackframe": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.1.1.tgz",
+      "integrity": "sha512-0PlYhdKh6AfFxRyK/v+6/k+/mMfyiEBbTM5L94D0ZytQnJ166wuwoTYLHFWGbs2dpA8Rgq763KGWmN1EQEYHRQ=="
+    },
+    "stacktrace-gps": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.0.4.tgz",
+      "integrity": "sha512-qIr8x41yZVSldqdqe6jciXEaSCKw1U8XTXpjDuy0ki/apyTn/r3w9hDAAQOhZdxvsC93H+WwwEu5cq5VemzYeg==",
+      "requires": {
+        "source-map": "0.5.6",
+        "stackframe": "^1.1.1"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.6",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
+          "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI="
+        }
+      }
+    },
+    "stacktrace-js": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz",
+      "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==",
+      "requires": {
+        "error-stack-parser": "^2.0.6",
+        "stack-generator": "^2.0.5",
+        "stacktrace-gps": "^3.0.4"
+      }
+    },
     "static-extend": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",

+ 2 - 1
package.json

@@ -12,7 +12,8 @@
     "react": "16.12.0",
     "react-dom": "16.12.0",
     "react-scripts": "3.4.0",
-    "roughjs": "4.0.4"
+    "roughjs": "4.0.4",
+    "stacktrace-js": "2.0.2"
   },
   "description": "",
   "devDependencies": {

+ 69 - 13
src/index.tsx

@@ -2322,20 +2322,70 @@ export class App extends React.Component<any, AppState> {
 
 const rootElement = document.getElementById("root");
 
-class TopErrorBoundary extends React.Component {
-  state = { hasError: false, stack: "", localStorage: "" };
+interface TopErrorBoundaryState {
+  unresolvedError: Error[] | null;
+  hasError: boolean;
+  stack: string;
+  localStorage: string;
+}
+
+class TopErrorBoundary extends React.Component<any, TopErrorBoundaryState> {
+  state: TopErrorBoundaryState = {
+    unresolvedError: null,
+    hasError: false,
+    stack: "",
+    localStorage: "",
+  };
 
-  static getDerivedStateFromError(error: any) {
-    console.error(error);
-    return {
+  componentDidCatch(error: Error) {
+    const _localStorage: any = {};
+    for (const [key, value] of Object.entries({ ...localStorage })) {
+      try {
+        _localStorage[key] = JSON.parse(value);
+      } catch (err) {
+        _localStorage[key] = value;
+      }
+    }
+    this.setState(state => ({
       hasError: true,
-      localStorage: JSON.stringify({ ...localStorage }),
-      stack: error.stack,
-    };
+      unresolvedError: state.unresolvedError
+        ? state.unresolvedError.concat(error)
+        : [error],
+      localStorage: JSON.stringify(_localStorage),
+    }));
+  }
+
+  async componentDidUpdate() {
+    if (this.state.unresolvedError !== null) {
+      let stack = "";
+      for (const error of this.state.unresolvedError) {
+        if (stack) {
+          stack += `\n\n================\n\n`;
+        }
+        stack += `${error.message}:\n\n`;
+        try {
+          const StackTrace = await import("stacktrace-js");
+          stack += (await StackTrace.fromError(error)).join("\n");
+        } catch (err) {
+          console.error(err);
+          stack += error.stack || "";
+        }
+      }
+
+      this.setState(state => ({
+        unresolvedError: null,
+        stack: `${
+          state.stack ? `${state.stack}\n\n================\n\n${stack}` : stack
+        }`,
+      }));
+    }
   }
 
   private selectTextArea(event: React.MouseEvent<HTMLTextAreaElement>) {
-    (event.target as HTMLTextAreaElement).select();
+    if (event.target !== document.activeElement) {
+      event.preventDefault();
+      (event.target as HTMLTextAreaElement).select();
+    }
   }
 
   private async createGithubIssue() {
@@ -2391,14 +2441,20 @@ class TopErrorBoundary extends React.Component {
                   <label>Error stack trace:</label>
                   <textarea
                     rows={10}
-                    onClick={this.selectTextArea}
-                    defaultValue={this.state.stack}
+                    onPointerDown={this.selectTextArea}
+                    readOnly={true}
+                    value={
+                      this.state.unresolvedError
+                        ? "Loading data. please wait..."
+                        : this.state.stack
+                    }
                   />
                   <label>LocalStorage content:</label>
                   <textarea
                     rows={5}
-                    onClick={this.selectTextArea}
-                    defaultValue={this.state.localStorage}
+                    onPointerDown={this.selectTextArea}
+                    readOnly={true}
+                    value={this.state.localStorage}
                   />
                 </div>
               </div>