gecko-dev/browser/components/urlbar/UrlbarController.jsm
Mark Banner 10cebf3c34 Bug 1515083 - Re-implement telemetry for selected index/type on QuantumBar. r=adw
This makes the browser_UsageTelemetry_urlbar*.js tests pass for the all of the
FX_URLBAR_SELECTED_RESULT_* histograms apart from the "METHOD" one which will be handled
in bug 1500476.

I have handled the recording of telemetry in the controller, as this seems a better
location than BrowserUsageTelemetry.jsm due to needing to reach into the results and obtain
specific details.

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

--HG--
extra : moz-landing-system : lando
2019-02-15 14:57:23 +00:00

422 lines
14 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = [
"UrlbarController",
];
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
AppConstants: "resource://gre/modules/AppConstants.jsm",
// BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
URLBAR_SELECTED_RESULT_TYPES: "resource:///modules/BrowserUsageTelemetry.jsm",
});
const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
const TELEMETRY_6_FIRST_RESULTS = "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS";
/**
* The address bar controller handles queries from the address bar, obtains
* results and returns them to the UI for display.
*
* Listeners may be added to listen for the results. They must support the
* following methods which may be called when a query is run:
*
* - onQueryStarted(queryContext)
* - onQueryResults(queryContext)
* - onQueryCancelled(queryContext)
* - onQueryFinished(queryContext)
* - onQueryResultRemoved(index)
*/
class UrlbarController {
/**
* Initialises the class. The manager may be overridden here, this is for
* test purposes.
*
* @param {object} options
* The initial options for UrlbarController.
* @param {object} options.browserWindow
* The browser window this controller is operating within.
* @param {object} [options.manager]
* Optional fake providers manager to override the built-in providers manager.
* Intended for use in unit tests only.
*/
constructor(options = {}) {
if (!options.browserWindow) {
throw new Error("Missing options: browserWindow");
}
if (!options.browserWindow.location ||
options.browserWindow.location.href != AppConstants.BROWSER_CHROME_URL) {
throw new Error("browserWindow should be an actual browser window.");
}
this.manager = options.manager || UrlbarProvidersManager;
this.browserWindow = options.browserWindow;
this._listeners = new Set();
}
/**
* Hooks up the controller with an input.
*
* @param {UrlbarInput} input
* The UrlbarInput instance associated with this controller.
*/
setInput(input) {
this.input = input;
}
/**
* Hooks up the controller with a view.
*
* @param {UrlbarView} view
* The UrlbarView instance associated with this controller.
*/
setView(view) {
this.view = view;
}
/**
* Takes a query context and starts the query based on the user input.
*
* @param {UrlbarQueryContext} queryContext The query details.
*/
async startQuery(queryContext) {
// Cancel any running query.
if (this._lastQueryContext) {
this.cancelQuery(this._lastQueryContext);
}
this._lastQueryContext = queryContext;
queryContext.lastResultCount = 0;
TelemetryStopwatch.start(TELEMETRY_1ST_RESULT, queryContext);
TelemetryStopwatch.start(TELEMETRY_6_FIRST_RESULTS, queryContext);
this._notify("onQueryStarted", queryContext);
await this.manager.startQuery(queryContext, this);
this._notify("onQueryFinished", queryContext);
return queryContext;
}
/**
* Cancels an in-progress query. Note, queries may continue running if they
* can't be canceled.
*/
cancelQuery() {
if (!this._lastQueryContext) {
return;
}
TelemetryStopwatch.cancel(TELEMETRY_1ST_RESULT, this._lastQueryContext);
TelemetryStopwatch.cancel(TELEMETRY_6_FIRST_RESULTS, this._lastQueryContext);
this.manager.cancelQuery(this._lastQueryContext);
this._notify("onQueryCancelled", this._lastQueryContext);
delete this._lastQueryContext;
}
/**
* Receives results from a query.
*
* @param {UrlbarQueryContext} queryContext The query details.
*/
receiveResults(queryContext) {
if (queryContext.lastResultCount < 1 && queryContext.results.length >= 1) {
TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT, queryContext);
}
if (queryContext.lastResultCount < 6 && queryContext.results.length >= 6) {
TelemetryStopwatch.finish(TELEMETRY_6_FIRST_RESULTS, queryContext);
}
if (queryContext.lastResultCount == 0) {
if (queryContext.results.length && queryContext.results[0].autofill) {
this.input.setValueFromResult(queryContext.results[0]);
}
// The first time we receive results try to connect to the heuristic
// result.
this.speculativeConnect(queryContext, 0, "resultsadded");
}
this._notify("onQueryResults", queryContext);
// Update lastResultCount after notifying, so the view can use it.
queryContext.lastResultCount = queryContext.results.length;
}
/**
* Adds a listener for query actions and results.
*
* @param {object} listener The listener to add.
* @throws {TypeError} Throws if the listener is not an object.
*/
addQueryListener(listener) {
if (!listener || typeof listener != "object") {
throw new TypeError("Expected listener to be an object");
}
this._listeners.add(listener);
}
/**
* Removes a query listener.
*
* @param {object} listener The listener to add.
*/
removeQueryListener(listener) {
this._listeners.delete(listener);
}
/**
* When switching tabs, clear some internal caches to handle cases like
* backspace, autofill or repeated searches.
*/
tabContextChanged() {
// TODO: implementation needed (bug 1496685)
}
/**
* Receives keyboard events from the input and handles those that should
* navigate within the view or pick the currently selected item.
*
* @param {KeyboardEvent} event
* The DOM KeyboardEvent.
*/
handleKeyNavigation(event) {
const isMac = AppConstants.platform == "macosx";
// Handle readline/emacs-style navigation bindings on Mac.
if (isMac &&
this.view.isOpen &&
event.ctrlKey &&
(event.key == "n" || event.key == "p")) {
this.view.selectNextItem({ reverse: event.key == "p" });
event.preventDefault();
return;
}
if (this.view.isOpen) {
let queryContext = this._lastQueryContext;
if (queryContext) {
this.view.oneOffSearchButtons.handleKeyPress(
event,
queryContext.results.length,
this.view.allowEmptySelection,
queryContext.searchString);
if (event.defaultPrevented) {
return;
}
}
}
switch (event.keyCode) {
case KeyEvent.DOM_VK_ESCAPE:
this.input.handleRevert();
event.preventDefault();
break;
case KeyEvent.DOM_VK_RETURN:
if (isMac &&
event.metaKey) {
// Prevent beep on Mac.
event.preventDefault();
}
// TODO: We should have an input bufferrer so that we can use search results
// if appropriate.
this.input.handleCommand(event);
break;
case KeyEvent.DOM_VK_TAB:
if (this.view.isOpen) {
this.view.selectNextItem({ reverse: event.shiftKey });
event.preventDefault();
}
break;
case KeyEvent.DOM_VK_DOWN:
case KeyEvent.DOM_VK_UP:
if (!event.ctrlKey && !event.altKey) {
if (this.view.isOpen) {
this.view.selectNextItem({
reverse: event.keyCode == KeyEvent.DOM_VK_UP });
} else {
this.input.startQuery();
}
event.preventDefault();
}
break;
case KeyEvent.DOM_VK_DELETE:
if (isMac && !event.shiftKey) {
break;
}
if (this._handleDeleteEntry()) {
event.preventDefault();
}
break;
case KeyEvent.DOM_VK_BACK_SPACE:
if (isMac && event.shiftKey &&
this._handleDeleteEntry()) {
event.preventDefault();
}
break;
}
}
/**
* Tries to initialize a speculative connection on a result.
* Speculative connections are only supported for a subset of all the results.
* @param {UrlbarQueryContext} context The queryContext
* @param {number} resultIndex index of the result to speculative connect to.
* @param {string} reason Reason for the speculative connect request.
* @note speculative connect to:
* - Search engine heuristic results
* - autofill results
* - http/https results
*/
speculativeConnect(context, resultIndex, reason) {
// Never speculative connect in private contexts.
if (!this.input || context.isPrivate || context.results.length == 0) {
return;
}
let result = context.results[resultIndex];
let {url} = UrlbarUtils.getUrlFromResult(result);
if (!url) {
return;
}
switch (reason) {
case "resultsadded": {
// We should connect to an heuristic result, if it exists.
if ((resultIndex == 0 && context.preselected) || result.autofill) {
if (result.type == UrlbarUtils.RESULT_TYPE.SEARCH) {
// Speculative connect only if search suggestions are enabled.
if (UrlbarPrefs.get("suggest.searches") &&
UrlbarPrefs.get("browser.search.suggest.enabled")) {
let engine = Services.search.defaultEngine;
UrlbarUtils.setupSpeculativeConnection(engine, this.browserWindow);
}
} else if (result.autofill) {
UrlbarUtils.setupSpeculativeConnection(url, this.browserWindow);
}
}
return;
}
case "mousedown": {
// On mousedown, connect only to http/https urls.
if (url.startsWith("http")) {
UrlbarUtils.setupSpeculativeConnection(url, this.browserWindow);
}
return;
}
default: {
throw new Error("Invalid speculative connection reason");
}
}
}
/**
* Records details of the selected result in telemetry. We only record the
* type and index here,
* @param {Event} event The event which triggered the result to be selected.
* @param {UrlbarResult} result The result that was selected.
* @param {number} index The index of the result.
*/
recordSelectedResult(event, result, index) {
let telemetryType;
switch (result.type) {
case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
telemetryType = "switchtab";
break;
case UrlbarUtils.RESULT_TYPE.SEARCH:
telemetryType = result.payload.suggestion ? "searchsuggestion" : "searchengine";
break;
case UrlbarUtils.RESULT_TYPE.URL:
if (result.autofill) {
telemetryType = "autofill";
} else if (result.source == UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL &&
result.heuristic) {
telemetryType = "visiturl";
} else {
telemetryType = result.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS ? "bookmark" : "history";
}
break;
case UrlbarUtils.RESULT_TYPE.KEYWORD:
telemetryType = "keyword";
break;
case UrlbarUtils.RESULT_TYPE.OMNIBOX:
telemetryType = "extension";
break;
case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
telemetryType = "remotetab";
break;
default:
Cu.reportError(`Unknown Result Type ${result.type}`);
return;
}
Services.telemetry
.getHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX")
.add(index);
// You can add values but don't change any of the existing values.
// Otherwise you'll break our data.
if (telemetryType in URLBAR_SELECTED_RESULT_TYPES) {
Services.telemetry
.getHistogramById("FX_URLBAR_SELECTED_RESULT_TYPE")
.add(URLBAR_SELECTED_RESULT_TYPES[telemetryType]);
Services.telemetry
.getKeyedHistogramById("FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE")
.add(telemetryType, index);
} else {
Cu.reportError("Unknown FX_URLBAR_SELECTED_RESULT_TYPE type: " +
telemetryType);
}
}
/**
* Internal function handling deletion of entries. We only support removing
* of history entries - other result sources will be ignored.
*
* @returns {boolean} Returns true if the deletion was acted upon.
*/
_handleDeleteEntry() {
if (!this._lastQueryContext) {
Cu.reportError("Cannot delete - the latest query is not present");
return false;
}
const selectedResult = this.input.view.selectedResult;
if (!selectedResult ||
selectedResult.source != UrlbarUtils.RESULT_SOURCE.HISTORY) {
return false;
}
let index = this._lastQueryContext.results.indexOf(selectedResult);
if (!index) {
Cu.reportError("Failed to find the selected result in the results");
return false;
}
this._lastQueryContext.results.splice(index, 1);
this._notify("onQueryResultRemoved", index);
PlacesUtils.history.remove(selectedResult.payload.url).catch(Cu.reportError);
return true;
}
/**
* Internal function to notify listeners of results.
*
* @param {string} name Name of the notification.
* @param {object} params Parameters to pass with the notification.
*/
_notify(name, ...params) {
for (let listener of this._listeners) {
try {
listener[name](...params);
} catch (ex) {
Cu.reportError(ex);
}
}
}
}