Bug 1230194 Part 2 - Show stack for console evaluations that throw something, r=nchevobbe.
authorBrian Hackett <bhackett1024@gmail.com>
Tue, 16 Apr 2019 11:51:52 -1000
changeset 534930 068483d9fedf266181d4c297686a81a08388658c
parent 534929 b583dd39759908c9cb42ed6508f1b9349ba30dc0
child 534931 26b7736f1f870568c867ee28dad3ef8e3a812a84
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnchevobbe
bugs1230194
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1230194 Part 2 - Show stack for console evaluations that throw something, r=nchevobbe. Differential Revision: https://phabricator.services.mozilla.com/D27827
devtools/client/webconsole/components/message-types/EvaluationResult.js
devtools/client/webconsole/utils/messages.js
devtools/server/actors/webconsole.js
devtools/server/actors/webconsole/utils.js
devtools/shared/specs/webconsole.js
--- a/devtools/client/webconsole/components/message-types/EvaluationResult.js
+++ b/devtools/client/webconsole/components/message-types/EvaluationResult.js
@@ -15,35 +15,42 @@ const GripMessageBody = require("devtool
 EvaluationResult.displayName = "EvaluationResult";
 
 EvaluationResult.propTypes = {
   dispatch: PropTypes.func.isRequired,
   message: PropTypes.object.isRequired,
   timestampsVisible: PropTypes.bool.isRequired,
   serviceContainer: PropTypes.object,
   maybeScrollToBottom: PropTypes.func,
+  open: PropTypes.bool,
+};
+
+EvaluationResult.defaultProps = {
+  open: false,
 };
 
 function EvaluationResult(props) {
   const {
     dispatch,
     message,
     serviceContainer,
     timestampsVisible,
     maybeScrollToBottom,
+    open,
   } = props;
 
   const {
     source,
     type,
     helperType,
     level,
     id: messageId,
     indent,
     exceptionDocURL,
+    stacktrace,
     frame,
     timeStamp,
     parameters,
     notes,
   } = message;
 
   let messageBody;
   if (typeof message.messageText !== "undefined" && message.messageText !== null) {
@@ -67,25 +74,29 @@ function EvaluationResult(props) {
       helperType,
       maybeScrollToBottom,
     });
   }
 
   const topLevelClasses = ["cm-s-mozilla"];
 
   return Message({
+    dispatch,
     source,
     type,
     level,
     indent,
     topLevelClasses,
     messageBody,
     messageId,
     serviceContainer,
     exceptionDocURL,
+    stacktrace,
+    collapsible: Array.isArray(stacktrace),
+    open,
     frame,
     timeStamp,
     parameters,
     notes,
     timestampsVisible,
     maybeScrollToBottom,
   });
 }
--- a/devtools/client/webconsole/utils/messages.js
+++ b/devtools/client/webconsole/utils/messages.js
@@ -288,16 +288,17 @@ function transformNetworkEventPacket(pac
 }
 
 function transformEvaluationResultPacket(packet) {
   let {
     exceptionMessage,
     errorMessageName,
     exceptionDocURL,
     exception,
+    exceptionStack,
     frame,
     result,
     helperResult,
     timestamp: timeStamp,
     notes,
   } = packet;
 
   const parameter = helperResult && helperResult.object
@@ -323,16 +324,17 @@ function transformEvaluationResultPacket
     source: MESSAGE_SOURCE.JAVASCRIPT,
     type: MESSAGE_TYPE.RESULT,
     helperType: helperResult ? helperResult.type : null,
     level,
     messageText: exceptionMessage,
     parameters: [parameter],
     errorMessageName,
     exceptionDocURL,
+    stacktrace: exceptionStack,
     frame,
     timeStamp,
     notes,
     private: packet.private,
   });
 }
 
 // Helpers
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -1005,17 +1005,17 @@ WebConsoleActor.prototype =
     const evalInfo = evalWithDebugger(input, evalOptions, this);
 
     this.parentActor.threadActor.insideClientEvaluation = false;
 
     const evalResult = evalInfo.result;
     const helperResult = evalInfo.helperResult;
 
     let result, errorDocURL, errorMessage, errorNotes = null, errorGrip = null,
-      frame = null, awaitResult, errorMessageName;
+      frame = null, awaitResult, errorMessageName, exceptionStack;
     if (evalResult) {
       if ("return" in evalResult) {
         result = evalResult.return;
         if (
           mapped &&
           mapped.await &&
           result &&
           result.class === "Promise" &&
@@ -1024,16 +1024,31 @@ WebConsoleActor.prototype =
           awaitResult = result.unsafeDereference();
         }
       } else if ("yield" in evalResult) {
         result = evalResult.yield;
       } else if ("throw" in evalResult) {
         const error = evalResult.throw;
         errorGrip = this.createValueGrip(error);
 
+        exceptionStack = this.prepareStackForRemote(evalResult.stack);
+
+        if (exceptionStack) {
+          // Set the frame based on the topmost stack frame for the exception.
+          const {
+            filename: source,
+            sourceId,
+            lineNumber: line,
+            columnNumber: column,
+          } = exceptionStack[0];
+          frame = { source, sourceId, line, column };
+
+          exceptionStack = WebConsoleUtils.removeFramesAboveDebuggerEval(exceptionStack);
+        }
+
         errorMessage = String(error);
         if (typeof error === "object" && error !== null) {
           try {
             errorMessage = DevToolsUtils.callPropertyOnObject(error, "toString");
           } catch (e) {
             // If the debuggee is not allowed to access the "toString" property
             // of the error object, calling this property from the debuggee's
             // compartment will fail. The debugger should show the error object
@@ -1127,16 +1142,17 @@ WebConsoleActor.prototype =
       from: this.actorID,
       input: input,
       result: resultGrip,
       awaitResult,
       timestamp: timestamp,
       exception: errorGrip,
       exceptionMessage: this._createStringGrip(errorMessage),
       exceptionDocURL: errorDocURL,
+      exceptionStack,
       errorMessageName,
       frame,
       helperResult: helperResult,
       notes: errorNotes,
     };
   },
 
   /**
@@ -1461,42 +1477,55 @@ WebConsoleActor.prototype =
   },
 
   getActorIdForInternalSourceId(id) {
     const actor = this.parentActor.sources.getSourceActorByInternalSourceId(id);
     return actor ? actor.actorID : null;
   },
 
   /**
+   * Prepare a SavedFrame stack to be sent to the client.
+   *
+   * @param SavedFrame errorStack
+   *        Stack for an error we need to send to the client.
+   * @return object
+   *         The object you can send to the remote client.
+   */
+  prepareStackForRemote(errorStack) {
+    // Convert stack objects to the JSON attributes expected by client code
+    // Bug 1348885: If the global from which this error came from has been
+    // nuked, stack is going to be a dead wrapper.
+    if (!errorStack || (Cu && Cu.isDeadWrapper(errorStack))) {
+      return null;
+    }
+    const stack = [];
+    let s = errorStack;
+    while (s) {
+      stack.push({
+        filename: s.source,
+        sourceId: this.getActorIdForInternalSourceId(s.sourceId),
+        lineNumber: s.line,
+        columnNumber: s.column,
+        functionName: s.functionDisplayName,
+      });
+      s = s.parent;
+    }
+    return stack;
+  },
+
+  /**
    * Prepare an nsIScriptError to be sent to the client.
    *
    * @param nsIScriptError pageError
    *        The page error we need to send to the client.
    * @return object
    *         The object you can send to the remote client.
    */
   preparePageErrorForRemote: function(pageError) {
-    let stack = null;
-    // Convert stack objects to the JSON attributes expected by client code
-    // Bug 1348885: If the global from which this error came from has been
-    // nuked, stack is going to be a dead wrapper.
-    if (pageError.stack && !Cu.isDeadWrapper(pageError.stack)) {
-      stack = [];
-      let s = pageError.stack;
-      while (s !== null) {
-        stack.push({
-          filename: s.source,
-          sourceId: this.getActorIdForInternalSourceId(s.sourceId),
-          lineNumber: s.line,
-          columnNumber: s.column,
-          functionName: s.functionDisplayName,
-        });
-        s = s.parent;
-      }
-    }
+    const stack = this.prepareStackForRemote(pageError.stack);
     let lineText = pageError.sourceLine;
     if (lineText && lineText.length > DebuggerServer.LONG_STRING_INITIAL_LENGTH) {
       lineText = lineText.substr(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH);
     }
 
     let notesArray = null;
     const notes = pageError.notes;
     if (notes && notes.length) {
--- a/devtools/server/actors/webconsole/utils.js
+++ b/devtools/server/actors/webconsole/utils.js
@@ -188,16 +188,39 @@ var WebConsoleUtils = {
       case "function":
         return objectWrapper(value);
       default:
         console.error("Failed to provide a grip for value of " + typeof value
                       + ": " + value);
         return null;
     }
   },
+
+  /**
+   * Remove any frames in a stack that are above a debugger-triggered evaluation
+   * and will correspond with devtools server code, which we never want to show
+   * to the user.
+   *
+   * @param array stack
+   *        An array of frames, with the topmost first, and each of which has a
+   *        'filename' property.
+   * @return array
+   *         An array of stack frames with any devtools server frames removed.
+   *         The original array is not modified.
+   */
+  removeFramesAboveDebuggerEval(stack) {
+    // Remove any frames for server code above the debugger eval.
+    const evalIndex = stack.findIndex(({ filename }) => {
+      return filename == "debugger eval code";
+    });
+    if (evalIndex != -1) {
+      return stack.slice(0, evalIndex + 1);
+    }
+    return stack;
+  },
 };
 
 exports.WebConsoleUtils = WebConsoleUtils;
 
 /**
  * WebConsole commands manager.
  *
  * Defines a set of functions /variables ("commands") that are available from
--- a/devtools/shared/specs/webconsole.js
+++ b/devtools/shared/specs/webconsole.js
@@ -44,16 +44,17 @@ const webconsoleSpecPrototype = {
     evaluationResult: {
       resultID: Option(0, "string"),
       type: "evaluationResult",
       awaitResult: Option(0, "nullable:boolean"),
       errorMessageName: Option(0, "nullable:string"),
       exception: Option(0, "nullable:json"),
       exceptionMessage: Option(0, "nullable:string"),
       exceptionDocURL: Option(0, "nullable:string"),
+      exceptionStack: Option(0, "nullable:json"),
       frame: Option(0, "nullable:json"),
       helperResult: Option(0, "nullable:json"),
       input: Option(0, "nullable:string"),
       notes: Option(0, "nullable:string"),
       result: Option(0, "nullable:json"),
       timestamp: Option(0, "string"),
       topLevelAwaitRejected: Option(0, "nullable:boolean"),
     },