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
3// found in the LICENSE file.
4
5#include "components/omnibox/omnibox_field_trial.h"
6
7#include <cmath>
8#include <string>
9
10#include "base/command_line.h"
11#include "base/metrics/field_trial.h"
12#include "base/strings/string_number_conversions.h"
13#include "base/strings/string_split.h"
14#include "base/strings/string_util.h"
15#include "base/strings/stringprintf.h"
16#include "base/time/time.h"
17#include "components/metrics/proto/omnibox_event.pb.h"
18#include "components/omnibox/omnibox_switches.h"
19#include "components/search/search.h"
20#include "components/variations/active_field_trials.h"
21#include "components/variations/metrics_util.h"
22#include "components/variations/variations_associated_data.h"
23
24using metrics::OmniboxEventProto;
25
26namespace {
27
28typedef std::map<std::string, std::string> VariationParams;
29typedef HUPScoringParams::ScoreBuckets ScoreBuckets;
30
31// Field trial names.
32const char kStopTimerFieldTrialName[] = "OmniboxStopTimer";
33
34// The autocomplete dynamic field trial name prefix.  Each field trial is
35// configured dynamically and is retrieved automatically by Chrome during
36// the startup.
37const char kAutocompleteDynamicFieldTrialPrefix[] = "AutocompleteDynamicTrial_";
38// The maximum number of the autocomplete dynamic field trials (aka layers).
39const int kMaxAutocompleteDynamicFieldTrials = 5;
40
41
42// Concatenates the autocomplete dynamic field trial prefix with a field trial
43// ID to form a complete autocomplete field trial name.
44std::string DynamicFieldTrialName(int id) {
45  return base::StringPrintf("%s%d", kAutocompleteDynamicFieldTrialPrefix, id);
46}
47
48void InitializeScoreBuckets(const VariationParams& params,
49                            const char* relevance_cap_param,
50                            const char* half_life_param,
51                            const char* score_buckets_param,
52                            ScoreBuckets* score_buckets) {
53  VariationParams::const_iterator it = params.find(relevance_cap_param);
54  if (it != params.end()) {
55    int relevance_cap;
56    if (base::StringToInt(it->second, &relevance_cap))
57      score_buckets->set_relevance_cap(relevance_cap);
58  }
59
60  it = params.find(half_life_param);
61  if (it != params.end()) {
62    int half_life_days;
63    if (base::StringToInt(it->second, &half_life_days))
64      score_buckets->set_half_life_days(half_life_days);
65  }
66
67  it = params.find(score_buckets_param);
68  if (it != params.end()) {
69    // The value of the score bucket is a comma-separated list of
70    // {DecayedCount + ":" + MaxRelevance}.
71    base::StringPairs kv_pairs;
72    if (base::SplitStringIntoKeyValuePairs(it->second, ':', ',', &kv_pairs)) {
73      for (base::StringPairs::const_iterator it = kv_pairs.begin();
74           it != kv_pairs.end(); ++it) {
75        ScoreBuckets::CountMaxRelevance bucket;
76        base::StringToDouble(it->first, &bucket.first);
77        base::StringToInt(it->second, &bucket.second);
78        score_buckets->buckets().push_back(bucket);
79      }
80      std::sort(score_buckets->buckets().begin(),
81                score_buckets->buckets().end(),
82                std::greater<ScoreBuckets::CountMaxRelevance>());
83    }
84  }
85}
86
87}  // namespace
88
89HUPScoringParams::ScoreBuckets::ScoreBuckets()
90    : relevance_cap_(-1),
91      half_life_days_(-1) {
92}
93
94HUPScoringParams::ScoreBuckets::~ScoreBuckets() {
95}
96
97double HUPScoringParams::ScoreBuckets::HalfLifeTimeDecay(
98    const base::TimeDelta& elapsed_time) const {
99  double time_ms;
100  if ((half_life_days_ <= 0) ||
101      ((time_ms = elapsed_time.InMillisecondsF()) <= 0))
102    return 1.0;
103
104  const double half_life_intervals =
105      time_ms / base::TimeDelta::FromDays(half_life_days_).InMillisecondsF();
106  return pow(2.0, -half_life_intervals);
107}
108
109void OmniboxFieldTrial::ActivateDynamicTrials() {
110  // Initialize all autocomplete dynamic field trials.  This method may be
111  // called multiple times.
112  for (int i = 0; i < kMaxAutocompleteDynamicFieldTrials; ++i)
113    base::FieldTrialList::FindValue(DynamicFieldTrialName(i));
114}
115
116int OmniboxFieldTrial::GetDisabledProviderTypes() {
117  // Make sure that Autocomplete dynamic field trials are activated.  It's OK to
118  // call this method multiple times.
119  ActivateDynamicTrials();
120
121  // Look for group names in form of "DisabledProviders_<mask>" where "mask"
122  // is a bitmap of disabled provider types (AutocompleteProvider::Type).
123  int provider_types = 0;
124  for (int i = 0; i < kMaxAutocompleteDynamicFieldTrials; ++i) {
125    std::string group_name = base::FieldTrialList::FindFullName(
126        DynamicFieldTrialName(i));
127    const char kDisabledProviders[] = "DisabledProviders_";
128    if (!StartsWithASCII(group_name, kDisabledProviders, true))
129      continue;
130    int types = 0;
131    if (!base::StringToInt(base::StringPiece(
132            group_name.substr(strlen(kDisabledProviders))), &types))
133      continue;
134    provider_types |= types;
135  }
136  return provider_types;
137}
138
139void OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes(
140    std::vector<uint32>* field_trial_hashes) {
141  field_trial_hashes->clear();
142  for (int i = 0; i < kMaxAutocompleteDynamicFieldTrials; ++i) {
143    const std::string& trial_name = DynamicFieldTrialName(i);
144    if (base::FieldTrialList::TrialExists(trial_name))
145      field_trial_hashes->push_back(metrics::HashName(trial_name));
146  }
147  if (base::FieldTrialList::TrialExists(kBundledExperimentFieldTrialName)) {
148    field_trial_hashes->push_back(
149        metrics::HashName(kBundledExperimentFieldTrialName));
150  }
151}
152
153base::TimeDelta OmniboxFieldTrial::StopTimerFieldTrialDuration() {
154  int stop_timer_ms;
155  if (base::StringToInt(
156      base::FieldTrialList::FindFullName(kStopTimerFieldTrialName),
157          &stop_timer_ms))
158    return base::TimeDelta::FromMilliseconds(stop_timer_ms);
159  return base::TimeDelta::FromMilliseconds(1500);
160}
161
162bool OmniboxFieldTrial::InZeroSuggestFieldTrial() {
163  if (variations::GetVariationParamValue(
164          kBundledExperimentFieldTrialName, kZeroSuggestRule) == "true")
165    return true;
166  if (variations::GetVariationParamValue(
167          kBundledExperimentFieldTrialName, kZeroSuggestRule) == "false")
168    return false;
169#if defined(OS_WIN) || defined(OS_CHROMEOS) || defined(OS_LINUX) || \
170    (defined(OS_MACOSX) && !defined(OS_IOS))
171  return true;
172#else
173  return false;
174#endif
175}
176
177bool OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial() {
178  return variations::GetVariationParamValue(
179      kBundledExperimentFieldTrialName,
180      kZeroSuggestVariantRule) == "MostVisited";
181}
182
183bool OmniboxFieldTrial::InZeroSuggestAfterTypingFieldTrial() {
184  return variations::GetVariationParamValue(
185      kBundledExperimentFieldTrialName,
186      kZeroSuggestVariantRule) == "AfterTyping";
187}
188
189bool OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial() {
190  return variations::GetVariationParamValue(
191      kBundledExperimentFieldTrialName,
192      kZeroSuggestVariantRule) == "Personalized";
193}
194
195bool OmniboxFieldTrial::ShortcutsScoringMaxRelevance(
196    OmniboxEventProto::PageClassification current_page_classification,
197    int* max_relevance) {
198  // The value of the rule is a string that encodes an integer containing
199  // the max relevance.
200  const std::string& max_relevance_str =
201      OmniboxFieldTrial::GetValueForRuleInContext(
202          kShortcutsScoringMaxRelevanceRule, current_page_classification);
203  if (max_relevance_str.empty())
204    return false;
205  if (!base::StringToInt(max_relevance_str, max_relevance))
206    return false;
207  return true;
208}
209
210bool OmniboxFieldTrial::SearchHistoryPreventInlining(
211    OmniboxEventProto::PageClassification current_page_classification) {
212  return OmniboxFieldTrial::GetValueForRuleInContext(
213      kSearchHistoryRule, current_page_classification) == "PreventInlining";
214}
215
216bool OmniboxFieldTrial::SearchHistoryDisable(
217    OmniboxEventProto::PageClassification current_page_classification) {
218  return OmniboxFieldTrial::GetValueForRuleInContext(
219      kSearchHistoryRule, current_page_classification) == "Disable";
220}
221
222void OmniboxFieldTrial::GetDemotionsByType(
223    OmniboxEventProto::PageClassification current_page_classification,
224    DemotionMultipliers* demotions_by_type) {
225  demotions_by_type->clear();
226  std::string demotion_rule = OmniboxFieldTrial::GetValueForRuleInContext(
227      kDemoteByTypeRule, current_page_classification);
228  // If there is no demotion rule for this context, then use the default
229  // value for that context.  At the moment the default value is non-empty
230  // only for the fakebox-focus context.
231  if (demotion_rule.empty() &&
232      (current_page_classification ==
233       OmniboxEventProto::INSTANT_NTP_WITH_FAKEBOX_AS_STARTING_FOCUS))
234    demotion_rule = "1:61,2:61,3:61,4:61,16:61";
235
236  // The value of the DemoteByType rule is a comma-separated list of
237  // {ResultType + ":" + Number} where ResultType is an AutocompleteMatchType::
238  // Type enum represented as an integer and Number is an integer number
239  // between 0 and 100 inclusive.   Relevance scores of matches of that result
240  // type are multiplied by Number / 100.  100 means no change.
241  base::StringPairs kv_pairs;
242  if (base::SplitStringIntoKeyValuePairs(demotion_rule, ':', ',', &kv_pairs)) {
243    for (base::StringPairs::const_iterator it = kv_pairs.begin();
244         it != kv_pairs.end(); ++it) {
245      // This is a best-effort conversion; we trust the hand-crafted parameters
246      // downloaded from the server to be perfect.  There's no need to handle
247      // errors smartly.
248      int k, v;
249      base::StringToInt(it->first, &k);
250      base::StringToInt(it->second, &v);
251      (*demotions_by_type)[static_cast<AutocompleteMatchType::Type>(k)] =
252          static_cast<float>(v) / 100.0f;
253    }
254  }
255}
256
257void OmniboxFieldTrial::GetExperimentalHUPScoringParams(
258    HUPScoringParams* scoring_params) {
259  scoring_params->experimental_scoring_enabled = false;
260
261  VariationParams params;
262  if (!variations::GetVariationParams(kBundledExperimentFieldTrialName,
263                                      &params))
264    return;
265
266  VariationParams::const_iterator it = params.find(kHUPNewScoringEnabledParam);
267  if (it != params.end()) {
268    int enabled = 0;
269    if (base::StringToInt(it->second, &enabled))
270      scoring_params->experimental_scoring_enabled = (enabled != 0);
271  }
272
273  InitializeScoreBuckets(params, kHUPNewScoringTypedCountRelevanceCapParam,
274      kHUPNewScoringTypedCountHalfLifeTimeParam,
275      kHUPNewScoringTypedCountScoreBucketsParam,
276      &scoring_params->typed_count_buckets);
277  InitializeScoreBuckets(params, kHUPNewScoringVisitedCountRelevanceCapParam,
278      kHUPNewScoringVisitedCountHalfLifeTimeParam,
279      kHUPNewScoringVisitedCountScoreBucketsParam,
280      &scoring_params->visited_count_buckets);
281}
282
283int OmniboxFieldTrial::HQPBookmarkValue() {
284  std::string bookmark_value_str =
285      variations::GetVariationParamValue(kBundledExperimentFieldTrialName,
286                                         kHQPBookmarkValueRule);
287  if (bookmark_value_str.empty())
288    return 10;
289  // This is a best-effort conversion; we trust the hand-crafted parameters
290  // downloaded from the server to be perfect.  There's no need for handle
291  // errors smartly.
292  int bookmark_value;
293  base::StringToInt(bookmark_value_str, &bookmark_value);
294  return bookmark_value;
295}
296
297bool OmniboxFieldTrial::HQPAllowMatchInTLDValue() {
298  return variations::GetVariationParamValue(
299      kBundledExperimentFieldTrialName,
300      kHQPAllowMatchInTLDRule) == "true";
301}
302
303bool OmniboxFieldTrial::HQPAllowMatchInSchemeValue() {
304  return variations::GetVariationParamValue(
305      kBundledExperimentFieldTrialName,
306      kHQPAllowMatchInSchemeRule) == "true";
307}
308
309bool OmniboxFieldTrial::DisableInlining() {
310  return variations::GetVariationParamValue(
311      kBundledExperimentFieldTrialName,
312      kDisableInliningRule) == "true";
313}
314
315bool OmniboxFieldTrial::EnableAnswersInSuggest() {
316  const CommandLine* cl = CommandLine::ForCurrentProcess();
317  if (cl->HasSwitch(switches::kDisableAnswersInSuggest))
318    return false;
319  if (cl->HasSwitch(switches::kEnableAnswersInSuggest))
320    return true;
321
322  return variations::GetVariationParamValue(
323      kBundledExperimentFieldTrialName,
324      kAnswersInSuggestRule) == "true";
325}
326
327bool OmniboxFieldTrial::AddUWYTMatchEvenIfPromotedURLs() {
328  return variations::GetVariationParamValue(
329      kBundledExperimentFieldTrialName,
330      kAddUWYTMatchEvenIfPromotedURLsRule) == "true";
331}
332
333bool OmniboxFieldTrial::DisplayHintTextWhenPossible() {
334  return variations::GetVariationParamValue(
335      kBundledExperimentFieldTrialName,
336      kDisplayHintTextWhenPossibleRule) == "true";
337}
338
339const char OmniboxFieldTrial::kBundledExperimentFieldTrialName[] =
340    "OmniboxBundledExperimentV1";
341const char OmniboxFieldTrial::kShortcutsScoringMaxRelevanceRule[] =
342    "ShortcutsScoringMaxRelevance";
343const char OmniboxFieldTrial::kSearchHistoryRule[] = "SearchHistory";
344const char OmniboxFieldTrial::kDemoteByTypeRule[] = "DemoteByType";
345const char OmniboxFieldTrial::kHQPBookmarkValueRule[] =
346    "HQPBookmarkValue";
347const char OmniboxFieldTrial::kHQPAllowMatchInTLDRule[] = "HQPAllowMatchInTLD";
348const char OmniboxFieldTrial::kHQPAllowMatchInSchemeRule[] =
349    "HQPAllowMatchInScheme";
350const char OmniboxFieldTrial::kZeroSuggestRule[] = "ZeroSuggest";
351const char OmniboxFieldTrial::kZeroSuggestVariantRule[] = "ZeroSuggestVariant";
352const char OmniboxFieldTrial::kDisableInliningRule[] = "DisableInlining";
353const char OmniboxFieldTrial::kAnswersInSuggestRule[] = "AnswersInSuggest";
354const char OmniboxFieldTrial::kAddUWYTMatchEvenIfPromotedURLsRule[] =
355    "AddUWYTMatchEvenIfPromotedURLs";
356const char OmniboxFieldTrial::kDisplayHintTextWhenPossibleRule[] =
357    "DisplayHintTextWhenPossible";
358
359const char OmniboxFieldTrial::kHUPNewScoringEnabledParam[] =
360    "HUPExperimentalScoringEnabled";
361const char OmniboxFieldTrial::kHUPNewScoringTypedCountRelevanceCapParam[] =
362    "TypedCountRelevanceCap";
363const char OmniboxFieldTrial::kHUPNewScoringTypedCountHalfLifeTimeParam[] =
364    "TypedCountHalfLifeTime";
365const char OmniboxFieldTrial::kHUPNewScoringTypedCountScoreBucketsParam[] =
366    "TypedCountScoreBuckets";
367const char OmniboxFieldTrial::kHUPNewScoringVisitedCountRelevanceCapParam[] =
368    "VisitedCountRelevanceCap";
369const char OmniboxFieldTrial::kHUPNewScoringVisitedCountHalfLifeTimeParam[] =
370    "VisitedCountHalfLifeTime";
371const char OmniboxFieldTrial::kHUPNewScoringVisitedCountScoreBucketsParam[] =
372    "VisitedCountScoreBuckets";
373
374// Background and implementation details:
375//
376// Each experiment group in any field trial can come with an optional set of
377// parameters (key-value pairs).  In the bundled omnibox experiment
378// (kBundledExperimentFieldTrialName), each experiment group comes with a
379// list of parameters in the form:
380//   key=<Rule>:
381//       <OmniboxEventProto::PageClassification (as an int)>:
382//       <whether Instant Extended is enabled (as a 1 or 0)>
383//     (note that there are no linebreaks in keys; this format is for
384//      presentation only>
385//   value=<arbitrary string>
386// Both the OmniboxEventProto::PageClassification and the Instant Extended
387// entries can be "*", which means this rule applies for all values of the
388// matching portion of the context.
389// One example parameter is
390//   key=SearchHistory:6:1
391//   value=PreventInlining
392// This means in page classification context 6 (a search result page doing
393// search term replacement) with Instant Extended enabled, the SearchHistory
394// experiment should PreventInlining.
395//
396// When an exact match to the rule in the current context is missing, we
397// give preference to a wildcard rule that matches the instant extended
398// context over a wildcard rule that matches the page classification
399// context.  Hopefully, though, users will write their field trial configs
400// so as not to rely on this fall back order.
401//
402// In short, this function tries to find the value associated with key
403// |rule|:|page_classification|:|instant_extended|, failing that it looks up
404// |rule|:*:|instant_extended|, failing that it looks up
405// |rule|:|page_classification|:*, failing that it looks up |rule|:*:*,
406// and failing that it returns the empty string.
407std::string OmniboxFieldTrial::GetValueForRuleInContext(
408    const std::string& rule,
409    OmniboxEventProto::PageClassification page_classification) {
410  VariationParams params;
411  if (!variations::GetVariationParams(kBundledExperimentFieldTrialName,
412                                      &params)) {
413    return std::string();
414  }
415  const std::string page_classification_str =
416      base::IntToString(static_cast<int>(page_classification));
417  const std::string instant_extended =
418      chrome::IsInstantExtendedAPIEnabled() ? "1" : "0";
419  // Look up rule in this exact context.
420  VariationParams::const_iterator it = params.find(
421      rule + ":" + page_classification_str + ":" + instant_extended);
422  if (it != params.end())
423    return it->second;
424  // Fall back to the global page classification context.
425  it = params.find(rule + ":*:" + instant_extended);
426  if (it != params.end())
427    return it->second;
428  // Fall back to the global instant extended context.
429  it = params.find(rule + ":" + page_classification_str + ":*");
430  if (it != params.end())
431    return it->second;
432  // Look up rule in the global context.
433  it = params.find(rule + ":*:*");
434  return (it != params.end()) ? it->second : std::string();
435}
436