Bug 1303333 - Implement the probe to count URI loads triggered by search. r=mak, data-review=rweiss
authorAlessio Placitelli <alessio.placitelli@gmail.com>
Fri, 28 Oct 2016 23:13:00 +0200
changeset 320188 e7c1f4f53f7c1e950f107e3dae0b4e5a93d237de
parent 320187 8109faaf35c0f8886aaabf31c61f7ece6ec33ddd
child 320189 eab68afb07088c28bd8bdff5615ef1131799e28f
push id20751
push userphilringnalda@gmail.com
push dateSun, 30 Oct 2016 18:06:35 +0000
treeherderfx-team@e3279760cd97 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs1303333
milestone52.0a1
Bug 1303333 - Implement the probe to count URI loads triggered by search. r=mak, data-review=rweiss MozReview-Commit-ID: 3toynbrFeLP
browser/base/content/browser.js
browser/base/content/urlbarBindings.xml
browser/components/search/content/search.xml
browser/modules/BrowserUsageTelemetry.jsm
browser/modules/ContentSearch.jsm
browser/modules/test/browser.ini
browser/modules/test/browser_UsageTelemetry_content.js
browser/modules/test/browser_UsageTelemetry_content_aboutHome.js
browser/modules/test/browser_UsageTelemetry_searchbar.js
browser/modules/test/browser_UsageTelemetry_urlbar.js
browser/modules/test/head.js
browser/modules/test/usageTelemetrySearchSuggestions.sjs
browser/modules/test/usageTelemetrySearchSuggestions.xml
toolkit/components/telemetry/Scalars.yaml
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3792,35 +3792,58 @@ const BrowserSearch = {
   /**
    * Helper to record a search with Telemetry.
    *
    * Telemetry records only search counts and nothing pertaining to the search itself.
    *
    * @param engine
    *        (nsISearchEngine) The engine handling the search.
    * @param source
-   *        (string) Where the search originated from. See the FHR
-   *        SearchesProvider for allowed values.
-   * @param selection [optional]
-   *        ({index: The selected index, kind: "key" or "mouse"}) If
-   *        the search was a suggested search, this indicates where the
-   *        item was in the suggestion list and how the user selected it.
+   *        (string) Where the search originated from. See BrowserUsageTelemetry for
+   *        allowed values.
+   * @param details [optional]
+   *        An optional parameter passed to |BrowserUsageTelemetry.recordSearch|.
+   *        See its documentation for allowed options.
+   *        Additionally, if the search was a suggested search, |details.selection|
+   *        indicates where the item was in the suggestion list and how the user
+   *        selected it: {selection: {index: The selected index, kind: "key" or "mouse"}}
    */
-  recordSearchInTelemetry: function (engine, source, selection) {
-    BrowserUITelemetry.countSearchEvent(source, null, selection);
+  recordSearchInTelemetry: function (engine, source, details={}) {
+    BrowserUITelemetry.countSearchEvent(source, null, details.selection);
     try {
-      BrowserUsageTelemetry.recordSearch(engine, source);
+      BrowserUsageTelemetry.recordSearch(engine, source, details);
     } catch (ex) {
       Cu.reportError(ex);
     }
   },
 
+  /**
+   * Helper to record a one-off search with Telemetry.
+   *
+   * Telemetry records only search counts and nothing pertaining to the search itself.
+   *
+   * @param engine
+   *        (nsISearchEngine) The engine handling the search.
+   * @param source
+   *        (string) Where the search originated from. See BrowserUsageTelemetry for
+   *        allowed values.
+   * @param type
+   *        (string) Indicates how the user selected the search item.
+   * @param where
+   *        (string) Where was the search link opened (e.g. new tab, current tab, ..).
+   */
   recordOneoffSearchInTelemetry: function (engine, source, type, where) {
     let id = this._getSearchEngineId(engine) + "." + source;
     BrowserUITelemetry.countOneoffSearchEvent(id, type, where);
+    try {
+      const details = {type, isOneOff: true};
+      BrowserUsageTelemetry.recordSearch(engine, source, details);
+    } catch (ex) {
+      Cu.reportError(ex);
+    }
   }
 };
 
 XPCOMUtils.defineConstant(this, "BrowserSearch", BrowserSearch);
 
 function FillHistoryMenu(aParent) {
   // Lazily add the hover listeners on first showing and never remove them
   if (!aParent.hasStatusListener) {
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -448,22 +448,27 @@ file, You can obtain one at http://mozil
                   return;
                 }
                 break;
               case "searchengine":
                 if (selectedOneOff && selectedOneOff.engine) {
                   // Replace the engine with the selected one-off engine.
                   action.params.engineName = selectedOneOff.engine.name;
                 }
+                const actionDetails = {
+                  isSuggestion: !!action.params.searchSuggestion,
+                  isAlias: !!action.params.alias
+                };
                 [url, postData] = this._recordSearchEngineLoad(
                   action.params.engineName,
                   action.params.searchSuggestion || action.params.searchQuery,
                   event,
                   where,
-                  openUILinkParams
+                  openUILinkParams,
+                  actionDetails
                 );
                 break;
             }
             this._loadURL(url, browser, postData, where, openUILinkParams,
                           matchLastLocationChange, mayInheritPrincipal);
             return;
           }
 
@@ -584,24 +589,37 @@ file, You can obtain one at http://mozil
       </method>
 
       <method name="_recordSearchEngineLoad">
         <parameter name="engineOrEngineName"/>
         <parameter name="query"/>
         <parameter name="event"/>
         <parameter name="openUILinkWhere"/>
         <parameter name="openUILinkParams"/>
+        <parameter name="searchActionDetails"/>
         <body><![CDATA[
           let engine =
             typeof(engineOrEngineName) == "string" ?
               Services.search.getEngineByName(engineOrEngineName) :
               engineOrEngineName;
-          BrowserSearch.recordSearchInTelemetry(engine, "urlbar");
-          this.popup.oneOffSearchButtons
+          let isOneOff = this.popup.oneOffSearchButtons
               .maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams);
+          // Infer the type of the even which triggered the search.
+          let eventType = "unknown";
+          if (event instanceof KeyboardEvent) {
+            eventType = "key";
+          } else if (event instanceof MouseEvent) {
+            eventType = "mouse";
+          }
+          // Augment the search action details object.
+          let details = searchActionDetails || {};
+          details.isOneOff = isOneOff;
+          details.type = eventType;
+
+          BrowserSearch.recordSearchInTelemetry(engine, "urlbar", details);
           let submission = engine.getSubmission(query, null, "keyword");
           return [submission.uri.spec, submission.postData];
         ]]></body>
       </method>
 
       <method name="maybeCanonizeURL">
         <parameter name="aTriggeringEvent"/>
         <parameter name="aUrl"/>
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -368,22 +368,22 @@
         <parameter name="aEngine"/>
         <parameter name="aWhere"/>
         <parameter name="aParams"/>
         <body><![CDATA[
           var textBox = this._textbox;
           var textValue = textBox.value;
 
           let selection = this.telemetrySearchDetails;
-          this.doSearch(textValue, aWhere, aEngine, aParams);
+          let oneOffRecorded = false;
 
           if (!selection || (selection.index == -1)) {
-            let recorded = this.textbox.popup.oneOffButtons
-                               .maybeRecordTelemetry(aEvent, aWhere, aParams);
-            if (!recorded) {
+            oneOffRecorded = this.textbox.popup.oneOffButtons
+                                 .maybeRecordTelemetry(aEvent, aWhere, aParams);
+            if (!oneOffRecorded) {
               let source = "unknown";
               let type = "unknown";
               let target = aEvent.originalTarget;
               if (aEvent instanceof KeyboardEvent) {
                 type = "key";
               } else if (aEvent instanceof MouseEvent) {
                 type = "mouse";
                 if (target.classList.contains("search-panel-header") ||
@@ -398,26 +398,30 @@
               if (!aEngine) {
                 aEngine = this.currentEngine;
               }
               BrowserSearch.recordOneoffSearchInTelemetry(aEngine, source, type,
                                                           aWhere);
             }
           }
 
+          // This is a one-off search only if oneOffRecorded is true.
+          this.doSearch(textValue, aWhere, aEngine, aParams, oneOffRecorded);
+
           if (aWhere == "tab" && aParams && aParams.inBackground)
             this.focus();
         ]]></body>
       </method>
 
       <method name="doSearch">
         <parameter name="aData"/>
         <parameter name="aWhere"/>
         <parameter name="aEngine"/>
         <parameter name="aParams"/>
+        <parameter name="aOneOff"/>
         <body><![CDATA[
           var textBox = this._textbox;
 
           // Save the current value in the form history
           if (aData && !PrivateBrowsingUtils.isWindowPrivate(window) && this.FormHistory.enabled) {
             this.FormHistory.update(
               { op : "bump",
                 fieldname : textBox.getAttribute("autocompletesearchparam"),
@@ -429,17 +433,23 @@
 
           let engine = aEngine || this.currentEngine;
           var submission = engine.getSubmission(aData, null, "searchbar");
           let telemetrySearchDetails = this.telemetrySearchDetails;
           this.telemetrySearchDetails = null;
           if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
             telemetrySearchDetails = null;
           }
-          BrowserSearch.recordSearchInTelemetry(engine, "searchbar", telemetrySearchDetails);
+          // If we hit here, we come either from a one-off, a plain search or a suggestion.
+          const details = {
+            isOneOff: aOneOff,
+            isSuggestion: (!aOneOff && telemetrySearchDetails),
+            selection: telemetrySearchDetails
+          };
+          BrowserSearch.recordSearchInTelemetry(engine, "searchbar", details);
           // null parameter below specifies HTML response for search
           let params = {
             postData: submission.postData,
           };
           if (aParams) {
             for (let key in aParams) {
               params[key] = aParams[key];
             }
--- a/browser/modules/BrowserUsageTelemetry.jsm
+++ b/browser/modules/BrowserUsageTelemetry.jsm
@@ -37,16 +37,22 @@ const UNFILTERED_URI_COUNT_SCALAR_NAME =
 const KNOWN_SEARCH_SOURCES = [
   "abouthome",
   "contextmenu",
   "newtab",
   "searchbar",
   "urlbar",
 ];
 
+const KNOWN_ONEOFF_SOURCES = [
+  "oneoff-urlbar",
+  "oneoff-searchbar",
+  "unknown", // Edge case: this is the searchbar (see bug 1195733 comment 7).
+];
+
 function getOpenTabsAndWinsCounts() {
   let tabCount = 0;
   let winCount = 0;
 
   let browserEnum = Services.wm.getEnumerator("navigator:browser");
   while (browserEnum.hasMoreElements()) {
     let win = browserEnum.getNext();
     winCount++;
@@ -241,30 +247,120 @@ let BrowserUsageTelemetry = {
 
   /**
    * The main entry point for recording search related Telemetry. This includes
    * search counts and engagement measurements.
    *
    * Telemetry records only search counts per engine and action origin, but
    * nothing pertaining to the search contents themselves.
    *
-   * @param engine
-   *        (nsISearchEngine) The engine handling the search.
-   * @param source
-   *        (string) Where the search originated from. See
-   *        KNOWN_SEARCH_SOURCES for allowed values.
+   * @param {nsISearchEngine} engine
+   *        The engine handling the search.
+   * @param {String} source
+   *        Where the search originated from. See KNOWN_SEARCH_SOURCES for allowed
+   *        values.
+   * @param {Object} [details] Options object.
+   * @param {Boolean} [details.isOneOff=false]
+   *        true if this event was generated by a one-off search.
+   * @param {Boolean} [details.isSuggestion=false]
+   *        true if this event was generated by a suggested search.
+   * @param {Boolean} [details.isAlias=false]
+   *        true if this event was generated by a search using an alias.
+   * @param {Object} [details.type=null]
+   *        The object describing the event that triggered the search.
    * @throws if source is not in the known sources list.
    */
-  recordSearch(engine, source) {
-    if (!KNOWN_SEARCH_SOURCES.includes(source)) {
-      throw new Error("Unknown source for search: " + source);
+  recordSearch(engine, source, details={}) {
+    const isOneOff = !!details.isOneOff;
+
+    if (isOneOff) {
+      if (!KNOWN_ONEOFF_SOURCES.includes(source)) {
+        throw new Error("Unknown source for one-off search: " + source);
+      }
+    } else {
+      if (!KNOWN_SEARCH_SOURCES.includes(source)) {
+        throw new Error("Unknown source for search: " + source);
+      }
+      let countId = getSearchEngineId(engine) + "." + source;
+      Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").add(countId);
     }
 
-    let countId = getSearchEngineId(engine) + "." + source;
-    Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").add(countId);
+    // Dispatch the search signal to other handlers.
+    this._handleSearchAction(source, details);
+  },
+
+  _handleSearchAction(source, details) {
+    switch (source) {
+      case "urlbar":
+      case "oneoff-urlbar":
+      case "searchbar":
+      case "oneoff-searchbar":
+      case "unknown": // Edge case: this is the searchbar (see bug 1195733 comment 7).
+        this._handleSearchAndUrlbar(source, details);
+        break;
+      case "abouthome":
+        Services.telemetry.keyedScalarAdd("browser.engagement.navigation.about_home",
+                                          "search_enter", 1);
+        break;
+      case "newtab":
+        Services.telemetry.keyedScalarAdd("browser.engagement.navigation.about_newtab",
+                                          "search_enter", 1);
+        break;
+      case "contextmenu":
+        Services.telemetry.keyedScalarAdd("browser.engagement.navigation.contextmenu",
+                                          "search", 1);
+        break;
+    }
+  },
+
+  /**
+   * This function handles the "urlbar", "urlbar-oneoff", "searchbar" and
+   * "searchbar-oneoff" sources.
+   */
+  _handleSearchAndUrlbar(source, details) {
+    // We want "urlbar" and "urlbar-oneoff" (and similar cases) to go in the same
+    // scalar, but in a different key.
+
+    // When using one-offs in the searchbar we get an "unknown" source. See bug
+    // 1195733 comment 7 for the context. Fix-up the label here.
+    const plainSourceName =
+      (source === "unknown") ? "searchbar" : source.replace("oneoff-", "");
+    const scalarName = "browser.engagement.navigation." + plainSourceName;
+
+    const isOneOff = !!details.isOneOff;
+    if (isOneOff) {
+      // We will receive a signal from the "urlbar"/"searchbar" even when the
+      // search came from "oneoff-urlbar". That's because both signals
+      // are propagated from search.xml. Skip it if that's the case.
+      // Moreover, we skip the "unknown" source that comes from the searchbar
+      // when performing searches from the default search engine. See bug 1195733
+      // comment 7 for context.
+      if (["urlbar", "searchbar", "unknown"].includes(source)) {
+        return;
+      }
+
+      // If that's a legit one-off search signal, increment the scalar using the
+      // relative key.
+      Services.telemetry.keyedScalarAdd(scalarName, "search_oneoff", 1);
+      return;
+    }
+
+    // The search was not a one-off. It was a search with the default search engine.
+    if (details.isSuggestion) {
+      // It came from a suggested search, so count it as such.
+      Services.telemetry.keyedScalarAdd(scalarName, "search_suggestion", 1);
+      return;
+    } else if (details.isAlias) {
+      // This one came from a search that used an alias.
+      Services.telemetry.keyedScalarAdd(scalarName, "search_alias", 1);
+      return;
+    }
+
+    // The search signal was generated by typing something and pressing enter.
+    Services.telemetry.keyedScalarAdd(scalarName, "search_enter", 1);
   },
 
   /**
    * This gets called shortly after the SessionStore has finished restoring
    * windows and tabs. It counts the open tabs and adds listeners to all the
    * windows.
    */
   _setupAfterRestore() {
--- a/browser/modules/ContentSearch.jsm
+++ b/browser/modules/ContentSearch.jsm
@@ -250,17 +250,17 @@ this.ContentSearch = {
     } else {
       let params = {
         postData: submission.postData,
         inBackground: Services.prefs.getBoolPref("browser.tabs.loadInBackground"),
       };
       win.openUILinkIn(submission.uri.spec, where, params);
     }
     win.BrowserSearch.recordSearchInTelemetry(engine, data.healthReportKey,
-                                              data.selection || null);
+                                              { selection: data.selection });
     return;
   },
 
   getSuggestions: Task.async(function* (engineName, searchString, browser, remoteTimeout=null) {
     let engine = Services.search.getEngineByName(engineName);
     if (!engine) {
       throw new Error("Unknown engine name: " + engineName);
     }
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -22,9 +22,22 @@ support-files =
   ../../components/uitour/test/uitour.html
   ../../components/uitour/UITour-lib.js
 [browser_taskbar_preview.js]
 skip-if = os != "win"
 [browser_UnsubmittedCrashHandler.js]
 run-if = crashreporter
 [browser_UsageTelemetry.js]
 [browser_UsageTelemetry_private_and_restore.js]
+[browser_UsageTelemetry_urlbar.js]
+support-files =
+  usageTelemetrySearchSuggestions.sjs
+  usageTelemetrySearchSuggestions.xml
+[browser_UsageTelemetry_searchbar.js]
+support-files =
+  usageTelemetrySearchSuggestions.sjs
+  usageTelemetrySearchSuggestions.xml
+[browser_UsageTelemetry_content.js]
+[browser_UsageTelemetry_content_aboutHome.js]
+# Disabled for intermittent failures.
+# Re-enabling this test is tracked in bug 1313825.
+skip-if = true
 [browser_urlBar_zoom.js]
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UsageTelemetry_content.js
@@ -0,0 +1,96 @@
+"use strict";
+
+const BASE_PROBE_NAME = "browser.engagement.navigation.";
+const SCALAR_CONTEXT_MENU = BASE_PROBE_NAME + "contextmenu";
+const SCALAR_ABOUT_NEWTAB = BASE_PROBE_NAME + "about_newtab";
+
+add_task(function* setup() {
+  // Create two new search engines. Mark one as the default engine, so
+  // the test don't crash. We need to engines for this test as the searchbar
+  // in content doesn't display the default search engine among the one-off engines.
+  Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  Services.search.addEngineWithDetails("MozSearch2", "", "mozalias2", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  // Make the first engine the default search engine.
+  let engineDefault = Services.search.getEngineByName("MozSearch");
+  let originalEngine = Services.search.currentEngine;
+  Services.search.currentEngine = engineDefault;
+
+  // Move the second engine at the beginning of the one-off list.
+  let engineOneOff = Services.search.getEngineByName("MozSearch2");
+  Services.search.moveEngine(engineOneOff, 0);
+
+  // Make sure to restore the engine once we're done.
+  registerCleanupFunction(function* () {
+    Services.search.currentEngine = originalEngine;
+    Services.search.removeEngine(engineDefault);
+    Services.search.removeEngine(engineOneOff);
+  });
+});
+
+add_task(function* test_context_menu() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  // Open a new tab with a page containing some text.
+  let tab =
+    yield BrowserTestUtils.openNewForegroundTab(gBrowser, "data:text/plain;charset=utf8,test%20search");
+
+  info("Select all the text in the page.");
+  yield ContentTask.spawn(tab.linkedBrowser, "", function*() {
+    return new Promise(resolve => {
+      content.document.addEventListener("selectionchange", () => resolve(), { once: true });
+      content.document.getSelection().selectAllChildren(content.document.body);
+    });
+  });
+
+  info("Open the context menu.");
+  let contextMenu = document.getElementById("contentAreaContextMenu");
+  let popupPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+  BrowserTestUtils.synthesizeMouseAtCenter("body", { type: "contextmenu", button: 2 },
+                                           gBrowser.selectedBrowser);
+  yield popupPromise;
+
+  info("Click on search.");
+  let searchItem = contextMenu.getElementsByAttribute("id", "context-searchselect")[0];
+  searchItem.click();
+
+  info("Validate the search counts.");
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_CONTEXT_MENU, "search", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_CONTEXT_MENU]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  contextMenu.hidePopup();
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* test_about_newtab() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false);
+  yield ContentTask.spawn(tab.linkedBrowser, null, function* () {
+    yield ContentTaskUtils.waitForCondition(() => !content.document.hidden);
+  });
+
+  info("Trigger a simple serch, just text + enter.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield typeInSearchField(tab.linkedBrowser, "test query", "newtab-search-text");
+  yield BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser);
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_ABOUT_NEWTAB, "search_enter", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_ABOUT_NEWTAB]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UsageTelemetry_content_aboutHome.js
@@ -0,0 +1,64 @@
+"use strict";
+
+const SCALAR_ABOUT_HOME = "browser.engagement.navigation.about_home";
+
+add_task(function* setup() {
+  // about:home uses IndexedDB. However, the test finishes too quickly and doesn't
+  // allow it enougth time to save. So it throws. This disables all the uncaught
+  // exception in this file and that's the reason why we split about:home tests
+  // out of the other UsageTelemetry files.
+  ignoreAllUncaughtExceptions();
+
+  // Create two new search engines. Mark one as the default engine, so
+  // the test don't crash. We need to engines for this test as the searchbar
+  // in content doesn't display the default search engine among the one-off engines.
+  Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  Services.search.addEngineWithDetails("MozSearch2", "", "mozalias2", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  // Make the first engine the default search engine.
+  let engineDefault = Services.search.getEngineByName("MozSearch");
+  let originalEngine = Services.search.currentEngine;
+  Services.search.currentEngine = engineDefault;
+
+  // Move the second engine at the beginning of the one-off list.
+  let engineOneOff = Services.search.getEngineByName("MozSearch2");
+  Services.search.moveEngine(engineOneOff, 0);
+
+  // Make sure to restore the engine once we're done.
+  registerCleanupFunction(function* () {
+    Services.search.currentEngine = originalEngine;
+    Services.search.removeEngine(engineDefault);
+    Services.search.removeEngine(engineOneOff);
+  });
+});
+
+add_task(function* test_abouthome_simpleQuery() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");
+  yield new Promise(resolve => {
+    tab.linkedBrowser.addEventListener("AboutHomeLoadSnippetsCompleted", function loadListener(event) {
+      tab.linkedBrowser.removeEventListener("AboutHomeLoadSnippetsCompleted", loadListener, true);
+      resolve();
+    }, true, true);
+  });
+
+  info("Trigger a simple serch, just test + enter.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield typeInSearchField(tab.linkedBrowser, "test query", "searchText");
+  yield BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser);
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_ABOUT_HOME, "search_enter", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_ABOUT_HOME]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UsageTelemetry_searchbar.js
@@ -0,0 +1,162 @@
+"use strict";
+
+const SCALAR_SEARCHBAR = "browser.engagement.navigation.searchbar";
+
+let searchInSearchbar = Task.async(function* (inputText) {
+  let win = window;
+  yield new Promise(r => waitForFocus(r, win));
+  let sb = BrowserSearch.searchBar;
+  // Write the search query in the searchbar.
+  sb.focus();
+  sb.value = inputText;
+  //sb.textbox.openPopup();
+  sb.textbox.controller.startSearch(inputText);
+  // Wait for the popup to show.
+  yield BrowserTestUtils.waitForEvent(sb.textbox.popup, "popupshown");
+  // And then for the search to complete.
+  yield BrowserTestUtils.waitForCondition(() => sb.textbox.controller.searchStatus >=
+                                                Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH,
+                                          "The search in the searchbar must complete.");
+});
+
+/**
+ * Click one of the entries in the search suggestion popup.
+ *
+ * @param {String} entryName
+ *        The name of the elemet to click on.
+ */
+function clickSearchbarSuggestion(entryName) {
+  let popup = BrowserSearch.searchBar.textbox.popup;
+  let column = popup.tree.columns[0];
+
+  for (let rowID = 0; rowID < popup.tree.view.rowCount; rowID++) {
+    const suggestion = popup.tree.view.getValueAt(rowID, column);
+    if (suggestion !== entryName) {
+      continue;
+    }
+
+    // Make sure the suggestion is visible, just in case.
+    let tbo = popup.tree.treeBoxObject;
+    tbo.ensureRowIsVisible(rowID);
+    // Calculate the click coordinates.
+    let rect = tbo.getCoordsForCellItem(rowID, column, "text");
+    let x = rect.x + rect.width / 2;
+    let y = rect.y + rect.height / 2;
+    // Simulate the click.
+    EventUtils.synthesizeMouse(popup.tree.body, x, y, {},
+                               popup.tree.ownerGlobal);
+    break;
+  }
+}
+
+add_task(function* setup() {
+  // Create two new search engines. Mark one as the default engine, so
+  // the test don't crash. We need to engines for this test as the searchbar
+  // doesn't display the default search engine among the one-off engines.
+  Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  Services.search.addEngineWithDetails("MozSearch2", "", "mozalias2", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  // Make the first engine the default search engine.
+  let engineDefault = Services.search.getEngineByName("MozSearch");
+  let originalEngine = Services.search.currentEngine;
+  Services.search.currentEngine = engineDefault;
+
+  // Move the second engine at the beginning of the one-off list.
+  let engineOneOff = Services.search.getEngineByName("MozSearch2");
+  Services.search.moveEngine(engineOneOff, 0);
+
+  // Make sure to restore the engine once we're done.
+  registerCleanupFunction(function* () {
+    Services.search.currentEngine = originalEngine;
+    Services.search.removeEngine(engineDefault);
+    Services.search.removeEngine(engineOneOff);
+  });
+});
+
+add_task(function* test_plainQuery() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Simulate entering a simple search.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInSearchbar("simple query");
+  EventUtils.sendKey("return");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_SEARCHBAR, "search_enter", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_SEARCHBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* test_oneOff() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Perform a one-off search using the first engine.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInSearchbar("query");
+
+  info("Pressing Alt+Down to highlight the first one off engine.");
+  EventUtils.synthesizeKey("VK_DOWN", { altKey: true });
+  EventUtils.sendKey("return");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_SEARCHBAR, "search_oneoff", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_SEARCHBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* test_suggestion() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  // Create an engine to generate search suggestions and add it as default
+  // for this test.
+  const url = getRootDirectory(gTestPath) + "usageTelemetrySearchSuggestions.xml";
+  let suggestionEngine = yield new Promise((resolve, reject) => {
+    Services.search.addEngine(url, null, "", false, {
+      onSuccess(engine) { resolve(engine) },
+      onError() { reject() }
+    });
+  });
+
+  let previousEngine = Services.search.currentEngine;
+  Services.search.currentEngine = suggestionEngine;
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Perform a one-off search using the first engine.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInSearchbar("query");
+  info("Clicking the searchbar suggestion.");
+  clickSearchbarSuggestion("queryfoo");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_SEARCHBAR, "search_suggestion", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_SEARCHBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  Services.search.currentEngine = previousEngine;
+  Services.search.removeEngine(suggestionEngine);
+  yield BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_UsageTelemetry_urlbar.js
@@ -0,0 +1,171 @@
+"use strict";
+
+const SCALAR_URLBAR = "browser.engagement.navigation.urlbar";
+
+// The preference to enable suggestions in the urlbar.
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+// The name of the search engine used to generate suggestions.
+const SUGGESTION_ENGINE_NAME = "browser_UsageTelemetry usageTelemetrySearchSuggestions.xml";
+
+let searchInAwesomebar = Task.async(function* (inputText, win=window) {
+  yield new Promise(r => waitForFocus(r, win));
+  // Write the search query in the urlbar.
+  win.gURLBar.focus();
+  win.gURLBar.value = inputText;
+  win.gURLBar.controller.startSearch(inputText);
+  // Wait for the popup to show.
+  yield BrowserTestUtils.waitForEvent(win.gURLBar.popup, "popupshown");
+  // And then for the search to complete.
+  yield BrowserTestUtils.waitForCondition(() => win.gURLBar.controller.searchStatus >=
+                                                Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
+});
+
+/**
+ * Click one of the entries in the urlbar suggestion popup.
+ *
+ * @param {String} entryName
+ *        The name of the elemet to click on.
+ */
+function clickURLBarSuggestion(entryName) {
+  // The entry in the suggestion list should follow the format:
+  // "<search term> <engine name> Search"
+  const expectedSuggestionName = entryName + " " + SUGGESTION_ENGINE_NAME + " Search";
+  for (let child of gURLBar.popup.richlistbox.children) {
+    if (child.label === expectedSuggestionName) {
+      // This entry is the search suggestion we're looking for.
+      child.click();
+      return;
+    }
+  }
+}
+
+add_task(function* setup() {
+  // Create a new search engine.
+  Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET",
+                                       "http://example.com/?q={searchTerms}");
+
+  // Make it the default search engine.
+  let engine = Services.search.getEngineByName("MozSearch");
+  let originalEngine = Services.search.currentEngine;
+  Services.search.currentEngine = engine;
+
+  // And the first one-off engine.
+  Services.search.moveEngine(engine, 0);
+
+  // Enable search suggestions in the urlbar.
+  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+
+  // Make sure to restore the engine once we're done.
+  registerCleanupFunction(function* () {
+    Services.search.currentEngine = originalEngine;
+    Services.search.removeEngine(engine);
+    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF, true);
+  });
+});
+
+add_task(function* test_simpleQuery() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Simulate entering a simple search.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInAwesomebar("simple query");
+  EventUtils.sendKey("return");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_URLBAR, "search_enter", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* test_searchAlias() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Search using a search alias.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInAwesomebar("mozalias query");
+  EventUtils.sendKey("return");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_URLBAR, "search_alias", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* test_oneOff() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Perform a one-off search using the first engine.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInAwesomebar("query");
+
+  info("Pressing Alt+Down to take us to the first one-off engine.");
+  EventUtils.synthesizeKey("VK_DOWN", { altKey: true });
+  EventUtils.sendKey("return");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_URLBAR, "search_oneoff", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* test_suggestion() {
+  // Let's reset the counts.
+  Services.telemetry.clearScalars();
+
+  // Create an engine to generate search suggestions and add it as default
+  // for this test.
+  const url = getRootDirectory(gTestPath) + "usageTelemetrySearchSuggestions.xml";
+  let suggestionEngine = yield new Promise((resolve, reject) => {
+    Services.search.addEngine(url, null, "", false, {
+      onSuccess(engine) { resolve(engine) },
+      onError() { reject() }
+    });
+  });
+
+  let previousEngine = Services.search.currentEngine;
+  Services.search.currentEngine = suggestionEngine;
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank");
+
+  info("Perform a one-off search using the first engine.");
+  let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  yield searchInAwesomebar("query");
+  info("Clicking the urlbar suggestion.");
+  clickURLBarSuggestion("queryfoo");
+  yield p;
+
+  // Check if the scalars contain the expected values.
+  const scalars =
+    Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+  checkKeyedScalar(scalars, SCALAR_URLBAR, "search_suggestion", 1);
+  Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1,
+               "This search must only increment one entry in the scalar.");
+
+  Services.search.currentEngine = previousEngine;
+  Services.search.removeEngine(suggestionEngine);
+  yield BrowserTestUtils.removeTab(tab);
+});
--- a/browser/modules/test/head.js
+++ b/browser/modules/test/head.js
@@ -27,8 +27,53 @@ function waitForConditionPromise(conditi
   return defer.promise;
 }
 
 function waitForCondition(condition, nextTest, errorMsg) {
   waitForConditionPromise(condition, errorMsg).then(nextTest, (reason) => {
     ok(false, reason + (reason.stack ? "\n" + reason.stack : ""));
   });
 }
+
+/**
+ * Checks if the snapshotted keyed scalars contain the expected
+ * data.
+ *
+ * @param {Object} scalars
+ *        The snapshot of the keyed scalars.
+ * @param {String} scalarName
+ *        The name of the keyed scalar to check.
+ * @param {String} key
+ *        The key that must be within the keyed scalar.
+ * @param {String|Boolean|Number} expectedValue
+ *        The expected value for the provided key in the scalar.
+ */
+function checkKeyedScalar(scalars, scalarName, key, expectedValue) {
+  Assert.ok(scalarName in scalars,
+            scalarName + " must be recorded.");
+  Assert.ok(key in scalars[scalarName],
+            scalarName + " must contain the '" + key + "' key.");
+  Assert.ok(scalars[scalarName][key], expectedValue,
+            scalarName + "['" + key + "'] must contain the expected value");
+}
+
+/**
+ * An utility function to write some text in the search input box
+ * in a content page.
+ * @param {Object} browser
+ *        The browser that contains the content.
+ * @param {String} text
+ *        The string to write in the search field.
+ * @param {String} fieldName
+ *        The name of the field to write to.
+ */
+let typeInSearchField = Task.async(function* (browser, text, fieldName) {
+  yield ContentTask.spawn(browser, { fieldName, text }, function* ({fieldName, text}) {
+    // Avoid intermittent failures.
+    if (fieldName === "searchText") {
+      content.wrappedJSObject.gContentSearchController.remoteTimeout = 5000;
+    }
+    // Put the focus on the search box.
+    let searchInput = content.document.getElementById(fieldName);
+    searchInput.focus();
+    searchInput.value = text;
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/usageTelemetrySearchSuggestions.sjs
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(req, resp) {
+  let suffixes = ["foo", "bar"];
+  let data = [req.queryString, suffixes.map(s => req.queryString + s)];
+  resp.setHeader("Content-Type", "application/json", false);
+  resp.write(JSON.stringify(data));
+}
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/usageTelemetrySearchSuggestions.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_UsageTelemetry usageTelemetrySearchSuggestions.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/modules/test/usageTelemetrySearchSuggestions.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://example.com" rel="searchform"/>
+</SearchPlugin>
--- a/toolkit/components/telemetry/Scalars.yaml
+++ b/toolkit/components/telemetry/Scalars.yaml
@@ -91,16 +91,83 @@ browser.engagement:
       This does not include background page requests and domains from embedded pages
       or private browsing. The count is limited to 100 unique domains.
     expires: "55"
     kind: uint
     notification_emails:
       - rweiss@mozilla.com
     release_channel_collection: opt-out
 
+# The following section contains the browser engagement scalars.
+browser.engagement.navigation:
+  urlbar:
+    bug_numbers:
+      - 1271313
+    description: >
+      The count URI loads triggered in a subsession from the urlbar (awesomebar),
+      broken down by the originating action.
+    expires: "55"
+    kind: uint
+    keyed: true
+    notification_emails:
+      - bcolloran@mozilla.com
+    release_channel_collection: opt-out
+
+  searchbar:
+    bug_numbers:
+      - 1271313
+    description: >
+      The count URI loads triggered in a subsession from the searchbar,
+      broken down by the originating action.
+    expires: "55"
+    kind: uint
+    keyed: true
+    notification_emails:
+      - bcolloran@mozilla.com
+    release_channel_collection: opt-out
+
+  about_home:
+    bug_numbers:
+      - 1271313
+    description: >
+      The count URI loads triggered in a subsession from about:home,
+      broken down by the originating action.
+    expires: "55"
+    kind: uint
+    keyed: true
+    notification_emails:
+      - bcolloran@mozilla.com
+    release_channel_collection: opt-out
+
+  about_newtab:
+    bug_numbers:
+      - 1271313
+    description: >
+      The count URI loads triggered in a subsession from about:newtab,
+      broken down by the originating action.
+    expires: "55"
+    kind: uint
+    keyed: true
+    notification_emails:
+      - bcolloran@mozilla.com
+    release_channel_collection: opt-out
+
+  contextmenu:
+    bug_numbers:
+      - 1271313
+    description: >
+      The count URI loads triggered in a subsession from the contextmenu,
+      broken down by the originating action.
+    expires: "55"
+    kind: uint
+    keyed: true
+    notification_emails:
+      - bcolloran@mozilla.com
+    release_channel_collection: opt-out
+
 # The following section is for probes testing the Telemetry system. They will not be
 # submitted in pings and are only used for testing.
 telemetry.test:
   unsigned_int_kind:
     bug_numbers:
       - 1276190
     description: >
       This is a test uint type with a really long description, maybe spanning even multiple