Bug 1559136 - Add urlbar event telemetry behind a pref. r=adw

Differential Revision: https://phabricator.services.mozilla.com/D38521

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Marco Bonardo 2019-07-25 12:39:02 +00:00
Родитель 79d0a81073
Коммит 3a39214a77
8 изменённых файлов: 923 добавлений и 14 удалений

Просмотреть файл

@ -309,7 +309,10 @@ var gURLBarHandler = {
get urlbar() {
if (!this._urlbar) {
let textbox = document.getElementById("urlbar");
this._urlbar = new UrlbarInput({ textbox });
this._urlbar = new UrlbarInput({
textbox,
eventTelemetryCategory: "urlbar",
});
if (this._lastValue) {
this._urlbar.value = this._lastValue;
delete this._lastValue;

Просмотреть файл

@ -65,6 +65,8 @@ class UrlbarController {
this._listeners = new Set();
this._userSelectionBehavior = "none";
this.engagementEvent = new TelemetryEvent(options.eventTelemetryCategory);
}
/**
@ -339,6 +341,7 @@ class UrlbarController {
}
if (executeAction) {
this.userSelectionBehavior = "arrow";
this.engagementEvent.start(event);
this.input.startQuery({ searchString: this.input.textValue });
}
}
@ -581,3 +584,196 @@ class UrlbarController {
}
}
}
/**
* Tracks and records telemetry events for the given category, if provided,
* otherwise it's a no-op.
* It is currently designed around the "urlbar" category, even if it can
* potentially be extended to other categories.
* To record an event, invoke start() with a starting event, then either
* invoke record() with a final event, or discard() to drop the recording.
* @see Events.yaml
*/
class TelemetryEvent {
constructor(category) {
this._category = category;
}
/**
* Start measuring the elapsed time from an input event.
* After this has been invoked, any subsequent calls to start() are ignored,
* until either record() or discard() are invoked. Thus, it is safe to keep
* invoking this on every input event.
* @param {event} event A DOM input event.
* @note This should never throw, or it may break the urlbar.
*/
start(event) {
// Start is invoked at any input, but we only count the first one.
// Once an engagement or abandoment happens, we clear the _startEventInfo.
if (!this._category || this._startEventInfo) {
return;
}
if (!event) {
Cu.reportError("Must always provide an event");
return;
}
if (!["input", "drop", "mousedown", "keydown"].includes(event.type)) {
Cu.reportError("Can't start recording from event type: " + event.type);
return;
}
// "typed" is used when the user types something, while "pasted" and
// "dropped" are used when the text is inserted at once, by a paste or drop
// operation. "topsites" is a bit special, it is used when the user opens
// the empty search dropdown, that is supposed to show top sites. That
// happens by clicking on the urlbar dropmarker, or pressing DOWN with an
// empty input field. Even if the user later types something, we still
// report "topsites", with a positive numChars.
let interactionType = "topsites";
if (event.type == "input") {
interactionType = UrlbarUtils.isPasteEvent(event) ? "pasted" : "typed";
} else if (event.type == "drop") {
interactionType = "dropped";
}
this._startEventInfo = {
timeStamp: event.timeStamp || Cu.now(),
interactionType,
};
}
/**
* Record an engagement telemetry event.
* When the user picks a result from a search through the mouse or keyboard,
* an engagement event is recorded. If instead the user abandons a search, by
* blurring the input field, an abandonment event is recorded.
* @param {event} [event] A DOM event.
* @param {object} details An object describing action details.
* @param {string} details.numChars Number of input characters.
* @param {string} details.selIndex Index of the selected result, undefined
* for "blur".
* @param {string} details.selType type of the selected element, undefined
* for "blur". One of "none", "autofill", "visit", "bookmark",
* "history", "keyword", "search", "searchsuggestion", "switchtab",
* "remotetab", "extension", "oneoff".
* @note event can be null, that usually happens for paste&go or drop&go.
* If there's no _startEventInfo this is a no-op.
*/
record(event, details) {
// This should never throw, or it may break the urlbar.
try {
this._internalRecord(event, details);
} catch (ex) {
Cu.reportError("Could not record event: " + ex);
} finally {
this._startEventInfo = null;
}
}
_internalRecord(event, details) {
if (!this._category || !this._startEventInfo) {
return;
}
if (
!event &&
this._startEventInfo.interactionType != "pasted" &&
this._startEventInfo.interactionType != "dropped"
) {
// If no event is passed, we must be executing either paste&go or drop&go.
throw new Error("Event must be defined, unless input was pasted/dropped");
}
if (!details) {
throw new Error("Invalid event details: " + details);
}
let endTime = (event && event.timeStamp) || Cu.now();
let startTime = this._startEventInfo.timeStamp || endTime;
// Synthesized events in tests may have a bogus timeStamp, causing a
// subtraction between monotonic and non-monotonic timestamps; that's why
// abs is necessary here. It should only happen in tests, anyway.
let elapsed = Math.abs(Math.round(endTime - startTime));
let action;
if (!event) {
action =
this._startEventInfo.interactionType == "dropped"
? "drop_go"
: "paste_go";
} else if (event.type == "blur") {
action = "blur";
} else {
action = event instanceof MouseEvent ? "click" : "enter";
}
let method = action == "blur" ? "abandonment" : "engagement";
let value = this._startEventInfo.interactionType;
// Rather than listening to the pref, just update status when we record an
// event, if the pref changed from the last time.
let recordingEnabled = UrlbarPrefs.get("eventTelemetry.enabled");
if (this._eventRecordingEnabled != recordingEnabled) {
this._eventRecordingEnabled = recordingEnabled;
Services.telemetry.setEventRecordingEnabled("urlbar", recordingEnabled);
}
let extra = {
elapsed: elapsed.toString(),
numChars: details.numChars.toString(),
};
if (method == "engagement") {
extra.selIndex = details.selIndex.toString();
extra.selType = details.selType;
}
// We invoke recordEvent regardless, if recording is disabled this won't
// report the events remotely, but will count it in the event_counts scalar.
Services.telemetry.recordEvent(
this._category,
method,
action,
value,
extra
);
}
/**
* Resets the currently tracked input event, that was registered via start(),
* so it won't be recorded.
* If there's no tracked input event, this is a no-op.
*/
discard() {
this._startEventInfo = null;
}
/**
* Extracts a type from a result, to be used in the telemetry event.
* @param {UrlbarResult} result The result to analyze.
* @returns {string} a string type for the telemetry event.
*/
typeFromResult(result) {
if (result) {
switch (result.type) {
case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
return "switchtab";
case UrlbarUtils.RESULT_TYPE.SEARCH:
return result.payload.suggestion ? "searchsuggestion" : "search";
case UrlbarUtils.RESULT_TYPE.URL:
if (result.autofill) {
return "autofill";
}
if (result.heuristic) {
return "visit";
}
return result.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS
? "bookmark"
: "history";
case UrlbarUtils.RESULT_TYPE.KEYWORD:
return "keyword";
case UrlbarUtils.RESULT_TYPE.OMNIBOX:
return "extension";
case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
return "remotetab";
}
}
return "none";
}
}

Просмотреть файл

@ -93,6 +93,7 @@ class UrlbarInput {
options.controller ||
new UrlbarController({
browserWindow: this.window,
eventTelemetryCategory: options.eventTelemetryCategory,
});
this.controller.setInput(this);
this.view = new UrlbarView(this);
@ -207,6 +208,10 @@ class UrlbarInput {
this.view.panel.addEventListener("popupshowing", this);
this.view.panel.addEventListener("popuphidden", this);
// This is used to detect commands launched from the panel, to avoid
// recording abandonment events when the command causes a blur event.
this.view.panel.addEventListener("command", this, true);
this._copyCutController = new CopyCutController(this);
this.inputField.controllers.insertControllerAt(0, this._copyCutController);
@ -363,7 +368,7 @@ class UrlbarInput {
/**
* Handles an event which would cause a url or text to be opened.
*
* @param {Event} event The event triggering the open.
* @param {Event} [event] The event triggering the open.
* @param {string} [openWhere] Where we expect the result to be opened.
* @param {object} [openParams]
* The parameters related to where the result will be opened.
@ -390,6 +395,7 @@ class UrlbarInput {
}
// Do the command of the selected one-off if it's not an engine.
if (selectedOneOff && !selectedOneOff.engine) {
this.controller.engagementEvent.discard();
selectedOneOff.doCommand();
return;
}
@ -404,7 +410,11 @@ class UrlbarInput {
}
let url;
let selType = this.controller.engagementEvent.typeFromResult(result);
let numChars = this.textValue.length;
if (selectedOneOff) {
selType = "oneoff";
numChars = this._lastSearchString.length;
// If there's a selected one-off button then load a search using
// the button's engine.
result = this._resultForCurrentValue;
@ -436,6 +446,12 @@ class UrlbarInput {
openParams.allowInheritPrincipal = false;
url = this._maybeCanonizeURL(event, url) || url.trim();
this.controller.engagementEvent.record(event, {
numChars,
selIndex: this.view.selectedIndex,
selType,
});
try {
new URL(url);
} catch (ex) {
@ -479,6 +495,7 @@ class UrlbarInput {
allowInheritPrincipal: false,
};
let selIndex = this.view.selectedIndex;
if (!result.payload.isKeywordOffer) {
this.view.close();
}
@ -486,6 +503,11 @@ class UrlbarInput {
this.controller.recordSelectedResult(event, result);
if (isCanonized) {
this.controller.engagementEvent.record(event, {
numChars: this._lastSearchString.length,
selIndex,
selType: "canonized",
});
this._loadURL(this.value, where, openParams);
return;
}
@ -514,14 +536,18 @@ class UrlbarInput {
),
};
if (
this.window.switchToTabHavingURI(
Services.io.newURI(url),
false,
loadOpts
) &&
prevTab.isEmpty
) {
this.controller.engagementEvent.record(event, {
numChars: this._lastSearchString.length,
selIndex,
selType: "tabswitch",
});
let switched = this.window.switchToTabHavingURI(
Services.io.newURI(url),
false,
loadOpts
);
if (switched && prevTab.isEmpty) {
this.window.gBrowser.removeTab(prevTab);
}
return;
@ -533,6 +559,12 @@ class UrlbarInput {
// the user can directly start typing a query string at that point.
this.selectionStart = this.selectionEnd = this.value.length;
this.controller.engagementEvent.record(event, {
numChars: this._lastSearchString.length,
selIndex,
selType: "keywordoffer",
});
// Picking a keyword offer just fills it in the input and doesn't
// visit anything. The user can then type a search string. Also
// start a new search so that the offer appears in the view by itself
@ -549,6 +581,12 @@ class UrlbarInput {
break;
}
case UrlbarUtils.RESULT_TYPE.OMNIBOX: {
this.controller.engagementEvent.record(event, {
numChars: this._lastSearchString.length,
selIndex,
selType: "extension",
});
// The urlbar needs to revert to the loaded url when a command is
// handled by the extension.
this.handleRevert();
@ -578,6 +616,12 @@ class UrlbarInput {
);
}
this.controller.engagementEvent.record(event, {
numChars: this._lastSearchString.length,
selIndex,
selType: this.controller.engagementEvent.typeFromResult(result),
});
this._loadURL(url, where, openParams, {
source: result.source,
type: result.type,
@ -1393,7 +1437,23 @@ class UrlbarInput {
// Event handlers below.
_on_command(event) {
// Something is executing a command, likely causing a focus change. This
// should not be recorded as an abandonment.
this.controller.engagementEvent.discard();
}
_on_blur(event) {
// We cannot count every blur events after a missed engagement as abandoment
// because the user may have clicked on some view element that executes
// a command causing a focus change. For example opening preferences from
// the oneoff settings button, or from a contextual tip button.
// For now we detect that case by discarding the event on command, but we
// may want to figure out a more robust way to detect abandonment.
this.controller.engagementEvent.record(event, {
numChars: this._lastSearchString.length,
});
this.removeAttribute("focused");
this.formatValue();
this._resetSearchState();
@ -1469,6 +1529,7 @@ class UrlbarInput {
this.editor.selectAll();
event.preventDefault();
} else if (this.openViewOnFocus && !this.view.isOpen) {
this.controller.engagementEvent.start(event);
this.startQuery({
allowAutofill: false,
});
@ -1481,6 +1542,7 @@ class UrlbarInput {
this.view.close();
} else {
this.focus();
this.controller.engagementEvent.start(event);
this.startQuery({
allowAutofill: false,
});
@ -1534,12 +1596,13 @@ class UrlbarInput {
return;
}
this.controller.engagementEvent.start(event);
// Autofill only when text is inserted (i.e., event.data is not empty) and
// it's not due to pasting.
let allowAutofill =
!!event.data &&
!event.inputType.startsWith("insertFromPaste") &&
event.inputType != "insertFromYank" &&
!UrlbarUtils.isPasteEvent(event) &&
this._maybeAutofillOnInput(value);
this.startQuery({
@ -1754,6 +1817,9 @@ class UrlbarInput {
this.value = droppedURL;
this.window.SetPageProxyState("invalid");
this.focus();
// To simplify tracking of events, register an initial event for event
// telemetry, to replace the missing input event.
this.controller.engagementEvent.start(event);
this.handleCommand(null, undefined, undefined, principal);
// For safety reasons, in the drop case we don't want to immediately show
// the the dropped value, instead we want to keep showing the current page

Просмотреть файл

@ -66,6 +66,9 @@ const PREF_URLBAR_DEFAULTS = new Map([
// clipboard on systems that support it.
["doubleClickSelectsAll", false],
// Whether telemetry events should be recorded.
["eventTelemetry.enabled", false],
// When true, `javascript:` URLs are not included in search results.
["filter.javascript", true],
@ -102,6 +105,7 @@ const PREF_URLBAR_DEFAULTS = new Map([
// should be opened in new tabs by default.
["openintab", false],
// Whether to open the urlbar view when the input field is focused by the user.
["openViewOnFocus", false],
// Whether the quantum bar is enabled.

Просмотреть файл

@ -434,6 +434,19 @@ var UrlbarUtils = {
);
});
},
/**
* Whether the passed-in input event is paste event.
* @param {DOMEvent} event an input DOM event.
* @returns {boolean} Whether the event is a paste event.
*/
isPasteEvent(event) {
return (
event.inputType &&
(event.inputType.startsWith("insertFromPaste") ||
event.inputType == "insertFromYank")
);
},
};
XPCOMUtils.defineLazyGetter(UrlbarUtils.ICON, "DEFAULT", () => {

Просмотреть файл

@ -3,8 +3,6 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
[DEFAULT]
prefs=browser.urlbar.quantumbar=true
tags=quantumbar
support-files =
dummy_page.html
head.js
@ -119,6 +117,10 @@ support-files =
[browser_urlbar_content_opener.js]
[browser_urlbar_display_selectedAction_Extensions.js]
[browser_urlbar_empty_search.js]
[browser_urlbar_event_telemetry.js]
support-files =
searchSuggestionEngine.xml
searchSuggestionEngine.sjs
[browser_urlbar_locationchange_urlbar_edit_dos.js]
support-files =
file_urlbar_edit_dos.html

Просмотреть файл

@ -0,0 +1,579 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
function copyToClipboard(str) {
return new Promise((resolve, reject) => {
waitForClipboard(
str,
() => {
Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper)
.copyString(str);
},
resolve,
reject
);
});
}
// Each test is a function that executes an urlbar action and returns the
// expected event object, or null if no event is expected.
const tests = [
/*
* Engagement tests.
*/
async function() {
info("Type something, press Enter.");
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await promiseAutocompleteResultPopup("x", window, true);
EventUtils.synthesizeKey("VK_RETURN");
await promise;
return {
category: "urlbar",
method: "engagement",
object: "enter",
value: "typed",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "1",
selIndex: "0",
selType: "search",
},
};
},
async function() {
info("Paste something, press Enter.");
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await copyToClipboard("test");
document.commandDispatcher
.getControllerForCommand("cmd_paste")
.doCommand("cmd_paste");
EventUtils.synthesizeKey("VK_RETURN");
await promise;
return {
category: "urlbar",
method: "engagement",
object: "enter",
value: "pasted",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "4",
selIndex: "0",
selType: "search",
},
};
},
async function() {
info("Type something, click one-off.");
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await promiseAutocompleteResultPopup("moz", window, true);
EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton.click();
await promise;
return {
category: "urlbar",
method: "engagement",
object: "click",
value: "typed",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "3",
selIndex: "0",
selType: "oneoff",
},
};
},
async function() {
info("Type something, select one-off, Enter.");
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await promiseAutocompleteResultPopup("moz", window, true);
EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
Assert.ok(UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton);
EventUtils.synthesizeKey("VK_RETURN");
await promise;
return {
category: "urlbar",
method: "engagement",
object: "enter",
value: "typed",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "3",
selIndex: "0",
selType: "oneoff",
},
};
},
async function() {
info("Type something, ESC, type something else, press Enter.");
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
EventUtils.synthesizeKey("x");
EventUtils.synthesizeKey("VK_ESCAPE");
EventUtils.synthesizeKey("y");
EventUtils.synthesizeKey("VK_RETURN");
await promise;
return {
category: "urlbar",
method: "engagement",
object: "enter",
value: "typed",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "1",
selIndex: "0",
selType: "search",
},
};
},
async function() {
info("Type a keyword, Enter.");
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await promiseAutocompleteResultPopup("kw test", window, true);
EventUtils.synthesizeKey("VK_RETURN");
await promise;
return {
category: "urlbar",
method: "engagement",
object: "enter",
value: "typed",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "7",
selIndex: "0",
selType: "keyword",
},
};
},
async function() {
info("Type something and canonize");
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await promiseAutocompleteResultPopup("example", window, true);
EventUtils.synthesizeKey("VK_RETURN", { ctrlKey: true });
await promise;
return {
category: "urlbar",
method: "engagement",
object: "enter",
value: "typed",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "7",
selIndex: "0",
selType: "canonized",
},
};
},
async function() {
info("Type something, click on bookmark entry.");
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await promiseAutocompleteResultPopup("exa", window, true);
while (gURLBar.value != "http://example.com/?q=%s") {
EventUtils.synthesizeKey("KEY_ArrowDown");
}
let element = UrlbarTestUtils.getSelectedElement(window);
EventUtils.synthesizeMouseAtCenter(element, {});
await promise;
return {
category: "urlbar",
method: "engagement",
object: "click",
value: "typed",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "3",
selIndex: val => parseInt(val) > 0,
selType: "bookmark",
},
};
},
async function() {
info("Type an autofilled string, Enter.");
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await promiseAutocompleteResultPopup("exa", window, true);
// Check it's autofilled.
Assert.equal(gURLBar.selectionStart, 3);
Assert.equal(gURLBar.selectionEnd, 12);
EventUtils.synthesizeKey("VK_RETURN");
await promise;
return {
category: "urlbar",
method: "engagement",
object: "enter",
value: "typed",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "3",
selIndex: "0",
selType: "autofill",
},
};
},
async function() {
info("Type something, select bookmark entry, Enter.");
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await promiseAutocompleteResultPopup("exa", window, true);
while (gURLBar.value != "http://example.com/?q=%s") {
EventUtils.synthesizeKey("KEY_ArrowDown");
}
EventUtils.synthesizeKey("VK_RETURN");
await promise;
return {
category: "urlbar",
method: "engagement",
object: "enter",
value: "typed",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "3",
selIndex: val => parseInt(val) > 0,
selType: "bookmark",
},
};
},
async function() {
info("Type @, Enter on a keywordoffer");
gURLBar.select();
await promiseAutocompleteResultPopup("@", window, true);
while (gURLBar.value != "@test ") {
EventUtils.synthesizeKey("KEY_ArrowDown");
}
EventUtils.synthesizeKey("VK_RETURN");
await UrlbarTestUtils.promiseSearchComplete(window);
return {
category: "urlbar",
method: "engagement",
object: "enter",
value: "typed",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "1",
selIndex: val => parseInt(val) > 0,
selType: "keywordoffer",
},
};
},
async function() {
info("Drop something.");
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
EventUtils.synthesizeDrop(
document.getElementById("home-button"),
gURLBar.inputField,
[[{ type: "text/plain", data: "www.example.com" }]],
"copy",
window
);
await promise;
return {
category: "urlbar",
method: "engagement",
object: "drop_go",
value: "dropped",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "15",
selIndex: "-1",
selType: "none",
},
};
},
async function() {
info("Paste & Go something.");
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await copyToClipboard("www.example.com");
let inputBox = gURLBar.querySelector("moz-input-box");
let cxmenu = inputBox.menupopup;
let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown");
EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {
type: "contextmenu",
button: 2,
});
await cxmenuPromise;
let menuitem = inputBox.getMenuItem("paste-and-go");
EventUtils.synthesizeMouseAtCenter(menuitem, {});
await promise;
return {
category: "urlbar",
method: "engagement",
object: "paste_go",
value: "pasted",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "15",
selIndex: "-1",
selType: "none",
},
};
},
async function() {
info("Open the panel with DOWN, select with DOWN, Enter.");
gURLBar.value = "";
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await UrlbarTestUtils.promisePopupOpen(window, () => {
EventUtils.synthesizeKey("KEY_ArrowDown", {});
});
await UrlbarTestUtils.promiseSearchComplete(window);
while (gURLBar.value != "http://mochi.test:8888/") {
EventUtils.synthesizeKey("KEY_ArrowDown");
}
EventUtils.synthesizeKey("VK_RETURN");
await promise;
return {
category: "urlbar",
method: "engagement",
object: "enter",
value: "topsites",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "0",
selType: "history",
selIndex: val => parseInt(val) >= 0,
},
};
},
async function() {
info("Open the panel with DOWN, click on entry.");
gURLBar.value = "";
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await UrlbarTestUtils.promisePopupOpen(window, () => {
EventUtils.synthesizeKey("KEY_ArrowDown", {});
});
while (gURLBar.value != "http://mochi.test:8888/") {
EventUtils.synthesizeKey("KEY_ArrowDown");
}
let element = UrlbarTestUtils.getSelectedElement(window);
EventUtils.synthesizeMouseAtCenter(element, {});
await promise;
return {
category: "urlbar",
method: "engagement",
object: "click",
value: "topsites",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "0",
selType: "history",
selIndex: "0",
},
};
},
async function() {
info("Open the panel with dropmarker, type something, Enter.");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: "about:blank" },
async browser => {
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
await UrlbarTestUtils.promisePopupOpen(window, () => {
EventUtils.synthesizeMouseAtCenter(gURLBar.dropmarker, {}, window);
});
await promiseAutocompleteResultPopup("x", window, true);
EventUtils.synthesizeKey("VK_RETURN");
await promise;
}
);
return {
category: "urlbar",
method: "engagement",
object: "enter",
value: "topsites",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "1",
selType: "search",
selIndex: "0",
},
};
},
/*
* Abandonment tests.
*/
async function() {
info("Type something, blur.");
gURLBar.select();
EventUtils.synthesizeKey("x");
gURLBar.blur();
return {
category: "urlbar",
method: "abandonment",
object: "blur",
value: "typed",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "1",
},
};
},
async function() {
info("Open the panel with DOWN, don't type, blur it.");
gURLBar.value = "";
gURLBar.select();
await UrlbarTestUtils.promisePopupOpen(window, () => {
EventUtils.synthesizeKey("KEY_ArrowDown", {});
});
gURLBar.blur();
return {
category: "urlbar",
method: "abandonment",
object: "blur",
value: "topsites",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "0",
},
};
},
async function() {
info("Open the panel with dropmarker, type something, blur it.");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: "about:blank" },
async browser => {
gURLBar.select();
await UrlbarTestUtils.promisePopupOpen(window, () => {
EventUtils.synthesizeMouseAtCenter(gURLBar.dropmarker, {}, window);
});
EventUtils.synthesizeKey("x");
gURLBar.blur();
}
);
return {
category: "urlbar",
method: "abandonment",
object: "blur",
value: "topsites",
extra: {
elapsed: val => parseInt(val) > 0,
numChars: "1",
},
};
},
/*
* No event tests.
*/
async function() {
info("Type something, click on search settings.");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: "about:blank" },
async browser => {
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(browser);
await promiseAutocompleteResultPopup("x", window, true);
UrlbarTestUtils.getOneOffSearchButtons(window).settingsButton.click();
await promise;
}
);
return null;
},
async function() {
info("Type something, Up, Enter on search settings.");
await BrowserTestUtils.withNewTab(
{ gBrowser, url: "about:blank" },
async browser => {
gURLBar.select();
let promise = BrowserTestUtils.browserLoaded(browser);
await promiseAutocompleteResultPopup("x", window, true);
EventUtils.synthesizeKey("KEY_ArrowUp");
Assert.ok(
UrlbarTestUtils.getOneOffSearchButtons(
window
).selectedButton.classList.contains("search-setting-button-compact"),
"Should have selected the settings button"
);
EventUtils.synthesizeKey("VK_RETURN");
await promise;
}
);
return null;
},
];
add_task(async function test() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.urlbar.eventTelemetry.enabled", true],
["browser.urlbar.suggest.searches", true],
],
});
// Create a new search engine and mark it as default
let engine = await SearchTestUtils.promiseNewSearchEngine(
getRootDirectory(gTestPath) + "searchSuggestionEngine.xml"
);
let oldDefaultEngine = await Services.search.getDefault();
await Services.search.setDefault(engine);
await Services.search.moveEngine(engine, 0);
let aliasEngine = await Services.search.addEngineWithDetails("Test", {
alias: "@test",
template: "http://example.com/?search={searchTerms}",
});
// Add a bookmark and a keyword.
let bm = await PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
url: "http://example.com/?q=%s",
title: "test",
});
await PlacesUtils.keywords.insert({
keyword: "kw",
url: "http://example.com/?q=%s",
});
await PlacesTestUtils.addVisits([
{
uri: "http://mochi.test:8888/",
transition: PlacesUtils.history.TRANSITIONS.TYPED,
},
]);
registerCleanupFunction(async function() {
await Services.search.setDefault(oldDefaultEngine);
await Services.search.removeEngine(aliasEngine);
await PlacesUtils.keywords.remove("kw");
await PlacesUtils.bookmarks.remove(bm);
await PlacesUtils.history.clear();
});
// This is not necessary after each loop, because assertEvents does it.
Services.telemetry.clearEvents();
for (let testFn of tests) {
let expectedEvents = [await testFn()].filter(e => !!e);
// Always blur to ensure it's not accounted as an additional abandonment.
gURLBar.blur();
TelemetryTestUtils.assertEvents(expectedEvents);
}
});

Просмотреть файл

@ -317,6 +317,52 @@ navigation:
extra_keys:
engine: The id of the search engine used.
urlbar:
engagement:
objects: ["click", "enter", "paste_go", "drop_go"]
release_channel_collection: opt-out
products:
- "firefox"
record_in_processes: ["main"]
description: >
This is recorded on urlbar engagement, that is when the user picks a
search result.
The value field records the initial interaction type. One of:
"typed", "dropped", "pasted", "topsites"
bug_numbers: [1559136]
notification_emails:
- "rharter@mozilla.com"
- "fx-search@mozilla.com"
expiry_version: never
extra_keys:
elapsed: engagement time in milliseconds.
numChars: number of input characters.
selIndex: index of the selected result in the urlbar panel, or -1.
selType: >
type of the selected result in the urlbar panel. One of:
"autofill", "visit", "bookmark", "history", "keyword", "search",
"searchsuggestion", "switchtab", "remotetab", "extension", "oneoff",
"keywordoffer", "canonized", "none"
abandonment:
objects: ["blur"]
release_channel_collection: opt-out
products:
- "firefox"
record_in_processes: ["main"]
description: >
This is recorded on urlbar search abandon, that is when the user starts
an interaction but then blurs the urlbar.
The value field records the initial interaction type. One of:
"typed", "dropped", "pasted", "topsites"
bug_numbers: [1559136]
notification_emails:
- "rharter@mozilla.com"
- "fx-search@mozilla.com"
expiry_version: never
extra_keys:
elapsed: abandonement time in milliseconds.
numChars: number of input characters.
normandy:
enroll:
objects: ["preference_study", "addon_study", "preference_rollout"]