blob: b08e27dbce2d92ea7f23c3e601e32273e64e9f7e [file] [log] [blame] [edit]
// Copyright 2023 The Chromium Authors
// 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_desktop.h"
#include <algorithm>
#include <memory>
#include <utility>
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/task/sequenced_task_runner.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/sessions/exit_type_service.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/hats/hats_service.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/metrics_services_manager/metrics_services_manager.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.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/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "net/base/network_change_notifier.h"
constexpr char kHatsShouldShowSurveyReasonHistogram[] =
"Feedback.HappinessTrackingSurvey.ShouldShowSurveyReason";
namespace {
// TODO(crbug.com/40162245): 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::Days(180);
constexpr base::TimeDelta kMinimumTimeBetweenAnySurveyStarts = base::Days(180);
constexpr base::TimeDelta kMinimumTimeBetweenSurveyChecks = base::Days(1);
constexpr base::TimeDelta kMinimumProfileAge = base::Days(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";
}
// The last time a survey without the cool down period override was triggered.
constexpr char kAnyLastSurveyStartedTimePath[] = "any_last_survey_started_time";
// The last time a survey with the cool down period override was triggered.
constexpr char kAnyLastSurveyWithCooldownOverrideStartedTimePath[] =
"any_last_survey_with_cooldown_override_started_time";
// Checks if the survey can be shown to the user subject to the cooldown period
// restrictions. The default cooldown period is equal to the
// `kMinimumTimeBetweenAnySurveyStarts`. Some configs in special cases can
// override the cool down period. This should be used only in situations such as
// early dogfood launches. Surveys with and without cooldown override are
// handled separately. Any survey with cooldown override won't affect the survey
// without cooldown override and vice versa. For example, if a survey A (with
// overridden cool down period) is prompted at T0 and a survey B (with default
// cool down period) is prompted at T1, then at T2 the elapsed time for the
// survey A will be T2 - T0 and for the survey B - T2 - T1.
bool DoesCooldownApply(Profile* profile,
PrefService* prefs,
const hats::SurveyConfig& config) {
const std::optional<base::TimeDelta> cooldown_override =
config.GetCooldownPeriodOverride(profile);
const bool has_cooldown_override = cooldown_override.has_value();
const base::Value::Dict& pref_data =
prefs->GetDict(prefs::kHatsSurveyMetadata);
const std::optional<base::Time> last_started_time = base::ValueToTime(
pref_data.Find(has_cooldown_override
? kAnyLastSurveyWithCooldownOverrideStartedTimePath
: kAnyLastSurveyStartedTimePath));
// The cooldown period has not started because no survey has been launched
// before.
if (!last_started_time.has_value()) {
return false;
}
const base::TimeDelta elapsed_time_since_started =
base::Time::Now() - *last_started_time;
const base::TimeDelta cooldown_period =
has_cooldown_override ? *cooldown_override
: kMinimumTimeBetweenAnySurveyStarts;
// If a user has received any HaTS survey too recently, they are also
// ineligible.
if (elapsed_time_since_started < cooldown_period) {
return true;
}
return false;
}
} // namespace
HatsServiceDesktop::DelayedSurveyTask::DelayedSurveyTask(
HatsServiceDesktop* hats_service,
std::string trigger,
content::WebContents* web_contents,
const SurveyBitsData& product_specific_bits_data,
const SurveyStringData& product_specific_string_data,
NavigationBehavior navigation_behavior,
base::OnceClosure success_callback,
base::OnceClosure failure_callback,
std::optional<std::string_view> supplied_trigger_id)
: hats_service_(hats_service),
trigger_(trigger),
product_specific_bits_data_(product_specific_bits_data),
product_specific_string_data_(product_specific_string_data),
navigation_behavior_(navigation_behavior),
success_callback_(std::move(success_callback)),
failure_callback_(std::move(failure_callback)),
supplied_trigger_id_(std::move(supplied_trigger_id)) {
Observe(web_contents);
}
HatsServiceDesktop::DelayedSurveyTask::~DelayedSurveyTask() = default;
base::WeakPtr<HatsServiceDesktop::DelayedSurveyTask>
HatsServiceDesktop::DelayedSurveyTask::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void HatsServiceDesktop::DelayedSurveyTask::Launch() {
// web_contents() might be null here despite removing the task in
// WebContentsDestroyed.
if (web_contents() &&
web_contents()->GetVisibility() == content::Visibility::VISIBLE) {
hats_service_->LaunchSurveyForWebContents(
trigger_, web_contents(), product_specific_bits_data_,
product_specific_string_data_, std::move(success_callback_),
std::move(failure_callback_), supplied_trigger_id_, SurveyOptions());
}
hats_service_->RemoveTask(*this);
}
void HatsServiceDesktop::DelayedSurveyTask::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
if (hats_service_->IsNavigationAllowed(navigation_handle,
navigation_behavior_)) {
return;
}
if (!failure_callback_.is_null()) {
std::move(failure_callback_).Run();
}
hats_service_->RemoveTask(*this);
}
void HatsServiceDesktop::DelayedSurveyTask::WebContentsDestroyed() {
if (!failure_callback_.is_null()) {
std::move(failure_callback_).Run();
}
hats_service_->RemoveTask(*this);
}
HatsServiceDesktop::HatsServiceDesktop(Profile* profile)
: HatsService(profile) {}
HatsServiceDesktop::~HatsServiceDesktop() = default;
// static
void HatsServiceDesktop::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterDictionaryPref(
prefs::kHatsSurveyMetadata,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
}
void HatsServiceDesktop::LaunchSurvey(
const std::string& trigger,
base::OnceClosure success_callback,
base::OnceClosure failure_callback,
const SurveyBitsData& product_specific_bits_data,
const SurveyStringData& product_specific_string_data,
const std::optional<std::string>& supplied_trigger_id,
const SurveyOptions& survey_options) {
CHECK(!survey_options.custom_invitation.has_value() &&
!survey_options.message_identifier.has_value())
<< "Custom invitation strings and message types are not supported on "
"desktop.";
if (!ShouldShowSurvey(trigger)) {
if (!failure_callback.is_null()) {
std::move(failure_callback).Run();
}
return;
}
LaunchSurveyForBrowser(chrome::FindLastActiveWithProfile(profile()), trigger,
std::move(success_callback),
std::move(failure_callback),
product_specific_bits_data,
product_specific_string_data, supplied_trigger_id);
}
void HatsServiceDesktop::LaunchSurveyForWebContents(
const std::string& trigger,
content::WebContents* web_contents,
const SurveyBitsData& product_specific_bits_data,
const SurveyStringData& product_specific_string_data,
base::OnceClosure success_callback,
base::OnceClosure failure_callback,
const std::optional<std::string>& supplied_trigger_id,
const SurveyOptions& survey_options) {
CHECK(!survey_options.custom_invitation.has_value() &&
!survey_options.message_identifier.has_value())
<< "Custom invitation strings and message types are not supported on "
"desktop.";
if (ShouldShowSurvey(trigger) && web_contents &&
web_contents->GetVisibility() == content::Visibility::VISIBLE) {
LaunchSurveyForBrowser(chrome::FindBrowserWithTab(web_contents), trigger,
std::move(success_callback),
std::move(failure_callback),
product_specific_bits_data,
product_specific_string_data, supplied_trigger_id);
}
}
bool HatsServiceDesktop::LaunchDelayedSurvey(
const std::string& trigger,
int timeout_ms,
const SurveyBitsData& product_specific_bits_data,
const SurveyStringData& product_specific_string_data) {
void (HatsServiceDesktop::*launch_survey)(
const std::string&, base::OnceClosure, base::OnceClosure,
const SurveyBitsData&, const SurveyStringData&,
const std::optional<std::string>&, const SurveyOptions&) =
&HatsServiceDesktop::LaunchSurvey;
return base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(launch_survey, weak_ptr_factory_.GetWeakPtr(), trigger,
base::DoNothing(), base::DoNothing(),
product_specific_bits_data, product_specific_string_data,
std::nullopt, SurveyOptions()),
base::Milliseconds(timeout_ms));
}
bool HatsServiceDesktop::LaunchDelayedSurveyForWebContents(
const std::string& trigger,
content::WebContents* web_contents,
int timeout_ms,
const SurveyBitsData& product_specific_bits_data,
const SurveyStringData& product_specific_string_data,
NavigationBehavior navigation_behavior,
base::OnceClosure success_callback,
base::OnceClosure failure_callback,
const std::optional<std::string>& supplied_trigger_id,
const SurveyOptions& survey_options) {
CHECK(!survey_options.custom_invitation.has_value() &&
!survey_options.message_identifier.has_value())
<< "Custom invitation strings and message types are not supported on "
"desktop.";
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (survey_configs_by_triggers_.find(trigger) ==
survey_configs_by_triggers_.end()) {
// Survey configuration is not available.
if (!failure_callback.is_null()) {
std::move(failure_callback).Run();
}
return false;
}
if (!web_contents) {
if (!failure_callback.is_null()) {
std::move(failure_callback).Run();
}
return false;
}
auto result = pending_tasks_.emplace(
this, trigger, web_contents, product_specific_bits_data,
product_specific_string_data, navigation_behavior,
std::move(success_callback), std::move(failure_callback),
supplied_trigger_id);
if (!result.second) {
return false;
}
auto success =
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&HatsServiceDesktop::DelayedSurveyTask::Launch,
const_cast<HatsServiceDesktop::DelayedSurveyTask&>(
*(result.first))
.GetWeakPtr()),
base::Milliseconds(timeout_ms));
if (!success) {
pending_tasks_.erase(result.first);
}
return success;
}
void HatsServiceDesktop::SetSurveyMetadataForTesting(
const HatsService::SurveyMetadata& metadata) {
const std::string& trigger = kHatsSurveyTriggerSettings;
ScopedDictPrefUpdate update(GetPrefsForHatsMetadata(),
prefs::kHatsSurveyMetadata);
base::Value::Dict& 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.RemoveByDottedPath(trigger);
}
if (metadata.last_major_version.has_value()) {
pref_data.SetByDottedPath(GetMajorVersionPath(trigger),
*metadata.last_major_version);
} else {
pref_data.RemoveByDottedPath(GetMajorVersionPath(trigger));
}
if (metadata.last_survey_started_time.has_value()) {
pref_data.SetByDottedPath(
GetLastSurveyStartedTime(trigger),
base::TimeToValue(*metadata.last_survey_started_time));
} else {
pref_data.RemoveByDottedPath(GetLastSurveyStartedTime(trigger));
}
if (metadata.any_last_survey_started_time.has_value()) {
pref_data.SetByDottedPath(
kAnyLastSurveyStartedTimePath,
base::TimeToValue(*metadata.any_last_survey_started_time));
} else {
pref_data.RemoveByDottedPath(kAnyLastSurveyStartedTimePath);
}
if (metadata.any_last_survey_with_cooldown_override_started_time
.has_value()) {
pref_data.SetByDottedPath(
kAnyLastSurveyWithCooldownOverrideStartedTimePath,
base::TimeToValue(
*metadata.any_last_survey_with_cooldown_override_started_time));
} else {
pref_data.RemoveByDottedPath(
kAnyLastSurveyWithCooldownOverrideStartedTimePath);
}
if (metadata.is_survey_full.has_value()) {
pref_data.SetByDottedPath(GetIsSurveyFull(trigger),
*metadata.is_survey_full);
} else {
pref_data.RemoveByDottedPath(GetIsSurveyFull(trigger));
}
if (metadata.last_survey_check_time.has_value()) {
pref_data.SetByDottedPath(
GetLastSurveyCheckTime(trigger),
base::TimeToValue(*metadata.last_survey_check_time));
} else {
pref_data.RemoveByDottedPath(GetLastSurveyCheckTime(trigger));
}
}
void HatsServiceDesktop::GetSurveyMetadataForTesting(
HatsService::SurveyMetadata* metadata) const {
const std::string& trigger = kHatsSurveyTriggerSettings;
ScopedDictPrefUpdate update(GetPrefsForHatsMetadata(),
prefs::kHatsSurveyMetadata);
base::Value::Dict& pref_data = update.Get();
std::optional<int> last_major_version =
pref_data.FindIntByDottedPath(GetMajorVersionPath(trigger));
if (last_major_version.has_value()) {
metadata->last_major_version = last_major_version;
}
std::optional<base::Time> last_survey_started_time = base::ValueToTime(
pref_data.FindByDottedPath(GetLastSurveyStartedTime(trigger)));
if (last_survey_started_time.has_value()) {
metadata->last_survey_started_time = last_survey_started_time;
}
std::optional<base::Time> any_last_survey_started_time = base::ValueToTime(
pref_data.FindByDottedPath(kAnyLastSurveyStartedTimePath));
if (any_last_survey_started_time.has_value()) {
metadata->any_last_survey_started_time = any_last_survey_started_time;
}
std::optional<base::Time>
any_last_survey_with_cooldown_override_started_time =
base::ValueToTime(pref_data.FindByDottedPath(
kAnyLastSurveyWithCooldownOverrideStartedTimePath));
if (any_last_survey_with_cooldown_override_started_time.has_value()) {
metadata->any_last_survey_with_cooldown_override_started_time =
any_last_survey_with_cooldown_override_started_time;
}
std::optional<bool> is_survey_full =
pref_data.FindBoolByDottedPath(GetIsSurveyFull(trigger));
if (is_survey_full.has_value()) {
metadata->is_survey_full = is_survey_full;
}
std::optional<base::Time> last_survey_check_time = base::ValueToTime(
pref_data.FindByDottedPath(GetLastSurveyCheckTime(trigger)));
if (last_survey_check_time.has_value()) {
metadata->last_survey_check_time = last_survey_check_time;
}
}
bool HatsServiceDesktop::HasPendingTasks() {
return !pending_tasks_.empty();
}
bool HatsServiceDesktop::CanShowSurvey(const std::string& trigger) const {
// 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 hats::SurveyConfig config = config_iterator->second;
// Do not show if a survey dialog already exists.
if (hats_next_dialog_exists_) {
UMA_HISTOGRAM_ENUMERATION(
kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoSurveyAlreadyInProgress);
return false;
}
// Always show the survey in demo mode. This check is duplicated in
// `CanShowAnySurvey()`, but because of the semantics of that function, must
// be included here.
if (base::FeatureList::IsEnabled(
features::kHappinessTrackingSurveysForDesktopDemo)) {
return true;
}
if (!CanShowAnySurvey(config.user_prompted)) {
return false;
}
if (DoesCooldownApply(profile(), GetPrefsForHatsMetadata(), config)) {
UMA_HISTOGRAM_ENUMERATION(
kHatsShouldShowSurveyReasonHistogram,
HatsServiceDesktop::ShouldShowSurveyReasons::kNoAnyLastSurveyTooRecent);
return false;
}
// 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;
}
const base::Value::Dict& pref_data =
GetPrefsForHatsMetadata()->GetDict(prefs::kHatsSurveyMetadata);
std::optional<int> last_major_version =
pref_data.FindIntByDottedPath(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;
}
if (!config.user_prompted) {
std::optional<base::Time> last_survey_started_time = base::ValueToTime(
pref_data.FindByDottedPath(GetLastSurveyStartedTime(trigger)));
if (last_survey_started_time.has_value()) {
const base::TimeDelta elapsed_time_since_last_start =
base::Time::Now() - *last_survey_started_time;
if (elapsed_time_since_last_start < kMinimumTimeBetweenSurveyStarts) {
UMA_HISTOGRAM_ENUMERATION(
kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoLastSurveyTooRecent);
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.
std::optional<base::Time> last_survey_check_time = base::ValueToTime(
pref_data.FindByDottedPath(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) {
UMA_HISTOGRAM_ENUMERATION(
kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoLastSurveyCheckTooRecent);
return false;
}
}
return true;
}
bool HatsServiceDesktop::CanShowAnySurvey(bool user_prompted) const {
// HaTS requires metrics consent to run. This is also how HaTS can be
// disabled by policy.
if (!g_browser_process->GetMetricsServicesManager() ||
!g_browser_process->GetMetricsServicesManager()
->IsMetricsConsentGiven()) {
return false;
}
// HaTs can also be disabled by policy if metrics consent is given.
if (!profile()->GetPrefs()->GetBoolean(
policy::policy_prefs::kFeedbackSurveysEnabled)) {
return false;
}
// Surveys can always be shown in Demo mode.
if (base::FeatureList::IsEnabled(
features::kHappinessTrackingSurveysForDesktopDemo)) {
return true;
}
// Do not show surveys if Chrome's last exit was a crash. This avoids
// biasing survey results unnecessarily.
if (ExitTypeService::GetLastSessionExitType(profile()) ==
ExitType::kCrashed) {
UMA_HISTOGRAM_ENUMERATION(kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoLastSessionCrashed);
return false;
}
// Some surveys may be "user prompted", which means the user has already
// been asked in context if they would like to take a survey (in a less
// confrontational manner than the standard HaTS prompt). The bar for
// whether a user is eligible is thus lower for these types of surveys.
if (user_prompted) {
return true;
}
// If the profile is too new, measured as the age of the profile
// directory, the user is ineligible.
base::Time now = base::Time::Now();
auto creation_time = profile()->GetOriginalProfile()->GetCreationTime();
if ((now - creation_time) < kMinimumProfileAge) {
UMA_HISTOGRAM_ENUMERATION(kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoProfileTooNew);
return false;
}
return true;
}
void HatsServiceDesktop::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::ranges::find(survey_configs_by_triggers_, trigger_id,
[](const SurveyConfigs::value_type& pair) {
return pair.second.trigger_id;
});
CHECK(trigger_survey_config != survey_configs_by_triggers_.end());
std::string trigger = trigger_survey_config->first;
UMA_HISTOGRAM_ENUMERATION(kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kYes);
ScopedDictPrefUpdate update(GetPrefsForHatsMetadata(),
prefs::kHatsSurveyMetadata);
base::Value::Dict& pref_data = update.Get();
pref_data.SetByDottedPath(
GetMajorVersionPath(trigger),
static_cast<int>(version_info::GetVersion().components()[0]));
pref_data.SetByDottedPath(GetLastSurveyStartedTime(trigger),
base::TimeToValue(base::Time::Now()));
pref_data.SetByDottedPath(
trigger_survey_config->second.IsCooldownOverrideEnabled(profile())
? kAnyLastSurveyWithCooldownOverrideStartedTimePath
: kAnyLastSurveyStartedTimePath,
base::TimeToValue(base::Time::Now()));
}
void HatsServiceDesktop::HatsNextDialogClosed() {
hats_next_dialog_exists_ = false;
}
PrefService* HatsServiceDesktop::GetPrefsForHatsMetadata() const {
// Make sure we persist HaTS metadata to the original profile, otherwise HaTS
// shown in OTR will not influence HaTS in the original profile.
return profile()->GetOriginalProfile()->GetPrefs();
}
void HatsServiceDesktop::RemoveTask(const DelayedSurveyTask& task) {
pending_tasks_.erase(task);
}
bool HatsServiceDesktop::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;
}
bool HatsServiceDesktop::IsRightBrowserType(
Browser* browser,
hats::SurveyConfig::RequestedBrowserType requested_browser_type) const {
if (!browser ||
(!browser->is_type_normal() && !browser->is_type_devtools())) {
return false;
}
switch (requested_browser_type) {
case hats::SurveyConfig::RequestedBrowserType::kRegular:
return profiles::IsRegularOrGuestSession(browser);
case hats::SurveyConfig::RequestedBrowserType::kIncognito:
return browser->profile()->IsIncognitoProfile();
}
}
void HatsServiceDesktop::LaunchSurveyForBrowser(
Browser* browser,
const std::string& trigger,
base::OnceClosure success_callback,
base::OnceClosure failure_callback,
const SurveyBitsData& product_specific_bits_data,
const SurveyStringData& product_specific_string_data,
const std::optional<std::string_view>& supplied_trigger_id) {
if (!browser || browser->IsAttemptingToCloseBrowser()) {
// Don't launch surveys during browser closure.
return;
}
CHECK(survey_configs_by_triggers_.find(trigger) !=
survey_configs_by_triggers_.end());
auto survey_config = survey_configs_by_triggers_[trigger];
if (!IsRightBrowserType(browser, survey_config.requested_browser_type)) {
UMA_HISTOGRAM_ENUMERATION(kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoWrongBrowserType);
if (!failure_callback.is_null()) {
std::move(failure_callback).Run();
}
return;
}
if (IncognitoModePrefs::GetAvailability(profile()->GetPrefs()) ==
policy::IncognitoModeAvailability::kDisabled) {
// Incognito mode needs to be enabled to create an off-the-record profile
// for HaTS dialog.
UMA_HISTOGRAM_ENUMERATION(kHatsShouldShowSurveyReasonHistogram,
ShouldShowSurveyReasons::kNoIncognitoDisabled);
if (!failure_callback.is_null()) {
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_bits_data,
product_specific_string_data, supplied_trigger_id);
}
void HatsServiceDesktop::CheckSurveyStatusAndMaybeShow(
Browser* browser,
const std::string& trigger,
base::OnceClosure success_callback,
base::OnceClosure failure_callback,
const SurveyBitsData& product_specific_bits_data,
const SurveyStringData& product_specific_string_data,
const std::optional<std::string_view>& supplied_trigger_id) {
// 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::Value::Dict& pref_data =
GetPrefsForHatsMetadata()->GetDict(prefs::kHatsSurveyMetadata);
std::optional<int> is_full =
pref_data.FindBoolByDottedPath(GetIsSurveyFull(trigger));
if (is_full.has_value() && is_full) {
if (!failure_callback.is_null()) {
std::move(failure_callback).Run();
}
return;
}
CHECK(survey_configs_by_triggers_.find(trigger) !=
survey_configs_by_triggers_.end());
auto survey_config = survey_configs_by_triggers_[trigger];
// Check that the |product_specific_bits_data| matches the fields for this
// trigger. If fields are set for a trigger, they must be provided.
CHECK_EQ(product_specific_bits_data.size(),
survey_config.product_specific_bits_data_fields.size());
for (const auto& field_value : product_specific_bits_data) {
CHECK(base::Contains(survey_config.product_specific_bits_data_fields,
field_value.first));
}
// Check that the |product_specific_string_data| matches the fields for this
// trigger. If fields are set for a trigger, they must be provided.
CHECK_EQ(product_specific_string_data.size(),
survey_config.product_specific_string_data_fields.size());
for (const auto& field_value : product_specific_string_data) {
CHECK(base::Contains(survey_config.product_specific_string_data_fields,
field_value.first));
}
// As soon as the HaTS Next dialog is created it will attempt to contact
// the HaTS servers to check for a survey.
ScopedDictPrefUpdate update(GetPrefsForHatsMetadata(),
prefs::kHatsSurveyMetadata);
update->SetByDottedPath(GetLastSurveyCheckTime(trigger),
base::TimeToValue(base::Time::Now()));
DCHECK(!hats_next_dialog_exists_);
if (supplied_trigger_id.has_value()) {
survey_configs_by_triggers_[trigger].trigger_id =
std::string(supplied_trigger_id.value());
}
browser->window()->ShowHatsDialog(
survey_configs_by_triggers_[trigger].trigger_id,
survey_configs_by_triggers_[trigger].hats_histogram_name,
survey_configs_by_triggers_[trigger].hats_survey_ukm_id,
std::move(success_callback), std::move(failure_callback),
product_specific_bits_data, product_specific_string_data);
hats_next_dialog_exists_ = true;
}