зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
79d0a81073
Коммит
3a39214a77
|
@ -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"]
|
||||
|
|
Загрузка…
Ссылка в новой задаче