Bug 1334630 - Measure the edit distance when selecting a search suggestion. draft
authorDrew Willcoxon <adw@mozilla.com>
Fri, 17 Mar 2017 22:31:44 -0700
changeset 500975 06422168be895cb98c603d8a37e7af3153f14914
parent 496768 a8d497b09753c91783b68c5805c64f34a2f39629
child 549764 04db393516604b31e1e620e06797cd6444bda223
push id49858
push userdwillcoxon@mozilla.com
push dateSat, 18 Mar 2017 05:33:12 +0000
bugs1334630
milestone55.0a1
Bug 1334630 - Measure the edit distance when selecting a search suggestion. MozReview-Commit-ID: DGyQUmA0Yuv
browser/base/content/urlbarBindings.xml
browser/components/search/content/search.xml
browser/modules/BrowserUsageTelemetry.jsm
toolkit/components/telemetry/Histograms.json
toolkit/content/widgets/autocomplete.xml
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -470,17 +470,18 @@ file, You can obtain one at http://mozil
                 }
                 break;
               case "searchengine":
                 if (selectedOneOff && selectedOneOff.engine) {
                   // Replace the engine with the selected one-off engine.
                   action.params.engineName = selectedOneOff.engine.name;
                 }
                 const actionDetails = {
-                  isSuggestion: !!action.params.searchSuggestion,
+                  query: action.params.searchQuery,
+                  suggestion: action.params.searchSuggestion,
                   isAlias: !!action.params.alias
                 };
                 [url, postData] = this._parseAndRecordSearchEngineLoad(
                   action.params.engineName,
                   action.params.searchSuggestion || action.params.searchQuery,
                   event,
                   where,
                   openUILinkParams,
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -437,19 +437,25 @@
           let engine = aEngine || this.currentEngine;
           var submission = engine.getSubmission(aData, null, "searchbar");
           let telemetrySearchDetails = this.telemetrySearchDetails;
           this.telemetrySearchDetails = null;
           if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
             telemetrySearchDetails = null;
           }
           // If we hit here, we come either from a one-off, a plain search or a suggestion.
+          let query =
+            telemetrySearchDetails ? telemetrySearchDetails.query : null;
+          let suggestion =
+            aOneOff || !telemetrySearchDetails ? null :
+            textBox.mController.getValueAt(telemetrySearchDetails.index);
           const details = {
+            query: query,
             isOneOff: aOneOff,
-            isSuggestion: (!aOneOff && telemetrySearchDetails),
+            suggestion: suggestion,
             selection: telemetrySearchDetails
           };
           BrowserSearch.recordSearchInTelemetry(engine, "searchbar", details);
           // null parameter below specifies HTML response for search
           let params = {
             postData: submission.postData,
           };
           if (aParams) {
@@ -956,16 +962,17 @@
             return;
 
           var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
 
           var searchBar = BrowserSearch.searchBar;
           var popupForSearchBar = searchBar && searchBar.textbox == this.mInput;
           if (popupForSearchBar) {
             searchBar.telemetrySearchDetails = {
+              query: this.mInput.value,
               index: controller.selection.currentIndex,
               kind: "mouse"
             };
           }
 
           // Check for unmodified left-click, and use default behavior
           if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey &&
               !aEvent.altKey && !aEvent.metaKey) {
--- a/browser/modules/BrowserUsageTelemetry.jsm
+++ b/browser/modules/BrowserUsageTelemetry.jsm
@@ -7,16 +7,19 @@
 
 this.EXPORTED_SYMBOLS = ["BrowserUsageTelemetry", "URLBAR_SELECTED_RESULT_TYPES"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "NLP",
+                                  "resource://gre/modules/NLP.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 // The upper bound for the count of the visited unique domain names.
 const MAX_UNIQUE_VISITED_DOMAINS = 100;
 
 // Observed topic names.
 const WINDOWS_RESTORED_TOPIC = "sessionstore-windows-restored";
@@ -346,20 +349,23 @@ let BrowserUsageTelemetry = {
    * nothing pertaining to the search contents themselves.
    *
    * @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 {String} [details.query=null]
+   *        The search query string as entered by the user.
    * @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.suggestion=null]
+   *        The search suggestion if this event was generated by a suggested
+   *        search.
    * @param {Boolean} [details.isAlias=false]
    *        true if this event was generated by a search using an alias.
    * @param {Object} [details.type=null]
    *        The object describing the event that triggered the search.
    * @throws if source is not in the known sources list.
    */
   recordSearch(engine, source, details = {}) {
     const isOneOff = !!details.isOneOff;
@@ -444,30 +450,45 @@ let BrowserUsageTelemetry = {
       }
 
       // If that's a legit one-off search signal, record it using the relative key.
       this._recordSearch(engine, sourceName, "oneoff");
       return;
     }
 
     // The search was not a one-off. It was a search with the default search engine.
-    if (details.isSuggestion) {
+    if (details.suggestion) {
       // It came from a suggested search, so count it as such.
-      this._recordSearch(engine, sourceName, "suggestion");
+      this._recordSearchSuggestion(engine, sourceName, details);
       return;
     } else if (details.isAlias) {
       // This one came from a search that used an alias.
       this._recordSearch(engine, sourceName, "alias");
       return;
     }
 
     // The search signal was generated by typing something and pressing enter.
     this._recordSearch(engine, sourceName, "enter");
   },
 
+  _recordSearchSuggestion(engine, source, details) {
+    this._recordSearch(engine, source, "suggestion");
+    if (!details.query || !details.suggestion) {
+      return;
+    }
+    let histogram = Services.telemetry.getKeyedHistogramById(
+      "FX_SEARCH_SUGGESTION_EDIT_DISTANCE"
+    );
+    let queryLengths = [6, 12, 24, 48];
+    let key = queryLengths.find(l => details.query.length <= l) ||
+              ">" + queryLengths[queryLengths.length - 1];
+    let distance = NLP.levenshtein(details.query, details.suggestion);
+    histogram.add(String(key), distance);
+  },
+
   /**
    * This gets called shortly after the SessionStore has finished restoring
    * windows and tabs. It counts the open tabs and adds listeners to all the
    * windows.
    */
   _setupAfterRestore() {
     // Make sure to catch new chrome windows and subsession splits.
     Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, false);
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5481,16 +5481,26 @@
     "bug_numbers": [1324167],
     "alert_emails": ["jaws@mozilla.com"],
     "expires_in_version": "56",
     "kind": "categorical",
     "labels": ["unknown", "general", "search", "content", "applications", "privacy", "security", "sync", "advancedGeneral", "advancedDataChoices", "advancedNetwork", "advancedUpdates", "advancedCerts"],
     "releaseChannelCollection": "opt-out",
     "description": "Count how often each preference category is opened."
   },
+  "FX_SEARCH_SUGGESTION_EDIT_DISTANCE": {
+    "bug_numbers": [1334630],
+    "alert_emails": ["adw@mozilla.com"],
+    "expires_in_version": "58",
+    "kind": "exponential",
+    "keyed": true,
+    "high": 200,
+    "n_buckets": 20,
+    "description": "The edit (Levenshtein) distance between the user's query string and the search suggestion they clicked, keyed on approximate query string length. Keys: '6' => query strings of lengths in the range (0, 6]; '12' => (6, 12]; '24' => (12, 24]; '48' => (24, 48]; '>48' for lengths > 48."
+  },
   "INPUT_EVENT_RESPONSE_MS": {
     "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
     "bug_numbers": [1235908],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 10000,
     "n_buckets": 50,
     "description": "Time (ms) from the Input event being created to the end of it being handled"
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -525,16 +525,17 @@
             case KeyEvent.DOM_VK_RETURN:
               if (/Mac/.test(navigator.platform)) {
                 // Prevent the default action, since it will beep on Mac
                 if (aEvent.metaKey)
                   aEvent.preventDefault();
               }
               if (this.mController.selection) {
                 this._selectionDetails = {
+                  query: this.value,
                   index: this.mController.selection.currentIndex,
                   kind: "key"
                 };
               }
               cancel = this.handleEnter(aEvent);
               break;
             case KeyEvent.DOM_VK_DELETE:
               if (/Mac/.test(navigator.platform) && !aEvent.shiftKey) {