blob: bd5f99a8b72ba9d1c12c22fa7bd2afaced132628 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/permissions/permission_hats_trigger_helper.h"
#include <utility>
#include "base/check_is_test.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "components/permissions/constants.h"
#include "components/permissions/features.h"
#include "components/permissions/permission_uma_util.h"
#include "components/permissions/pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
namespace {
bool is_test = false;
std::vector<std::string> SplitCsvString(const std::string& csv_string) {
return base::SplitString(csv_string, ",", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
}
bool StringMatchesFilter(const std::string& string, const std::string& filter) {
return filter.empty() ||
base::ranges::any_of(SplitCsvString(filter),
[string](base::StringPiece current_filter) {
return base::EqualsCaseInsensitiveASCII(
string, current_filter);
});
}
std::map<std::string, std::pair<std::string, std::string>>
GetKeyToValueFilterPairMap(
permissions::PermissionHatsTriggerHelper::PromptParametersForHaTS
prompt_parameters) {
// configuration key -> {current value for key, configured filter for key}
return {
{permissions::kPermissionsPromptSurveyPromptDispositionKey,
{permissions::PermissionUmaUtil::GetPromptDispositionString(
prompt_parameters.prompt_disposition),
permissions::feature_params::
kPermissionsPromptSurveyPromptDispositionFilter.Get()}},
{permissions::kPermissionsPromptSurveyPromptDispositionReasonKey,
{permissions::PermissionUmaUtil::GetPromptDispositionReasonString(
prompt_parameters.prompt_disposition_reason),
permissions::feature_params::
kPermissionsPromptSurveyPromptDispositionReasonFilter.Get()}},
{permissions::kPermissionsPromptSurveyActionKey,
{prompt_parameters.action.has_value()
? permissions::PermissionUmaUtil::GetPermissionActionString(
prompt_parameters.action.value())
: "",
permissions::feature_params::kPermissionsPromptSurveyActionFilter
.Get()}},
{permissions::kPermissionsPromptSurveyRequestTypeKey,
{permissions::PermissionUmaUtil::GetRequestTypeString(
prompt_parameters.request_type),
permissions::feature_params::kPermissionsPromptSurveyRequestTypeFilter
.Get()}},
{permissions::kPermissionsPromptSurveyHadGestureKey,
{prompt_parameters.gesture_type ==
permissions::PermissionRequestGestureType::GESTURE
? permissions::kTrueStr
: permissions::kFalseStr,
permissions::feature_params::kPermissionsPromptSurveyHadGestureFilter
.Get()}},
{permissions::kPermissionsPromptSurveyReleaseChannelKey,
{prompt_parameters.channel,
permissions::feature_params::kPermissionPromptSurveyReleaseChannelFilter
.Get()}},
{permissions::kPermissionsPromptSurveyDisplayTimeKey,
{prompt_parameters.survey_display_time,
permissions::feature_params::kPermissionsPromptSurveyDisplayTime
.Get()}},
{permissions::kPermissionPromptSurveyOneTimePromptsDecidedBucketKey,
{permissions::PermissionHatsTriggerHelper::
GetOneTimePromptsDecidedBucketString(
prompt_parameters.one_time_prompts_decided_bucket),
permissions::feature_params::
kPermissionPromptSurveyOneTimePromptsDecidedBucket.Get()}},
{permissions::kPermissionPromptSurveyUrlKey,
{prompt_parameters.url, ""}}};
}
// Typos in the gcl configuration cannot be verified and may be missed by
// reviewers. In the worst case, no filters are configured. By definition of
// our filters, this would match all requests. To safeguard against this kind
// of misconfiguration (which would lead to very high HaTS QPS), we enforce
// that at least one valid filter must be configured.
bool IsValidConfiguration(
permissions::PermissionHatsTriggerHelper::PromptParametersForHaTS
prompt_parameters) {
auto filter_pair_map = GetKeyToValueFilterPairMap(prompt_parameters);
if (filter_pair_map[permissions::kPermissionsPromptSurveyDisplayTimeKey]
.second.empty()) {
// When no display time is configured, the survey should never be triggered.
return false;
}
// Returns false if all filter parameters are empty.
return !base::ranges::all_of(
filter_pair_map,
[](std::pair<std::string, std::pair<std::string, std::string>> entry) {
return entry.second.second.empty();
});
}
std::vector<double> ParseProbabilityVector(std::string probability_vector_csv) {
std::vector<std::string> probability_string_vector =
base::SplitString(permissions::feature_params::kProbabilityVector.Get(),
",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
std::vector<double> checked_probability_vector;
double probability;
for (std::string probability_string : probability_string_vector) {
if (!base::StringToDouble(probability_string, &probability)) {
// Parsing failed, configuration error. Return empty array.
return std::vector<double>();
}
checked_probability_vector.push_back(probability);
}
return checked_probability_vector;
}
std::vector<double>& GetProbabilityVector(std::string probability_vector_csv) {
static base::NoDestructor<std::vector<double>> probability_vector(
[probability_vector_csv] {
return ParseProbabilityVector(probability_vector_csv);
}());
if (is_test) {
CHECK_IS_TEST();
*probability_vector = ParseProbabilityVector(probability_vector_csv);
}
return *probability_vector;
}
std::vector<std::string> ParseRequestFilterVector(
std::string request_vector_csv) {
return base::SplitString(
permissions::feature_params::kPermissionsPromptSurveyRequestTypeFilter
.Get(),
",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
}
std::vector<std::string>& GetRequestFilterVector(
std::string request_vector_csv) {
static base::NoDestructor<std::vector<std::string>> request_filter_vector(
[request_vector_csv] {
return ParseRequestFilterVector(request_vector_csv);
}());
if (is_test) {
CHECK_IS_TEST();
*request_filter_vector = ParseRequestFilterVector(request_vector_csv);
}
return *request_filter_vector;
}
std::vector<std::pair<std::string, std::string>>
ComputePermissionPromptTriggerIdPairs(const std::string& trigger_name_base) {
std::vector<std::string> permission_trigger_id_vector(base::SplitString(
permissions::feature_params::kPermissionsPromptSurveyTriggerId.Get(), ",",
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY));
int trigger_index = 0;
std::vector<std::pair<std::string, std::string>> pairs;
pairs.clear();
for (const auto& trigger_id : permission_trigger_id_vector) {
pairs.emplace_back(
trigger_name_base + base::NumberToString(trigger_index++), trigger_id);
}
return pairs;
}
} // namespace
namespace permissions {
PermissionHatsTriggerHelper::PromptParametersForHaTS::PromptParametersForHaTS(
permissions::RequestType request_type,
absl::optional<permissions::PermissionAction> action,
permissions::PermissionPromptDisposition prompt_disposition,
permissions::PermissionPromptDispositionReason prompt_disposition_reason,
permissions::PermissionRequestGestureType gesture_type,
const std::string& channel,
const std::string& survey_display_time,
absl::optional<base::TimeDelta> prompt_display_duration,
OneTimePermissionPromptsDecidedBucket one_time_prompts_decided_bucket,
absl::optional<GURL> gurl)
: request_type(request_type),
action(action),
prompt_disposition(prompt_disposition),
prompt_disposition_reason(prompt_disposition_reason),
gesture_type(gesture_type),
channel(channel),
survey_display_time(survey_display_time),
prompt_display_duration(prompt_display_duration),
one_time_prompts_decided_bucket(one_time_prompts_decided_bucket),
url(gurl.has_value() ? gurl->spec() : "") {}
PermissionHatsTriggerHelper::PromptParametersForHaTS::PromptParametersForHaTS(
const PromptParametersForHaTS& other) = default;
PermissionHatsTriggerHelper::PromptParametersForHaTS::
~PromptParametersForHaTS() = default;
PermissionHatsTriggerHelper::SurveyProductSpecificData::
SurveyProductSpecificData(SurveyBitsData survey_bits_data,
SurveyStringData survey_string_data)
: survey_bits_data(survey_bits_data),
survey_string_data(survey_string_data) {}
PermissionHatsTriggerHelper::SurveyProductSpecificData::
~SurveyProductSpecificData() = default;
PermissionHatsTriggerHelper::SurveyProductSpecificData
PermissionHatsTriggerHelper::SurveyProductSpecificData::PopulateFrom(
PromptParametersForHaTS prompt_parameters) {
const static std::vector<std::string> product_specific_bits_fields = {
kPermissionsPromptSurveyHadGestureKey};
const static std::vector<std::string> product_specific_string_fields{
kPermissionsPromptSurveyPromptDispositionKey,
kPermissionsPromptSurveyPromptDispositionReasonKey,
kPermissionsPromptSurveyActionKey,
kPermissionsPromptSurveyRequestTypeKey,
kPermissionsPromptSurveyReleaseChannelKey,
kPermissionsPromptSurveyDisplayTimeKey,
kPermissionPromptSurveyOneTimePromptsDecidedBucketKey,
kPermissionPromptSurveyUrlKey};
auto key_to_value_filter_pair = GetKeyToValueFilterPairMap(prompt_parameters);
std::map<std::string, bool> bits_data;
for (auto product_specific_bits_field : product_specific_bits_fields) {
auto value_type =
key_to_value_filter_pair.find(product_specific_bits_field);
if (value_type != key_to_value_filter_pair.end()) {
bits_data.insert(
{value_type->first, value_type->second.first == kTrueStr});
}
}
std::map<std::string, std::string> string_data;
for (auto product_specific_string_field : product_specific_string_fields) {
auto value_type =
key_to_value_filter_pair.find(product_specific_string_field);
if (value_type != key_to_value_filter_pair.end()) {
string_data.insert({value_type->first, value_type->second.first});
}
}
return SurveyProductSpecificData(bits_data, string_data);
}
// static
void PermissionHatsTriggerHelper::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterIntegerPref(
permissions::prefs::kOneTimePermissionPromptsDecidedCount, 0);
}
bool PermissionHatsTriggerHelper::ArePromptTriggerCriteriaSatisfied(
PromptParametersForHaTS prompt_parameters,
const std::string& trigger_name_base) {
auto trigger_and_probability = permissions::PermissionHatsTriggerHelper::
GetPermissionPromptTriggerNameAndProbabilityForRequestType(
trigger_name_base,
permissions::PermissionUmaUtil::GetRequestTypeString(
prompt_parameters.request_type));
if (!trigger_and_probability.has_value() ||
base::RandDouble() >= trigger_and_probability->second) {
return false;
}
if (!IsValidConfiguration(prompt_parameters)) {
return false;
}
if (prompt_parameters.action == permissions::PermissionAction::IGNORED &&
prompt_parameters.prompt_display_duration >
permissions::feature_params::
kPermissionPromptSurveyIgnoredPromptsMaximumAge.Get()) {
return false;
}
auto key_to_value_filter_pair = GetKeyToValueFilterPairMap(prompt_parameters);
for (auto value_type : key_to_value_filter_pair) {
const auto& value = value_type.second.first;
const auto& filter = value_type.second.second;
if (!StringMatchesFilter(value, filter)) {
// if any filter doesn't match, no survey should be triggered
return false;
}
}
return true;
}
// static
void PermissionHatsTriggerHelper::
IncrementOneTimePermissionPromptsDecidedIfApplicable(
ContentSettingsType type,
PrefService* pref_service) {
if (base::FeatureList::IsEnabled(permissions::features::kOneTimePermission) &&
PermissionUtil::CanPermissionBeAllowedOnce(type)) {
pref_service->SetInteger(
prefs::kOneTimePermissionPromptsDecidedCount,
pref_service->GetInteger(prefs::kOneTimePermissionPromptsDecidedCount) +
1);
}
}
// static
PermissionHatsTriggerHelper::OneTimePermissionPromptsDecidedBucket
PermissionHatsTriggerHelper::GetOneTimePromptsDecidedBucket(
PrefService* pref_service) {
int count =
pref_service->GetInteger(prefs::kOneTimePermissionPromptsDecidedCount);
if (count <= 1) {
return OneTimePermissionPromptsDecidedBucket::BUCKET_0_1;
} else if (count <= 3) {
return OneTimePermissionPromptsDecidedBucket::BUCKET_2_3;
} else if (count <= 5) {
return OneTimePermissionPromptsDecidedBucket::BUCKET_4_5;
} else if (count <= 10) {
return OneTimePermissionPromptsDecidedBucket::BUCKET_6_10;
} else if (count <= 20) {
return OneTimePermissionPromptsDecidedBucket::BUCKET_11_20;
} else {
return OneTimePermissionPromptsDecidedBucket::BUCKET_GT20;
}
}
// static
std::string PermissionHatsTriggerHelper::GetOneTimePromptsDecidedBucketString(
OneTimePermissionPromptsDecidedBucket bucket) {
switch (bucket) {
case BUCKET_0_1:
return "0_1";
case BUCKET_2_3:
return "2_3";
case BUCKET_4_5:
return "4_5";
case BUCKET_6_10:
return "6_10";
case BUCKET_11_20:
return "11_20";
case BUCKET_GT20:
return "GT20";
default:
NOTREACHED();
}
}
// static
std::vector<std::pair<std::string, std::string>>&
PermissionHatsTriggerHelper::GetPermissionPromptTriggerIdPairs(
const std::string& trigger_name_base) {
static base::NoDestructor<std::vector<std::pair<std::string, std::string>>>
trigger_id_pairs([trigger_name_base] {
return ComputePermissionPromptTriggerIdPairs(trigger_name_base);
}());
if (is_test) {
CHECK_IS_TEST();
*trigger_id_pairs =
ComputePermissionPromptTriggerIdPairs(trigger_name_base);
}
return *trigger_id_pairs;
}
// static
absl::optional<std::pair<std::string, double>> PermissionHatsTriggerHelper::
GetPermissionPromptTriggerNameAndProbabilityForRequestType(
const std::string& trigger_name_base,
const std::string& request_type) {
auto& trigger_id_pairs = GetPermissionPromptTriggerIdPairs(trigger_name_base);
auto& probability_vector = GetProbabilityVector(
permissions::feature_params::kProbabilityVector.Get());
if (trigger_id_pairs.size() == 1 && probability_vector.size() <= 1) {
// If a value is configured, use it, otherwise set it to 1.
return std::make_pair(
trigger_id_pairs[0].first,
probability_vector.size() == 1 ? probability_vector[0] : 1.0);
} else if (trigger_id_pairs.size() != probability_vector.size()) {
// Configuration error
return absl::nullopt;
} else {
auto& request_filter_vector = GetRequestFilterVector(
permissions::feature_params::kPermissionsPromptSurveyRequestTypeFilter
.Get());
if (request_filter_vector.size() != trigger_id_pairs.size()) {
// Configuration error
return absl::nullopt;
}
for (unsigned long i = 0; i < trigger_id_pairs.size(); i++) {
if (base::EqualsCaseInsensitiveASCII(request_type,
request_filter_vector[i])) {
return std::make_pair(trigger_id_pairs.at(i).first,
probability_vector[i]);
}
}
// No matching filter
return absl::nullopt;
}
}
// static
void PermissionHatsTriggerHelper::SetIsTest() {
is_test = true;
}
} // namespace permissions