1 // Copyright 2022 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/permissions/permission_hats_trigger_helper.h"
9 #include "base/check_is_test.h"
10 #include "base/no_destructor.h"
11 #include "base/rand_util.h"
12 #include "base/ranges/algorithm.h"
13 #include "base/strings/string_number_conversions.h"
14 #include "base/strings/string_split.h"
15 #include "base/strings/string_util.h"
16 #include "base/time/time.h"
17 #include "components/permissions/constants.h"
18 #include "components/permissions/features.h"
19 #include "components/permissions/permission_uma_util.h"
20 #include "components/permissions/pref_names.h"
21 #include "components/pref_registry/pref_registry_syncable.h"
22 #include "components/prefs/pref_service.h"
24 namespace permissions {
30 std::vector<std::string> SplitCsvString(const std::string& csv_string) {
31 return base::SplitString(csv_string, ",", base::TRIM_WHITESPACE,
32 base::SPLIT_WANT_NONEMPTY);
35 bool StringMatchesFilter(const std::string& string, const std::string& filter) {
36 return filter.empty() ||
37 base::ranges::any_of(SplitCsvString(filter),
38 [string](base::StringPiece current_filter) {
39 return base::EqualsCaseInsensitiveASCII(
40 string, current_filter);
44 std::map<std::string, std::pair<std::string, std::string>>
45 GetKeyToValueFilterPairMap(
46 PermissionHatsTriggerHelper::PromptParametersForHaTS prompt_parameters) {
47 // configuration key -> {current value for key, configured filter for key}
49 {kPermissionsPromptSurveyPromptDispositionKey,
50 {PermissionUmaUtil::GetPromptDispositionString(
51 prompt_parameters.prompt_disposition),
52 feature_params::kPermissionsPromptSurveyPromptDispositionFilter.Get()}},
53 {kPermissionsPromptSurveyPromptDispositionReasonKey,
54 {PermissionUmaUtil::GetPromptDispositionReasonString(
55 prompt_parameters.prompt_disposition_reason),
56 feature_params::kPermissionsPromptSurveyPromptDispositionReasonFilter
58 {kPermissionsPromptSurveyActionKey,
59 {prompt_parameters.action.has_value()
60 ? PermissionUmaUtil::GetPermissionActionString(
61 prompt_parameters.action.value())
63 feature_params::kPermissionsPromptSurveyActionFilter.Get()}},
64 {kPermissionsPromptSurveyRequestTypeKey,
65 {PermissionUmaUtil::GetRequestTypeString(prompt_parameters.request_type),
66 feature_params::kPermissionsPromptSurveyRequestTypeFilter.Get()}},
67 {kPermissionsPromptSurveyHadGestureKey,
68 {prompt_parameters.gesture_type == PermissionRequestGestureType::GESTURE
71 feature_params::kPermissionsPromptSurveyHadGestureFilter.Get()}},
72 {kPermissionsPromptSurveyReleaseChannelKey,
73 {prompt_parameters.channel,
74 feature_params::kPermissionPromptSurveyReleaseChannelFilter.Get()}},
75 {kPermissionsPromptSurveyDisplayTimeKey,
76 {prompt_parameters.survey_display_time,
77 feature_params::kPermissionsPromptSurveyDisplayTime.Get()}},
78 {kPermissionPromptSurveyOneTimePromptsDecidedBucketKey,
79 {PermissionHatsTriggerHelper::GetOneTimePromptsDecidedBucketString(
80 prompt_parameters.one_time_prompts_decided_bucket),
81 feature_params::kPermissionPromptSurveyOneTimePromptsDecidedBucket
83 {kPermissionPromptSurveyUrlKey, {prompt_parameters.url, ""}}};
86 // Typos in the gcl configuration cannot be verified and may be missed by
87 // reviewers. In the worst case, no filters are configured. By definition of
88 // our filters, this would match all requests. To safeguard against this kind
89 // of misconfiguration (which would lead to very high HaTS QPS), we enforce
90 // that at least one valid filter must be configured.
91 bool IsValidConfiguration(
92 PermissionHatsTriggerHelper::PromptParametersForHaTS prompt_parameters) {
93 auto filter_pair_map = GetKeyToValueFilterPairMap(prompt_parameters);
95 if (filter_pair_map[kPermissionsPromptSurveyDisplayTimeKey].second.empty()) {
96 // When no display time is configured, the survey should never be triggered.
100 // Returns false if all filter parameters are empty.
101 return !base::ranges::all_of(
103 [](std::pair<std::string, std::pair<std::string, std::string>> entry) {
104 return entry.second.second.empty();
108 std::vector<double> ParseProbabilityVector(std::string probability_vector_csv) {
109 std::vector<std::string> probability_string_vector =
110 base::SplitString(feature_params::kProbabilityVector.Get(), ",",
111 base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
112 std::vector<double> checked_probability_vector;
114 for (std::string probability_string : probability_string_vector) {
115 if (!base::StringToDouble(probability_string, &probability)) {
116 // Parsing failed, configuration error. Return empty array.
117 return std::vector<double>();
119 checked_probability_vector.push_back(probability);
121 return checked_probability_vector;
124 std::vector<double>& GetProbabilityVector(std::string probability_vector_csv) {
125 static base::NoDestructor<std::vector<double>> probability_vector(
126 [probability_vector_csv] {
127 return ParseProbabilityVector(probability_vector_csv);
132 *probability_vector = ParseProbabilityVector(probability_vector_csv);
134 return *probability_vector;
137 std::vector<std::string> ParseRequestFilterVector(
138 std::string request_vector_csv) {
139 return base::SplitString(
140 feature_params::kPermissionsPromptSurveyRequestTypeFilter.Get(), ",",
141 base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
144 std::vector<std::string>& GetRequestFilterVector(
145 std::string request_vector_csv) {
146 static base::NoDestructor<std::vector<std::string>> request_filter_vector(
147 [request_vector_csv] {
148 return ParseRequestFilterVector(request_vector_csv);
152 *request_filter_vector = ParseRequestFilterVector(request_vector_csv);
154 return *request_filter_vector;
157 std::vector<std::pair<std::string, std::string>>
158 ComputePermissionPromptTriggerIdPairs(const std::string& trigger_name_base) {
159 std::vector<std::string> permission_trigger_id_vector(
160 base::SplitString(feature_params::kPermissionsPromptSurveyTriggerId.Get(),
161 ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY));
162 int trigger_index = 0;
163 std::vector<std::pair<std::string, std::string>> pairs;
165 for (const auto& trigger_id : permission_trigger_id_vector) {
167 trigger_name_base + base::NumberToString(trigger_index++), trigger_id);
174 PermissionHatsTriggerHelper::PromptParametersForHaTS::PromptParametersForHaTS(
175 RequestType request_type,
176 absl::optional<PermissionAction> action,
177 PermissionPromptDisposition prompt_disposition,
178 PermissionPromptDispositionReason prompt_disposition_reason,
179 PermissionRequestGestureType gesture_type,
180 const std::string& channel,
181 const std::string& survey_display_time,
182 absl::optional<base::TimeDelta> prompt_display_duration,
183 OneTimePermissionPromptsDecidedBucket one_time_prompts_decided_bucket,
184 absl::optional<GURL> gurl)
185 : request_type(request_type),
187 prompt_disposition(prompt_disposition),
188 prompt_disposition_reason(prompt_disposition_reason),
189 gesture_type(gesture_type),
191 survey_display_time(survey_display_time),
192 prompt_display_duration(prompt_display_duration),
193 one_time_prompts_decided_bucket(one_time_prompts_decided_bucket),
194 url(gurl.has_value() ? gurl->spec() : "") {}
196 PermissionHatsTriggerHelper::PromptParametersForHaTS::PromptParametersForHaTS(
197 const PromptParametersForHaTS& other) = default;
198 PermissionHatsTriggerHelper::PromptParametersForHaTS::
199 ~PromptParametersForHaTS() = default;
201 PermissionHatsTriggerHelper::SurveyProductSpecificData::
202 SurveyProductSpecificData(SurveyBitsData survey_bits_data,
203 SurveyStringData survey_string_data)
204 : survey_bits_data(survey_bits_data),
205 survey_string_data(survey_string_data) {}
207 PermissionHatsTriggerHelper::SurveyProductSpecificData::
208 ~SurveyProductSpecificData() = default;
210 PermissionHatsTriggerHelper::SurveyProductSpecificData
211 PermissionHatsTriggerHelper::SurveyProductSpecificData::PopulateFrom(
212 PromptParametersForHaTS prompt_parameters) {
213 static const char* const kProductSpecificBitsFields[] = {
214 kPermissionsPromptSurveyHadGestureKey};
215 static const char* const kProductSpecificStringFields[] = {
216 kPermissionsPromptSurveyPromptDispositionKey,
217 kPermissionsPromptSurveyPromptDispositionReasonKey,
218 kPermissionsPromptSurveyActionKey,
219 kPermissionsPromptSurveyRequestTypeKey,
220 kPermissionsPromptSurveyReleaseChannelKey,
221 kPermissionsPromptSurveyDisplayTimeKey,
222 kPermissionPromptSurveyOneTimePromptsDecidedBucketKey,
223 kPermissionPromptSurveyUrlKey};
225 auto key_to_value_filter_pair = GetKeyToValueFilterPairMap(prompt_parameters);
226 std::map<std::string, bool> bits_data;
227 for (const char* product_specific_bits_field : kProductSpecificBitsFields) {
228 auto it = key_to_value_filter_pair.find(product_specific_bits_field);
229 if (it != key_to_value_filter_pair.end()) {
230 bits_data.insert({it->first, it->second.first == kTrueStr});
234 std::map<std::string, std::string> string_data;
235 for (const char* product_specific_string_field :
236 kProductSpecificStringFields) {
237 auto it = key_to_value_filter_pair.find(product_specific_string_field);
238 if (it != key_to_value_filter_pair.end()) {
239 string_data.insert({it->first, it->second.first});
243 return SurveyProductSpecificData(bits_data, string_data);
247 void PermissionHatsTriggerHelper::RegisterProfilePrefs(
248 user_prefs::PrefRegistrySyncable* registry) {
249 registry->RegisterIntegerPref(prefs::kOneTimePermissionPromptsDecidedCount,
253 bool PermissionHatsTriggerHelper::ArePromptTriggerCriteriaSatisfied(
254 PromptParametersForHaTS prompt_parameters,
255 const std::string& trigger_name_base) {
256 auto trigger_and_probability = PermissionHatsTriggerHelper::
257 GetPermissionPromptTriggerNameAndProbabilityForRequestType(
258 trigger_name_base, PermissionUmaUtil::GetRequestTypeString(
259 prompt_parameters.request_type));
261 if (!trigger_and_probability.has_value() ||
262 base::RandDouble() >= trigger_and_probability->second) {
266 if (!IsValidConfiguration(prompt_parameters)) {
270 if (prompt_parameters.action == PermissionAction::IGNORED &&
271 prompt_parameters.prompt_display_duration >
272 feature_params::kPermissionPromptSurveyIgnoredPromptsMaximumAge
277 auto key_to_value_filter_pair = GetKeyToValueFilterPairMap(prompt_parameters);
278 for (const auto& value_type : key_to_value_filter_pair) {
279 const auto& value = value_type.second.first;
280 const auto& filter = value_type.second.second;
281 if (!StringMatchesFilter(value, filter)) {
282 // if any filter doesn't match, no survey should be triggered
291 void PermissionHatsTriggerHelper::
292 IncrementOneTimePermissionPromptsDecidedIfApplicable(
293 ContentSettingsType type,
294 PrefService* pref_service) {
295 if (base::FeatureList::IsEnabled(features::kOneTimePermission) &&
296 PermissionUtil::CanPermissionBeAllowedOnce(type)) {
297 pref_service->SetInteger(
298 prefs::kOneTimePermissionPromptsDecidedCount,
299 pref_service->GetInteger(prefs::kOneTimePermissionPromptsDecidedCount) +
305 PermissionHatsTriggerHelper::OneTimePermissionPromptsDecidedBucket
306 PermissionHatsTriggerHelper::GetOneTimePromptsDecidedBucket(
307 PrefService* pref_service) {
309 pref_service->GetInteger(prefs::kOneTimePermissionPromptsDecidedCount);
311 return OneTimePermissionPromptsDecidedBucket::BUCKET_0_1;
312 } else if (count <= 3) {
313 return OneTimePermissionPromptsDecidedBucket::BUCKET_2_3;
314 } else if (count <= 5) {
315 return OneTimePermissionPromptsDecidedBucket::BUCKET_4_5;
316 } else if (count <= 10) {
317 return OneTimePermissionPromptsDecidedBucket::BUCKET_6_10;
318 } else if (count <= 20) {
319 return OneTimePermissionPromptsDecidedBucket::BUCKET_11_20;
321 return OneTimePermissionPromptsDecidedBucket::BUCKET_GT20;
326 std::string PermissionHatsTriggerHelper::GetOneTimePromptsDecidedBucketString(
327 OneTimePermissionPromptsDecidedBucket bucket) {
347 std::vector<std::pair<std::string, std::string>>&
348 PermissionHatsTriggerHelper::GetPermissionPromptTriggerIdPairs(
349 const std::string& trigger_name_base) {
350 static base::NoDestructor<std::vector<std::pair<std::string, std::string>>>
351 trigger_id_pairs([trigger_name_base] {
352 return ComputePermissionPromptTriggerIdPairs(trigger_name_base);
357 ComputePermissionPromptTriggerIdPairs(trigger_name_base);
359 return *trigger_id_pairs;
363 absl::optional<std::pair<std::string, double>> PermissionHatsTriggerHelper::
364 GetPermissionPromptTriggerNameAndProbabilityForRequestType(
365 const std::string& trigger_name_base,
366 const std::string& request_type) {
367 auto& trigger_id_pairs = GetPermissionPromptTriggerIdPairs(trigger_name_base);
368 auto& probability_vector =
369 GetProbabilityVector(feature_params::kProbabilityVector.Get());
371 if (trigger_id_pairs.size() == 1 && probability_vector.size() <= 1) {
372 // If a value is configured, use it, otherwise set it to 1.
373 return std::make_pair(
374 trigger_id_pairs[0].first,
375 probability_vector.size() == 1 ? probability_vector[0] : 1.0);
376 } else if (trigger_id_pairs.size() != probability_vector.size()) {
377 // Configuration error
378 return absl::nullopt;
380 auto& request_filter_vector = GetRequestFilterVector(
381 feature_params::kPermissionsPromptSurveyRequestTypeFilter.Get());
383 if (request_filter_vector.size() != trigger_id_pairs.size()) {
384 // Configuration error
385 return absl::nullopt;
388 for (unsigned long i = 0; i < trigger_id_pairs.size(); i++) {
389 if (base::EqualsCaseInsensitiveASCII(request_type,
390 request_filter_vector[i])) {
391 return std::make_pair(trigger_id_pairs.at(i).first,
392 probability_vector[i]);
396 // No matching filter
397 return absl::nullopt;
402 void PermissionHatsTriggerHelper::SetIsTest() {
406 } // namespace permissions