Commit 5cf95b6b authored by Mark Banner's avatar Mark Banner
Browse files

Bug 1680735 - Move search specific telemetry out of BrowserUsageTelemetry.jsm...

Bug 1680735 - Move search specific telemetry out of BrowserUsageTelemetry.jsm into a new module. r=daleharvey

Differential Revision: https://phabricator.services.mozilla.com/D98753
parent 7715cf6e
Loading
Loading
Loading
Loading
+3 −2
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
  AddonManager: "resource://gre/modules/AddonManager.jsm",
  AMTelemetry: "resource://gre/modules/AddonManager.jsm",
  NewTabPagePreloading: "resource:///modules/NewTabPagePreloading.jsm",
  BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.jsm",
  BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm",
  BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
@@ -4509,7 +4510,7 @@ const BrowserSearch = {
   */
  recordSearchInTelemetry(engine, source, details = {}) {
    try {
      BrowserUsageTelemetry.recordSearch(gBrowser, engine, source, details);
      BrowserSearchTelemetry.recordSearch(gBrowser, engine, source, details);
    } catch (ex) {
      Cu.reportError(ex);
    }
@@ -4531,7 +4532,7 @@ const BrowserSearch = {
  recordOneoffSearchInTelemetry(engine, source, type) {
    try {
      const details = { type, isOneOff: true };
      BrowserUsageTelemetry.recordSearch(gBrowser, engine, source, details);
      BrowserSearchTelemetry.recordSearch(gBrowser, engine, source, details);
    } catch (ex) {
      Cu.reportError(ex);
    }
+365 −0
Original line number Diff line number Diff line
/* 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 = ["BrowserSearchTelemetry"];

const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);

XPCOMUtils.defineLazyModuleGetters(this, {
  PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.jsm",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
  Services: "resource://gre/modules/Services.jsm",
  UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
});

// A list of known search origins.
const KNOWN_SEARCH_SOURCES = [
  "abouthome",
  "contextmenu",
  "newtab",
  "searchbar",
  "system",
  "urlbar",
  "urlbar-searchmode",
  "webextension",
];

const KNOWN_ONEOFF_SOURCES = [
  "oneoff-urlbar",
  "oneoff-searchbar",
  "unknown", // Edge case: this is the searchbar (see bug 1195733 comment 7).
];

/**
 * The handler class. TODO
 */
class BrowserSearchTelemetryHandler {
  /**
   * Determines if we should record a search for this browser instance.
   * Private Browsing mode is normally skipped.
   *
   * @param {tabbrowser} tabbrowser
   *   The browser where the search was loaded.
   * @returns {boolean}
   *   True if the search should be recorded, false otherwise.
   */
  shouldRecordSearchCount(tabbrowser) {
    return (
      !PrivateBrowsingUtils.isWindowPrivate(tabbrowser.ownerGlobal) ||
      !Services.prefs.getBoolPref("browser.engagement.search_counts.pbm", false)
    );
  }

  /**
   * 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 {tabbrowser} tabbrowser
   *        The tabbrowser where the search was loaded.
   * @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.isFormHistory=false]
   *        true if this event was generated by a form history result.
   * @param {string} [details.alias=null]
   *        The search engine alias used in the search, if any.
   * @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(tabbrowser, engine, source, details = {}) {
    if (!this.shouldRecordSearchCount(tabbrowser)) {
      return;
    }

    const countIdPrefix = `${engine.telemetryId}.`;
    const countIdSource = countIdPrefix + source;
    let histogram = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");

    if (details.isOneOff) {
      if (!KNOWN_ONEOFF_SOURCES.includes(source)) {
        // Silently drop the error if this bogus call
        // came from 'urlbar' or 'searchbar'. They're
        // calling |recordSearch| twice from two different
        // code paths because they want to record the search
        // in SEARCH_COUNTS.
        if (["urlbar", "searchbar"].includes(source)) {
          histogram.add(countIdSource);
          PartnerLinkAttribution.makeSearchEngineRequest(
            engine,
            details.url
          ).catch(Cu.reportError);
          return;
        }
        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);
      }
      if (
        details.alias &&
        engine.isAppProvided &&
        engine.aliases.includes(details.alias)
      ) {
        // This is a keyword search using an AppProvided engine.
        // Record the source as "alias", not "urlbar".
        histogram.add(countIdPrefix + "alias");
      } else {
        histogram.add(countIdSource);
      }
    }

    // Dispatch the search signal to other handlers.
    this._handleSearchAction(engine, source, details);
  }

  _recordSearch(engine, url, source, action = null) {
    // The one-off buttons are logged in two places, if we hit here with the
    // action as oneoff and no url, then we are hitting the attribution case
    // in `recordSearch` above. Really this needs re-architecturing so we
    // do not have two distinct calls to `recordSearch` for one-offs
    // (see bug 1662553).
    if (!(action == "oneoff" && !url)) {
      PartnerLinkAttribution.makeSearchEngineRequest(engine, url).catch(
        Cu.reportError
      );
    }

    let scalarKey = action ? "search_" + action : "search";
    Services.telemetry.keyedScalarAdd(
      "browser.engagement.navigation." + source,
      scalarKey,
      1
    );
    Services.telemetry.recordEvent("navigation", "search", source, action, {
      engine: engine.telemetryId,
    });
  }

  /**
   * Records entry into the Urlbar's search mode.
   *
   * Telemetry records only which search mode is entered and how it was entered.
   * It does not record anything pertaining to searches made within search mode.
   * @param {object} searchMode
   *   A search mode object. See UrlbarInput.setSearchMode documentation for
   *   details.
   */
  recordSearchMode(searchMode) {
    // Search mode preview is not search mode. Recording it would just create
    // noise.
    if (searchMode.isPreview) {
      return;
    }
    let scalarKey;
    if (searchMode.engineName) {
      let engine = Services.search.getEngineByName(searchMode.engineName);
      let resultDomain = engine.getResultDomain();
      // For built-in engines, sanitize the data in a few special cases to make
      // analysis easier.
      if (!engine.isAppProvided) {
        scalarKey = "other";
      } else if (resultDomain.includes("amazon.")) {
        // Group all the localized Amazon sites together.
        scalarKey = "Amazon";
      } else if (resultDomain.endsWith("wikipedia.org")) {
        // Group all the localized Wikipedia sites together.
        scalarKey = "Wikipedia";
      } else {
        scalarKey = searchMode.engineName;
      }
    } else if (searchMode.source) {
      scalarKey = UrlbarUtils.getResultSourceName(searchMode.source) || "other";
    }

    Services.telemetry.keyedScalarAdd(
      "urlbar.searchmode." + searchMode.entry,
      scalarKey,
      1
    );
  }

  _handleSearchAction(engine, 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(engine, source, details);
        break;
      case "urlbar-searchmode":
        this._handleSearchAndUrlbar(engine, "urlbar_searchmode", details);
        break;
      case "abouthome":
        this._recordSearch(engine, details.url, "about_home", "enter");
        break;
      case "newtab":
        this._recordSearch(engine, details.url, "about_newtab", "enter");
        break;
      case "contextmenu":
      case "system":
      case "webextension":
        this._recordSearch(engine, details.url, source);
        break;
    }
  }

  /**
   * This function handles the "urlbar", "urlbar-oneoff", "searchbar" and
   * "searchbar-oneoff" sources.
   *
   * @param {msISearchEngine} engine
   *   The engine handling the search.
   * @param {string} source
   *   Where the search originated from.
   * @param {object} details
   *   @see recordSearch
   */
  _handleSearchAndUrlbar(engine, 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 sourceName =
      source === "unknown" ? "searchbar" : source.replace("oneoff-", "");

    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, record it using the relative key.
      this._recordSearch(engine, details.url, sourceName, "oneoff");
      return;
    }

    // The search was not a one-off. It was a search with the default search engine.
    if (details.isFormHistory) {
      // It came from a form history result.
      this._recordSearch(engine, details.url, sourceName, "formhistory");
      return;
    } else if (details.isSuggestion) {
      // It came from a suggested search, so count it as such.
      this._recordSearch(engine, details.url, sourceName, "suggestion");
      return;
    } else if (details.alias) {
      // This one came from a search that used an alias.
      this._recordSearch(engine, details.url, sourceName, "alias");
      return;
    }

    // The search signal was generated by typing something and pressing enter.
    this._recordSearch(engine, details.url, sourceName, "enter");
  }

  /**
   * Records the method by which the user selected a result from the urlbar.
   *
   * @param {Event} event
   *        The event that triggered the selection.
   * @param {number} index
   *        The index that the user chose in the popup, or -1 if there wasn't a
   *        selection.
   * @param {string} userSelectionBehavior
   *        How the user cycled through results before picking the current match.
   *        Could be one of "tab", "arrow" or "none".
   */
  recordUrlbarSelectedResultMethod(
    event,
    index,
    userSelectionBehavior = "none"
  ) {
    this._recordUrlOrSearchbarSelectedResultMethod(
      event,
      index,
      "FX_URLBAR_SELECTED_RESULT_METHOD",
      userSelectionBehavior
    );
  }

  /**
   * Records the method by which the user selected a searchbar result.
   *
   * @param {Event} event
   *        The event that triggered the selection.
   * @param {number} highlightedIndex
   *        The index that the user chose in the popup, or -1 if there wasn't a
   *        selection.
   */
  recordSearchbarSelectedResultMethod(event, highlightedIndex) {
    this._recordUrlOrSearchbarSelectedResultMethod(
      event,
      highlightedIndex,
      "FX_SEARCHBAR_SELECTED_RESULT_METHOD",
      "none"
    );
  }

  _recordUrlOrSearchbarSelectedResultMethod(
    event,
    highlightedIndex,
    histogramID,
    userSelectionBehavior
  ) {
    // If the contents of the histogram are changed then
    // `UrlbarTestUtils.SELECTED_RESULT_METHODS` should also be updated.

    let histogram = Services.telemetry.getHistogramById(histogramID);
    // command events are from the one-off context menu.  Treat them as clicks.
    // Note that we don't care about MouseEvent subclasses here, since
    // those are not clicks.
    let isClick =
      event &&
      (ChromeUtils.getClassName(event) == "MouseEvent" ||
        event.type == "command");
    let category;
    if (isClick) {
      category = "click";
    } else if (highlightedIndex >= 0) {
      switch (userSelectionBehavior) {
        case "tab":
          category = "tabEnterSelection";
          break;
        case "arrow":
          category = "arrowEnterSelection";
          break;
        case "rightClick":
          // Selected by right mouse button.
          category = "rightClickEnter";
          break;
        default:
          category = "enterSelection";
      }
    } else {
      category = "enter";
    }
    histogram.add(category);
  }
}

var BrowserSearchTelemetry = new BrowserSearchTelemetryHandler();
+6 −0
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ const { XPCOMUtils } = ChromeUtils.import(
);

XPCOMUtils.defineLazyModuleGetters(this, {
  BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.jsm",
  RemoteSettings: "resource://services-settings/remote-settings.js",
  SearchUtils: "resource://gre/modules/SearchUtils.jsm",
  Services: "resource://gre/modules/Services.jsm",
@@ -191,6 +192,11 @@ class TelemetryHandler {
   * @param {string} url The url that was loaded in the browser.
   */
  updateTrackingStatus(browser, url) {
    if (
      !BrowserSearchTelemetry.shouldRecordSearchCount(browser.getTabBrowser())
    ) {
      return;
    }
    let info = this._checkURLForSerpMatch(url);
    if (!info) {
      this.stopTrackingBrowser(browser);
+1 −1
Original line number Diff line number Diff line
@@ -348,7 +348,7 @@
      let selection = this.telemetrySearchDetails;
      let oneOffRecorded = false;

      BrowserUsageTelemetry.recordSearchbarSelectedResultMethod(
      BrowserSearchTelemetry.recordSearchbarSelectedResultMethod(
        aEvent,
        selection ? selection.index : -1
      );
+1 −0
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

EXTRA_JS_MODULES += [
    "BrowserSearchTelemetry.jsm",
    "SearchOneOffs.jsm",
    "SearchTelemetry.jsm",
    "SearchUIUtils.jsm",
Loading