[Password Manager] Introduce UMA histogram for username detection method

Bug: 786404
Change-Id: Ib04d8064a16369b4daf162cb1deee4c98b44f398
Reviewed-on: https://chromium-review.googlesource.com/776668
Commit-Queue: Maxim Kolosovskiy <kolos@chromium.org>
Reviewed-by: Ilya Sherman <isherman@chromium.org>
Reviewed-by: Vadym Doroshenko <dvadym@chromium.org>
Cr-Commit-Position: refs/heads/master@{#518192}
diff --git a/components/autofill/content/renderer/password_form_conversion_utils.cc b/components/autofill/content/renderer/password_form_conversion_utils.cc
index 20594f4..f7ba02e7 100644
--- a/components/autofill/content/renderer/password_form_conversion_utils.cc
+++ b/components/autofill/content/renderer/password_form_conversion_utils.cc
@@ -43,6 +43,7 @@
 using blink::WebString;
 
 namespace autofill {
+
 namespace {
 
 // PasswordForms can be constructed for both WebFormElements and for collections
@@ -439,6 +440,8 @@
     const FormsPredictionsMap* form_predictions,
     UsernameDetectorCache* username_detector_cache) {
   WebInputElement username_element;
+  UsernameDetectionMethod username_detection_method =
+      UsernameDetectionMethod::NO_USERNAME_DETECTED;
   password_form->username_marked_by_site = false;
 
   std::vector<WebInputElement> passwords;
@@ -457,6 +460,18 @@
   if (form_predictions) {
     FindPredictedElements(form, password_form->form_data, *form_predictions,
                           &predicted_elements);
+    WebInputElement predicted_username_element;
+    bool map_has_username_prediction = MapContainsPrediction(
+        predicted_elements, PREDICTION_USERNAME, &predicted_username_element);
+
+    // Let server predictions override the selection of the username field. This
+    // allows instant adjusting without changing Chromium code.
+    if (map_has_username_prediction) {
+      username_element = predicted_username_element;
+      password_form->was_parsed_using_autofill_predictions = true;
+      username_detection_method =
+          UsernameDetectionMethod::SERVER_SIDE_PREDICTION;
+    }
   }
 
   bool found_password_with_user_input = false;
@@ -581,8 +596,14 @@
           // we might have made before). Furthermore, drop all other possible
           // usernames we have accrued so far: they come from fields not marked
           // with the autocomplete attribute, making them unlikely alternatives.
-          username_element = *input_element;
-          password_form->username_marked_by_site = true;
+
+          // Don't override the server-side prediction if any.
+          if (username_element.IsNull()) {
+            username_element = *input_element;
+            password_form->username_marked_by_site = true;
+            username_detection_method =
+                UsernameDetectionMethod::AUTOCOMPLETE_ATTRIBUTE;
+          }
         }
       } else {
         if (password_form->username_marked_by_site) {
@@ -611,6 +632,7 @@
     passwords = passwords_without_heuristics;
     last_text_input_before_password = last_text_input_without_heuristics;
   }
+  DCHECK_EQ(passwords.size(), last_text_input_before_password.size());
 
   // |passwords| must be non-empty. Just in case the heuristics above have a
   // bug, return now.
@@ -626,6 +648,9 @@
       GetUsernameFieldBasedOnHtmlAttributes(
           all_possible_usernames, password_form->form_data, &username_element,
           username_detector_cache);
+      if (!username_element.IsNull())
+        username_detection_method =
+            UsernameDetectionMethod::HTML_BASED_CLASSIFIER;
     }
   }
 
@@ -662,29 +687,32 @@
   }
 
   // Base heuristic for username detection.
-  DCHECK_EQ(passwords.size(), last_text_input_before_password.size());
+  WebInputElement base_heuristic_prediction;
+  if (!password.IsNull())
+    base_heuristic_prediction = last_text_input_before_password[password];
+  if (base_heuristic_prediction.IsNull() && !new_password.IsNull())
+    base_heuristic_prediction = last_text_input_before_password[new_password];
+
   if (username_element.IsNull()) {
-    if (!password.IsNull())
-      username_element = last_text_input_before_password[password];
-    if (username_element.IsNull() && !new_password.IsNull())
-      username_element = last_text_input_before_password[new_password];
+    username_element = base_heuristic_prediction;
+    if (!base_heuristic_prediction.IsNull())
+      username_detection_method = UsernameDetectionMethod::BASE_HEURISTIC;
+  } else if (base_heuristic_prediction == username_element &&
+             username_detection_method !=
+                 UsernameDetectionMethod::AUTOCOMPLETE_ATTRIBUTE) {
+    // TODO(crbug.com/786404): when the bug is fixed, remove this block and
+    // calculate |base_heuristic_prediction| only if |username_element.IsNull()|
+    // This block was added to measure the impact of server-side predictions and
+    // HTML based classifier compared to "old classifiers" (the based heuristic
+    // and 'autocomplete' attribute).
+    username_detection_method = UsernameDetectionMethod::BASE_HEURISTIC;
   }
+  UMA_HISTOGRAM_ENUMERATION(
+      "PasswordManager.UsernameDetectionMethod", username_detection_method,
+      UsernameDetectionMethod::USERNAME_DETECTION_METHOD_COUNT);
+
   password_form->layout = SequenceToLayout(layout_sequence);
 
-  WebInputElement predicted_username_element;
-  bool map_has_username_prediction = MapContainsPrediction(
-      predicted_elements, PREDICTION_USERNAME, &predicted_username_element);
-
-  // Let server predictions override the selection of the username field. This
-  // allows instant adjusting without changing Chromium code.
-  auto username_element_iterator = predicted_elements.find(username_element);
-  if (map_has_username_prediction &&
-      (username_element_iterator == predicted_elements.end() ||
-       username_element_iterator->second != PREDICTION_USERNAME)) {
-    username_element = predicted_username_element;
-    password_form->was_parsed_using_autofill_predictions = true;
-  }
-
   if (!username_element.IsNull()) {
     password_form->username_element =
         FieldName(username_element, "anonymous_username");
diff --git a/components/autofill/content/renderer/password_form_conversion_utils.h b/components/autofill/content/renderer/password_form_conversion_utils.h
index a64b9da..d310b30 100644
--- a/components/autofill/content/renderer/password_form_conversion_utils.h
+++ b/components/autofill/content/renderer/password_form_conversion_utils.h
@@ -33,6 +33,15 @@
 
 struct PasswordForm;
 
+enum UsernameDetectionMethod {
+  NO_USERNAME_DETECTED,
+  BASE_HEURISTIC,
+  HTML_BASED_CLASSIFIER,
+  AUTOCOMPLETE_ATTRIBUTE,
+  SERVER_SIDE_PREDICTION,
+  USERNAME_DETECTION_METHOD_COUNT
+};
+
 // The caller of this function is responsible for deleting the returned object.
 re2::RE2* CreateMatcher(void* instance, const char* pattern);
 
diff --git a/components/autofill/content/renderer/password_form_conversion_utils_browsertest.cc b/components/autofill/content/renderer/password_form_conversion_utils_browsertest.cc
index edf9c60..dd139c5 100644
--- a/components/autofill/content/renderer/password_form_conversion_utils_browsertest.cc
+++ b/components/autofill/content/renderer/password_form_conversion_utils_browsertest.cc
@@ -10,6 +10,7 @@
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
+#include "base/test/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "build/build_config.h"
 #include "components/autofill/content/renderer/form_autofill_util.h"
@@ -597,12 +598,16 @@
 
   // No signals from HTML attributes. The classifier found nothing and cached
   // it.
+  base::HistogramTester histogram_tester;
   std::unique_ptr<PasswordForm> password_form = CreatePasswordFormFromWebForm(
       form, nullptr, nullptr, &username_detector_cache);
   EXPECT_TRUE(password_form);
   ASSERT_EQ(1u, username_detector_cache.size());
   EXPECT_EQ(form, username_detector_cache.begin()->first);
   EXPECT_EQ(blink::WebInputElement(), username_detector_cache.begin()->second);
+  histogram_tester.ExpectUniqueSample("PasswordManager.UsernameDetectionMethod",
+                                      UsernameDetectionMethod::BASE_HEURISTIC,
+                                      1);
 
   // Changing attributes would change the classifier's output. But the output
   // will be the same because it was cached in |username_detector_cache|.
@@ -615,6 +620,9 @@
   ASSERT_EQ(1u, username_detector_cache.size());
   EXPECT_EQ(form, username_detector_cache.begin()->first);
   EXPECT_EQ(blink::WebInputElement(), username_detector_cache.begin()->second);
+  histogram_tester.ExpectUniqueSample("PasswordManager.UsernameDetectionMethod",
+                                      UsernameDetectionMethod::BASE_HEURISTIC,
+                                      2);
 
   // Clear the cache. The classifier will find username field and cache it.
   username_detector_cache.clear();
@@ -627,6 +635,11 @@
   ASSERT_FALSE(username_detector_cache.begin()->second.IsNull());
   EXPECT_EQ("login",
             username_detector_cache.begin()->second.NameForAutofill().Utf8());
+  EXPECT_THAT(
+      histogram_tester.GetAllSamples("PasswordManager.UsernameDetectionMethod"),
+      testing::UnorderedElementsAre(
+          base::Bucket(UsernameDetectionMethod::BASE_HEURISTIC, 2),
+          base::Bucket(UsernameDetectionMethod::HTML_BASED_CLASSIFIER, 1)));
 
   // Change the attributes again ("username" is stronger signal than "login"),
   // but keep the cache. The classifier's output should be the same.
@@ -639,6 +652,11 @@
   ASSERT_FALSE(username_detector_cache.begin()->second.IsNull());
   EXPECT_EQ("login",
             username_detector_cache.begin()->second.NameForAutofill().Utf8());
+  EXPECT_THAT(
+      histogram_tester.GetAllSamples("PasswordManager.UsernameDetectionMethod"),
+      testing::UnorderedElementsAre(
+          base::Bucket(UsernameDetectionMethod::BASE_HEURISTIC, 2),
+          base::Bucket(UsernameDetectionMethod::HTML_BASED_CLASSIFIER, 2)));
 }
 
 TEST_F(MAYBE_PasswordFormConversionUtilsTest,
@@ -1275,6 +1293,26 @@
   }
 }
 
+TEST_F(MAYBE_PasswordFormConversionUtilsTest,
+       UsernameDetection_AutocompleteAttribute) {
+  PasswordFormBuilder builder(kTestFormActionURL);
+  builder.AddTextField("username", "JohnSmith", "username");
+  builder.AddTextField("Full name", "John A. Smith", nullptr);
+  builder.AddPasswordField("password", "secret", nullptr);
+  builder.AddSubmitButton("submit");
+  std::string html = builder.ProduceHTML();
+
+  base::HistogramTester histogram_tester;
+  std::unique_ptr<PasswordForm> password_form =
+      LoadHTMLAndConvertForm(html, nullptr, false);
+  ASSERT_TRUE(password_form);
+  EXPECT_EQ(base::UTF8ToUTF16("username"), password_form->username_element);
+  EXPECT_EQ(base::UTF8ToUTF16("JohnSmith"), password_form->username_value);
+  histogram_tester.ExpectUniqueSample(
+      "PasswordManager.UsernameDetectionMethod",
+      UsernameDetectionMethod::AUTOCOMPLETE_ATTRIBUTE, 1);
+}
+
 TEST_F(MAYBE_PasswordFormConversionUtilsTest, IgnoreInvisibledTextFields) {
   PasswordFormBuilder builder(kTestFormActionURL);
 
@@ -1745,6 +1783,31 @@
             password_form->password_value);
 }
 
+TEST_F(MAYBE_PasswordFormConversionUtilsTest, UsernamePredictionFromServer) {
+  PasswordFormBuilder builder(kTestFormActionURL);
+  builder.AddTextField("username", "JohnSmith", nullptr);
+  // 'autocomplete' attribute cannot override server's prediction.
+  builder.AddTextField("Full name", "John A. Smith", "username");
+  builder.AddPasswordField("password", "secret", nullptr);
+  builder.AddSubmitButton("submit");
+  std::string html = builder.ProduceHTML();
+
+  std::map<int, PasswordFormFieldPredictionType> predictions_positions;
+  predictions_positions[0] = PREDICTION_USERNAME;
+  FormsPredictionsMap predictions;
+  SetPredictions(html, &predictions, predictions_positions);
+
+  base::HistogramTester histogram_tester;
+  std::unique_ptr<PasswordForm> password_form =
+      LoadHTMLAndConvertForm(html, &predictions, false);
+  ASSERT_TRUE(password_form);
+  EXPECT_EQ(base::UTF8ToUTF16("username"), password_form->username_element);
+  EXPECT_EQ(base::UTF8ToUTF16("JohnSmith"), password_form->username_value);
+  histogram_tester.ExpectUniqueSample(
+      "PasswordManager.UsernameDetectionMethod",
+      UsernameDetectionMethod::SERVER_SIDE_PREDICTION, 1);
+}
+
 TEST_F(MAYBE_PasswordFormConversionUtilsTest,
        CreditCardVerificationNumberWithTypePasswordForm) {
   PasswordFormBuilder builder(kTestFormActionURL);
@@ -2124,6 +2187,7 @@
   builder.AddPasswordField("password", "secret", nullptr);
   builder.AddSubmitButton("submit");
   std::string html = builder.ProduceHTML();
+  base::HistogramTester histogram_tester;
 
   std::unique_ptr<PasswordForm> password_form =
       LoadHTMLAndConvertForm(html, nullptr, false);
@@ -2134,6 +2198,9 @@
   EXPECT_TRUE(password_form->username_value.empty());
   EXPECT_EQ(base::UTF8ToUTF16("password"), password_form->password_element);
   EXPECT_EQ(base::UTF8ToUTF16("secret"), password_form->password_value);
+  histogram_tester.ExpectUniqueSample(
+      "PasswordManager.UsernameDetectionMethod",
+      UsernameDetectionMethod::NO_USERNAME_DETECTED, 1);
 }
 
 }  // namespace autofill
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index e9b058dc..44ab738 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -41142,6 +41142,14 @@
   <int value="0" label="WiFi Scan"/>
 </enum>
 
+<enum name="UsernameDetectionMethod">
+  <int value="0" label="No username detected"/>
+  <int value="1" label="Base heuristic"/>
+  <int value="2" label="HTML based classifier"/>
+  <int value="3" label="'autocomplete' attribute"/>
+  <int value="4" label="Server-side prediction"/>
+</enum>
+
 <enum name="UserPodsDisplay">
   <int value="0" label="Enabled by local settings"/>
   <int value="1" label="Enabled by domain policy"/>
diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml
index 9b4b377c..a967c9f9 100644
--- a/tools/metrics/histograms/histograms.xml
+++ b/tools/metrics/histograms/histograms.xml
@@ -58980,6 +58980,20 @@
   </summary>
 </histogram>
 
+<histogram name="PasswordManager.UsernameDetectionMethod"
+    enum="UsernameDetectionMethod">
+  <owner>kolos@chromium.org</owner>
+  <summary>
+    Measures what method was used for username field detection in the renderer
+    code. The metric may be recorded several times for page visit because it is
+    recorded at every PasswordForm creation. If a site changes HTML attributes
+    of fields or server-side predictions is received, different values can be
+    recorded for the same form. If an outcome of HTML classifier or a
+    server-side prediction coincides with the outcome of base heuristic, the
+    metric points to base heuristic method.
+  </summary>
+</histogram>
+
 <histogram name="PasswordManager.UserStoredPasswordWithInvalidSSLCert"
     enum="Boolean">
   <obsolete>