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.
5 #include "chrome/browser/omnibox/omnibox_field_trial.h"
10 #include "base/metrics/field_trial.h"
11 #include "base/strings/string_number_conversions.h"
12 #include "base/strings/string_split.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/stringprintf.h"
15 #include "base/time/time.h"
16 #include "chrome/browser/autocomplete/autocomplete_input.h"
17 #include "chrome/browser/search/search.h"
18 #include "chrome/common/metrics/variations/variation_ids.h"
19 #include "components/variations/active_field_trials.h"
20 #include "components/variations/metrics_util.h"
21 #include "components/variations/variations_associated_data.h"
25 typedef std::map
<std::string
, std::string
> VariationParams
;
26 typedef HUPScoringParams::ScoreBuckets ScoreBuckets
;
29 const char kHUPCullRedirectsFieldTrialName
[] = "OmniboxHUPCullRedirects";
30 const char kHUPCreateShorterMatchFieldTrialName
[] =
31 "OmniboxHUPCreateShorterMatch";
32 const char kStopTimerFieldTrialName
[] = "OmniboxStopTimer";
34 // In dynamic field trials, we use these group names to switch between
35 // different zero suggest implementations.
36 const char kEnableZeroSuggestGroupPrefix
[] = "EnableZeroSuggest";
37 const char kEnableZeroSuggestMostVisitedGroupPrefix
[] =
38 "EnableZeroSuggestMostVisited";
39 const char kEnableZeroSuggestAfterTypingGroupPrefix
[] =
40 "EnableZeroSuggestAfterTyping";
41 const char kEnableZeroSuggestPersonalizedGroupPrefix
[] =
42 "EnableZeroSuggestPersonalized";
44 // The autocomplete dynamic field trial name prefix. Each field trial is
45 // configured dynamically and is retrieved automatically by Chrome during
47 const char kAutocompleteDynamicFieldTrialPrefix
[] = "AutocompleteDynamicTrial_";
48 // The maximum number of the autocomplete dynamic field trials (aka layers).
49 const int kMaxAutocompleteDynamicFieldTrials
= 5;
51 // Field trial experiment probabilities.
53 // For HistoryURL provider cull redirects field trial, put 0% ( = 0/100 )
54 // of the users in the don't-cull-redirects experiment group.
55 // TODO(mpearson): Remove this field trial and the code it uses once I'm
56 // sure it's no longer needed.
57 const base::FieldTrial::Probability kHUPCullRedirectsFieldTrialDivisor
= 100;
58 const base::FieldTrial::Probability
59 kHUPCullRedirectsFieldTrialExperimentFraction
= 0;
61 // For HistoryURL provider create shorter match field trial, put 0%
62 // ( = 25/100 ) of the users in the don't-create-a-shorter-match
64 // TODO(mpearson): Remove this field trial and the code it uses once I'm
65 // sure it's no longer needed.
66 const base::FieldTrial::Probability
67 kHUPCreateShorterMatchFieldTrialDivisor
= 100;
68 const base::FieldTrial::Probability
69 kHUPCreateShorterMatchFieldTrialExperimentFraction
= 0;
72 // Though they are not literally "const", they are set only once, in
73 // ActivateStaticTrials() below.
75 // Whether the static field trials have been initialized by
76 // ActivateStaticTrials() method.
77 bool static_field_trials_initialized
= false;
79 // Field trial ID for the HistoryURL provider cull redirects experiment group.
80 int hup_dont_cull_redirects_experiment_group
= 0;
82 // Field trial ID for the HistoryURL provider create shorter match
84 int hup_dont_create_shorter_match_experiment_group
= 0;
87 // Concatenates the autocomplete dynamic field trial prefix with a field trial
88 // ID to form a complete autocomplete field trial name.
89 std::string
DynamicFieldTrialName(int id
) {
90 return base::StringPrintf("%s%d", kAutocompleteDynamicFieldTrialPrefix
, id
);
93 void InitializeScoreBuckets(const VariationParams
& params
,
94 const char* relevance_cap_param
,
95 const char* half_life_param
,
96 const char* score_buckets_param
,
97 ScoreBuckets
* score_buckets
) {
98 VariationParams::const_iterator it
= params
.find(relevance_cap_param
);
99 if (it
!= params
.end()) {
101 if (base::StringToInt(it
->second
, &relevance_cap
))
102 score_buckets
->set_relevance_cap(relevance_cap
);
105 it
= params
.find(half_life_param
);
106 if (it
!= params
.end()) {
108 if (base::StringToInt(it
->second
, &half_life_days
))
109 score_buckets
->set_half_life_days(half_life_days
);
112 it
= params
.find(score_buckets_param
);
113 if (it
!= params
.end()) {
114 // The value of the score bucket is a comma-separated list of
115 // {DecayedCount + ":" + MaxRelevance}.
116 base::StringPairs kv_pairs
;
117 if (base::SplitStringIntoKeyValuePairs(it
->second
, ':', ',', &kv_pairs
)) {
118 for (base::StringPairs::const_iterator it
= kv_pairs
.begin();
119 it
!= kv_pairs
.end(); ++it
) {
120 ScoreBuckets::CountMaxRelevance bucket
;
121 base::StringToDouble(it
->first
, &bucket
.first
);
122 base::StringToInt(it
->second
, &bucket
.second
);
123 score_buckets
->buckets().push_back(bucket
);
125 std::sort(score_buckets
->buckets().begin(),
126 score_buckets
->buckets().end(),
127 std::greater
<ScoreBuckets::CountMaxRelevance
>());
134 HUPScoringParams::ScoreBuckets::ScoreBuckets()
135 : relevance_cap_(-1),
136 half_life_days_(-1) {
139 HUPScoringParams::ScoreBuckets::~ScoreBuckets() {
142 double HUPScoringParams::ScoreBuckets::HalfLifeTimeDecay(
143 const base::TimeDelta
& elapsed_time
) const {
145 if ((half_life_days_
<= 0) ||
146 ((time_ms
= elapsed_time
.InMillisecondsF()) <= 0))
149 const double half_life_intervals
=
150 time_ms
/ base::TimeDelta::FromDays(half_life_days_
).InMillisecondsF();
151 return pow(2.0, -half_life_intervals
);
154 void OmniboxFieldTrial::ActivateStaticTrials() {
155 DCHECK(!static_field_trials_initialized
);
157 // Create the HistoryURL provider cull redirects field trial.
158 // Make it expire on March 1, 2013.
159 scoped_refptr
<base::FieldTrial
> trial(
160 base::FieldTrialList::FactoryGetFieldTrial(
161 kHUPCullRedirectsFieldTrialName
, kHUPCullRedirectsFieldTrialDivisor
,
162 "Standard", 2013, 3, 1, base::FieldTrial::ONE_TIME_RANDOMIZED
, NULL
));
163 hup_dont_cull_redirects_experiment_group
=
164 trial
->AppendGroup("DontCullRedirects",
165 kHUPCullRedirectsFieldTrialExperimentFraction
);
167 // Create the HistoryURL provider create shorter match field trial.
168 // Make it expire on March 1, 2013.
169 trial
= base::FieldTrialList::FactoryGetFieldTrial(
170 kHUPCreateShorterMatchFieldTrialName
,
171 kHUPCreateShorterMatchFieldTrialDivisor
, "Standard", 2013, 3, 1,
172 base::FieldTrial::ONE_TIME_RANDOMIZED
, NULL
);
173 hup_dont_create_shorter_match_experiment_group
=
174 trial
->AppendGroup("DontCreateShorterMatch",
175 kHUPCreateShorterMatchFieldTrialExperimentFraction
);
177 static_field_trials_initialized
= true;
180 void OmniboxFieldTrial::ActivateDynamicTrials() {
181 // Initialize all autocomplete dynamic field trials. This method may be
182 // called multiple times.
183 for (int i
= 0; i
< kMaxAutocompleteDynamicFieldTrials
; ++i
)
184 base::FieldTrialList::FindValue(DynamicFieldTrialName(i
));
187 int OmniboxFieldTrial::GetDisabledProviderTypes() {
188 // Make sure that Autocomplete dynamic field trials are activated. It's OK to
189 // call this method multiple times.
190 ActivateDynamicTrials();
192 // Look for group names in form of "DisabledProviders_<mask>" where "mask"
193 // is a bitmap of disabled provider types (AutocompleteProvider::Type).
194 int provider_types
= 0;
195 for (int i
= 0; i
< kMaxAutocompleteDynamicFieldTrials
; ++i
) {
196 std::string group_name
= base::FieldTrialList::FindFullName(
197 DynamicFieldTrialName(i
));
198 const char kDisabledProviders
[] = "DisabledProviders_";
199 if (!StartsWithASCII(group_name
, kDisabledProviders
, true))
202 if (!base::StringToInt(base::StringPiece(
203 group_name
.substr(strlen(kDisabledProviders
))), &types
))
205 provider_types
|= types
;
207 return provider_types
;
210 void OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes(
211 std::vector
<uint32
>* field_trial_hashes
) {
212 field_trial_hashes
->clear();
213 for (int i
= 0; i
< kMaxAutocompleteDynamicFieldTrials
; ++i
) {
214 const std::string
& trial_name
= DynamicFieldTrialName(i
);
215 if (base::FieldTrialList::TrialExists(trial_name
))
216 field_trial_hashes
->push_back(metrics::HashName(trial_name
));
218 if (base::FieldTrialList::TrialExists(kBundledExperimentFieldTrialName
)) {
219 field_trial_hashes
->push_back(
220 metrics::HashName(kBundledExperimentFieldTrialName
));
224 bool OmniboxFieldTrial::InHUPCullRedirectsFieldTrial() {
225 return base::FieldTrialList::TrialExists(kHUPCullRedirectsFieldTrialName
);
228 bool OmniboxFieldTrial::InHUPCullRedirectsFieldTrialExperimentGroup() {
229 if (!base::FieldTrialList::TrialExists(kHUPCullRedirectsFieldTrialName
))
232 // Return true if we're in the experiment group.
233 const int group
= base::FieldTrialList::FindValue(
234 kHUPCullRedirectsFieldTrialName
);
235 return group
== hup_dont_cull_redirects_experiment_group
;
238 bool OmniboxFieldTrial::InHUPCreateShorterMatchFieldTrial() {
240 base::FieldTrialList::TrialExists(kHUPCreateShorterMatchFieldTrialName
);
243 bool OmniboxFieldTrial::InHUPCreateShorterMatchFieldTrialExperimentGroup() {
244 if (!base::FieldTrialList::TrialExists(kHUPCreateShorterMatchFieldTrialName
))
247 // Return true if we're in the experiment group.
248 const int group
= base::FieldTrialList::FindValue(
249 kHUPCreateShorterMatchFieldTrialName
);
250 return group
== hup_dont_create_shorter_match_experiment_group
;
253 base::TimeDelta
OmniboxFieldTrial::StopTimerFieldTrialDuration() {
255 if (base::StringToInt(
256 base::FieldTrialList::FindFullName(kStopTimerFieldTrialName
),
258 return base::TimeDelta::FromMilliseconds(stop_timer_ms
);
259 return base::TimeDelta::FromMilliseconds(1500);
262 bool OmniboxFieldTrial::HasDynamicFieldTrialGroupPrefix(
263 const char* group_prefix
) {
264 // Make sure that Autocomplete dynamic field trials are activated. It's OK to
265 // call this method multiple times.
266 ActivateDynamicTrials();
268 // Look for group names starting with |group_prefix|.
269 for (int i
= 0; i
< kMaxAutocompleteDynamicFieldTrials
; ++i
) {
270 const std::string
& group_name
= base::FieldTrialList::FindFullName(
271 DynamicFieldTrialName(i
));
272 if (StartsWithASCII(group_name
, group_prefix
, true))
278 bool OmniboxFieldTrial::InZeroSuggestFieldTrial() {
279 return HasDynamicFieldTrialGroupPrefix(kEnableZeroSuggestGroupPrefix
) ||
280 chrome_variations::GetVariationParamValue(
281 kBundledExperimentFieldTrialName
, kZeroSuggestRule
) == "true";
284 bool OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial() {
285 return HasDynamicFieldTrialGroupPrefix(
286 kEnableZeroSuggestMostVisitedGroupPrefix
) ||
287 chrome_variations::GetVariationParamValue(
288 kBundledExperimentFieldTrialName
,
289 kZeroSuggestVariantRule
) == "MostVisited";
292 bool OmniboxFieldTrial::InZeroSuggestAfterTypingFieldTrial() {
293 return HasDynamicFieldTrialGroupPrefix(
294 kEnableZeroSuggestAfterTypingGroupPrefix
) ||
295 chrome_variations::GetVariationParamValue(
296 kBundledExperimentFieldTrialName
,
297 kZeroSuggestVariantRule
) == "AfterTyping";
300 bool OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial() {
301 return HasDynamicFieldTrialGroupPrefix(
302 kEnableZeroSuggestPersonalizedGroupPrefix
) ||
303 chrome_variations::GetVariationParamValue(
304 kBundledExperimentFieldTrialName
,
305 kZeroSuggestVariantRule
) == "Personalized";
308 bool OmniboxFieldTrial::ShortcutsScoringMaxRelevance(
309 AutocompleteInput::PageClassification current_page_classification
,
310 int* max_relevance
) {
311 // The value of the rule is a string that encodes an integer containing
312 // the max relevance.
313 const std::string
& max_relevance_str
=
314 OmniboxFieldTrial::GetValueForRuleInContext(
315 kShortcutsScoringMaxRelevanceRule
, current_page_classification
);
316 if (max_relevance_str
.empty())
318 if (!base::StringToInt(max_relevance_str
, max_relevance
))
323 bool OmniboxFieldTrial::SearchHistoryPreventInlining(
324 AutocompleteInput::PageClassification current_page_classification
) {
325 return OmniboxFieldTrial::GetValueForRuleInContext(
326 kSearchHistoryRule
, current_page_classification
) == "PreventInlining";
329 bool OmniboxFieldTrial::SearchHistoryDisable(
330 AutocompleteInput::PageClassification current_page_classification
) {
331 return OmniboxFieldTrial::GetValueForRuleInContext(
332 kSearchHistoryRule
, current_page_classification
) == "Disable";
335 void OmniboxFieldTrial::GetDemotionsByType(
336 AutocompleteInput::PageClassification current_page_classification
,
337 DemotionMultipliers
* demotions_by_type
) {
338 demotions_by_type
->clear();
339 std::string demotion_rule
= OmniboxFieldTrial::GetValueForRuleInContext(
340 kDemoteByTypeRule
, current_page_classification
);
341 // If there is no demotion rule for this context, then use the default
342 // value for that context. At the moment the default value is non-empty
343 // only for the fakebox-focus context.
344 if (demotion_rule
.empty() &&
345 (current_page_classification
==
346 AutocompleteInput::INSTANT_NTP_WITH_FAKEBOX_AS_STARTING_FOCUS
))
347 demotion_rule
= "1:61,2:61,3:61,4:61,12:61";
349 // The value of the DemoteByType rule is a comma-separated list of
350 // {ResultType + ":" + Number} where ResultType is an AutocompleteMatchType::
351 // Type enum represented as an integer and Number is an integer number
352 // between 0 and 100 inclusive. Relevance scores of matches of that result
353 // type are multiplied by Number / 100. 100 means no change.
354 base::StringPairs kv_pairs
;
355 if (base::SplitStringIntoKeyValuePairs(demotion_rule
, ':', ',', &kv_pairs
)) {
356 for (base::StringPairs::const_iterator it
= kv_pairs
.begin();
357 it
!= kv_pairs
.end(); ++it
) {
358 // This is a best-effort conversion; we trust the hand-crafted parameters
359 // downloaded from the server to be perfect. There's no need to handle
362 base::StringToInt(it
->first
, &k
);
363 base::StringToInt(it
->second
, &v
);
364 (*demotions_by_type
)[static_cast<AutocompleteMatchType::Type
>(k
)] =
365 static_cast<float>(v
) / 100.0f
;
370 void OmniboxFieldTrial::GetExperimentalHUPScoringParams(
371 HUPScoringParams
* scoring_params
) {
372 scoring_params
->experimental_scoring_enabled
= false;
374 VariationParams params
;
375 if (!chrome_variations::GetVariationParams(kBundledExperimentFieldTrialName
,
379 VariationParams::const_iterator it
= params
.find(kHUPNewScoringEnabledParam
);
380 if (it
!= params
.end()) {
382 if (base::StringToInt(it
->second
, &enabled
))
383 scoring_params
->experimental_scoring_enabled
= (enabled
!= 0);
386 InitializeScoreBuckets(params
, kHUPNewScoringTypedCountRelevanceCapParam
,
387 kHUPNewScoringTypedCountHalfLifeTimeParam
,
388 kHUPNewScoringTypedCountScoreBucketsParam
,
389 &scoring_params
->typed_count_buckets
);
390 InitializeScoreBuckets(params
, kHUPNewScoringVisitedCountRelevanceCapParam
,
391 kHUPNewScoringVisitedCountHalfLifeTimeParam
,
392 kHUPNewScoringVisitedCountScoreBucketsParam
,
393 &scoring_params
->visited_count_buckets
);
396 int OmniboxFieldTrial::HQPBookmarkValue() {
397 std::string bookmark_value_str
= chrome_variations::
398 GetVariationParamValue(kBundledExperimentFieldTrialName
,
399 kHQPBookmarkValueRule
);
400 if (bookmark_value_str
.empty())
402 // This is a best-effort conversion; we trust the hand-crafted parameters
403 // downloaded from the server to be perfect. There's no need for handle
406 base::StringToInt(bookmark_value_str
, &bookmark_value
);
407 return bookmark_value
;
410 bool OmniboxFieldTrial::HQPAllowMatchInTLDValue() {
411 return chrome_variations::GetVariationParamValue(
412 kBundledExperimentFieldTrialName
,
413 kHQPAllowMatchInTLDRule
) == "true";
416 bool OmniboxFieldTrial::HQPAllowMatchInSchemeValue() {
417 return chrome_variations::GetVariationParamValue(
418 kBundledExperimentFieldTrialName
,
419 kHQPAllowMatchInSchemeRule
) == "true";
422 bool OmniboxFieldTrial::BookmarksIndexURLsValue() {
423 return chrome_variations::GetVariationParamValue(
424 kBundledExperimentFieldTrialName
,
425 kBookmarksIndexURLsRule
) == "true";
428 const char OmniboxFieldTrial::kBundledExperimentFieldTrialName
[] =
429 "OmniboxBundledExperimentV1";
430 const char OmniboxFieldTrial::kShortcutsScoringMaxRelevanceRule
[] =
431 "ShortcutsScoringMaxRelevance";
432 const char OmniboxFieldTrial::kSearchHistoryRule
[] = "SearchHistory";
433 const char OmniboxFieldTrial::kDemoteByTypeRule
[] = "DemoteByType";
434 const char OmniboxFieldTrial::kHQPBookmarkValueRule
[] =
436 const char OmniboxFieldTrial::kHQPAllowMatchInTLDRule
[] = "HQPAllowMatchInTLD";
437 const char OmniboxFieldTrial::kHQPAllowMatchInSchemeRule
[] =
438 "HQPAllowMatchInScheme";
439 const char OmniboxFieldTrial::kZeroSuggestRule
[] = "ZeroSuggest";
440 const char OmniboxFieldTrial::kZeroSuggestVariantRule
[] = "ZeroSuggestVariant";
441 const char OmniboxFieldTrial::kBookmarksIndexURLsRule
[] = "BookmarksIndexURLs";
443 const char OmniboxFieldTrial::kHUPNewScoringEnabledParam
[] =
444 "HUPExperimentalScoringEnabled";
445 const char OmniboxFieldTrial::kHUPNewScoringTypedCountRelevanceCapParam
[] =
446 "TypedCountRelevanceCap";
447 const char OmniboxFieldTrial::kHUPNewScoringTypedCountHalfLifeTimeParam
[] =
448 "TypedCountHalfLifeTime";
449 const char OmniboxFieldTrial::kHUPNewScoringTypedCountScoreBucketsParam
[] =
450 "TypedCountScoreBuckets";
451 const char OmniboxFieldTrial::kHUPNewScoringVisitedCountRelevanceCapParam
[] =
452 "VisitedCountRelevanceCap";
453 const char OmniboxFieldTrial::kHUPNewScoringVisitedCountHalfLifeTimeParam
[] =
454 "VisitedCountHalfLifeTime";
455 const char OmniboxFieldTrial::kHUPNewScoringVisitedCountScoreBucketsParam
[] =
456 "VisitedCountScoreBuckets";
458 // Background and implementation details:
460 // Each experiment group in any field trial can come with an optional set of
461 // parameters (key-value pairs). In the bundled omnibox experiment
462 // (kBundledExperimentFieldTrialName), each experiment group comes with a
463 // list of parameters in the form:
465 // <AutocompleteInput::PageClassification (as an int)>:
466 // <whether Instant Extended is enabled (as a 1 or 0)>
467 // (note that there are no linebreaks in keys; this format is for
468 // presentation only>
469 // value=<arbitrary string>
470 // Both the AutocompleteInput::PageClassification and the Instant Extended
471 // entries can be "*", which means this rule applies for all values of the
472 // matching portion of the context.
473 // One example parameter is
474 // key=SearchHistory:6:1
475 // value=PreventInlining
476 // This means in page classification context 6 (a search result page doing
477 // search term replacement) with Instant Extended enabled, the SearchHistory
478 // experiment should PreventInlining.
480 // When an exact match to the rule in the current context is missing, we
481 // give preference to a wildcard rule that matches the instant extended
482 // context over a wildcard rule that matches the page classification
483 // context. Hopefully, though, users will write their field trial configs
484 // so as not to rely on this fall back order.
486 // In short, this function tries to find the value associated with key
487 // |rule|:|page_classification|:|instant_extended|, failing that it looks up
488 // |rule|:*:|instant_extended|, failing that it looks up
489 // |rule|:|page_classification|:*, failing that it looks up |rule|:*:*,
490 // and failing that it returns the empty string.
491 std::string
OmniboxFieldTrial::GetValueForRuleInContext(
492 const std::string
& rule
,
493 AutocompleteInput::PageClassification page_classification
) {
494 VariationParams params
;
495 if (!chrome_variations::GetVariationParams(kBundledExperimentFieldTrialName
,
497 return std::string();
499 const std::string page_classification_str
=
500 base::IntToString(static_cast<int>(page_classification
));
501 const std::string instant_extended
=
502 chrome::IsInstantExtendedAPIEnabled() ? "1" : "0";
503 // Look up rule in this exact context.
504 VariationParams::const_iterator it
= params
.find(
505 rule
+ ":" + page_classification_str
+ ":" + instant_extended
);
506 if (it
!= params
.end())
508 // Fall back to the global page classification context.
509 it
= params
.find(rule
+ ":*:" + instant_extended
);
510 if (it
!= params
.end())
512 // Fall back to the global instant extended context.
513 it
= params
.find(rule
+ ":" + page_classification_str
+ ":*");
514 if (it
!= params
.end())
516 // Look up rule in the global context.
517 it
= params
.find(rule
+ ":*:*");
518 return (it
!= params
.end()) ? it
->second
: std::string();