1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/omnibox/omnibox_field_trial.h" 6 7 #include <string> 8 9 #include "base/metrics/field_trial.h" 10 #include "base/strings/string_number_conversions.h" 11 #include "base/strings/string_split.h" 12 #include "base/strings/string_util.h" 13 #include "base/strings/stringprintf.h" 14 #include "chrome/browser/autocomplete/autocomplete_input.h" 15 #include "chrome/browser/search/search.h" 16 #include "chrome/common/metrics/variations/variation_ids.h" 17 #include "chrome/common/metrics/variations/variations_util.h" 18 #include "components/variations/metrics_util.h" 19 20 namespace { 21 22 // Field trial names. 23 const char kHUPCullRedirectsFieldTrialName[] = "OmniboxHUPCullRedirects"; 24 const char kHUPCreateShorterMatchFieldTrialName[] = 25 "OmniboxHUPCreateShorterMatch"; 26 const char kStopTimerFieldTrialName[] = "OmniboxStopTimer"; 27 const char kEnableZeroSuggestGroupPrefix[] = "EnableZeroSuggest"; 28 const char kEnableZeroSuggestMostVisitedGroupPrefix[] = 29 "EnableZeroSuggestMostVisited"; 30 const char kEnableZeroSuggestAfterTypingGroupPrefix[] = 31 "EnableZeroSuggestAfterTyping"; 32 33 // The autocomplete dynamic field trial name prefix. Each field trial is 34 // configured dynamically and is retrieved automatically by Chrome during 35 // the startup. 36 const char kAutocompleteDynamicFieldTrialPrefix[] = "AutocompleteDynamicTrial_"; 37 // The maximum number of the autocomplete dynamic field trials (aka layers). 38 const int kMaxAutocompleteDynamicFieldTrials = 5; 39 40 // Field trial experiment probabilities. 41 42 // For HistoryURL provider cull redirects field trial, put 0% ( = 0/100 ) 43 // of the users in the don't-cull-redirects experiment group. 44 // TODO(mpearson): Remove this field trial and the code it uses once I'm 45 // sure it's no longer needed. 46 const base::FieldTrial::Probability kHUPCullRedirectsFieldTrialDivisor = 100; 47 const base::FieldTrial::Probability 48 kHUPCullRedirectsFieldTrialExperimentFraction = 0; 49 50 // For HistoryURL provider create shorter match field trial, put 0% 51 // ( = 25/100 ) of the users in the don't-create-a-shorter-match 52 // experiment group. 53 // TODO(mpearson): Remove this field trial and the code it uses once I'm 54 // sure it's no longer needed. 55 const base::FieldTrial::Probability 56 kHUPCreateShorterMatchFieldTrialDivisor = 100; 57 const base::FieldTrial::Probability 58 kHUPCreateShorterMatchFieldTrialExperimentFraction = 0; 59 60 // Experiment group names. 61 62 const char kStopTimerExperimentGroupName[] = "UseStopTimer"; 63 64 // Field trial IDs. 65 // Though they are not literally "const", they are set only once, in 66 // ActivateStaticTrials() below. 67 68 // Whether the static field trials have been initialized by 69 // ActivateStaticTrials() method. 70 bool static_field_trials_initialized = false; 71 72 // Field trial ID for the HistoryURL provider cull redirects experiment group. 73 int hup_dont_cull_redirects_experiment_group = 0; 74 75 // Field trial ID for the HistoryURL provider create shorter match 76 // experiment group. 77 int hup_dont_create_shorter_match_experiment_group = 0; 78 79 80 // Concatenates the autocomplete dynamic field trial prefix with a field trial 81 // ID to form a complete autocomplete field trial name. 82 std::string DynamicFieldTrialName(int id) { 83 return base::StringPrintf("%s%d", kAutocompleteDynamicFieldTrialPrefix, id); 84 } 85 86 } // namespace 87 88 89 void OmniboxFieldTrial::ActivateStaticTrials() { 90 DCHECK(!static_field_trials_initialized); 91 92 // Create the HistoryURL provider cull redirects field trial. 93 // Make it expire on March 1, 2013. 94 scoped_refptr<base::FieldTrial> trial( 95 base::FieldTrialList::FactoryGetFieldTrial( 96 kHUPCullRedirectsFieldTrialName, kHUPCullRedirectsFieldTrialDivisor, 97 "Standard", 2013, 3, 1, base::FieldTrial::ONE_TIME_RANDOMIZED, NULL)); 98 hup_dont_cull_redirects_experiment_group = 99 trial->AppendGroup("DontCullRedirects", 100 kHUPCullRedirectsFieldTrialExperimentFraction); 101 102 // Create the HistoryURL provider create shorter match field trial. 103 // Make it expire on March 1, 2013. 104 trial = base::FieldTrialList::FactoryGetFieldTrial( 105 kHUPCreateShorterMatchFieldTrialName, 106 kHUPCreateShorterMatchFieldTrialDivisor, "Standard", 2013, 3, 1, 107 base::FieldTrial::ONE_TIME_RANDOMIZED, NULL); 108 hup_dont_create_shorter_match_experiment_group = 109 trial->AppendGroup("DontCreateShorterMatch", 110 kHUPCreateShorterMatchFieldTrialExperimentFraction); 111 112 static_field_trials_initialized = true; 113 } 114 115 void OmniboxFieldTrial::ActivateDynamicTrials() { 116 // Initialize all autocomplete dynamic field trials. This method may be 117 // called multiple times. 118 for (int i = 0; i < kMaxAutocompleteDynamicFieldTrials; ++i) 119 base::FieldTrialList::FindValue(DynamicFieldTrialName(i)); 120 } 121 122 int OmniboxFieldTrial::GetDisabledProviderTypes() { 123 // Make sure that Autocomplete dynamic field trials are activated. It's OK to 124 // call this method multiple times. 125 ActivateDynamicTrials(); 126 127 // Look for group names in form of "DisabledProviders_<mask>" where "mask" 128 // is a bitmap of disabled provider types (AutocompleteProvider::Type). 129 int provider_types = 0; 130 for (int i = 0; i < kMaxAutocompleteDynamicFieldTrials; ++i) { 131 std::string group_name = base::FieldTrialList::FindFullName( 132 DynamicFieldTrialName(i)); 133 const char kDisabledProviders[] = "DisabledProviders_"; 134 if (!StartsWithASCII(group_name, kDisabledProviders, true)) 135 continue; 136 int types = 0; 137 if (!base::StringToInt(base::StringPiece( 138 group_name.substr(strlen(kDisabledProviders))), &types)) 139 continue; 140 provider_types |= types; 141 } 142 return provider_types; 143 } 144 145 void OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes( 146 std::vector<uint32>* field_trial_hashes) { 147 field_trial_hashes->clear(); 148 for (int i = 0; i < kMaxAutocompleteDynamicFieldTrials; ++i) { 149 const std::string& trial_name = DynamicFieldTrialName(i); 150 if (base::FieldTrialList::TrialExists(trial_name)) 151 field_trial_hashes->push_back(metrics::HashName(trial_name)); 152 } 153 } 154 155 bool OmniboxFieldTrial::InHUPCullRedirectsFieldTrial() { 156 return base::FieldTrialList::TrialExists(kHUPCullRedirectsFieldTrialName); 157 } 158 159 bool OmniboxFieldTrial::InHUPCullRedirectsFieldTrialExperimentGroup() { 160 if (!base::FieldTrialList::TrialExists(kHUPCullRedirectsFieldTrialName)) 161 return false; 162 163 // Return true if we're in the experiment group. 164 const int group = base::FieldTrialList::FindValue( 165 kHUPCullRedirectsFieldTrialName); 166 return group == hup_dont_cull_redirects_experiment_group; 167 } 168 169 bool OmniboxFieldTrial::InHUPCreateShorterMatchFieldTrial() { 170 return 171 base::FieldTrialList::TrialExists(kHUPCreateShorterMatchFieldTrialName); 172 } 173 174 bool OmniboxFieldTrial::InHUPCreateShorterMatchFieldTrialExperimentGroup() { 175 if (!base::FieldTrialList::TrialExists(kHUPCreateShorterMatchFieldTrialName)) 176 return false; 177 178 // Return true if we're in the experiment group. 179 const int group = base::FieldTrialList::FindValue( 180 kHUPCreateShorterMatchFieldTrialName); 181 return group == hup_dont_create_shorter_match_experiment_group; 182 } 183 184 bool OmniboxFieldTrial::InStopTimerFieldTrialExperimentGroup() { 185 return (base::FieldTrialList::FindFullName(kStopTimerFieldTrialName) == 186 kStopTimerExperimentGroupName); 187 } 188 189 bool OmniboxFieldTrial::HasDynamicFieldTrialGroupPrefix( 190 const char* group_prefix) { 191 // Make sure that Autocomplete dynamic field trials are activated. It's OK to 192 // call this method multiple times. 193 ActivateDynamicTrials(); 194 195 // Look for group names starting with |group_prefix|. 196 for (int i = 0; i < kMaxAutocompleteDynamicFieldTrials; ++i) { 197 const std::string& group_name = base::FieldTrialList::FindFullName( 198 DynamicFieldTrialName(i)); 199 if (StartsWithASCII(group_name, group_prefix, true)) 200 return true; 201 } 202 return false; 203 } 204 205 bool OmniboxFieldTrial::InZeroSuggestFieldTrial() { 206 return HasDynamicFieldTrialGroupPrefix(kEnableZeroSuggestGroupPrefix); 207 } 208 209 bool OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial() { 210 return HasDynamicFieldTrialGroupPrefix( 211 kEnableZeroSuggestMostVisitedGroupPrefix); 212 } 213 214 bool OmniboxFieldTrial::InZeroSuggestAfterTypingFieldTrial() { 215 return HasDynamicFieldTrialGroupPrefix( 216 kEnableZeroSuggestAfterTypingGroupPrefix); 217 } 218 219 bool OmniboxFieldTrial::ShortcutsScoringMaxRelevance( 220 AutocompleteInput::PageClassification current_page_classification, 221 int* max_relevance) { 222 // The value of the rule is a string that encodes an integer containing 223 // the max relevance. 224 const std::string& max_relevance_str = 225 OmniboxFieldTrial::GetValueForRuleInContext( 226 kShortcutsScoringMaxRelevanceRule, current_page_classification); 227 if (max_relevance_str.empty()) 228 return false; 229 if (!base::StringToInt(max_relevance_str, max_relevance)) 230 return false; 231 return true; 232 } 233 234 bool OmniboxFieldTrial::SearchHistoryPreventInlining( 235 AutocompleteInput::PageClassification current_page_classification) { 236 return OmniboxFieldTrial::GetValueForRuleInContext( 237 kSearchHistoryRule, current_page_classification) == "PreventInlining"; 238 } 239 240 bool OmniboxFieldTrial::SearchHistoryDisable( 241 AutocompleteInput::PageClassification current_page_classification) { 242 return OmniboxFieldTrial::GetValueForRuleInContext( 243 kSearchHistoryRule, current_page_classification) == "Disable"; 244 } 245 246 void OmniboxFieldTrial::GetDemotionsByType( 247 AutocompleteInput::PageClassification current_page_classification, 248 DemotionMultipliers* demotions_by_type) { 249 demotions_by_type->clear(); 250 const std::string demotion_rule = 251 OmniboxFieldTrial::GetValueForRuleInContext( 252 kDemoteByTypeRule, 253 current_page_classification); 254 // The value of the DemoteByType rule is a comma-separated list of 255 // {ResultType + ":" + Number} where ResultType is an AutocompleteMatchType:: 256 // Type enum represented as an integer and Number is an integer number 257 // between 0 and 100 inclusive. Relevance scores of matches of that result 258 // type are multiplied by Number / 100. 100 means no change. 259 base::StringPairs kv_pairs; 260 if (base::SplitStringIntoKeyValuePairs(demotion_rule, ':', ',', &kv_pairs)) { 261 for (base::StringPairs::const_iterator it = kv_pairs.begin(); 262 it != kv_pairs.end(); ++it) { 263 // This is a best-effort conversion; we trust the hand-crafted parameters 264 // downloaded from the server to be perfect. There's no need to handle 265 // errors smartly. 266 int k, v; 267 base::StringToInt(it->first, &k); 268 base::StringToInt(it->second, &v); 269 (*demotions_by_type)[static_cast<AutocompleteMatchType::Type>(k)] = 270 static_cast<float>(v) / 100.0f; 271 } 272 } 273 } 274 275 OmniboxFieldTrial::UndemotableTopMatchTypes 276 OmniboxFieldTrial::GetUndemotableTopTypes( 277 AutocompleteInput::PageClassification current_page_classification) { 278 UndemotableTopMatchTypes undemotable_types; 279 const std::string types_rule = 280 OmniboxFieldTrial::GetValueForRuleInContext( 281 kUndemotableTopTypeRule, 282 current_page_classification); 283 // The value of the UndemotableTopTypes rule is a comma-separated list of 284 // AutocompleteMatchType::Type enums represented as an integer. The 285 // DemoteByType rule does not apply to the top match if the type of the top 286 // match is in this list. 287 std::vector<std::string> types; 288 base::SplitString(types_rule, ',', &types); 289 for (std::vector<std::string>::const_iterator it = types.begin(); 290 it != types.end(); ++it) { 291 // This is a best-effort conversion; we trust the hand-crafted parameters 292 // downloaded from the server to be perfect. There's no need to handle 293 // errors smartly. 294 int t; 295 base::StringToInt(*it, &t); 296 undemotable_types.insert(static_cast<AutocompleteMatchType::Type>(t)); 297 } 298 return undemotable_types; 299 } 300 301 bool OmniboxFieldTrial::ReorderForLegalDefaultMatch( 302 AutocompleteInput::PageClassification current_page_classification) { 303 return OmniboxFieldTrial::GetValueForRuleInContext( 304 kReorderForLegalDefaultMatchRule, current_page_classification) != 305 kReorderForLegalDefaultMatchRuleDisabled; 306 } 307 308 int OmniboxFieldTrial::HQPBookmarkValue() { 309 std::string bookmark_value_str = chrome_variations:: 310 GetVariationParamValue(kBundledExperimentFieldTrialName, 311 kHQPBookmarkValueRule); 312 if (bookmark_value_str.empty()) 313 return 1; 314 // This is a best-effort conversion; we trust the hand-crafted parameters 315 // downloaded from the server to be perfect. There's no need for handle 316 // errors smartly. 317 int bookmark_value; 318 base::StringToInt(bookmark_value_str, &bookmark_value); 319 return bookmark_value; 320 } 321 322 bool OmniboxFieldTrial::HQPDiscountFrecencyWhenFewVisits() { 323 std::string discount_frecency_str = chrome_variations:: 324 GetVariationParamValue(kBundledExperimentFieldTrialName, 325 kHQPDiscountFrecencyWhenFewVisitsRule); 326 return discount_frecency_str == "true"; 327 } 328 329 bool OmniboxFieldTrial::HQPAllowMatchInTLDValue() { 330 std::string allow_match_in_tld_str = chrome_variations:: 331 GetVariationParamValue(kBundledExperimentFieldTrialName, 332 kHQPAllowMatchInTLDRule); 333 return allow_match_in_tld_str == "true"; 334 } 335 336 bool OmniboxFieldTrial::HQPAllowMatchInSchemeValue() { 337 std::string allow_match_in_scheme_str = chrome_variations:: 338 GetVariationParamValue(kBundledExperimentFieldTrialName, 339 kHQPAllowMatchInSchemeRule); 340 return allow_match_in_scheme_str == "true"; 341 } 342 343 const char OmniboxFieldTrial::kBundledExperimentFieldTrialName[] = 344 "OmniboxBundledExperimentV1"; 345 const char OmniboxFieldTrial::kShortcutsScoringMaxRelevanceRule[] = 346 "ShortcutsScoringMaxRelevance"; 347 const char OmniboxFieldTrial::kSearchHistoryRule[] = "SearchHistory"; 348 const char OmniboxFieldTrial::kDemoteByTypeRule[] = "DemoteByType"; 349 const char OmniboxFieldTrial::kUndemotableTopTypeRule[] = "UndemotableTopTypes"; 350 const char OmniboxFieldTrial::kReorderForLegalDefaultMatchRule[] = 351 "ReorderForLegalDefaultMatch"; 352 const char OmniboxFieldTrial::kHQPBookmarkValueRule[] = 353 "HQPBookmarkValue"; 354 const char OmniboxFieldTrial::kHQPDiscountFrecencyWhenFewVisitsRule[] = 355 "HQPDiscountFrecencyWhenFewVisits"; 356 const char OmniboxFieldTrial::kHQPAllowMatchInTLDRule[] = "HQPAllowMatchInTLD"; 357 const char OmniboxFieldTrial::kHQPAllowMatchInSchemeRule[] = 358 "HQPAllowMatchInScheme"; 359 const char OmniboxFieldTrial::kReorderForLegalDefaultMatchRuleDisabled[] = 360 "DontReorderForLegalDefaultMatch"; 361 362 // Background and implementation details: 363 // 364 // Each experiment group in any field trial can come with an optional set of 365 // parameters (key-value pairs). In the bundled omnibox experiment 366 // (kBundledExperimentFieldTrialName), each experiment group comes with a 367 // list of parameters in the form: 368 // key=<Rule>: 369 // <AutocompleteInput::PageClassification (as an int)>: 370 // <whether Instant Extended is enabled (as a 1 or 0)> 371 // (note that there are no linebreaks in keys; this format is for 372 // presentation only> 373 // value=<arbitrary string> 374 // Both the AutocompleteInput::PageClassification and the Instant Extended 375 // entries can be "*", which means this rule applies for all values of the 376 // matching portion of the context. 377 // One example parameter is 378 // key=SearchHistory:6:1 379 // value=PreventInlining 380 // This means in page classification context 6 (a search result page doing 381 // search term replacement) with Instant Extended enabled, the SearchHistory 382 // experiment should PreventInlining. 383 // 384 // When an exact match to the rule in the current context is missing, we 385 // give preference to a wildcard rule that matches the instant extended 386 // context over a wildcard rule that matches the page classification 387 // context. Hopefully, though, users will write their field trial configs 388 // so as not to rely on this fall back order. 389 // 390 // In short, this function tries to find the value associated with key 391 // |rule|:|page_classification|:|instant_extended|, failing that it looks up 392 // |rule|:*:|instant_extended|, failing that it looks up 393 // |rule|:|page_classification|:*, failing that it looks up |rule|:*:*, 394 // and failing that it returns the empty string. 395 std::string OmniboxFieldTrial::GetValueForRuleInContext( 396 const std::string& rule, 397 AutocompleteInput::PageClassification page_classification) { 398 std::map<std::string, std::string> params; 399 if (!chrome_variations::GetVariationParams(kBundledExperimentFieldTrialName, 400 ¶ms)) { 401 return std::string(); 402 } 403 const std::string page_classification_str = 404 base::IntToString(static_cast<int>(page_classification)); 405 const std::string instant_extended = 406 chrome::IsInstantExtendedAPIEnabled() ? "1" : "0"; 407 // Look up rule in this exact context. 408 std::map<std::string, std::string>::iterator it = params.find( 409 rule + ":" + page_classification_str + ":" + instant_extended); 410 if (it != params.end()) 411 return it->second; 412 // Fall back to the global page classification context. 413 it = params.find(rule + ":*:" + instant_extended); 414 if (it != params.end()) 415 return it->second; 416 // Fall back to the global instant extended context. 417 it = params.find(rule + ":" + page_classification_str + ":*"); 418 if (it != params.end()) 419 return it->second; 420 // Look up rule in the global context. 421 it = params.find(rule + ":*:*"); 422 return (it != params.end()) ? it->second : std::string(); 423 } 424