blob: d5953f1a7ef2cd17842c57c9727741b24f4b37b5 [file] [log] [blame]
// Copyright 2018 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/hats/hats_service.h"
#include <memory>
#include <utility>
#include "base/feature_list.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/util/values/values_util.h"
#include "base/values.h"
#include "base/version.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/prefs/incognito_mode_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/metrics_services_manager/metrics_services_manager.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "net/base/network_change_notifier.h"
constexpr char kHatsSurveyTriggerTesting[] = "testing";
constexpr char kHatsSurveyTriggerPrivacySandbox[] = "privacy-sandbox";
constexpr char kHatsSurveyTriggerSettings[] = "settings";
constexpr char kHatsSurveyTriggerSettingsPrivacy[] = "settings-privacy";
constexpr char kHatsSurveyTriggerNtpModules[] = "ntp-modules";
constexpr char kHatsNextSurveyTriggerIDTesting[] =
"zishSVViB0kPN8UwQ150VGjBKuBP";
constexpr char kHatsShouldShowSurveyReasonHistogram[] =
"Feedback.HappinessTrackingSurvey.ShouldShowSurveyReason";
namespace {
constexpr char kHatsSurveyProbability[] = "probability";
constexpr char kHatsSurveyEnSiteID[] = "en_site_id";
constexpr double kHatsSurveyProbabilityDefault = 0;
// TODO(crbug.com/1160661): When the minimum time between any survey, and the
// minimum time between a specific survey, are the same, the logic supporting
// the latter check is superfluous.
constexpr base::TimeDelta kMinimumTimeBetweenSurveyStarts =
base::TimeDelta::FromDays(180);
constexpr base::TimeDelta kMinimumTimeBetweenAnySurveyStarts =
base::TimeDelta::FromDays(180);
constexpr base::TimeDelta kMinimumTimeBetweenSurveyChecks =
base::TimeDelta::FromDays(1);
constexpr base::TimeDelta kMinimumProfileAge = base::TimeDelta::FromDays(30);
// Preferences Data Model
// The kHatsSurveyMetadata pref points to a dictionary.
// The valid keys and value types for this dictionary are as follows:
// [trigger].last_major_version ---> Integer
// [trigger].last_survey_started_time ---> Time
// [trigger].is_survey_full ---> Bool
// [trigger].last_survey_check_time ---> Time
// any_last_survey_started_time ---> Time
std::string GetMajorVersionPath(const std::string& trigger) {
return trigger + ".last_major_version";
}
std::string GetLastSurveyStartedTime(const std::string& trigger) {
return trigger + ".last_survey_started_time";
}
std::string GetIsSurveyFull(const std::string& trigger) {
return trigger + ".is_survey_full";
}
std::string GetLastSurveyCheckTime(const std::string& trigger) {
return trigger + ".last_survey_check_time";
}
constexpr char kAnyLastSurveyStartedTimePath[] = "any_last_survey_started_time";
std::vector<HatsService::SurveyConfig> GetSurveyConfigs() {
std::vector<HatsService::SurveyConfig> survey_configs;
// Dev tools surveys.
survey_configs.emplace_back(&features::kHaTSDesktopDevToolsIssuesCOEP,
"devtools-issues-coep",
"1DbEs89FS0ugnJ3q1cK0Nx6T99yT");
survey_configs.emplace_back(&features::kHaTSDesktopDevToolsIssuesMixedContent,
"devtools-issues-mixed-content",
"BhCYpUmyf0ugnJ3q1cK0VtxCftzo");
survey_configs.emplace_back(
&features::
kHappinessTrackingSurveysForDesktopDevToolsIssuesCookiesSameSite,
"devtools-issues-cookies-samesite", "w9JqqpmEr0ugnJ3q1cK0NezVg4iK");
survey_configs.emplace_back(&features::kHaTSDesktopDevToolsIssuesHeavyAd,
"devtools-issues-heavy-ad",
"bAeiT5J4P0ugnJ3q1cK0Ra6jg7s8");
survey_configs.emplace_back(&features::kHaTSDesktopDevToolsIssuesCSP,
"devtools-issues-csp",
"c9fjDmwjb0ugnJ3q1cK0USeAJJ9C");
survey_configs.emplace_back(&features::kHaTSDesktopDevToolsLayoutPanel,
"devtools-layout-panel",
"hhoMFLFq70ugnJ3q1cK0XYpqkErh");
// Settings surveys.
survey_configs.emplace_back(
&features::kHappinessTrackingSurveysForDesktopSettings,
kHatsSurveyTriggerSettings);
survey_configs.emplace_back(
&features::kHappinessTrackingSurveysForDesktopSettingsPrivacy,
kHatsSurveyTriggerSettingsPrivacy,
/*presupplied_trigger_id=*/base::nullopt,
std::vector<std::string>{"3P cookies blocked",
"Privacy Sandbox enabled"});
survey_configs.emplace_back(
&features::kHappinessTrackingSurveysForDesktopPrivacySandbox,
kHatsSurveyTriggerPrivacySandbox,
/*presupplied_trigger_id=*/base::nullopt,
std::vector<std::string>{"3P cookies blocked",
"Privacy Sandbox enabled"});
// NTP modules survey.
survey_configs.emplace_back(
&features::kHappinessTrackingSurveysForDesktopNtpModules,
kHatsSurveyTriggerNtpModules);
return survey_configs;
}
} // namespace
HatsService::SurveyConfig::SurveyConfig(
const base::Feature* feature,
const std::string& trigger,
const base::Optional<std::string>& presupplied_trigger_id,
const std::vector<std::string>& product_specific_data_fields)
: trigger(trigger),
product_specific_data_fields(product_specific_data_fields) {
DCHECK(product_specific_data_fields.size() <= 3)
<< "A maximum of 3 survey specific data fields is supported";
enabled = base::FeatureList::IsEnabled(*feature);
if (!enabled)
return;
probability = base::FeatureParam<double>(feature, kHatsSurveyProbability,
kHatsSurveyProbabilityDefault)
.Get();
// The trigger_id may be provided through the associated feature parameter or
// may have been included in the source code. The latter is required to enable
// multiple surveys with a single finch group, as a limitation with finch
// prevents duplicate param names even for different features within a group.
// The feature parameter name is "en_site_id" for legacy reasons, as this
// was the HaTS v1 equivalent of a trigger ID in HaTS Next.
trigger_id = presupplied_trigger_id ? *presupplied_trigger_id
: base::FeatureParam<std::string>(
feature, kHatsSurveyEnSiteID, "")
.Get();
user_prompted =
base::FeatureParam<bool>(feature, "user_prompted", false).Get();
}
HatsService::SurveyConfig::SurveyConfig() = default;
HatsService::SurveyConfig::SurveyConfig(const SurveyConfig&) = default;
HatsService::SurveyConfig::~SurveyConfig() = default;
HatsService::SurveyMetadata::SurveyMetadata() = default;
HatsService::SurveyMetadata::~SurveyMetadata() = default;
HatsService::DelayedSurveyTask::DelayedSurveyTask(
HatsService* hats_service,
const std::string& trigger,
content::WebContents* web_contents,
const std::map<std::string, bool>& product_specific_data)
: hats_service_(hats_service),
trigger_(trigger),
product_specific_data_(product_specific_data) {
Observe(web_contents);
}
HatsService::DelayedSurveyTask::~DelayedSurveyTask() = default;
base::WeakPtr<HatsService::DelayedSurveyTask>
HatsService::DelayedSurveyTask::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void HatsService::DelayedSurveyTask::Launch() {
hats_service_->LaunchSurveyForWebContents(trigger_, web_contents(),
product_specific_data_);
hats_service_->RemoveTask(*this);
}
void HatsService::DelayedSurveyTask::WebContentsDestroyed() {
hats_service_->RemoveTask(*this);
}
struct SurveyIdentifiers {
const base::Feature* feature;
const char* trigger;
const char* trigger_id;
std::vector<std::string> product_specific_data_fields;
};
HatsService::HatsService(Profile* profile) : profile_(profile) {
auto surveys = GetSurveyConfigs();
// Filter down to active surveys configs and store them in a map for faster
// access. Triggers within the browser may attempt to show surveys regardless
// of whether the feature is enabled, so checking whether a particular survey
// is enabled should be fast.
for (const SurveyConfig& survey : surveys) {
if (!survey.enabled)
continue;
survey_configs_by_triggers_.emplace(survey.trigger, survey);
}
// Ensure a default survey exists (for testing and demo purpose).
SurveyConfig default_survey;
default_survey.enabled = true;
default_survey.probability = 1.0f;
default_survey.trigger = kHatsSurveyTriggerTesting;
default_survey.trigger_id = kHatsNextSurveyTriggerIDTesting;
default_survey.product_specific_data_fields = {"Test Field 1", "Test Field 2",
"Test Field 3"};
survey_configs_by_triggers_.emplace(kHatsSurveyTriggerTesting,
default_survey);
}
HatsService::~HatsService() = default;
// static
void HatsService::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterDictionaryPref(
prefs::kHatsSurveyMetadata,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
}
void HatsService::LaunchSurvey(
const std::string& trigger,
base::OnceClosure success_callback,
base::OnceClosure failure_callback,
const std::map<std::string, bool>& product_specific_data) {
if (!ShouldShowSurvey(trigger)) {
std::move(failure_callback).Run();
return;
}
LaunchSurveyForBrowser(chrome::FindLastActiveWithProfile(profile_), trigger,
std::move(success_callback),
std::move(failure_callback), product_specific_data);
}
bool HatsService::LaunchDelayedSurvey(
const std::string& trigger,
int timeout_ms,
const std::map<std::string, bool>& product_specific_data) {
return base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&HatsService::LaunchSurvey, weak_ptr_factory_.GetWeakPtr(),
trigger, base::DoNothing::Once(), base::DoNothing::Once(),
product_specific_data),
base::TimeDelta::FromMilliseconds(timeout_ms));
}
bool HatsService::LaunchDelayedSurveyForWebContents(
const std::string& trigger,
content::WebContents* web_contents,
int timeout_ms,
const std::map<std::string, bool>& product_specific_data) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!web_contents)
return false;
auto result = pending_tasks_.emplace(this, trigger, web_contents,
product_specific_data);
if (!result.second)
return false;
auto success = base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(
&HatsService::DelayedSurveyTask::Launch,
const_cast<HatsService::DelayedSurveyTask&>(*(result.first))
.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(timeout_ms));
if (!success) {
pending_tasks_.erase(result.first);
}
return success;
}
void HatsService::RecordSurveyAsShown(std::string trigger_id) {
// Record the trigger associated with the trigger_id. This is recorded instead
// of the trigger ID itself, as the ID is specific to individual survey
// versions. There should be a cooldown before a user is prompted to take a
// survey from the same trigger, regardless of whether the survey was updated.
auto trigger_survey_config = std::find_if(
survey_configs_by_triggers_.begin(), survey_configs_by_triggers_.end(),
[&](const std::pair<std::string, SurveyConfig>& pair) {
return pair.second.trigger_id == trigger_id;
});
DCHECK(trigger_survey_config != survey_configs_by_triggers_.end());
std::string trigger = trigger_survey_config->first;
UMA_HISTOGRAM_ENUMERATION(kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kYes);
DictionaryPrefUpdate update(profile_->GetPrefs(), prefs::kHatsSurveyMetadata);
base::DictionaryValue* pref_data = update.Get();
pref_data->SetIntPath(GetMajorVersionPath(trigger),
version_info::GetVersion().components()[0]);
pref_data->SetPath(GetLastSurveyStartedTime(trigger),
util::TimeToValue(base::Time::Now()));
pref_data->SetPath(kAnyLastSurveyStartedTimePath,
util::TimeToValue(base::Time::Now()));
}
void HatsService::HatsNextDialogClosed() {
hats_next_dialog_exists_ = false;
}
void HatsService::SetSurveyMetadataForTesting(
const HatsService::SurveyMetadata& metadata) {
const std::string& trigger = kHatsSurveyTriggerSettings;
DictionaryPrefUpdate update(profile_->GetPrefs(), prefs::kHatsSurveyMetadata);
base::DictionaryValue* pref_data = update.Get();
if (!metadata.last_major_version.has_value() &&
!metadata.last_survey_started_time.has_value() &&
!metadata.is_survey_full.has_value() &&
!metadata.last_survey_check_time.has_value()) {
pref_data->RemovePath(trigger);
}
if (metadata.last_major_version.has_value()) {
pref_data->SetIntPath(GetMajorVersionPath(trigger),
*metadata.last_major_version);
} else {
pref_data->RemovePath(GetMajorVersionPath(trigger));
}
if (metadata.last_survey_started_time.has_value()) {
pref_data->SetPath(GetLastSurveyStartedTime(trigger),
util::TimeToValue(*metadata.last_survey_started_time));
} else {
pref_data->RemovePath(GetLastSurveyStartedTime(trigger));
}
if (metadata.any_last_survey_started_time.has_value()) {
pref_data->SetPath(
kAnyLastSurveyStartedTimePath,
util::TimeToValue(*metadata.any_last_survey_started_time));
} else {
pref_data->RemovePath(kAnyLastSurveyStartedTimePath);
}
if (metadata.is_survey_full.has_value()) {
pref_data->SetBoolPath(GetIsSurveyFull(trigger), *metadata.is_survey_full);
} else {
pref_data->RemovePath(GetIsSurveyFull(trigger));
}
if (metadata.last_survey_check_time.has_value()) {
pref_data->SetPath(GetLastSurveyCheckTime(trigger),
util::TimeToValue(*metadata.last_survey_check_time));
} else {
pref_data->RemovePath(GetLastSurveyCheckTime(trigger));
}
}
void HatsService::GetSurveyMetadataForTesting(
HatsService::SurveyMetadata* metadata) const {
const std::string& trigger = kHatsSurveyTriggerSettings;
DictionaryPrefUpdate update(profile_->GetPrefs(), prefs::kHatsSurveyMetadata);
base::DictionaryValue* pref_data = update.Get();
base::Optional<int> last_major_version =
pref_data->FindIntPath(GetMajorVersionPath(trigger));
if (last_major_version.has_value())
metadata->last_major_version = last_major_version;
base::Optional<base::Time> last_survey_started_time =
util::ValueToTime(pref_data->FindPath(GetLastSurveyStartedTime(trigger)));
if (last_survey_started_time.has_value())
metadata->last_survey_started_time = last_survey_started_time;
base::Optional<base::Time> any_last_survey_started_time =
util::ValueToTime(pref_data->FindPath(kAnyLastSurveyStartedTimePath));
if (any_last_survey_started_time.has_value())
metadata->any_last_survey_started_time = any_last_survey_started_time;
base::Optional<bool> is_survey_full =
pref_data->FindBoolPath(GetIsSurveyFull(trigger));
if (is_survey_full.has_value())
metadata->is_survey_full = is_survey_full;
base::Optional<base::Time> last_survey_check_time =
util::ValueToTime(pref_data->FindPath(GetLastSurveyCheckTime(trigger)));
if (last_survey_check_time.has_value())
metadata->last_survey_check_time = last_survey_check_time;
}
void HatsService::RemoveTask(const DelayedSurveyTask& task) {
pending_tasks_.erase(task);
}
bool HatsService::HasPendingTasks() {
return !pending_tasks_.empty();
}
void HatsService::LaunchSurveyForWebContents(
const std::string& trigger,
content::WebContents* web_contents,
const std::map<std::string, bool>& product_specific_data) {
if (ShouldShowSurvey(trigger) && web_contents &&
web_contents->GetVisibility() == content::Visibility::VISIBLE) {
LaunchSurveyForBrowser(chrome::FindBrowserWithWebContents(web_contents),
trigger, base::DoNothing(), base::DoNothing(),
product_specific_data);
}
}
void HatsService::LaunchSurveyForBrowser(
Browser* browser,
const std::string& trigger,
base::OnceClosure success_callback,
base::OnceClosure failure_callback,
const std::map<std::string, bool>& product_specific_data) {
if (!browser ||
(!browser->is_type_normal() && !browser->is_type_devtools()) ||
!profiles::IsRegularOrGuestSession(browser)) {
// Never show HaTS bubble for Incognito mode.
UMA_HISTOGRAM_ENUMERATION(kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoNotRegularBrowser);
std::move(failure_callback).Run();
return;
}
if (IncognitoModePrefs::GetAvailability(profile_->GetPrefs()) ==
IncognitoModePrefs::DISABLED) {
// Incognito mode needs to be enabled to create an off-the-record profile
// for HaTS dialog.
UMA_HISTOGRAM_ENUMERATION(kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoIncognitoDisabled);
std::move(failure_callback).Run();
return;
}
// Checking survey's status could be costly due to a network request, so
// we check it at the last.
CheckSurveyStatusAndMaybeShow(browser, trigger, std::move(success_callback),
std::move(failure_callback),
product_specific_data);
}
bool HatsService::CanShowSurvey(const std::string& trigger) const {
// Do not show if a survey dialog already exists.
if (hats_next_dialog_exists_) {
UMA_HISTOGRAM_ENUMERATION(
kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoSurveyAlreadyInProgress);
return false;
}
// Survey should not be loaded if the corresponding survey config is
// unavailable.
const auto config_iterator = survey_configs_by_triggers_.find(trigger);
if (config_iterator == survey_configs_by_triggers_.end()) {
UMA_HISTOGRAM_ENUMERATION(
kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoTriggerStringMismatch);
return false;
}
const SurveyConfig config = config_iterator->second;
if (base::FeatureList::IsEnabled(
features::kHappinessTrackingSurveysForDesktopDemo)) {
// Always show the survey in demo mode.
return true;
}
// Survey can not be loaded and shown if there is no network connection.
if (net::NetworkChangeNotifier::IsOffline()) {
UMA_HISTOGRAM_ENUMERATION(kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoOffline);
return false;
}
bool consent_given =
g_browser_process->GetMetricsServicesManager()->IsMetricsConsentGiven();
if (!consent_given)
return false;
if (profile_->GetLastSessionExitType() == Profile::EXIT_CRASHED) {
UMA_HISTOGRAM_ENUMERATION(kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoLastSessionCrashed);
return false;
}
const base::DictionaryValue* pref_data =
profile_->GetPrefs()->GetDictionary(prefs::kHatsSurveyMetadata);
base::Optional<int> last_major_version =
pref_data->FindIntPath(GetMajorVersionPath(trigger));
if (last_major_version.has_value() &&
static_cast<uint32_t>(*last_major_version) ==
version_info::GetVersion().components()[0]) {
UMA_HISTOGRAM_ENUMERATION(
kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoReceivedSurveyInCurrentMilestone);
return false;
}
base::Time now = base::Time::Now();
if (!config.user_prompted) {
if ((now - profile_->GetCreationTime()) < kMinimumProfileAge) {
UMA_HISTOGRAM_ENUMERATION(kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoProfileTooNew);
return false;
}
base::Optional<base::Time> last_survey_started_time = util::ValueToTime(
pref_data->FindPath(GetLastSurveyStartedTime(trigger)));
if (last_survey_started_time.has_value()) {
base::TimeDelta elapsed_time_since_last_start =
now - *last_survey_started_time;
if (elapsed_time_since_last_start < kMinimumTimeBetweenSurveyStarts) {
UMA_HISTOGRAM_ENUMERATION(
kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoLastSurveyTooRecent);
return false;
}
}
// The time any survey was started will always be equal or more recent than
// the time a particular survey was started, so it is checked afterwards to
// improve UMA logging.
base::Optional<base::Time> last_any_started_time =
util::ValueToTime(pref_data->FindPath(kAnyLastSurveyStartedTimePath));
if (last_any_started_time.has_value()) {
base::TimeDelta elapsed_time_any_started = now - *last_any_started_time;
if (elapsed_time_any_started < kMinimumTimeBetweenAnySurveyStarts) {
UMA_HISTOGRAM_ENUMERATION(
kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoAnyLastSurveyTooRecent);
return false;
}
}
}
// If an attempt to check with the HaTS servers whether a survey should be
// delivered was made too recently, another survey cannot be shown.
base::Optional<base::Time> last_survey_check_time =
util::ValueToTime(pref_data->FindPath(GetLastSurveyCheckTime(trigger)));
if (last_survey_check_time.has_value()) {
base::TimeDelta elapsed_time_since_last_check =
base::Time::Now() - *last_survey_check_time;
if (elapsed_time_since_last_check < kMinimumTimeBetweenSurveyChecks)
return false;
}
return true;
}
bool HatsService::ShouldShowSurvey(const std::string& trigger) const {
if (!CanShowSurvey(trigger))
return false;
auto probability = survey_configs_by_triggers_.at(trigger).probability;
bool should_show_survey = base::RandDouble() < probability;
if (!should_show_survey) {
UMA_HISTOGRAM_ENUMERATION(
kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoBelowProbabilityLimit);
}
return should_show_survey;
}
void HatsService::CheckSurveyStatusAndMaybeShow(
Browser* browser,
const std::string& trigger,
base::OnceClosure success_callback,
base::OnceClosure failure_callback,
const std::map<std::string, bool>& product_specific_data) {
// Check the survey status in profile first.
// We record the survey's over capacity information in user profile to avoid
// duplicated checks since the survey won't change once it is full.
const base::DictionaryValue* pref_data =
profile_->GetPrefs()->GetDictionary(prefs::kHatsSurveyMetadata);
base::Optional<int> is_full =
pref_data->FindBoolPath(GetIsSurveyFull(trigger));
if (is_full.has_value() && is_full) {
std::move(failure_callback).Run();
return;
}
DCHECK(survey_configs_by_triggers_.find(trigger) !=
survey_configs_by_triggers_.end());
auto survey_config = survey_configs_by_triggers_[trigger];
// Check that the |product_specific_data| matches the fields for this trigger.
// If fields are set for a trigger, they must be provided.
DCHECK_EQ(product_specific_data.size(),
survey_config.product_specific_data_fields.size());
for (auto field_value : product_specific_data) {
DCHECK(std::find(survey_config.product_specific_data_fields.begin(),
survey_config.product_specific_data_fields.end(),
field_value.first) !=
survey_config.product_specific_data_fields.end());
}
// As soon as the HaTS Next dialog is created it will attempt to contact
// the HaTS servers to check for a survey.
DictionaryPrefUpdate update(profile_->GetPrefs(), prefs::kHatsSurveyMetadata);
update->SetPath(GetLastSurveyCheckTime(trigger),
util::TimeToValue(base::Time::Now()));
DCHECK(!hats_next_dialog_exists_);
browser->window()->ShowHatsDialog(
survey_configs_by_triggers_[trigger].trigger_id,
std::move(success_callback), std::move(failure_callback),
product_specific_data);
hats_next_dialog_exists_ = true;
}