Bug 1006102 - Add support for filtering errors by addonId in webconsole ConsoleServiceListener helper. draft
authorLuca Greco <lgreco@mozilla.com>
Fri, 03 Jun 2016 16:22:17 +0200
changeset 375741 cacd5541678b70965a2e6ce1e1acfcb2625a4fc4
parent 375740 b67dee7e2d3f1d049b3524ebecf899b435ec1d55
child 522954 6aae9c8064c0ee54d5d8b35e0aa3118ba6c4d103
push id20366
push userluca.greco@alcacoop.it
push dateMon, 06 Jun 2016 15:45:16 +0000
bugs1006102
milestone49.0a1
Bug 1006102 - Add support for filtering errors by addonId in webconsole ConsoleServiceListener helper. MozReview-Commit-ID: hoLAJDNLbB
devtools/server/actors/addon.js
devtools/shared/tests/unit/test_errors_filtering.js
devtools/shared/tests/unit/xpcshell.ini
devtools/shared/webconsole/utils.js
--- a/devtools/server/actors/addon.js
+++ b/devtools/server/actors/addon.js
@@ -4,17 +4,17 @@
 
 "use strict";
 
 var { Ci, Cu } = require("chrome");
 var Services = require("Services");
 var { ActorPool } = require("devtools/server/actors/common");
 var { TabSources } = require("./utils/TabSources");
 var makeDebugger = require("./utils/make-debugger");
-var { ConsoleAPIListener } = require("devtools/shared/webconsole/utils");
+var { ConsoleAPIListener, ConsoleServiceListener } = require("devtools/shared/webconsole/utils");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 var { assert, update } = DevToolsUtils;
 
 loader.lazyRequireGetter(this, "AddonThreadActor", "devtools/server/actors/script", true);
 loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
 loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
 loader.lazyRequireGetter(this, "WebConsoleActor", "devtools/server/actors/webconsole", true);
 
@@ -321,16 +321,24 @@ update(AddonConsoleActor.prototype, {
         case "ConsoleAPI":
           if (!this.consoleAPIListener) {
             this.consoleAPIListener =
               new ConsoleAPIListener(null, this, { addonId: this.addon.id });
             this.consoleAPIListener.init();
           }
           startedListeners.push(listener);
           break;
+        case "PageError":
+          if (!this.consoleServiceListener) {
+            this.consoleServiceListener =
+              new ConsoleServiceListener(null, this, { addonIdL: this.addon.id });
+            this.consoleServiceListener.init();
+          }
+          startedListeners.push(listener);
+          break;
       }
     }
     return {
       startedListeners: startedListeners,
       nativeConsoleAPI: true,
       traits: this.traits,
     };
   },
new file mode 100644
--- /dev/null
+++ b/devtools/shared/tests/unit/test_errors_filtering.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { ConsoleServiceListener } = require("devtools/shared/webconsole/utils");
+const Services = require("Services");
+
+function createFakeAddonWindow({addonId} = {}) {
+  let baseURI = Services.io.newURI("about:blank", null, null);
+  let originAttributes = {addonId};
+  let principal = Services.scriptSecurityManager
+        .createCodebasePrincipal(baseURI, originAttributes);
+  let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
+  let docShell = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
+                             .getInterface(Ci.nsIDocShell);
+  docShell.createAboutBlankContentViewer(principal);
+  let window = docShell.contentViewer.DOMDocument.defaultView;
+
+  return {window, chromeWebNav};
+}
+
+/**
+ * Tests that the originAttributes property of the Error object gets passed
+ * through to console service.
+ */
+add_task(function* test_console_service_listener_filtering() {
+  // WebExtension page's errors tagged
+  // by 'originAttributes.addonId' are filtered correctly.
+  let addonIdOption = {addonId: "bar"};
+  let fakeAddon = createFakeAddonWindow(addonIdOption);
+
+  let waitForErrorMessage = new Promise((resolve, reject) => {
+    let listener = new ConsoleServiceListener(null, {
+      onConsoleServiceMessage: (msg) => {
+        listener.destroy();
+        resolve(msg);
+      },
+    }, addonIdOption);
+    listener.init();
+  });
+
+  let notAnAddon = createFakeAddonWindow({addonId:""});
+
+  // Produce an error from a non addon window
+  notAnAddon.window.eval("setTimeout(() => errorNotAddon(), 0);");
+
+  // Produce an error from the addon window
+  fakeAddon.window.eval("setTimeout(() => errorOnSetTimeout(), 0);");
+
+  // Check onConsoleServiceMessage receive the Error raised in the addonWindow
+  // filtered with the expected originAttributes
+  let error = yield waitForErrorMessage;
+
+  ok(error instanceof Ci.nsIScriptError, "Got an nsIScriptError instance");
+  ok(/errorOnSetTimeout/.test(error.message), "Got the expected error message");
+  ok(error.originAttributes && error.originAttributes.addonId == addonIdOption.addonId,
+     "Got the expected originAttributes");
+
+  let listener = new ConsoleServiceListener(null, {
+    onConsoleServiceMessage: () => {},
+  }, addonIdOption);
+  let errors = listener.getCachedMessages();
+
+  // Check filtered cached messages
+  do_check_eq(errors.length, 1, "Got the expected number of filtered cached messages");
+  ok(errors[0] instanceof Ci.nsIScriptError, "Got an nsIScriptError instance");
+  ok(/errorOnSetTimeout/.test(errors[0].message), "Got the expected error message");
+  ok(errors[0].originAttributes && errors[0].originAttributes.addonId == addonIdOption.addonId,
+     "Got the expected originAttributes");
+
+  let unfilteredListener = new ConsoleServiceListener(null, {
+    onConsoleServiceMessage: () => {},
+  });
+  let allErrors = unfilteredListener.getCachedMessages();
+  // Check non filtered cached messages
+  ok(allErrors.length > 1, "Got the expected number of unfiltered cached messages");
+
+  let foundError;
+  for (let error of allErrors) {
+    if (error instanceof Ci.nsIScriptError) {
+      if (/errorNotAddon/.test(error.message)) {
+        // Non Addon Errors
+        foundError = error;
+      }
+    }
+  }
+
+  ok(foundError, "Got the expected non addon error from the unfiltered listener");
+  do_check_eq(foundError.originAttributes.addonId, "",
+              `1 Got empty originAttributes on errors from non addon windows`);
+
+  // Close the addon window's chromeWebNav.
+  fakeAddon.chromeWebNav.close();
+  notAnAddon.chromeWebNav.close();
+});
--- a/devtools/shared/tests/unit/xpcshell.ini
+++ b/devtools/shared/tests/unit/xpcshell.ini
@@ -3,28 +3,29 @@ tags = devtools
 head = head_devtools.js
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 support-files =
   exposeLoader.js
 
 [test_assert.js]
+[test_console_filtering.js]
+[test_errors_filtering.js]
 [test_csslexer.js]
 [test_fetch-chrome.js]
 [test_fetch-file.js]
 [test_fetch-http.js]
 [test_fetch-resource.js]
 [test_flatten.js]
 [test_indentation.js]
 [test_independent_loaders.js]
 [test_invisible_loader.js]
 [test_isSet.js]
 [test_safeErrorString.js]
 [test_defineLazyPrototypeGetter.js]
 [test_async-utils.js]
-[test_console_filtering.js]
 [test_prettifyCSS.js]
 [test_require_lazy.js]
 [test_require.js]
 [test_stack.js]
 [test_defer.js]
 [test_executeSoon.js]
--- a/devtools/shared/webconsole/utils.js
+++ b/devtools/shared/webconsole/utils.js
@@ -626,34 +626,44 @@ WebConsoleUtils.L10n.prototype = {
  * @constructor
  * @param nsIDOMWindow [window]
  *        Optional - the window object for which we are created. This is used
  *        for filtering out messages that belong to other windows.
  * @param object listener
  *        The listener object must have one method:
  *        - onConsoleServiceMessage(). This method is invoked with one argument,
  *        the nsIConsoleMessage, whenever a relevant message is received.
+ * @param object filteringOptions
+ *        Optional - The filteringOptions that this listener should listen to:
+ *        - addonId: filter error messages based on the addonId.
  */
-function ConsoleServiceListener(window, listener) {
+function ConsoleServiceListener(window, listener, {addonId} = {}) {
   this.window = window;
   this.listener = listener;
+  this.addonId = addonId;
 }
 exports.ConsoleServiceListener = ConsoleServiceListener;
 
 ConsoleServiceListener.prototype =
 {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]),
 
   /**
    * The content window for which we listen to page errors.
    * @type nsIDOMWindow
    */
   window: null,
 
   /**
+   * The addonId that we listen for. If not null then only messages from this
+   * console will be returned.
+   */
+  addonId: null,
+
+  /**
    * The listener object which is notified of messages from the console service.
    * @type object
    */
   listener: null,
 
   /**
    * Initialize the nsIConsoleService listener.
    */
@@ -682,16 +692,24 @@ ConsoleServiceListener.prototype =
       }
 
       let errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID);
       if (!errorWindow || !isWindowIncluded(this.window, errorWindow)) {
         return;
       }
     }
 
+    if (this.addonId) {
+      if (message instanceof Ci.nsIScriptError &&
+          message.originAttributes &&
+          message.originAttributes.addonId !== this.addonId) {
+        return;
+      }
+    }
+
     this.listener.onConsoleServiceMessage(message);
   },
 
   /**
    * Check if the given message category is allowed to be tracked or not.
    * We ignore chrome-originating errors as we only care about content.
    *
    * @param string category
@@ -727,16 +745,27 @@ ConsoleServiceListener.prototype =
    *        windows. Defaults to false.
    * @return array
    *         The array of cached messages. Each element is an nsIScriptError or
    *         an nsIConsoleMessage
    */
   getCachedMessages: function (includePrivate = false) {
     let errors = Services.console.getMessageArray() || [];
 
+    if (this.addonId) {
+      errors = errors.filter((aError) => {
+        if (aError instanceof Ci.nsIScriptError) {
+          return aError.originAttributes &&
+            aError.originAttributes.addonId == this.addonId;
+        }
+
+        return false;
+      });
+    }
+
     // if !this.window, we're in a browser console. Still need to filter
     // private messages.
     if (!this.window) {
       return errors.filter((error) => {
         if (error instanceof Ci.nsIScriptError) {
           if (!includePrivate && error.isFromPrivateWindow) {
             return false;
           }