Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "components/variations/study_filtering.h" | 5 #include "components/variations/study_filtering.h" |
| 6 | 6 |
| 7 #include <stddef.h> | 7 #include <stddef.h> |
| 8 #include <stdint.h> | 8 #include <stdint.h> |
| 9 | 9 |
| 10 #include <set> | 10 #include <set> |
| 11 | 11 |
| 12 #include "base/stl_util.h" | 12 #include "base/stl_util.h" |
| 13 #include "build/build_config.h" | 13 #include "base/strings/string_util.h" |
| 14 #include "components/variations/client_filterable_state.h" | |
| 14 | 15 |
| 15 namespace variations { | 16 namespace variations { |
| 16 | |
| 17 namespace { | 17 namespace { |
| 18 | 18 |
| 19 Study_Platform GetCurrentPlatform() { | |
| 20 #if defined(OS_WIN) | |
| 21 return Study_Platform_PLATFORM_WINDOWS; | |
| 22 #elif defined(OS_IOS) | |
| 23 return Study_Platform_PLATFORM_IOS; | |
| 24 #elif defined(OS_MACOSX) | |
| 25 return Study_Platform_PLATFORM_MAC; | |
| 26 #elif defined(OS_CHROMEOS) | |
| 27 return Study_Platform_PLATFORM_CHROMEOS; | |
| 28 #elif defined(OS_ANDROID) | |
| 29 return Study_Platform_PLATFORM_ANDROID; | |
| 30 #elif defined(OS_LINUX) || defined(OS_BSD) || defined(OS_SOLARIS) | |
| 31 // Default BSD and SOLARIS to Linux to not break those builds, although these | |
| 32 // platforms are not officially supported by Chrome. | |
| 33 return Study_Platform_PLATFORM_LINUX; | |
| 34 #else | |
| 35 #error Unknown platform | |
| 36 #endif | |
| 37 } | |
| 38 | |
| 39 // Converts |date_time| in Study date format to base::Time. | 19 // Converts |date_time| in Study date format to base::Time. |
| 40 base::Time ConvertStudyDateToBaseTime(int64_t date_time) { | 20 base::Time ConvertStudyDateToBaseTime(int64_t date_time) { |
| 41 return base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(date_time); | 21 return base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(date_time); |
| 42 } | 22 } |
| 43 | 23 |
| 44 } // namespace | 24 } // namespace |
| 45 | 25 |
| 46 namespace internal { | 26 namespace internal { |
| 47 | 27 |
| 48 bool CheckStudyChannel(const Study_Filter& filter, Study_Channel channel) { | 28 bool CheckStudyChannel(const Study::Filter& filter, Study::Channel channel) { |
| 49 // An empty channel list matches all channels. | 29 // An empty channel list matches all channels. |
| 50 if (filter.channel_size() == 0) | 30 if (filter.channel_size() == 0) |
| 51 return true; | 31 return true; |
| 52 | 32 |
| 53 for (int i = 0; i < filter.channel_size(); ++i) { | 33 for (int i = 0; i < filter.channel_size(); ++i) { |
| 54 if (filter.channel(i) == channel) | 34 if (filter.channel(i) == channel) |
| 55 return true; | 35 return true; |
| 56 } | 36 } |
| 57 return false; | 37 return false; |
| 58 } | 38 } |
| 59 | 39 |
| 60 bool CheckStudyFormFactor(const Study_Filter& filter, | 40 bool CheckStudyFormFactor(const Study::Filter& filter, |
| 61 Study_FormFactor form_factor) { | 41 Study::FormFactor form_factor) { |
| 62 // Empty whitelist and blacklist signifies matching any form factor. | 42 // Empty whitelist and blacklist signifies matching any form factor. |
| 63 if (filter.form_factor_size() == 0 && filter.exclude_form_factor_size() == 0) | 43 if (filter.form_factor_size() == 0 && filter.exclude_form_factor_size() == 0) |
| 64 return true; | 44 return true; |
| 65 | 45 |
| 66 // Allow the form_factor if it matches the whitelist. | 46 // Allow the form_factor if it matches the whitelist. |
| 67 // Note if both a whitelist and blacklist are specified, the blacklist is | 47 // Note if both a whitelist and blacklist are specified, the blacklist is |
| 68 // ignored. We do not expect both to be present for Chrome due to server-side | 48 // ignored. We do not expect both to be present for Chrome due to server-side |
| 69 // checks. | 49 // checks. |
| 70 if (filter.form_factor_size() > 0) | 50 if (filter.form_factor_size() > 0) |
| 71 return base::ContainsValue(filter.form_factor(), form_factor); | 51 return base::ContainsValue(filter.form_factor(), form_factor); |
| 72 | 52 |
| 73 // Omit if we match the blacklist. | 53 // Omit if we match the blacklist. |
| 74 return !base::ContainsValue(filter.exclude_form_factor(), form_factor); | 54 return !base::ContainsValue(filter.exclude_form_factor(), form_factor); |
| 75 } | 55 } |
| 76 | 56 |
| 77 bool CheckStudyHardwareClass(const Study_Filter& filter, | 57 bool CheckStudyHardwareClass(const Study::Filter& filter, |
| 78 const std::string& hardware_class) { | 58 const std::string& hardware_class) { |
| 79 // Empty hardware_class and exclude_hardware_class matches all. | 59 // Empty hardware_class and exclude_hardware_class matches all. |
| 80 if (filter.hardware_class_size() == 0 && | 60 if (filter.hardware_class_size() == 0 && |
| 81 filter.exclude_hardware_class_size() == 0) { | 61 filter.exclude_hardware_class_size() == 0) { |
| 82 return true; | 62 return true; |
| 83 } | 63 } |
| 84 | 64 |
| 85 // Checks if we are supposed to filter for a specified set of | 65 // Checks if we are supposed to filter for a specified set of |
| 86 // hardware_classes. Note that this means this overrides the | 66 // hardware_classes. Note that this means this overrides the |
| 87 // exclude_hardware_class in case that ever occurs (which it shouldn't). | 67 // exclude_hardware_class in case that ever occurs (which it shouldn't). |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 102 size_t position = hardware_class.find( | 82 size_t position = hardware_class.find( |
| 103 filter.exclude_hardware_class(i)); | 83 filter.exclude_hardware_class(i)); |
| 104 if (position != std::string::npos) | 84 if (position != std::string::npos) |
| 105 return false; | 85 return false; |
| 106 } | 86 } |
| 107 | 87 |
| 108 // None of the exclusions match, so this accepts. | 88 // None of the exclusions match, so this accepts. |
| 109 return true; | 89 return true; |
| 110 } | 90 } |
| 111 | 91 |
| 112 bool CheckStudyLocale(const Study_Filter& filter, const std::string& locale) { | 92 bool CheckStudyLocale(const Study::Filter& filter, const std::string& locale) { |
| 113 // Empty locale and exclude_locale lists matches all locales. | 93 // Empty locale and exclude_locale lists matches all locales. |
| 114 if (filter.locale_size() == 0 && filter.exclude_locale_size() == 0) | 94 if (filter.locale_size() == 0 && filter.exclude_locale_size() == 0) |
| 115 return true; | 95 return true; |
| 116 | 96 |
| 117 // Check if we are supposed to filter for a specified set of countries. Note | 97 // Check if we are supposed to filter for a specified set of countries. Note |
| 118 // that this means this overrides the exclude_locale in case that ever occurs | 98 // that this means this overrides the exclude_locale in case that ever occurs |
| 119 // (which it shouldn't). | 99 // (which it shouldn't). |
| 120 if (filter.locale_size() > 0) | 100 if (filter.locale_size() > 0) |
| 121 return base::ContainsValue(filter.locale(), locale); | 101 return base::ContainsValue(filter.locale(), locale); |
| 122 | 102 |
| 123 // Omit if matches any of the exclude entries. | 103 // Omit if matches any of the exclude entries. |
| 124 return !base::ContainsValue(filter.exclude_locale(), locale); | 104 return !base::ContainsValue(filter.exclude_locale(), locale); |
| 125 } | 105 } |
| 126 | 106 |
| 127 bool CheckStudyPlatform(const Study_Filter& filter, Study_Platform platform) { | 107 bool CheckStudyPlatform(const Study::Filter& filter, Study::Platform platform) { |
| 128 // An empty platform list matches all platforms. | 108 // An empty platform list matches all platforms. |
| 129 if (filter.platform_size() == 0) | 109 if (filter.platform_size() == 0) |
| 130 return true; | 110 return true; |
| 131 | 111 |
| 132 for (int i = 0; i < filter.platform_size(); ++i) { | 112 for (int i = 0; i < filter.platform_size(); ++i) { |
| 133 if (filter.platform(i) == platform) | 113 if (filter.platform(i) == platform) |
| 134 return true; | 114 return true; |
| 135 } | 115 } |
| 136 return false; | 116 return false; |
| 137 } | 117 } |
| 138 | 118 |
| 139 bool CheckStudyStartDate(const Study_Filter& filter, | 119 bool CheckStudyStartDate(const Study::Filter& filter, |
| 140 const base::Time& date_time) { | 120 const base::Time& date_time) { |
| 141 if (filter.has_start_date()) { | 121 if (filter.has_start_date()) { |
| 142 const base::Time start_date = | 122 const base::Time start_date = |
| 143 ConvertStudyDateToBaseTime(filter.start_date()); | 123 ConvertStudyDateToBaseTime(filter.start_date()); |
| 144 return date_time >= start_date; | 124 return date_time >= start_date; |
| 145 } | 125 } |
| 146 | 126 |
| 147 return true; | 127 return true; |
| 148 } | 128 } |
| 149 | 129 |
| 150 bool CheckStudyEndDate(const Study_Filter& filter, | 130 bool CheckStudyEndDate(const Study::Filter& filter, |
| 151 const base::Time& date_time) { | 131 const base::Time& date_time) { |
| 152 if (filter.has_end_date()) { | 132 if (filter.has_end_date()) { |
| 153 const base::Time end_date = ConvertStudyDateToBaseTime(filter.end_date()); | 133 const base::Time end_date = ConvertStudyDateToBaseTime(filter.end_date()); |
| 154 return end_date >= date_time; | 134 return end_date >= date_time; |
| 155 } | 135 } |
| 156 | 136 |
| 157 return true; | 137 return true; |
| 158 } | 138 } |
| 159 | 139 |
| 160 bool CheckStudyVersion(const Study_Filter& filter, | 140 bool CheckStudyVersion(const Study::Filter& filter, |
| 161 const base::Version& version) { | 141 const base::Version& version) { |
| 162 if (filter.has_min_version()) { | 142 if (filter.has_min_version()) { |
| 163 if (version.CompareToWildcardString(filter.min_version()) < 0) | 143 if (version.CompareToWildcardString(filter.min_version()) < 0) |
| 164 return false; | 144 return false; |
| 165 } | 145 } |
| 166 | 146 |
| 167 if (filter.has_max_version()) { | 147 if (filter.has_max_version()) { |
| 168 if (version.CompareToWildcardString(filter.max_version()) > 0) | 148 if (version.CompareToWildcardString(filter.max_version()) > 0) |
| 169 return false; | 149 return false; |
| 170 } | 150 } |
| 171 | 151 |
| 172 return true; | 152 return true; |
| 173 } | 153 } |
| 174 | 154 |
| 175 bool CheckStudyCountry(const Study_Filter& filter, const std::string& country) { | 155 bool CheckStudyCountry(const Study::Filter& filter, |
| 156 const std::string& country) { | |
| 176 // Empty country and exclude_country matches all. | 157 // Empty country and exclude_country matches all. |
| 177 if (filter.country_size() == 0 && filter.exclude_country_size() == 0) | 158 if (filter.country_size() == 0 && filter.exclude_country_size() == 0) |
| 178 return true; | 159 return true; |
| 179 | 160 |
| 180 // Checks if we are supposed to filter for a specified set of countries. Note | 161 // Checks if we are supposed to filter for a specified set of countries. Note |
| 181 // that this means this overrides the exclude_country in case that ever occurs | 162 // that this means this overrides the exclude_country in case that ever occurs |
| 182 // (which it shouldn't). | 163 // (which it shouldn't). |
| 183 if (filter.country_size() > 0) | 164 if (filter.country_size() > 0) |
| 184 return base::ContainsValue(filter.country(), country); | 165 return base::ContainsValue(filter.country(), country); |
| 185 | 166 |
| 186 // Omit if matches any of the exclude entries. | 167 // Omit if matches any of the exclude entries. |
| 187 return !base::ContainsValue(filter.exclude_country(), country); | 168 return !base::ContainsValue(filter.exclude_country(), country); |
| 188 } | 169 } |
| 189 | 170 |
| 171 const std::string& GetStudyCountry(const Study& study, | |
|
Alexei Svitkine (slow)
2017/06/07 15:18:08
Nit: I think name is misleading - since this is ge
Ilya Sherman
2017/06/07 21:50:08
Done. (I dropped "State" though, i.e. GetClientCo
| |
| 172 const ClientFilterableState& client_state) { | |
| 173 switch (study.consistency()) { | |
| 174 case Study::SESSION: | |
| 175 return client_state.session_consistency_country; | |
| 176 case Study::PERMANENT: | |
| 177 // Use the saved country for permanent consistency studies. This allows | |
| 178 // Chrome to use the same country for filtering permanent consistency | |
| 179 // studies between Chrome upgrades. Since some studies have user-visible | |
| 180 // effects, this helps to avoid annoying users with experimental group | |
| 181 // churn while traveling. | |
| 182 return client_state.permanent_consistency_country; | |
| 183 } | |
| 184 | |
| 185 // Unless otherwise specified, use an empty country that won't pass any | |
| 186 // filters that specifically include countries, but will pass any filters | |
| 187 // that specifically exclude countries. | |
| 188 return base::EmptyString(); | |
| 189 } | |
| 190 | |
| 190 bool IsStudyExpired(const Study& study, const base::Time& date_time) { | 191 bool IsStudyExpired(const Study& study, const base::Time& date_time) { |
| 191 if (study.has_expiry_date()) { | 192 if (study.has_expiry_date()) { |
| 192 const base::Time expiry_date = | 193 const base::Time expiry_date = |
| 193 ConvertStudyDateToBaseTime(study.expiry_date()); | 194 ConvertStudyDateToBaseTime(study.expiry_date()); |
| 194 return date_time >= expiry_date; | 195 return date_time >= expiry_date; |
| 195 } | 196 } |
| 196 | 197 |
| 197 return false; | 198 return false; |
| 198 } | 199 } |
| 199 | 200 |
| 200 bool ShouldAddStudy( | 201 bool ShouldAddStudy(const Study& study, |
| 201 const Study& study, | 202 const ClientFilterableState& client_state) { |
| 202 const std::string& locale, | |
| 203 const base::Time& reference_date, | |
| 204 const base::Version& version, | |
| 205 Study_Channel channel, | |
| 206 Study_FormFactor form_factor, | |
| 207 const std::string& hardware_class, | |
| 208 const std::string& country) { | |
| 209 if (study.has_filter()) { | 203 if (study.has_filter()) { |
| 210 if (!CheckStudyChannel(study.filter(), channel)) { | 204 if (!CheckStudyChannel(study.filter(), client_state.channel)) { |
| 211 DVLOG(1) << "Filtered out study " << study.name() << " due to channel."; | 205 DVLOG(1) << "Filtered out study " << study.name() << " due to channel."; |
| 212 return false; | 206 return false; |
| 213 } | 207 } |
| 214 | 208 |
| 215 if (!CheckStudyFormFactor(study.filter(), form_factor)) { | 209 if (!CheckStudyFormFactor(study.filter(), client_state.form_factor)) { |
| 216 DVLOG(1) << "Filtered out study " << study.name() << | 210 DVLOG(1) << "Filtered out study " << study.name() << |
| 217 " due to form factor."; | 211 " due to form factor."; |
| 218 return false; | 212 return false; |
| 219 } | 213 } |
| 220 | 214 |
| 221 if (!CheckStudyLocale(study.filter(), locale)) { | 215 if (!CheckStudyLocale(study.filter(), client_state.locale)) { |
| 222 DVLOG(1) << "Filtered out study " << study.name() << " due to locale."; | 216 DVLOG(1) << "Filtered out study " << study.name() << " due to locale."; |
| 223 return false; | 217 return false; |
| 224 } | 218 } |
| 225 | 219 |
| 226 if (!CheckStudyPlatform(study.filter(), GetCurrentPlatform())) { | 220 if (!CheckStudyPlatform(study.filter(), client_state.platform)) { |
| 227 DVLOG(1) << "Filtered out study " << study.name() << " due to platform."; | 221 DVLOG(1) << "Filtered out study " << study.name() << " due to platform."; |
| 228 return false; | 222 return false; |
| 229 } | 223 } |
| 230 | 224 |
| 231 if (!CheckStudyVersion(study.filter(), version)) { | 225 if (!CheckStudyVersion(study.filter(), client_state.version)) { |
| 232 DVLOG(1) << "Filtered out study " << study.name() << " due to version."; | 226 DVLOG(1) << "Filtered out study " << study.name() << " due to version."; |
| 233 return false; | 227 return false; |
| 234 } | 228 } |
| 235 | 229 |
| 236 if (!CheckStudyStartDate(study.filter(), reference_date)) { | 230 if (!CheckStudyStartDate(study.filter(), client_state.reference_date)) { |
| 237 DVLOG(1) << "Filtered out study " << study.name() << | 231 DVLOG(1) << "Filtered out study " << study.name() << |
| 238 " due to start date."; | 232 " due to start date."; |
| 239 return false; | 233 return false; |
| 240 } | 234 } |
| 241 | 235 |
| 242 if (!CheckStudyEndDate(study.filter(), reference_date)) { | 236 if (!CheckStudyEndDate(study.filter(), client_state.reference_date)) { |
| 243 DVLOG(1) << "Filtered out study " << study.name() << " due to end date."; | 237 DVLOG(1) << "Filtered out study " << study.name() << " due to end date."; |
| 244 return false; | 238 return false; |
| 245 } | 239 } |
| 246 | 240 |
| 247 if (!CheckStudyHardwareClass(study.filter(), hardware_class)) { | 241 if (!CheckStudyHardwareClass(study.filter(), client_state.hardware_class)) { |
| 248 DVLOG(1) << "Filtered out study " << study.name() << | 242 DVLOG(1) << "Filtered out study " << study.name() << |
| 249 " due to hardware_class."; | 243 " due to hardware_class."; |
| 250 return false; | 244 return false; |
| 251 } | 245 } |
| 252 | 246 |
| 247 const std::string& country = GetStudyCountry(study, client_state); | |
| 253 if (!CheckStudyCountry(study.filter(), country)) { | 248 if (!CheckStudyCountry(study.filter(), country)) { |
| 254 DVLOG(1) << "Filtered out study " << study.name() << " due to country."; | 249 DVLOG(1) << "Filtered out study " << study.name() << " due to country."; |
| 255 return false; | 250 return false; |
| 256 } | 251 } |
| 257 } | 252 } |
| 258 | 253 |
| 259 DVLOG(1) << "Kept study " << study.name() << "."; | 254 DVLOG(1) << "Kept study " << study.name() << "."; |
| 260 return true; | 255 return true; |
| 261 } | 256 } |
| 262 | 257 |
| 263 } // namespace internal | 258 } // namespace internal |
| 264 | 259 |
| 265 void FilterAndValidateStudies(const VariationsSeed& seed, | 260 void FilterAndValidateStudies(const VariationsSeed& seed, |
| 266 const std::string& locale, | 261 const ClientFilterableState& client_state, |
| 267 const base::Time& reference_date, | |
| 268 const base::Version& version, | |
| 269 Study_Channel channel, | |
| 270 Study_FormFactor form_factor, | |
| 271 const std::string& hardware_class, | |
| 272 const std::string& session_consistency_country, | |
| 273 const std::string& permanent_consistency_country, | |
| 274 std::vector<ProcessedStudy>* filtered_studies) { | 262 std::vector<ProcessedStudy>* filtered_studies) { |
| 275 DCHECK(version.IsValid()); | 263 DCHECK(client_state.version.IsValid()); |
| 276 | 264 |
| 277 // Add expired studies (in a disabled state) only after all the non-expired | 265 // Add expired studies (in a disabled state) only after all the non-expired |
| 278 // studies have been added (and do not add an expired study if a corresponding | 266 // studies have been added (and do not add an expired study if a corresponding |
| 279 // non-expired study got added). This way, if there's both an expired and a | 267 // non-expired study got added). This way, if there's both an expired and a |
| 280 // non-expired study that applies, the non-expired study takes priority. | 268 // non-expired study that applies, the non-expired study takes priority. |
| 281 std::set<std::string> created_studies; | 269 std::set<std::string> created_studies; |
| 282 std::vector<const Study*> expired_studies; | 270 std::vector<const Study*> expired_studies; |
| 283 | 271 |
| 284 for (int i = 0; i < seed.study_size(); ++i) { | 272 for (int i = 0; i < seed.study_size(); ++i) { |
| 285 const Study& study = seed.study(i); | 273 const Study& study = seed.study(i); |
| 274 if (!internal::ShouldAddStudy(study, client_state)) | |
| 275 continue; | |
| 286 | 276 |
| 287 // Unless otherwise specified, use an empty country that won't pass any | 277 if (internal::IsStudyExpired(study, client_state.reference_date)) { |
| 288 // filters that specifically include countries, but will pass any filters | |
| 289 // that specifically exclude countries. | |
| 290 std::string country; | |
| 291 switch (study.consistency()) { | |
| 292 case Study_Consistency_SESSION: | |
| 293 country = session_consistency_country; | |
| 294 break; | |
| 295 case Study_Consistency_PERMANENT: | |
| 296 // Use the saved |permanent_consistency_country| for permanent | |
| 297 // consistency studies. This allows Chrome to use the same country for | |
| 298 // filtering permanent consistency studies between Chrome upgrades. | |
| 299 // Since some studies have user-visible effects, this helps to avoid | |
| 300 // annoying users with experimental group churn while traveling. | |
| 301 country = permanent_consistency_country; | |
| 302 break; | |
| 303 } | |
| 304 | |
| 305 if (!internal::ShouldAddStudy(study, locale, reference_date, version, | |
| 306 channel, form_factor, hardware_class, | |
| 307 country)) { | |
| 308 continue; | |
| 309 } | |
| 310 | |
| 311 if (internal::IsStudyExpired(study, reference_date)) { | |
| 312 expired_studies.push_back(&study); | 278 expired_studies.push_back(&study); |
| 313 } else if (!base::ContainsKey(created_studies, study.name())) { | 279 } else if (!base::ContainsKey(created_studies, study.name())) { |
| 314 ProcessedStudy::ValidateAndAppendStudy(&study, false, filtered_studies); | 280 ProcessedStudy::ValidateAndAppendStudy(&study, false, filtered_studies); |
| 315 created_studies.insert(study.name()); | 281 created_studies.insert(study.name()); |
| 316 } | 282 } |
| 317 } | 283 } |
| 318 | 284 |
| 319 for (size_t i = 0; i < expired_studies.size(); ++i) { | 285 for (size_t i = 0; i < expired_studies.size(); ++i) { |
| 320 if (!base::ContainsKey(created_studies, expired_studies[i]->name())) { | 286 if (!base::ContainsKey(created_studies, expired_studies[i]->name())) { |
| 321 ProcessedStudy::ValidateAndAppendStudy(expired_studies[i], true, | 287 ProcessedStudy::ValidateAndAppendStudy(expired_studies[i], true, |
| 322 filtered_studies); | 288 filtered_studies); |
| 323 } | 289 } |
| 324 } | 290 } |
| 325 } | 291 } |
| 326 | 292 |
| 327 } // namespace variations | 293 } // namespace variations |
| OLD | NEW |