From 1fa6c42cee1471df2a3f3e383472267f3819497a Mon Sep 17 00:00:00 2001
From: Vasilii Sukhanov <vasilii@chromium.org>
Date: Fri, 14 Nov 2014 14:00:04 +0100
Subject: Finch experiment for limiting the password bubble annoyance.

BUG=431739
TBR=battre@chromium.org,vabr@chromium.org

Review URL: https://codereview.chromium.org/711043002

Cr-Commit-Position: refs/heads/master@{#303630}
(cherry picked from commit 4d7cdf425d0b57f4c2a766433f5ffdc3cedd909f)

Review URL: https://codereview.chromium.org/731473003

Cr-Commit-Position: refs/branch-heads/2214@{#41}
Cr-Branched-From: 03655fd3f6d72165dc3c9bd2c89807305316fe6c-refs/heads/master@{#303346}
---
 chrome/browser/prefs/browser_prefs.cc              |   2 +
 .../ui/passwords/manage_passwords_bubble_model.cc  |  16 ++
 .../ui/passwords/manage_passwords_ui_controller.cc |   5 +
 .../ui/passwords/password_bubble_experiment.cc     | 205 +++++++++++++++++++++
 .../ui/passwords/password_bubble_experiment.h      |  57 ++++++
 .../password_bubble_experiment_unittest.cc         | 139 ++++++++++++++
 chrome/chrome_browser_ui.gypi                      |   2 +
 chrome/chrome_tests_unit.gypi                      |   1 +
 chrome/common/pref_names.cc                        |  10 +
 chrome/common/pref_names.h                         |   4 +
 10 files changed, 441 insertions(+)
 create mode 100644 chrome/browser/ui/passwords/password_bubble_experiment.cc
 create mode 100644 chrome/browser/ui/passwords/password_bubble_experiment.h
 create mode 100644 chrome/browser/ui/passwords/password_bubble_experiment_unittest.cc

diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc
index 56bac85..6ea64ff 100644
--- a/chrome/browser/prefs/browser_prefs.cc
+++ b/chrome/browser/prefs/browser_prefs.cc
@@ -65,6 +65,7 @@
 #include "chrome/browser/ui/browser_ui_prefs.h"
 #include "chrome/browser/ui/navigation_correction_tab_observer.h"
 #include "chrome/browser/ui/network_profile_bubble.h"
+#include "chrome/browser/ui/passwords/password_bubble_experiment.h"
 #include "chrome/browser/ui/prefs/prefs_tab_helper.h"
 #include "chrome/browser/ui/search_engines/keyword_editor_controller.h"
 #include "chrome/browser/ui/startup/autolaunch_prompt.h"
@@ -407,6 +408,7 @@ void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
   MediaStreamDevicesController::RegisterProfilePrefs(registry);
   NetPrefObserver::RegisterProfilePrefs(registry);
   password_manager::PasswordManager::RegisterProfilePrefs(registry);
+  password_bubble_experiment::RegisterPrefs(registry);
   PrefProxyConfigTrackerImpl::RegisterProfilePrefs(registry);
   PrefsTabHelper::RegisterProfilePrefs(registry);
   Profile::RegisterProfilePrefs(registry);
diff --git a/chrome/browser/ui/passwords/manage_passwords_bubble_model.cc b/chrome/browser/ui/passwords/manage_passwords_bubble_model.cc
index 1e1b433..dd59671 100644
--- a/chrome/browser/ui/passwords/manage_passwords_bubble_model.cc
+++ b/chrome/browser/ui/passwords/manage_passwords_bubble_model.cc
@@ -8,6 +8,7 @@
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_finder.h"
 #include "chrome/browser/ui/passwords/manage_passwords_ui_controller.h"
+#include "chrome/browser/ui/passwords/password_bubble_experiment.h"
 #include "chrome/grit/generated_resources.h"
 #include "components/password_manager/core/browser/password_store.h"
 #include "components/password_manager/core/common/password_manager_ui.h"
@@ -33,6 +34,15 @@ int GetFieldWidth(FieldType type) {
                                                    : kPasswordFieldSize);
 }
 
+void RecordExperimentStatistics(content::WebContents* web_contents,
+                                metrics_util::UIDismissalReason reason) {
+  if (!web_contents)
+    return;
+  Profile* profile =
+      Profile::FromBrowserContext(web_contents->GetBrowserContext());
+  password_bubble_experiment::RecordBubbleClosed(profile->GetPrefs(), reason);
+}
+
 }  // namespace
 
 ManagePasswordsBubbleModel::ManagePasswordsBubbleModel(
@@ -109,15 +119,20 @@ void ManagePasswordsBubbleModel::OnBubbleHidden() {
     return;
 
   metrics_util::LogUIDismissalReason(dismissal_reason_);
+  // Other use cases have been reported in the callbacks like OnSaveClicked().
+  if (dismissal_reason_ == metrics_util::NO_DIRECT_INTERACTION)
+    RecordExperimentStatistics(web_contents(), dismissal_reason_);
 }
 
 void ManagePasswordsBubbleModel::OnNopeClicked() {
   dismissal_reason_ = metrics_util::CLICKED_NOPE;
+  RecordExperimentStatistics(web_contents(), dismissal_reason_);
   state_ = password_manager::ui::PENDING_PASSWORD_STATE;
 }
 
 void ManagePasswordsBubbleModel::OnNeverForThisSiteClicked() {
   dismissal_reason_ = metrics_util::CLICKED_NEVER;
+  RecordExperimentStatistics(web_contents(), dismissal_reason_);
   ManagePasswordsUIController* manage_passwords_ui_controller =
       ManagePasswordsUIController::FromWebContents(web_contents());
   manage_passwords_ui_controller->NeverSavePassword();
@@ -134,6 +149,7 @@ void ManagePasswordsBubbleModel::OnUnblacklistClicked() {
 
 void ManagePasswordsBubbleModel::OnSaveClicked() {
   dismissal_reason_ = metrics_util::CLICKED_SAVE;
+  RecordExperimentStatistics(web_contents(), dismissal_reason_);
   ManagePasswordsUIController* manage_passwords_ui_controller =
       ManagePasswordsUIController::FromWebContents(web_contents());
   manage_passwords_ui_controller->SavePassword();
diff --git a/chrome/browser/ui/passwords/manage_passwords_ui_controller.cc b/chrome/browser/ui/passwords/manage_passwords_ui_controller.cc
index 039f589..4afd320 100644
--- a/chrome/browser/ui/passwords/manage_passwords_ui_controller.cc
+++ b/chrome/browser/ui/passwords/manage_passwords_ui_controller.cc
@@ -15,6 +15,7 @@
 #include "chrome/browser/ui/chrome_pages.h"
 #include "chrome/browser/ui/location_bar/location_bar.h"
 #include "chrome/browser/ui/passwords/manage_passwords_icon.h"
+#include "chrome/browser/ui/passwords/password_bubble_experiment.h"
 #include "chrome/common/url_constants.h"
 #include "components/password_manager/core/browser/password_store.h"
 #include "content/public/browser/notification_service.h"
@@ -268,6 +269,10 @@ void ManagePasswordsUIController::ShowBubbleWithoutUserInteraction() {
   Browser* browser = chrome::FindBrowserWithWebContents(web_contents());
   if (!browser || browser->toolbar_model()->input_in_progress())
     return;
+  if (state_ == password_manager::ui::PENDING_PASSWORD_AND_BUBBLE_STATE &&
+      !password_bubble_experiment::ShouldShowBubble(
+          browser->profile()->GetPrefs()))
+    return;
   CommandUpdater* updater = browser->command_controller()->command_updater();
   updater->ExecuteCommand(IDC_MANAGE_PASSWORDS_FOR_PAGE);
 #endif
diff --git a/chrome/browser/ui/passwords/password_bubble_experiment.cc b/chrome/browser/ui/passwords/password_bubble_experiment.cc
new file mode 100644
index 0000000..276a038
--- /dev/null
+++ b/chrome/browser/ui/passwords/password_bubble_experiment.cc
@@ -0,0 +1,205 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/passwords/password_bubble_experiment.h"
+
+#include "base/metrics/field_trial.h"
+#include "base/prefs/pref_service.h"
+#include "base/rand_util.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/time/time.h"
+#include "chrome/common/pref_names.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/variations/variations_associated_data.h"
+
+namespace password_bubble_experiment {
+namespace {
+
+bool IsNegativeEvent(password_manager::metrics_util::UIDismissalReason reason) {
+  return (reason == password_manager::metrics_util::NO_DIRECT_INTERACTION ||
+          reason == password_manager::metrics_util::CLICKED_NOPE ||
+          reason == password_manager::metrics_util::CLICKED_NEVER);
+}
+
+// "TimeSpan" experiment -----------------------------------------------------
+
+bool ExtractTimeSpanParams(base::TimeDelta* time_delta, int* nopes_limit) {
+  std::map<std::string, std::string> params;
+  bool retrieved = variations::GetVariationParams(kExperimentName, &params);
+  if (!retrieved)
+    return false;
+  int days = 0;
+  if (!base::StringToInt(params[kParamTimeSpan], &days) ||
+      !base::StringToInt(params[kParamTimeSpanNopeThreshold], nopes_limit))
+    return false;
+  *time_delta = base::TimeDelta::FromDays(days);
+  return true;
+}
+
+bool OverwriteTimeSpanPrefsIfNeeded(PrefService* prefs,
+                                    base::TimeDelta time_span) {
+  base::Time beginning = base::Time::FromInternalValue(
+      prefs->GetInt64(prefs::kPasswordBubbleTimeStamp));
+  base::Time now = base::Time::Now();
+  if (beginning + time_span < now) {
+    prefs->SetInt64(prefs::kPasswordBubbleTimeStamp, now.ToInternalValue());
+    prefs->SetInteger(prefs::kPasswordBubbleNopesCount, 0);
+    return true;
+  }
+  return false;
+}
+
+// If user dismisses the bubble >= kParamTimeSpanNopeThreshold times during
+// kParamTimeSpan days then the bubble isn't shown until the end of this time
+// span.
+bool ShouldShowBubbleTimeSpanExperiment(PrefService* prefs) {
+  base::TimeDelta time_span;
+  int nopes_limit = 0;
+  if (!ExtractTimeSpanParams(&time_span, &nopes_limit)) {
+    VLOG(2) << "Can't read parameters for "
+        << kExperimentName  << " experiment";
+    return true;
+  }
+  // Check if the new time span has started.
+  if (OverwriteTimeSpanPrefsIfNeeded(prefs, time_span))
+    return true;
+  int current_nopes = prefs->GetInteger(prefs::kPasswordBubbleNopesCount);
+  return current_nopes < nopes_limit;
+}
+
+// Increase the "Nope" counter in prefs and start a new time span if needed.
+void UpdateTimeSpanPrefs(
+    PrefService* prefs,
+    password_manager::metrics_util::UIDismissalReason reason) {
+  if (!IsNegativeEvent(reason))
+    return;
+  base::TimeDelta time_span;
+  int nopes_limit = 0;
+  if (!ExtractTimeSpanParams(&time_span, &nopes_limit)) {
+    VLOG(2) << "Can't read parameters for "
+        << kExperimentName << " experiment";
+    return;
+  }
+  OverwriteTimeSpanPrefsIfNeeded(prefs, time_span);
+  int current_nopes = prefs->GetInteger(prefs::kPasswordBubbleNopesCount);
+  prefs->SetInteger(prefs::kPasswordBubbleNopesCount, current_nopes + 1);
+}
+
+// "Probability" experiment --------------------------------------------------
+
+bool ExtractProbabilityParams(unsigned* history_length, unsigned* saves) {
+  std::map<std::string, std::string> params;
+  bool retrieved = variations::GetVariationParams(kExperimentName, &params);
+  if (!retrieved)
+    return false;
+  return base::StringToUint(params[kParamProbabilityInteractionsCount],
+                            history_length) &&
+      base::StringToUint(params[kParamProbabilityFakeSaves], saves);
+}
+
+std::vector<int> ReadInteractionHistory(PrefService* prefs) {
+  std::vector<int> interactions;
+  const base::ListValue* list =
+      prefs->GetList(prefs::kPasswordBubbleLastInteractions);
+  if (!list)
+    return interactions;
+  for (const base::Value* value : *list) {
+    int out_value;
+    if (value->GetAsInteger(&out_value))
+      interactions.push_back(out_value);
+  }
+  return interactions;
+}
+
+// We keep the history of last kParamProbabilityInteractionsCount interactions
+// with the bubble. We implicitly add kParamProbabilityFakeSaves "Save" clicks.
+// If there are x "Save" clicks among those kParamProbabilityInteractionsCount
+// then the bubble is shown with probability (x + kParamProbabilityFakeSaves)/
+// (kParamProbabilityInteractionsCount + kParamProbabilityFakeSaves).
+bool ShouldShowBubbleProbabilityExperiment(PrefService* prefs) {
+  unsigned history_length = 0, fake_saves = 0;
+  if (!ExtractProbabilityParams(&history_length, &fake_saves)) {
+    VLOG(2) << "Can't read parameters for "
+        << kExperimentName << " experiment";
+    return true;
+  }
+  std::vector<int> interactions = ReadInteractionHistory(prefs);
+  unsigned real_saves =
+      std::count(interactions.begin(), interactions.end(),
+                 password_manager::metrics_util::CLICKED_SAVE);
+  return (interactions.size() + fake_saves) * base::RandDouble() <=
+      real_saves + fake_saves;
+}
+
+void UpdateProbabilityPrefs(
+    PrefService* prefs,
+    password_manager::metrics_util::UIDismissalReason reason) {
+  if (!IsNegativeEvent(reason) &&
+      reason != password_manager::metrics_util::CLICKED_SAVE)
+    return;
+  unsigned history_length = 0, fake_saves = 0;
+  if (!ExtractProbabilityParams(&history_length, &fake_saves)) {
+    VLOG(2) << "Can't read parameters for "
+        << kExperimentName << " experiment";
+    return;
+  }
+  std::vector<int> interactions = ReadInteractionHistory(prefs);
+  interactions.push_back(reason);
+  size_t history_beginning = interactions.size() > history_length ?
+      interactions.size() - history_length : 0;
+  base::ListValue value;
+  for (size_t i = history_beginning; i < interactions.size(); ++i)
+    value.AppendInteger(interactions[i]);
+  prefs->Set(prefs::kPasswordBubbleLastInteractions, value);
+}
+
+} // namespace
+
+const char kExperimentName[] = "PasswordBubbleAlgorithm";
+const char kGroupTimeSpanBased[] = "TimeSpan";
+const char kGroupProbabilityBased[] = "Probability";
+const char kParamProbabilityFakeSaves[] = "saves_count";
+const char kParamProbabilityInteractionsCount[] = "last_interactions_count";
+const char kParamTimeSpan[] = "time_span";
+const char kParamTimeSpanNopeThreshold[] = "nope_threshold";
+
+void RegisterPrefs(user_prefs::PrefRegistrySyncable* registry) {
+  registry->RegisterInt64Pref(
+      prefs::kPasswordBubbleTimeStamp,
+      0,
+      user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
+  registry->RegisterIntegerPref(
+      prefs::kPasswordBubbleNopesCount,
+      0,
+      user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
+  registry->RegisterListPref(
+      prefs::kPasswordBubbleLastInteractions,
+      user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
+}
+
+bool ShouldShowBubble(PrefService* prefs) {
+  if (!base::FieldTrialList::TrialExists(kExperimentName))
+    return true;
+  std::string group_name =
+      base::FieldTrialList::FindFullName(kExperimentName);
+
+  if (group_name == kGroupTimeSpanBased) {
+    return ShouldShowBubbleTimeSpanExperiment(prefs);
+  }
+  if (group_name == kGroupProbabilityBased) {
+    return ShouldShowBubbleProbabilityExperiment(prefs);
+  }
+
+  // The "Show Always" should be the default case.
+  return true;
+}
+
+void RecordBubbleClosed(
+    PrefService* prefs,
+    password_manager::metrics_util::UIDismissalReason reason) {
+  UpdateTimeSpanPrefs(prefs, reason);
+  UpdateProbabilityPrefs(prefs, reason);
+}
+
+}  // namespace password_bubble_experiment
diff --git a/chrome/browser/ui/passwords/password_bubble_experiment.h b/chrome/browser/ui/passwords/password_bubble_experiment.h
new file mode 100644
index 0000000..9c4d302
--- /dev/null
+++ b/chrome/browser/ui/passwords/password_bubble_experiment.h
@@ -0,0 +1,57 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_PASSWORDS_PASSWORD_BUBBLE_EXPERIMENT_H_
+#define CHROME_BROWSER_UI_PASSWORDS_PASSWORD_BUBBLE_EXPERIMENT_H_
+
+#include "base/macros.h"
+#include "components/password_manager/core/browser/password_manager_metrics_util.h"
+
+namespace user_prefs {
+class PrefRegistrySyncable;
+}
+
+class PrefService;
+
+// These functions handle the algorithms according to which the "Save password?"
+// bubble is shown to user.
+namespace password_bubble_experiment {
+
+void RegisterPrefs(user_prefs::PrefRegistrySyncable* registry);
+
+// The decision is made based on the "PasswordBubbleAlgorithm" finch experiment.
+// The default value is true.
+// It should be called before showing the "Save Password?" dialog.
+bool ShouldShowBubble(PrefService* prefs);
+
+// Should be called when user dismisses the "Save Password?" dialog. It stores
+// the statistics about interactions with the bubble.
+void RecordBubbleClosed(
+    PrefService* prefs,
+    password_manager::metrics_util::UIDismissalReason reason);
+
+// The name of the finch experiment controlling the algorithm.
+extern const char kExperimentName[];
+
+// The group name for the time based algorithm.
+extern const char kGroupTimeSpanBased[];
+
+// The group name for the probability algorithm.
+extern const char kGroupProbabilityBased[];
+
+// For "Probability" group. The additional "Saves" to be added to the model.
+extern const char kParamProbabilityFakeSaves[];
+
+// For "Probability" group. The interaction history length.
+extern const char kParamProbabilityInteractionsCount[];
+
+// For "TimeSpan" group. The time span until the nope counter is zeroed.
+extern const char kParamTimeSpan[];
+
+// For "TimeSpan" group. The nopes threshold.
+extern const char kParamTimeSpanNopeThreshold[];
+
+}  // namespace password_bubble_experiment
+
+#endif  // CHROME_BROWSER_UI_PASSWORDS_PASSWORD_BUBBLE_EXPERIMENT_H_
diff --git a/chrome/browser/ui/passwords/password_bubble_experiment_unittest.cc b/chrome/browser/ui/passwords/password_bubble_experiment_unittest.cc
new file mode 100644
index 0000000..96265b5
--- /dev/null
+++ b/chrome/browser/ui/passwords/password_bubble_experiment_unittest.cc
@@ -0,0 +1,139 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/passwords/password_bubble_experiment.h"
+
+#include "base/files/scoped_temp_dir.h"
+#include "base/metrics/field_trial.h"
+#include "base/prefs/pref_service.h"
+#include "base/strings/string_number_conversions.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/variations/entropy_provider.h"
+#include "components/variations/variations_associated_data.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+const int kTimeSpanDays = 2;
+const int kTimeSpanThreshold = 3;
+const int kProbabilityFakeSaves = 0;
+const int kProbabilityHistory = 10;
+
+void SetupTimeSpanExperiment() {
+  ASSERT_TRUE(base::FieldTrialList::CreateFieldTrial(
+      password_bubble_experiment::kExperimentName,
+      password_bubble_experiment::kGroupTimeSpanBased));
+  std::map<std::string, std::string> params;
+  params[password_bubble_experiment::kParamTimeSpan] =
+      base::IntToString(kTimeSpanDays);
+  params[password_bubble_experiment::kParamTimeSpanNopeThreshold] =
+      base::IntToString(kTimeSpanThreshold);
+  ASSERT_TRUE(variations::AssociateVariationParams(
+      password_bubble_experiment::kExperimentName,
+      password_bubble_experiment::kGroupTimeSpanBased,
+      params));
+}
+
+void SetupProbabilityExperiment() {
+  ASSERT_TRUE(base::FieldTrialList::CreateFieldTrial(
+      password_bubble_experiment::kExperimentName,
+      password_bubble_experiment::kGroupProbabilityBased));
+  std::map<std::string, std::string> params;
+  params[password_bubble_experiment::kParamProbabilityFakeSaves] =
+      base::IntToString(kProbabilityFakeSaves);
+  params[password_bubble_experiment::kParamProbabilityInteractionsCount] =
+      base::IntToString(kProbabilityHistory);
+  ASSERT_TRUE(variations::AssociateVariationParams(
+      password_bubble_experiment::kExperimentName,
+      password_bubble_experiment::kGroupProbabilityBased,
+      params));
+}
+
+} // namespace
+
+class PasswordBubbleExperimentTest : public testing::Test {
+ public:
+  void SetUp() override {
+    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+    profile_.reset(new TestingProfile(temp_dir_.path()));
+
+    field_trial_list_.reset(new base::FieldTrialList(
+        new metrics::SHA1EntropyProvider("foo")));
+    variations::testing::ClearAllVariationParams();
+  }
+
+  PrefService* prefs() { return profile_->GetPrefs(); }
+
+ private:
+  base::ScopedTempDir temp_dir_;
+  scoped_ptr<TestingProfile> profile_;
+  scoped_ptr<base::FieldTrialList> field_trial_list_;
+};
+
+TEST_F(PasswordBubbleExperimentTest, TimeSpan) {
+  SetupTimeSpanExperiment();
+
+  EXPECT_TRUE(password_bubble_experiment::ShouldShowBubble(prefs()));
+  // Don't save password enough times.
+  for (int i = 0; i < kTimeSpanThreshold; ++i) {
+    password_manager::metrics_util::UIDismissalReason reason = i % 2 ?
+        password_manager::metrics_util::NO_DIRECT_INTERACTION :
+        password_manager::metrics_util::CLICKED_NOPE;
+    password_bubble_experiment::RecordBubbleClosed(prefs(), reason);
+  }
+  EXPECT_FALSE(password_bubble_experiment::ShouldShowBubble(prefs()));
+
+  // Save password many times. It doesn't bring the bubble back while the time
+  // span isn't over.
+  for (int i = 0; i < 2*kTimeSpanThreshold; ++i) {
+    password_bubble_experiment::RecordBubbleClosed(
+        prefs(),
+        password_manager::metrics_util::CLICKED_SAVE);
+  }
+  EXPECT_FALSE(password_bubble_experiment::ShouldShowBubble(prefs()));
+}
+
+TEST_F(PasswordBubbleExperimentTest, TimeSpanOver) {
+  SetupTimeSpanExperiment();
+
+  base::Time past_interval =
+      base::Time::Now() - base::TimeDelta::FromDays(kTimeSpanDays + 1);
+  prefs()->SetInt64(prefs::kPasswordBubbleTimeStamp,
+                    past_interval.ToInternalValue());
+  prefs()->SetInteger(prefs::kPasswordBubbleNopesCount, kTimeSpanThreshold);
+  // The time span is over. The bubble should be shown.
+  EXPECT_TRUE(password_bubble_experiment::ShouldShowBubble(prefs()));
+  EXPECT_EQ(0, prefs()->GetInteger(prefs::kPasswordBubbleNopesCount));
+
+  // Set the old time span again and record "Nope". The counter restarts from 0.
+  prefs()->SetInt64(prefs::kPasswordBubbleTimeStamp,
+                    past_interval.ToInternalValue());
+  password_bubble_experiment::RecordBubbleClosed(
+      prefs(), password_manager::metrics_util::CLICKED_NOPE);
+  EXPECT_TRUE(password_bubble_experiment::ShouldShowBubble(prefs()));
+  EXPECT_EQ(1, prefs()->GetInteger(prefs::kPasswordBubbleNopesCount));
+}
+
+TEST_F(PasswordBubbleExperimentTest, Probability) {
+  SetupProbabilityExperiment();
+
+  EXPECT_TRUE(password_bubble_experiment::ShouldShowBubble(prefs()));
+  // Don't save password enough times.
+  for (int i = 0; i < kProbabilityHistory; ++i) {
+    password_manager::metrics_util::UIDismissalReason reason = i % 2 ?
+        password_manager::metrics_util::NO_DIRECT_INTERACTION :
+        password_manager::metrics_util::CLICKED_NOPE;
+    password_bubble_experiment::RecordBubbleClosed(prefs(), reason);
+  }
+  EXPECT_FALSE(password_bubble_experiment::ShouldShowBubble(prefs()));
+
+  // Save password enough times.
+  for (int i = 0; i < kProbabilityHistory; ++i) {
+    password_bubble_experiment::RecordBubbleClosed(
+        prefs(),
+        password_manager::metrics_util::CLICKED_SAVE);
+  }
+  EXPECT_TRUE(password_bubble_experiment::ShouldShowBubble(prefs()));
+}
diff --git a/chrome/chrome_browser_ui.gypi b/chrome/chrome_browser_ui.gypi
index c1730a2..81c6d34 100644
--- a/chrome/chrome_browser_ui.gypi
+++ b/chrome/chrome_browser_ui.gypi
@@ -804,6 +804,8 @@
       'browser/ui/passwords/manage_passwords_icon.h',
       'browser/ui/passwords/manage_passwords_ui_controller.cc',
       'browser/ui/passwords/manage_passwords_ui_controller.h',
+      'browser/ui/passwords/password_bubble_experiment.cc',
+      'browser/ui/passwords/password_bubble_experiment.h',
       'browser/ui/passwords/password_manager_presenter.cc',
       'browser/ui/passwords/password_manager_presenter.h',
       'browser/ui/passwords/password_ui_view.h',
diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi
index f774021..a71b5f7 100644
--- a/chrome/chrome_tests_unit.gypi
+++ b/chrome/chrome_tests_unit.gypi
@@ -1145,6 +1145,7 @@
       'browser/ui/passwords/manage_passwords_bubble_model_unittest.cc',
       'browser/ui/passwords/manage_passwords_icon_mock.cc',
       'browser/ui/passwords/manage_passwords_ui_controller_unittest.cc',
+      'browser/ui/passwords/password_bubble_experiment_unittest.cc',
       'browser/ui/passwords/password_manager_presenter_unittest.cc',
       'browser/ui/search/instant_page_unittest.cc',
       'browser/ui/search/instant_search_prerenderer_unittest.cc',
diff --git a/chrome/common/pref_names.cc b/chrome/common/pref_names.cc
index 2c2d7d4..fef4477 100644
--- a/chrome/common/pref_names.cc
+++ b/chrome/common/pref_names.cc
@@ -2279,4 +2279,14 @@ const char kBrowserAddPersonEnabled[] = "profile.add_person_enabled";
 // A dictionary that maps user id to hardlock state.
 const char kEasyUnlockHardlockState[] = "easy_unlock.hardlock_state";
 
+// The beginning of time span when we count user's "Nope" for the password
+// bubble.
+const char kPasswordBubbleTimeStamp[] = "password_bubble.timestamp";
+
+// The count of user's "Nope" for the password bubble.
+const char kPasswordBubbleNopesCount[] = "password_bubble.nopes";
+
+// Last user's interaction with the password bubble.
+const char kPasswordBubbleLastInteractions[] = "password_bubble.interactions";
+
 }  // namespace prefs
diff --git a/chrome/common/pref_names.h b/chrome/common/pref_names.h
index 01a0089..214a3b5 100644
--- a/chrome/common/pref_names.h
+++ b/chrome/common/pref_names.h
@@ -805,6 +805,10 @@ extern const char kBrowserAddPersonEnabled[];
 
 extern const char kEasyUnlockHardlockState[];
 
+extern const char kPasswordBubbleTimeStamp[];
+extern const char kPasswordBubbleNopesCount[];
+extern const char kPasswordBubbleLastInteractions[];
+
 }  // namespace prefs
 
 #endif  // CHROME_COMMON_PREF_NAMES_H_
-- 
cgit v1.1