--- 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