Bug 851231 - Output Console.jsm API calls to the browser console; r=jwalker
authorMihai Sucan <mihai.sucan@gmail.com>
Mon, 15 Apr 2013 19:10:04 +0300
changeset 128960 2cecd940c2193dd35cb33ca47b8b36cc6449200e
parent 128959 543594f2647a48bbbb5afef4e6e349f74da6a8fe
child 128961 8c8c9b98d6c1a7894f38cedb7e2234fb9fae2563
push id26598
push userryanvm@gmail.com
push dateTue, 16 Apr 2013 20:04:18 +0000
treeherdermozilla-inbound@bad7da55f137 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker
bugs851231
milestone23.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 851231 - Output Console.jsm API calls to the browser console; r=jwalker
browser/devtools/webconsole/test/Makefile.in
browser/devtools/webconsole/test/browser_console_consolejsm_output.js
browser/devtools/webconsole/test/browser_longstring_hang.js
browser/devtools/webconsole/test/head.js
toolkit/devtools/Console.jsm
--- a/browser/devtools/webconsole/test/Makefile.in
+++ b/browser/devtools/webconsole/test/Makefile.in
@@ -116,16 +116,17 @@ MOCHITEST_BROWSER_FILES = \
 	browser_netpanel_longstring_expand.js \
 	browser_repeated_messages_accuracy.js \
 	browser_webconsole_bug_821877_csp_errors.js \
 	browser_eval_in_debugger_stackframe.js \
 	browser_console_variables_view.js \
 	browser_console_variables_view_while_debugging.js \
 	browser_console.js \
 	browser_longstring_hang.js \
+	browser_console_consolejsm_output.js \
 	head.js \
 	$(NULL)
 
 ifeq ($(OS_ARCH), Darwin)
 MOCHITEST_BROWSER_FILES += \
         browser_webconsole_bug_804845_ctrl_key_nav.js \
         $(NULL)
 endif
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_console_consolejsm_output.js
@@ -0,0 +1,118 @@
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that Console.jsm outputs messages to the Browser Console, bug 851231.
+
+function test()
+{
+  HUDConsoleUI.toggleBrowserConsole().then(consoleOpened);
+  let hud = null;
+
+  function consoleOpened(aHud)
+  {
+    hud = aHud;
+    hud.jsterm.clearOutput(true);
+
+    let console = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).console;
+
+    console.time("foobarTimer");
+    let foobar = { bug851231prop: "bug851231value" };
+
+    console.log("bug851231-log");
+    console.info("bug851231-info");
+    console.warn("bug851231-warn");
+    console.error("bug851231-error", foobar);
+    console.debug("bug851231-debug");
+    console.trace();
+    console.dir(document);
+    console.timeEnd("foobarTimer");
+
+    info("wait for the Console.jsm messages");
+
+    waitForMessages({
+      webconsole: hud,
+      messages: [
+        {
+          name: "console.log output",
+          text: "bug851231-log",
+          category: CATEGORY_WEBDEV,
+          severity: SEVERITY_LOG,
+        },
+        {
+          name: "console.info output",
+          text: "bug851231-info",
+          category: CATEGORY_WEBDEV,
+          severity: SEVERITY_INFO,
+        },
+        {
+          name: "console.warn output",
+          text: "bug851231-warn",
+          category: CATEGORY_WEBDEV,
+          severity: SEVERITY_WARNING,
+        },
+        {
+          name: "console.error output",
+          text: /\bbug851231-error\b.+\[object Object\]/,
+          category: CATEGORY_WEBDEV,
+          severity: SEVERITY_ERROR,
+          objects: true,
+        },
+        {
+          name: "console.debug output",
+          text: "bug851231-debug",
+          category: CATEGORY_WEBDEV,
+          severity: SEVERITY_LOG,
+        },
+        {
+          name: "console.trace output",
+          consoleTrace: {
+            file: "browser_console_consolejsm_output.js",
+            fn: "consoleOpened",
+          },
+        },
+        {
+          name: "console.dir output",
+          consoleDir: "[object XULDocument]",
+        },
+        {
+          name: "console.time output",
+          consoleTime: "foobarTimer",
+        },
+        {
+          name: "console.timeEnd output",
+          consoleTimeEnd: "foobarTimer",
+        },
+      ],
+    }).then((aResults) => {
+      let consoleErrorMsg = aResults[3];
+      ok(consoleErrorMsg, "console.error message element found");
+      let clickable = consoleErrorMsg.clickableElements[0];
+      ok(clickable, "clickable object found for console.error");
+
+      let onFetch = (aEvent, aVar) => {
+        // Skip the notification from console.dir variablesview-fetched.
+        if (aVar._variablesView != hud.jsterm._variablesView) {
+          return;
+        }
+        hud.jsterm.off("variablesview-fetched", onFetch);
+
+        ok(aVar, "object inspector opened on click");
+
+        findVariableViewProperties(aVar, [{
+          name: "bug851231prop",
+          value: "bug851231value",
+        }], { webconsole: hud }).then(finishTest);
+      };
+
+      hud.jsterm.on("variablesview-fetched", onFetch);
+
+      scrollOutputToNode(clickable);
+
+      info("wait for variablesview-fetched");
+      executeSoon(() =>
+        EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow));
+    });
+  }
+}
--- a/browser/devtools/webconsole/test/browser_longstring_hang.js
+++ b/browser/devtools/webconsole/test/browser_longstring_hang.js
@@ -37,23 +37,20 @@ function test()
           longString: true,
         },
       ],
     }).then(onInitialString);
   }
 
   function onInitialString(aResults)
   {
-    let msg = [...aResults[0].matched][0];
-    ok(msg, "console.log result message element");
-
-    let clickable = msg.querySelector(".longStringEllipsis");
+    let clickable = aResults[0].longStrings[0];
     ok(clickable, "long string ellipsis is shown");
 
-    scrollToVisible(clickable);
+    scrollOutputToNode(clickable);
 
     executeSoon(() => {
       EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
 
       info("wait for long string expansion");
 
       waitForMessages({
         webconsole: hud,
@@ -68,21 +65,9 @@ function test()
           {
             text: "too long to be displayed",
             longString: false,
           },
         ],
       }).then(finishTest);
     });
   }
-
-  function scrollToVisible(aNode)
-  {
-    let richListBoxNode = aNode.parentNode;
-    while (richListBoxNode.tagName != "richlistbox") {
-      richListBoxNode = richListBoxNode.parentNode;
-    }
-
-    let boxObject = richListBoxNode.scrollBoxObject;
-    let nsIScrollBoxObject = boxObject.QueryInterface(Ci.nsIScrollBoxObject);
-    nsIScrollBoxObject.ensureElementIsVisible(aNode);
-  }
 }
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -247,44 +247,57 @@ function waitForOpenContextMenu(aContext
 }
 
 /**
  * Dump the output of all open Web Consoles - used only for debugging purposes.
  */
 function dumpConsoles()
 {
   if (gPendingOutputTest) {
-    console.log("dumpConsoles");
+    console.log("dumpConsoles start");
     for each (let hud in HUDService.hudReferences) {
       if (!hud.outputNode) {
         console.debug("no output content for", hud.hudId);
         continue;
       }
 
       console.debug("output content for", hud.hudId);
       for (let elem of hud.outputNode.childNodes) {
-        let text = getMessageElementText(elem);
-        let repeats = elem.querySelector(".webconsole-msg-repeat");
-        if (repeats) {
-          repeats = repeats.getAttribute("value");
-        }
-        console.debug("date", elem.timestamp,
-                      "class", elem.className,
-                      "category", elem.category,
-                      "severity", elem.severity,
-                      "repeats", repeats,
-                      "clipboardText", elem.clipboardText,
-                      "text", text);
+        dumpMessageElement(elem);
       }
     }
+    console.log("dumpConsoles end");
 
     gPendingOutputTest = 0;
   }
 }
 
+/**
+ * Dump to output debug information for the given webconsole message.
+ *
+ * @param nsIDOMNode aMessage
+ *        The message element you want to display.
+ */
+function dumpMessageElement(aMessage)
+{
+  let text = getMessageElementText(aMessage);
+  let repeats = aMessage.querySelector(".webconsole-msg-repeat");
+  if (repeats) {
+    repeats = repeats.getAttribute("value");
+  }
+  console.debug("id", aMessage.getAttribute("id"),
+                "date", aMessage.timestamp,
+                "class", aMessage.className,
+                "category", aMessage.category,
+                "severity", aMessage.severity,
+                "repeats", repeats,
+                "clipboardText", aMessage.clipboardText,
+                "text", text);
+}
+
 function finishTest()
 {
   browser = hudId = hud = filterBox = outputNode = cs = null;
 
   dumpConsoles();
 
   if (HUDConsoleUI.browserConsole) {
     let hud = HUDConsoleUI.browserConsole;
@@ -883,64 +896,220 @@ function waitForMessages(aOptions)
       result = aText.indexOf(aRule) > -1;
     }
     else if (aRule instanceof RegExp) {
       result = aRule.test(aText);
     }
     return result;
   }
 
+  function checkConsoleTrace(aRule, aElement)
+  {
+    let elemText = getMessageElementText(aElement);
+    let trace = aRule.consoleTrace;
+
+    if (!checkText("Stack trace from ", elemText)) {
+      return false;
+    }
+
+    let clickable = aElement.querySelector(".hud-clickable");
+    if (!clickable) {
+      ok(false, "console.trace() message is missing .hud-clickable");
+      displayErrorContext(aRule, aElement);
+      return false;
+    }
+    aRule.clickableElements = [clickable];
+
+    if (trace.file &&
+        !checkText("from " + trace.file + ", ", elemText)) {
+      ok(false, "console.trace() message is missing the file name: " +
+                trace.file);
+      displayErrorContext(aRule, aElement);
+      return false;
+    }
+
+    if (trace.fn &&
+        !checkText(", function " + trace.fn + ", ", elemText)) {
+      ok(false, "console.trace() message is missing the function name: " +
+                trace.fn);
+      displayErrorContext(aRule, aElement);
+      return false;
+    }
+
+    if (trace.line &&
+        !checkText(", line " + trace.line + ".", elemText)) {
+      ok(false, "console.trace() message is missing the line number: " +
+                trace.line);
+      displayErrorContext(aRule, aElement);
+      return false;
+    }
+
+    aRule.category = CATEGORY_WEBDEV;
+    aRule.severity = SEVERITY_LOG;
+
+    return true;
+  }
+
+  function checkConsoleTime(aRule, aElement)
+  {
+    let elemText = getMessageElementText(aElement);
+    let time = aRule.consoleTime;
+
+    if (!checkText(time + ": timer started", elemText)) {
+      return false;
+    }
+
+    aRule.category = CATEGORY_WEBDEV;
+    aRule.severity = SEVERITY_LOG;
+
+    return true;
+  }
+
+  function checkConsoleTimeEnd(aRule, aElement)
+  {
+    let elemText = getMessageElementText(aElement);
+    let time = aRule.consoleTimeEnd;
+    let regex = new RegExp(time + ": \\d+ms");
+
+    if (!checkText(regex, elemText)) {
+      return false;
+    }
+
+    aRule.category = CATEGORY_WEBDEV;
+    aRule.severity = SEVERITY_LOG;
+
+    return true;
+  }
+
+  function checkConsoleDir(aRule, aElement)
+  {
+    if (!aElement.classList.contains("webconsole-msg-inspector")) {
+      return false;
+    }
+
+    let elemText = getMessageElementText(aElement);
+    if (!checkText(aRule.consoleDir, elemText)) {
+      return false;
+    }
+
+    let iframe = aElement.querySelector("iframe");
+    if (!iframe) {
+      ok(false, "console.dir message has no iframe");
+      return false;
+    }
+
+    return true;
+  }
+
   function checkMessage(aRule, aElement)
   {
     let elemText = getMessageElementText(aElement);
 
     if (aRule.text && !checkText(aRule.text, elemText)) {
       return false;
     }
 
     if (aRule.noText && checkText(aRule.noText, elemText)) {
       return false;
     }
 
-    if (aRule.category) {
-      if (aElement.category != aRule.category) {
-        return false;
-      }
+    if (aRule.consoleTrace && !checkConsoleTrace(aRule, aElement)) {
+      return false;
+    }
+
+    if (aRule.consoleTime && !checkConsoleTime(aRule, aElement)) {
+      return false;
+    }
+
+    if (aRule.consoleTimeEnd && !checkConsoleTimeEnd(aRule, aElement)) {
+      return false;
+    }
+
+    if (aRule.consoleDir && !checkConsoleDir(aRule, aElement)) {
+      return false;
     }
 
-    if (aRule.severity) {
-      if (aElement.severity != aRule.severity) {
-        return false;
+    let partialMatch = !!(aRule.consoleTrace || aRule.consoleTime ||
+                          aRule.consoleTimeEnd);
+
+    if (aRule.category && aElement.category != aRule.category) {
+      if (partialMatch) {
+        is(aElement.category, aRule.category,
+           "message category for rule: " + displayRule(aRule));
+        displayErrorContext(aRule, aElement);
       }
+      return false;
+    }
+
+    if (aRule.severity && aElement.severity != aRule.severity) {
+      if (partialMatch) {
+        is(aElement.severity, aRule.severity,
+           "message severity for rule: " + displayRule(aRule));
+        displayErrorContext(aRule, aElement);
+      }
+      return false;
     }
 
     if (aRule.repeats) {
       let repeats = aElement.querySelector(".webconsole-msg-repeat");
       if (!repeats || repeats.getAttribute("value") != aRule.repeats) {
         return false;
       }
     }
 
-    let longString = !!aElement.querySelector(".longStringEllipsis");
-    if ("longString" in aRule && aRule.longString != longString) {
-      return false;
+    if ("longString" in aRule) {
+      let longStrings = aElement.querySelectorAll(".longStringEllipsis");
+      if (aRule.longString != !!longStrings[0]) {
+        if (partialMatch) {
+          is(!!longStrings[0], aRule.longString,
+             "long string existence check failed for message rule: " +
+             displayRule(aRule));
+          displayErrorContext(aRule, aElement);
+        }
+        return false;
+      }
+      aRule.longStrings = longStrings;
+    }
+
+    if ("objects" in aRule) {
+      let clickables = aElement.querySelectorAll(".hud-clickable");
+      if (aRule.objects != !!clickables[0]) {
+        if (partialMatch) {
+          is(!!clickables[0], aRule.objects,
+             "objects existence check failed for message rule: " +
+             displayRule(aRule));
+          displayErrorContext(aRule, aElement);
+        }
+        return false;
+      }
+      aRule.clickableElements = clickables;
     }
 
     let count = aRule.count || 1;
     if (!aRule.matched) {
       aRule.matched = new Set();
     }
     aRule.matched.add(aElement);
 
     return aRule.matched.size == count;
   }
 
   function onMessagesAdded(aEvent, aNewElements)
   {
     for (let elem of aNewElements) {
+      let location = elem.querySelector(".webconsole-location");
+      if (location) {
+        let url = location.getAttribute("title");
+        // Prevent recursion with the browser console and any potential
+        // messages coming from head.js.
+        if (url.indexOf("browser/devtools/webconsole/test/head.js") != -1) {
+          continue;
+        }
+      }
+
       for (let rule of rules) {
         if (rule._ruleMatched) {
           continue;
         }
 
         let matched = checkMessage(rule, elem);
         if (matched) {
           rule._ruleMatched = true;
@@ -984,20 +1153,46 @@ function waitForMessages(aOptions)
     }
   }
 
   function displayRule(aRule)
   {
     return aRule.name || aRule.text;
   }
 
+  function displayErrorContext(aRule, aElement)
+  {
+    console.log("error occured during rule " + displayRule(aRule));
+    console.log("while checking the following message");
+    dumpMessageElement(aElement);
+  }
+
   executeSoon(() => {
     onMessagesAdded("messages-added", webconsole.outputNode.childNodes);
     if (rulesMatched != rules.length) {
       listenerAdded = true;
       registerCleanupFunction(testCleanup);
       webconsole.ui.on("messages-added", onMessagesAdded);
       webconsole.ui.on("messages-updated", onMessagesAdded);
     }
   });
 
   return deferred.promise;
 }
+
+
+/**
+ * Scroll the Web Console output to the given node.
+ *
+ * @param nsIDOMNode aNode
+ *        The node to scroll to.
+ */
+function scrollOutputToNode(aNode)
+{
+  let richListBoxNode = aNode.parentNode;
+  while (richListBoxNode.tagName != "richlistbox") {
+    richListBoxNode = richListBoxNode.parentNode;
+  }
+
+  let boxObject = richListBoxNode.scrollBoxObject;
+  let nsIScrollBoxObject = boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+  nsIScrollBoxObject.ensureElementIsVisible(aNode);
+}
--- a/toolkit/devtools/Console.jsm
+++ b/toolkit/devtools/Console.jsm
@@ -19,16 +19,21 @@
  * - The primary use of this API is debugging and error logging so the perfect
  *   implementation isn't always required (or even well defined)
  */
 
 this.EXPORTED_SYMBOLS = [ "console" ];
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
+
+let gTimerRegistry = new Map();
+
 /**
  * String utility to ensure that strings are a specified length. Strings
  * that are too long are truncated to the max length and the last char is
  * set to "_". Strings that are too short are left padded with spaces.
  *
  * @param {string} aStr
  *        The string to format to the correct length
  * @param {number} aMaxLen
@@ -245,98 +250,146 @@ function logProperty(aProp, aValue) {
   else {
     reply += "    - " + aProp + " = " + stringify(aValue) + "\n";
   }
   return reply;
 }
 
 /**
  * Parse a stack trace, returning an array of stack frame objects, where
- * each has file/line/call members
+ * each has filename/lineNumber/functionName members
  *
  * @param {string} aStack
  *        The serialized stack trace
  * @return {object[]}
  *        Array of { file: "...", line: NNN, call: "..." } objects
  */
 function parseStack(aStack) {
   let trace = [];
   aStack.split("\n").forEach(function(line) {
     if (!line) {
       return;
     }
     let at = line.lastIndexOf("@");
     let posn = line.substring(at + 1);
     trace.push({
-      file: posn.split(":")[0],
-      line: posn.split(":")[1],
-      call: line.substring(0, at)
+      filename: posn.split(":")[0],
+      lineNumber: posn.split(":")[1],
+      functionName: line.substring(0, at)
     });
   });
   return trace;
 }
 
 /**
- * parseStack() takes output from an exception from which it creates the an
- * array of stack frame objects, this has the same output but using data from
- * Components.stack
+ * Format a frame coming from Components.stack such that it can be used by the
+ * Browser Console, via console-api-log-event notifications.
  *
- * @param {string} aFrame
- *        The stack frame from which to begin the walk
+ * @param {object} aFrame
+ *        The stack frame from which to begin the walk.
+ * @param {number=0} aMaxDepth
+ *        Maximum stack trace depth. Default is 0 - no depth limit.
  * @return {object[]}
- *        Array of { file: "...", line: NNN, call: "..." } objects
+ *         An array of {filename, lineNumber, functionName, language} objects.
+ *         These objects follow the same format as other console-api-log-event
+ *         messages.
  */
-function getStack(aFrame) {
+function getStack(aFrame, aMaxDepth = 0) {
   if (!aFrame) {
     aFrame = Components.stack.caller;
   }
   let trace = [];
   while (aFrame) {
     trace.push({
-      file: aFrame.filename,
-      line: aFrame.lineNumber,
-      call: aFrame.name
+      filename: aFrame.filename,
+      lineNumber: aFrame.lineNumber,
+      functionName: aFrame.name,
+      language: aFrame.language,
     });
+    if (aMaxDepth == trace.length) {
+      break;
+    }
     aFrame = aFrame.caller;
   }
   return trace;
 }
 
 /**
  * Take the output from parseStack() and convert it to nice readable
  * output
  *
  * @param {object[]} aTrace
  *        Array of trace objects as created by parseStack()
  * @return {string} Multi line report of the stack trace
  */
 function formatTrace(aTrace) {
   let reply = "";
   aTrace.forEach(function(frame) {
-    reply += fmt(frame.file, 20, 20, { truncate: "start" }) + " " +
-             fmt(frame.line, 5, 5) + " " +
-             fmt(frame.call, 75, 75) + "\n";
+    reply += fmt(frame.filename, 20, 20, { truncate: "start" }) + " " +
+             fmt(frame.lineNumber, 5, 5) + " " +
+             fmt(frame.functionName, 75, 75) + "\n";
   });
   return reply;
 }
 
 /**
+ * Create a new timer by recording the current time under the specified name.
+ *
+ * @param {string} aName
+ *        The name of the timer.
+ * @param {number} [aTimestamp=Date.now()]
+ *        Optional timestamp that tells when the timer was originally started.
+ * @return {object}
+ *         The name property holds the timer name and the started property
+ *         holds the time the timer was started. In case of error, it returns
+ *         an object with the single property "error" that contains the key
+ *         for retrieving the localized error message.
+ */
+function startTimer(aName, aTimestamp) {
+  let key = aName.toString();
+  if (!gTimerRegistry.has(key)) {
+    gTimerRegistry.set(key, aTimestamp || Date.now());
+  }
+  return { name: aName, started: gTimerRegistry.get(key) };
+}
+
+/**
+ * Stop the timer with the specified name and retrieve the elapsed time.
+ *
+ * @param {string} aName
+ *        The name of the timer.
+ * @param {number} [aTimestamp=Date.now()]
+ *        Optional timestamp that tells when the timer was originally stopped.
+ * @return {object}
+ *         The name property holds the timer name and the duration property
+ *         holds the number of milliseconds since the timer was started.
+ */
+function stopTimer(aName, aTimestamp) {
+  let key = aName.toString();
+  let duration = (aTimestamp || Date.now()) - gTimerRegistry.get(key);
+  gTimerRegistry.delete(key);
+  return { name: aName, duration: duration };
+}
+
+/**
  * Create a function which will output a concise level of output when used
  * as a logging function
  *
  * @param {string} aLevel
  *        A prefix to all output generated from this function detailing the
  *        level at which output occurred
  * @return {function}
  *        A logging function
  * @see createMultiLineDumper()
  */
 function createDumper(aLevel) {
   return function() {
     let args = Array.prototype.slice.call(arguments, 0);
+    let frame = getStack(Components.stack.caller, 1)[0];
+    sendConsoleAPIMessage(aLevel, frame, args);
     let data = args.map(function(arg) {
       return stringify(arg);
     });
     dump("console." + aLevel + ": " + data.join(", ") + "\n");
   };
 }
 
 /**
@@ -349,37 +402,116 @@ function createDumper(aLevel) {
  * @return {function}
  *        A logging function
  * @see createDumper()
  */
 function createMultiLineDumper(aLevel) {
   return function() {
     dump("console." + aLevel + ": \n");
     let args = Array.prototype.slice.call(arguments, 0);
+    let frame = getStack(Components.stack.caller, 1)[0];
+    sendConsoleAPIMessage(aLevel, frame, args);
     args.forEach(function(arg) {
       dump(log(arg));
     });
   };
 }
 
 /**
+ * Send a Console API message. This function will send a console-api-log-event
+ * notification through the nsIObserverService.
+ *
+ * @param {string} aLevel
+ *        Message severity level. This is usually the name of the console method
+ *        that was called.
+ * @param {object} aFrame
+ *        The youngest stack frame coming from Components.stack, as formatted by
+ *        getStack().
+ * @param {array} aArgs
+ *        The arguments given to the console method.
+ * @param {object} aOptions
+ *        Object properties depend on the console method that was invoked:
+ *        - timer: for time() and timeEnd(). Holds the timer information.
+ *        - groupName: for group(), groupCollapsed() and groupEnd().
+ *        - stacktrace: for trace(). Holds the array of stack frames as given by
+ *        getStack().
+ */
+function sendConsoleAPIMessage(aLevel, aFrame, aArgs, aOptions = {})
+{
+  let consoleEvent = {
+    ID: aFrame.filename,
+    level: aLevel,
+    filename: aFrame.filename,
+    lineNumber: aFrame.lineNumber,
+    functionName: aFrame.functionName,
+    timeStamp: Date.now(),
+    arguments: aArgs,
+  };
+
+  consoleEvent.wrappedJSObject = consoleEvent;
+
+  switch (aLevel) {
+    case "trace":
+      consoleEvent.stacktrace = aOptions.stacktrace;
+      break;
+    case "time":
+    case "timeEnd":
+      consoleEvent.timer = aOptions.timer;
+      break;
+    case "group":
+    case "groupCollapsed":
+    case "groupEnd":
+      try {
+        consoleEvent.groupName = Array.prototype.join.call(aArgs, " ");
+      }
+      catch (ex) {
+        Cu.reportError(ex);
+        Cu.reportError(ex.stack);
+        return;
+      }
+      break;
+  }
+
+  Services.obs.notifyObservers(consoleEvent, "console-api-log-event", null);
+}
+
+/**
  * This creates a console object that somewhat replicates Firebug's console
  * object. It currently writes to dump(), but should write to the web
  * console's chrome error section (when it has one)
  */
 this.console = {
   debug: createMultiLineDumper("debug"),
   log: createDumper("log"),
   info: createDumper("info"),
   warn: createDumper("warn"),
   error: createMultiLineDumper("error"),
 
   trace: function Console_trace() {
+    let args = Array.prototype.slice.call(arguments, 0);
     let trace = getStack(Components.stack.caller);
-    dump(formatTrace(trace) + "\n");
+    sendConsoleAPIMessage("trace", trace[0], args,
+                          { stacktrace: trace });
+    dump("console.trace:\n" + formatTrace(trace) + "\n");
   },
   clear: function Console_clear() {},
 
   dir: createMultiLineDumper("dir"),
   dirxml: createMultiLineDumper("dirxml"),
   group: createDumper("group"),
-  groupEnd: createDumper("groupEnd")
+  groupEnd: createDumper("groupEnd"),
+
+  time: function Console_time() {
+    let args = Array.prototype.slice.call(arguments, 0);
+    let frame = getStack(Components.stack.caller, 1)[0];
+    let timer = startTimer(args[0]);
+    sendConsoleAPIMessage("time", frame, args, { timer: timer });
+    dump("console.time: '" + timer.name + "' @ " + (new Date()) + "\n");
+  },
+
+  timeEnd: function Console_timeEnd() {
+    let args = Array.prototype.slice.call(arguments, 0);
+    let frame = getStack(Components.stack.caller, 1)[0];
+    let timer = stopTimer(args[0]);
+    sendConsoleAPIMessage("timeEnd", frame, args, { timer: timer });
+    dump("console.timeEnd: '" + timer.name + "' " + timer.duration + "ms\n");
+  },
 };