Bug 1317962 - use source maps in stack traces in the console; r?jryans draft
authorTom Tromey <tom@tromey.com>
Tue, 02 May 2017 12:51:51 -0600
changeset 580425 cb9fd2ef7e1fafb38e2b1e7266f0aaea30366732
parent 580424 da3f45e77632f87847a79cdbbf2be0679915b36f
child 629274 c194a53cf5c2a1ad914daf2f6c67ce04a3555cd7
push id59543
push userbmo:ttromey@mozilla.com
push dateThu, 18 May 2017 14:01:59 +0000
reviewersjryans
bugs1317962
milestone55.0a1
Bug 1317962 - use source maps in stack traces in the console; r?jryans MozReview-Commit-ID: 9SPZDi50XZv
devtools/client/framework/source-map-url-service.js
devtools/client/shared/components/stack-trace.js
devtools/client/shared/components/test/mochitest/chrome.ini
devtools/client/shared/components/test/mochitest/test_stack-trace-source-maps.html
devtools/client/webconsole/console-output.js
devtools/client/webconsole/net/components/net-info-body.js
devtools/client/webconsole/net/components/stacktrace-tab.js
devtools/client/webconsole/net/net-request.js
devtools/client/webconsole/new-console-output/components/console-output.js
devtools/client/webconsole/new-console-output/components/message-types/console-command.js
devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
devtools/client/webconsole/new-console-output/components/message-types/page-error.js
devtools/client/webconsole/new-console-output/components/message.js
--- a/devtools/client/framework/source-map-url-service.js
+++ b/devtools/client/framework/source-map-url-service.js
@@ -78,12 +78,18 @@ SourceMapURLService.prototype.originalPo
   if (!urlInfo) {
     return null;
   }
   // Call getOriginalURLs to make sure the source map has been
   // fetched.  We don't actually need the result of this though.
   await this._sourceMapService.getOriginalURLs(urlInfo);
   const location = { sourceId: urlInfo.id, line, column, sourceUrl: url };
   let resolvedLocation = await this._sourceMapService.getOriginalLocation(location);
-  return resolvedLocation === location ? null : resolvedLocation;
+  if (!resolvedLocation ||
+      (resolvedLocation.line === location.line &&
+       resolvedLocation.column === location.column &&
+       resolvedLocation.sourceUrl === location.sourceUrl)) {
+    return null;
+  }
+  return resolvedLocation;
 };
 
 exports.SourceMapURLService = SourceMapURLService;
--- a/devtools/client/shared/components/stack-trace.js
+++ b/devtools/client/shared/components/stack-trace.js
@@ -30,23 +30,26 @@ const AsyncFrame = createFactory(createC
 
 const StackTrace = createClass({
   displayName: "StackTrace",
 
   propTypes: {
     stacktrace: PropTypes.array.isRequired,
     onViewSourceInDebugger: PropTypes.func.isRequired,
     onViewSourceInScratchpad: PropTypes.func,
+    // Service to enable the source map feature.
+    sourceMapService: PropTypes.object,
   },
 
   render() {
     let {
       stacktrace,
       onViewSourceInDebugger,
-      onViewSourceInScratchpad
+      onViewSourceInScratchpad,
+      sourceMapService,
     } = this.props;
 
     let frames = [];
     stacktrace.forEach((s, i) => {
       if (s.asyncCause) {
         frames.push("\t", AsyncFrame({
           key: `${i}-asyncframe`,
           asyncCause: s.asyncCause
@@ -62,17 +65,18 @@ const StackTrace = createClass({
           line: s.lineNumber,
           column: s.columnNumber,
         },
         showFunctionName: true,
         showAnonymousFunctionName: true,
         showFullSourceUrl: true,
         onClick: (/^Scratchpad\/\d+$/.test(source))
           ? onViewSourceInScratchpad
-          : onViewSourceInDebugger
+          : onViewSourceInDebugger,
+        sourceMapService,
       }), "\n");
     });
 
     return dom.div({ className: "stack-trace" }, frames);
   }
 });
 
 module.exports = StackTrace;
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -6,16 +6,17 @@ support-files =
 [test_HSplitBox_01.html]
 [test_notification_box_01.html]
 [test_notification_box_02.html]
 [test_notification_box_03.html]
 [test_searchbox.html]
 [test_searchbox-with-autocomplete.html]
 [test_sidebar_toggle.html]
 [test_stack-trace.html]
+[test_stack-trace-source-maps.html]
 [test_tabs_accessibility.html]
 [test_tabs_menu.html]
 [test_tree_01.html]
 [test_tree_02.html]
 [test_tree_03.html]
 [test_tree_04.html]
 [test_tree_05.html]
 [test_tree_06.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_stack-trace-source-maps.html
@@ -0,0 +1,105 @@
+<!-- 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/. -->
+<!DOCTYPE HTML>
+<html>
+<!--
+Test the rendering of a stack trace with source maps
+-->
+<head>
+  <meta charset="utf-8">
+  <title>StackTrace component test</title>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<script src="head.js"></script>
+<script>
+/* import-globals-from head.js */
+"use strict";
+
+window.onload = function () {
+  let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+  let React = browserRequire("devtools/client/shared/vendor/react");
+  let StackTrace = React.createFactory(
+    browserRequire("devtools/client/shared/components/stack-trace")
+  );
+  ok(StackTrace, "Got the StackTrace factory");
+
+  add_task(function* () {
+    let stacktrace = [
+      {
+        filename: "https://bugzilla.mozilla.org/bundle.js",
+        lineNumber: 99,
+        columnNumber: 10
+      },
+      {
+        functionName: "loadFunc",
+        filename: "https://bugzilla.mozilla.org/bundle.js",
+        lineNumber: 108,
+      }
+    ];
+
+    let props = {
+      stacktrace,
+      onViewSourceInDebugger: () => {},
+      onViewSourceInScratchpad: () => {},
+      // A mock source map service.
+      sourceMapService: {
+	originalPositionFor: function (url, line, column) {
+	  let newLine = line === 99 ? 1 : 7;
+	  // Return a phony promise-like thing that resolves
+	  // immediately.
+	  return {
+	    then: function (consequence) {
+	      consequence({
+		sourceId: "whatever",
+		sourceUrl: "https://bugzilla.mozilla.org/original.js",
+		line: newLine,
+		column,
+	      });
+	    },
+	  };
+	}
+      },
+    };
+
+    let trace = ReactDOM.render(StackTrace(props), window.document.body);
+    yield forceRender(trace);
+
+    let traceEl = ReactDOM.findDOMNode(trace);
+    ok(traceEl, "Rendered StackTrace has an element");
+
+    // Get the child nodes and filter out the text-only whitespace ones
+    let frameEls = Array.from(traceEl.childNodes)
+      .filter(n => n.className && n.className.includes("frame"));
+    ok(frameEls, "Rendered StackTrace has frames");
+    is(frameEls.length, 2, "StackTrace has 2 frames");
+
+    checkFrameString({
+      el: frameEls[0],
+      functionName: "<anonymous>",
+      source: "https://bugzilla.mozilla.org/original.js",
+      file: "original.js",
+      line: 1,
+      column: 10,
+      shouldLink: true,
+      tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:1:10",
+    });
+
+    checkFrameString({
+      el: frameEls[1],
+      functionName: "loadFunc",
+      source: "https://bugzilla.mozilla.org/original.js",
+      file: "original.js",
+      line: 7,
+      column: null,
+      shouldLink: true,
+      tooltip: "View source in Debugger → https://bugzilla.mozilla.org/original.js:7",
+    });
+  });
+};
+</script>
+</body>
+</html>
--- a/devtools/client/webconsole/console-output.js
+++ b/devtools/client/webconsole/console-output.js
@@ -3551,19 +3551,22 @@ Widgets.Stacktrace.prototype = extend(Wi
     if (this.element) {
       return this;
     }
 
     let result = this.element = this.document.createElementNS(XHTML_NS, "div");
     result.className = "stacktrace devtools-monospace";
 
     if (this.stacktrace) {
+      const target = this.message.output.toolboxTarget;
+      const toolbox = gDevTools.getToolbox(target);
       this.output.owner.ReactDOM.render(this.output.owner.StackTraceView({
         stacktrace: this.stacktrace,
-        onViewSourceInDebugger: frame => this.output.openLocationInDebugger(frame)
+        onViewSourceInDebugger: frame => this.output.openLocationInDebugger(frame),
+        sourceMapService: toolbox ? toolbox.sourceMapURLService : null,
       }), result);
     }
 
     return this;
   }
 });
 
 /**
--- a/devtools/client/webconsole/net/components/net-info-body.js
+++ b/devtools/client/webconsole/net/components/net-info-body.js
@@ -33,17 +33,19 @@ const PropTypes = React.PropTypes;
  */
 var NetInfoBody = React.createClass({
   propTypes: {
     tabActive: PropTypes.number.isRequired,
     actions: PropTypes.object.isRequired,
     data: PropTypes.shape({
       request: PropTypes.object.isRequired,
       response: PropTypes.object.isRequired
-    })
+    }),
+    // Service to enable the source map feature.
+    sourceMapService: PropTypes.object,
   },
 
   displayName: "NetInfoBody",
 
   getDefaultProps() {
     return {
       tabActive: 0
     };
@@ -71,17 +73,17 @@ var NetInfoBody = React.createClass({
   },
 
   hasStackTrace() {
     let {cause} = this.state.data;
     return cause && cause.stacktrace && cause.stacktrace.length > 0;
   },
 
   getTabPanels() {
-    let actions = this.props.actions;
+    let { actions, sourceMapService } = this.props;
     let data = this.state.data;
     let {request} = data;
 
     // Flags for optional tabs. Some tabs are visible only if there
     // are data to display.
     let hasParams = request.queryString && request.queryString.length;
     let hasPostData = request.bodySize > 0;
 
@@ -148,17 +150,18 @@ var NetInfoBody = React.createClass({
     if (this.hasStackTrace()) {
       panels.push(
         TabPanel({
           className: "stacktrace-tab",
           key: "stacktrace",
           title: Locale.$STR("netRequest.callstack")},
           StackTraceTab({
             data: data,
-            actions: actions
+            actions: actions,
+            sourceMapService: sourceMapService,
           })
         )
       );
     }
 
     return panels;
   },
 
--- a/devtools/client/webconsole/net/components/stacktrace-tab.js
+++ b/devtools/client/webconsole/net/components/stacktrace-tab.js
@@ -8,22 +8,24 @@ const StackTrace = createFactory(require
 
 const StackTraceTab = createClass({
   displayName: "StackTraceTab",
 
   propTypes: {
     data: PropTypes.object.isRequired,
     actions: PropTypes.shape({
       onViewSourceInDebugger: PropTypes.func.isRequired
-    })
+    }),
+    // Service to enable the source map feature.
+    sourceMapService: PropTypes.object,
   },
 
   render() {
     let { stacktrace } = this.props.data.cause;
-    let { actions } = this.props;
+    let { actions, sourceMapService } = this.props;
     let onViewSourceInDebugger = actions.onViewSourceInDebugger.bind(actions);
 
-    return StackTrace({ stacktrace, onViewSourceInDebugger });
+    return StackTrace({ stacktrace, onViewSourceInDebugger, sourceMapService });
   }
 });
 
 // Exports from this module
 module.exports = StackTraceTab;
--- a/devtools/client/webconsole/net/net-request.js
+++ b/devtools/client/webconsole/net/net-request.js
@@ -158,17 +158,18 @@ NetRequest.prototype = {
     let doc = messageBody.ownerDocument;
     this.netInfoBodyBox = doc.createElementNS(XHTML_NS, "div");
     this.netInfoBodyBox.classList.add("netInfoBody");
     messageBody.appendChild(this.netInfoBodyBox);
 
     // As soon as Redux is in place state and actions will come from
     // separate modules.
     let body = NetInfoBody({
-      actions: this
+      actions: this,
+      sourceMapService: this.owner.sourceMapURLService,
     });
 
     // Render net info body!
     this.body = ReactDOM.render(body, this.netInfoBodyBox);
 
     this.refresh();
   },
 
--- a/devtools/client/webconsole/new-console-output/components/console-output.js
+++ b/devtools/client/webconsole/new-console-output/components/console-output.js
@@ -25,16 +25,17 @@ const ConsoleOutput = createClass({
   displayName: "ConsoleOutput",
 
   propTypes: {
     messages: PropTypes.object.isRequired,
     messagesUi: PropTypes.object.isRequired,
     serviceContainer: PropTypes.shape({
       attachRefToHud: PropTypes.func.isRequired,
       openContextMenu: PropTypes.func.isRequired,
+      sourceMapService: PropTypes.object,
     }),
     autoscroll: PropTypes.bool.isRequired,
     dispatch: PropTypes.func.isRequired,
     timestampsVisible: PropTypes.bool,
     groups: PropTypes.object.isRequired,
     messagesTableData: PropTypes.object.isRequired,
   },
 
--- a/devtools/client/webconsole/new-console-output/components/message-types/console-command.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/console-command.js
@@ -15,44 +15,42 @@ const Message = createFactory(require("d
 
 ConsoleCommand.displayName = "ConsoleCommand";
 
 ConsoleCommand.propTypes = {
   message: PropTypes.object.isRequired,
   autoscroll: PropTypes.bool.isRequired,
   indent: PropTypes.number.isRequired,
   timestampsVisible: PropTypes.bool.isRequired,
+  serviceContainer: PropTypes.object,
 };
 
 ConsoleCommand.defaultProps = {
   indent: 0,
 };
 
 /**
  * Displays input from the console.
  */
 function ConsoleCommand(props) {
   const {
     autoscroll,
     indent,
     message,
     timestampsVisible,
+    serviceContainer,
   } = props;
 
   const {
     source,
     type,
     level,
     messageText: messageBody,
   } = message;
 
-  const {
-    serviceContainer,
-  } = props;
-
   return Message({
     source,
     type,
     level,
     topLevelClasses: [],
     messageBody,
     scrollToMessage: autoscroll,
     serviceContainer,
--- a/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/evaluation-result.js
@@ -15,16 +15,17 @@ const Message = createFactory(require("d
 const GripMessageBody = require("devtools/client/webconsole/new-console-output/components/grip-message-body");
 
 EvaluationResult.displayName = "EvaluationResult";
 
 EvaluationResult.propTypes = {
   message: PropTypes.object.isRequired,
   indent: PropTypes.number.isRequired,
   timestampsVisible: PropTypes.bool.isRequired,
+  serviceContainer: PropTypes.object,
 };
 
 EvaluationResult.defaultProps = {
   indent: 0,
 };
 
 function EvaluationResult(props) {
   const {
--- a/devtools/client/webconsole/new-console-output/components/message-types/page-error.js
+++ b/devtools/client/webconsole/new-console-output/components/message-types/page-error.js
@@ -15,16 +15,17 @@ const Message = createFactory(require("d
 
 PageError.displayName = "PageError";
 
 PageError.propTypes = {
   message: PropTypes.object.isRequired,
   open: PropTypes.bool,
   indent: PropTypes.number.isRequired,
   timestampsVisible: PropTypes.bool.isRequired,
+  serviceContainer: PropTypes.object,
 };
 
 PageError.defaultProps = {
   open: false,
   indent: 0,
 };
 
 function PageError(props) {
--- a/devtools/client/webconsole/new-console-output/components/message.js
+++ b/devtools/client/webconsole/new-console-output/components/message.js
@@ -143,16 +143,17 @@ const Message = createClass({
       attachment = dom.div(
         {
           className: "stacktrace devtools-monospace"
         },
         StackTrace({
           stacktrace: stacktrace,
           onViewSourceInDebugger: serviceContainer.onViewSourceInDebugger,
           onViewSourceInScratchpad: serviceContainer.onViewSourceInScratchpad,
+          sourceMapService: serviceContainer.sourceMapService,
         })
       );
     }
 
     // If there is an expandable part, make it collapsible.
     let collapse = null;
     if (collapsible) {
       collapse = CollapseButton({