variations_service.cc revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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/metrics/variations/variations_service.h"
6
7#include <set>
8
9#include "base/base64.h"
10#include "base/build_time.h"
11#include "base/command_line.h"
12#include "base/memory/scoped_ptr.h"
13#include "base/metrics/field_trial.h"
14#include "base/metrics/histogram.h"
15#include "base/prefs/pref_registry_simple.h"
16#include "base/prefs/pref_service.h"
17#include "base/version.h"
18#include "chrome/browser/browser_process.h"
19#include "chrome/browser/metrics/proto/trials_seed.pb.h"
20#include "chrome/common/chrome_switches.h"
21#include "chrome/common/metrics/variations/variations_util.h"
22#include "chrome/common/pref_names.h"
23#include "content/public/browser/browser_thread.h"
24#include "content/public/common/url_fetcher.h"
25#include "googleurl/src/gurl.h"
26#include "net/base/load_flags.h"
27#include "net/base/network_change_notifier.h"
28#include "net/base/url_util.h"
29#include "net/http/http_response_headers.h"
30#include "net/http/http_status_code.h"
31#include "net/http/http_util.h"
32#include "net/url_request/url_fetcher.h"
33#include "net/url_request/url_request_status.h"
34
35namespace chrome_variations {
36
37namespace {
38
39// Default server of Variations seed info.
40const char kDefaultVariationsServerURL[] =
41    "https://clients4.google.com/chrome-variations/seed";
42const int kMaxRetrySeedFetch = 5;
43
44// Time between seed fetches, in hours.
45const int kSeedFetchPeriodHours = 5;
46
47// TODO(mad): To be removed when we stop updating the NetworkTimeTracker.
48// For the HTTP date headers, the resolution of the server time is 1 second.
49const int64 kServerTimeResolutionMs = 1000;
50
51// Maps Study_Channel enum values to corresponding chrome::VersionInfo::Channel
52// enum values.
53chrome::VersionInfo::Channel ConvertStudyChannelToVersionChannel(
54    Study_Channel study_channel) {
55  switch (study_channel) {
56    case Study_Channel_CANARY:
57      return chrome::VersionInfo::CHANNEL_CANARY;
58    case Study_Channel_DEV:
59      return chrome::VersionInfo::CHANNEL_DEV;
60    case Study_Channel_BETA:
61      return chrome::VersionInfo::CHANNEL_BETA;
62    case Study_Channel_STABLE:
63      return chrome::VersionInfo::CHANNEL_STABLE;
64  }
65  // All enum values of |study_channel| were handled above.
66  NOTREACHED();
67  return chrome::VersionInfo::CHANNEL_UNKNOWN;
68}
69
70// Wrapper around channel checking, used to enable channel mocking for
71// testing. If the current browser channel is not UNKNOWN, this will return
72// that channel value. Otherwise, if the fake channel flag is provided, this
73// will return the fake channel. Failing that, this will return the UNKNOWN
74// channel.
75chrome::VersionInfo::Channel GetChannelForVariations() {
76  chrome::VersionInfo::Channel channel = chrome::VersionInfo::GetChannel();
77  if (channel != chrome::VersionInfo::CHANNEL_UNKNOWN)
78    return channel;
79  std::string forced_channel =
80      CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
81          switches::kFakeVariationsChannel);
82  if (forced_channel == "stable")
83    channel = chrome::VersionInfo::CHANNEL_STABLE;
84  else if (forced_channel == "beta")
85    channel = chrome::VersionInfo::CHANNEL_BETA;
86  else if (forced_channel == "dev")
87    channel = chrome::VersionInfo::CHANNEL_DEV;
88  else if (forced_channel == "canary")
89    channel = chrome::VersionInfo::CHANNEL_CANARY;
90  else
91    DVLOG(1) << "Invalid channel provided: " << forced_channel;
92  return channel;
93}
94
95Study_Platform GetCurrentPlatform() {
96#if defined(OS_WIN)
97  return Study_Platform_PLATFORM_WINDOWS;
98#elif defined(OS_MACOSX)
99  return Study_Platform_PLATFORM_MAC;
100#elif defined(OS_CHROMEOS)
101  return Study_Platform_PLATFORM_CHROMEOS;
102#elif defined(OS_ANDROID)
103  return Study_Platform_PLATFORM_ANDROID;
104#elif defined(OS_IOS)
105  return Study_Platform_PLATFORM_IOS;
106#elif defined(OS_LINUX) || defined(OS_BSD) || defined(OS_SOLARIS)
107  // Default BSD and SOLARIS to Linux to not break those builds, although these
108  // platforms are not officially supported by Chrome.
109  return Study_Platform_PLATFORM_LINUX;
110#else
111#error Unknown platform
112#endif
113}
114
115// Converts |date_time| in Study date format to base::Time.
116base::Time ConvertStudyDateToBaseTime(int64 date_time) {
117  return base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(date_time);
118}
119
120}  // namespace
121
122VariationsService::VariationsService(PrefService* local_state)
123    : local_state_(local_state),
124      variations_server_url_(GetVariationsServerURL(local_state)),
125      create_trials_from_seed_called_(false),
126      resource_request_allowed_notifier_(
127          new ResourceRequestAllowedNotifier) {
128  resource_request_allowed_notifier_->Init(this);
129}
130
131VariationsService::VariationsService(ResourceRequestAllowedNotifier* notifier)
132    : local_state_(NULL),
133      variations_server_url_(GetVariationsServerURL(NULL)),
134      create_trials_from_seed_called_(false),
135      resource_request_allowed_notifier_(notifier) {
136  resource_request_allowed_notifier_->Init(this);
137}
138
139VariationsService::~VariationsService() {
140}
141
142bool VariationsService::CreateTrialsFromSeed() {
143  create_trials_from_seed_called_ = true;
144
145  TrialsSeed seed;
146  if (!LoadTrialsSeedFromPref(local_state_, &seed))
147    return false;
148
149  const int64 date_value = local_state_->GetInt64(prefs::kVariationsSeedDate);
150  const base::Time seed_date = base::Time::FromInternalValue(date_value);
151  const base::Time build_time = base::GetBuildTime();
152  // Use the build time for date checks if either the seed date is invalid or
153  // the build time is newer than the seed date.
154  base::Time reference_date = seed_date;
155  if (seed_date.is_null() || seed_date < build_time)
156    reference_date = build_time;
157
158  const chrome::VersionInfo current_version_info;
159  if (!current_version_info.is_valid())
160    return false;
161
162  chrome::VersionInfo::Channel channel = GetChannelForVariations();
163  for (int i = 0; i < seed.study_size(); ++i) {
164    if (ShouldAddStudy(seed.study(i), current_version_info, reference_date,
165                       channel)) {
166      CreateTrialFromStudy(seed.study(i), reference_date);
167    }
168  }
169
170  // Log the "freshness" of the seed that was just used. The freshness is the
171  // time between the last successful seed download and now.
172  const int64 last_fetch_time_internal =
173      local_state_->GetInt64(prefs::kVariationsLastFetchTime);
174  if (last_fetch_time_internal) {
175    const base::Time now = base::Time::Now();
176    const base::TimeDelta delta =
177        now - base::Time::FromInternalValue(last_fetch_time_internal);
178    // Log the value in number of minutes.
179    UMA_HISTOGRAM_CUSTOM_COUNTS("Variations.SeedFreshness", delta.InMinutes(),
180        1, base::TimeDelta::FromDays(30).InMinutes(), 50);
181  }
182
183  return true;
184}
185
186void VariationsService::StartRepeatedVariationsSeedFetch() {
187  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
188
189  // Check that |CreateTrialsFromSeed| was called, which is necessary to
190  // retrieve the serial number that will be sent to the server.
191  DCHECK(create_trials_from_seed_called_);
192
193  // Perform the first fetch.
194  FetchVariationsSeed();
195
196  // Repeat this periodically.
197  timer_.Start(FROM_HERE, base::TimeDelta::FromHours(kSeedFetchPeriodHours),
198               this, &VariationsService::FetchVariationsSeed);
199}
200
201bool VariationsService::GetNetworkTime(base::Time* network_time,
202                                       base::TimeDelta* uncertainty) const {
203  return network_time_tracker_.GetNetworkTime(network_time, uncertainty);
204}
205
206// static
207GURL VariationsService::GetVariationsServerURL(PrefService* local_state) {
208  std::string server_url_string(CommandLine::ForCurrentProcess()->
209      GetSwitchValueASCII(switches::kVariationsServerURL));
210  if (server_url_string.empty())
211    server_url_string = kDefaultVariationsServerURL;
212  GURL server_url = GURL(server_url_string);
213  if (local_state) {
214    // Append the "restrict" parameter if it is found in prefs.
215    const std::string restrict_param =
216        local_state->GetString(prefs::kVariationsRestrictParameter);
217    if (!restrict_param.empty())
218      server_url = net::AppendOrReplaceQueryParameter(server_url,
219                                                      "restrict",
220                                                      restrict_param);
221  }
222  DCHECK(server_url.is_valid());
223  return server_url;
224}
225
226#if defined(OS_WIN)
227void VariationsService::StartGoogleUpdateRegistrySync() {
228  registry_syncer_.RequestRegistrySync();
229}
230#endif
231
232void VariationsService::SetCreateTrialsFromSeedCalledForTesting(bool called) {
233  create_trials_from_seed_called_ = called;
234}
235
236// static
237std::string VariationsService::GetDefaultVariationsServerURLForTesting() {
238  return kDefaultVariationsServerURL;
239}
240
241// static
242void VariationsService::RegisterPrefs(PrefRegistrySimple* registry) {
243  registry->RegisterStringPref(prefs::kVariationsSeed, std::string());
244  registry->RegisterInt64Pref(prefs::kVariationsSeedDate,
245                              base::Time().ToInternalValue());
246  registry->RegisterInt64Pref(prefs::kVariationsLastFetchTime, 0);
247  registry->RegisterStringPref(prefs::kVariationsRestrictParameter,
248                               std::string());
249}
250
251// static
252VariationsService* VariationsService::Create(PrefService* local_state) {
253// This is temporarily disabled for Android. See http://crbug.com/168224
254#if !defined(GOOGLE_CHROME_BUILD) || defined(OS_ANDROID)
255  // Unless the URL was provided, unsupported builds should return NULL to
256  // indicate that the service should not be used.
257  if (!CommandLine::ForCurrentProcess()->HasSwitch(
258          switches::kVariationsServerURL))
259    return NULL;
260#endif
261  return new VariationsService(local_state);
262}
263
264void VariationsService::DoActualFetch() {
265  pending_seed_request_.reset(net::URLFetcher::Create(
266      0, variations_server_url_, net::URLFetcher::GET, this));
267  pending_seed_request_->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
268                                      net::LOAD_DO_NOT_SAVE_COOKIES);
269  pending_seed_request_->SetRequestContext(
270      g_browser_process->system_request_context());
271  pending_seed_request_->SetMaxRetriesOn5xx(kMaxRetrySeedFetch);
272  if (!variations_serial_number_.empty()) {
273    pending_seed_request_->AddExtraRequestHeader("If-Match:" +
274                                                 variations_serial_number_);
275  }
276  pending_seed_request_->Start();
277
278  last_request_started_time_ = base::TimeTicks::Now();
279}
280
281void VariationsService::FetchVariationsSeed() {
282  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
283
284  if (!resource_request_allowed_notifier_->ResourceRequestsAllowed()) {
285    DVLOG(1) << "Resource requests were not allowed. Waiting for notification.";
286    return;
287  }
288
289  DoActualFetch();
290}
291
292void VariationsService::OnURLFetchComplete(const net::URLFetcher* source) {
293  DCHECK_EQ(pending_seed_request_.get(), source);
294  // The fetcher will be deleted when the request is handled.
295  scoped_ptr<const net::URLFetcher> request(pending_seed_request_.release());
296  if (request->GetStatus().status() != net::URLRequestStatus::SUCCESS) {
297    DVLOG(1) << "Variations server request failed.";
298    return;
299  }
300
301  // Log the response code.
302  const int response_code = request->GetResponseCode();
303  UMA_HISTOGRAM_CUSTOM_ENUMERATION("Variations.SeedFetchResponseCode",
304      net::HttpUtil::MapStatusCodeForHistogram(response_code),
305      net::HttpUtil::GetStatusCodesForHistogram());
306
307  const base::TimeDelta latency =
308      base::TimeTicks::Now() - last_request_started_time_;
309
310  base::Time response_date;
311  if (response_code == net::HTTP_OK ||
312      response_code == net::HTTP_NOT_MODIFIED) {
313    bool success = request->GetResponseHeaders()->GetDateValue(&response_date);
314    DCHECK(success || response_date.is_null());
315
316    if (!response_date.is_null()) {
317      network_time_tracker_.UpdateNetworkTime(
318          response_date,
319          base::TimeDelta::FromMilliseconds(kServerTimeResolutionMs),
320          latency);
321    }
322  }
323
324  if (response_code != net::HTTP_OK) {
325    DVLOG(1) << "Variations server request returned non-HTTP_OK response code: "
326             << response_code;
327    if (response_code == net::HTTP_NOT_MODIFIED) {
328      UMA_HISTOGRAM_MEDIUM_TIMES("Variations.FetchNotModifiedLatency", latency);
329      RecordLastFetchTime();
330    } else {
331      UMA_HISTOGRAM_MEDIUM_TIMES("Variations.FetchOtherLatency", latency);
332    }
333    return;
334  }
335  UMA_HISTOGRAM_MEDIUM_TIMES("Variations.FetchSuccessLatency", latency);
336
337  std::string seed_data;
338  bool success = request->GetResponseAsString(&seed_data);
339  DCHECK(success);
340
341  StoreSeedData(seed_data, response_date, local_state_);
342}
343
344void VariationsService::OnResourceRequestsAllowed() {
345  // Note that this only attempts to fetch the seed at most once per period
346  // (kSeedFetchPeriodHours). This works because
347  // |resource_request_allowed_notifier_| only calls this method if an
348  // attempt was made earlier that fails (which implies that the period had
349  // elapsed). After a successful attempt is made, the notifier will know not
350  // to call this method again until another failed attempt occurs.
351  DVLOG(1) << "Retrying fetch.";
352  DoActualFetch();
353  if (timer_.IsRunning())
354    timer_.Reset();
355}
356
357bool VariationsService::StoreSeedData(const std::string& seed_data,
358                                      const base::Time& seed_date,
359                                      PrefService* local_prefs) {
360  if (seed_data.empty()) {
361    VLOG(1) << "Variations Seed data from server is empty, rejecting the seed.";
362    return false;
363  }
364
365  // Only store the seed data if it parses correctly.
366  TrialsSeed seed;
367  if (!seed.ParseFromString(seed_data)) {
368    VLOG(1) << "Variations Seed data from server is not in valid proto format, "
369            << "rejecting the seed.";
370    return false;
371  }
372
373  std::string base64_seed_data;
374  if (!base::Base64Encode(seed_data, &base64_seed_data)) {
375    VLOG(1) << "Variations Seed data from server fails Base64Encode, rejecting "
376            << "the seed.";
377    return false;
378  }
379
380  local_prefs->SetString(prefs::kVariationsSeed, base64_seed_data);
381  local_prefs->SetInt64(prefs::kVariationsSeedDate,
382                        seed_date.ToInternalValue());
383  variations_serial_number_ = seed.serial_number();
384
385  RecordLastFetchTime();
386
387  return true;
388}
389
390// static
391bool VariationsService::ShouldAddStudy(
392    const Study& study,
393    const chrome::VersionInfo& version_info,
394    const base::Time& reference_date,
395    const chrome::VersionInfo::Channel channel) {
396  if (study.has_filter()) {
397    if (!CheckStudyChannel(study.filter(), channel)) {
398      DVLOG(1) << "Filtered out study " << study.name() << " due to channel.";
399      return false;
400    }
401
402    if (!CheckStudyLocale(study.filter(),
403                          g_browser_process->GetApplicationLocale())) {
404      DVLOG(1) << "Filtered out study " << study.name() << " due to locale.";
405      return false;
406    }
407
408    if (!CheckStudyPlatform(study.filter(), GetCurrentPlatform())) {
409      DVLOG(1) << "Filtered out study " << study.name() << " due to platform.";
410      return false;
411    }
412
413    if (!CheckStudyVersion(study.filter(), version_info.Version())) {
414      DVLOG(1) << "Filtered out study " << study.name() << " due to version.";
415      return false;
416    }
417
418    if (!CheckStudyStartDate(study.filter(), reference_date)) {
419      DVLOG(1) << "Filtered out study " << study.name() <<
420                  " due to start date.";
421      return false;
422    }
423  }
424
425  DVLOG(1) << "Kept study " << study.name() << ".";
426  return true;
427}
428
429// static
430bool VariationsService::CheckStudyChannel(
431    const Study_Filter& filter,
432    chrome::VersionInfo::Channel channel) {
433  // An empty channel list matches all channels.
434  if (filter.channel_size() == 0)
435    return true;
436
437  for (int i = 0; i < filter.channel_size(); ++i) {
438    if (ConvertStudyChannelToVersionChannel(filter.channel(i)) == channel)
439      return true;
440  }
441  return false;
442}
443
444// static
445bool VariationsService::CheckStudyLocale(
446    const chrome_variations::Study_Filter& filter,
447    const std::string& locale) {
448  // An empty locale list matches all locales.
449  if (filter.locale_size() == 0)
450    return true;
451
452  for (int i = 0; i < filter.locale_size(); ++i) {
453    if (filter.locale(i) == locale)
454      return true;
455  }
456  return false;
457}
458
459// static
460bool VariationsService::CheckStudyPlatform(
461    const Study_Filter& filter,
462    Study_Platform platform) {
463  // An empty platform list matches all platforms.
464  if (filter.platform_size() == 0)
465    return true;
466
467  for (int i = 0; i < filter.platform_size(); ++i) {
468    if (filter.platform(i) == platform)
469      return true;
470  }
471  return false;
472}
473
474// static
475bool VariationsService::CheckStudyVersion(
476    const Study_Filter& filter,
477    const std::string& version_string) {
478  const Version version(version_string);
479  if (!version.IsValid()) {
480    NOTREACHED();
481    return false;
482  }
483
484  if (filter.has_min_version()) {
485    if (version.CompareToWildcardString(filter.min_version()) < 0)
486      return false;
487  }
488
489  if (filter.has_max_version()) {
490    if (version.CompareToWildcardString(filter.max_version()) > 0)
491      return false;
492  }
493
494  return true;
495}
496
497// static
498bool VariationsService::CheckStudyStartDate(
499    const Study_Filter& filter,
500    const base::Time& date_time) {
501  if (filter.has_start_date()) {
502    const base::Time start_date =
503        ConvertStudyDateToBaseTime(filter.start_date());
504    return date_time >= start_date;
505  }
506
507  return true;
508}
509
510bool VariationsService::IsStudyExpired(const Study& study,
511                                       const base::Time& date_time) {
512  if (study.has_expiry_date()) {
513    const base::Time expiry_date =
514        ConvertStudyDateToBaseTime(study.expiry_date());
515    return date_time >= expiry_date;
516  }
517
518  return false;
519}
520
521// static
522bool VariationsService::ValidateStudyAndComputeTotalProbability(
523    const Study& study,
524    base::FieldTrial::Probability* total_probability) {
525  // At the moment, a missing default_experiment_name makes the study invalid.
526  if (study.default_experiment_name().empty()) {
527    DVLOG(1) << study.name() << " has no default experiment defined.";
528    return false;
529  }
530  if (study.filter().has_min_version() &&
531      !Version::IsValidWildcardString(study.filter().min_version())) {
532    DVLOG(1) << study.name() << " has invalid min version: "
533             << study.filter().min_version();
534    return false;
535  }
536  if (study.filter().has_max_version() &&
537      !Version::IsValidWildcardString(study.filter().max_version())) {
538    DVLOG(1) << study.name() << " has invalid max version: "
539             << study.filter().max_version();
540    return false;
541  }
542
543  const std::string& default_group_name = study.default_experiment_name();
544  base::FieldTrial::Probability divisor = 0;
545
546  bool found_default_group = false;
547  std::set<std::string> experiment_names;
548  for (int i = 0; i < study.experiment_size(); ++i) {
549    if (study.experiment(i).name().empty()) {
550      DVLOG(1) << study.name() << " is missing experiment " << i << " name";
551      return false;
552    }
553    if (!experiment_names.insert(study.experiment(i).name()).second) {
554      DVLOG(1) << study.name() << " has a repeated experiment name "
555               << study.experiment(i).name();
556      return false;
557    }
558    divisor += study.experiment(i).probability_weight();
559    if (study.experiment(i).name() == default_group_name)
560      found_default_group = true;
561  }
562
563  if (!found_default_group) {
564    DVLOG(1) << study.name() << " is missing default experiment in its "
565             << "experiment list";
566    // The default group was not found in the list of groups. This study is not
567    // valid.
568    return false;
569  }
570
571  *total_probability = divisor;
572  return true;
573}
574
575bool VariationsService::LoadTrialsSeedFromPref(PrefService* local_prefs,
576                                               TrialsSeed* seed) {
577  std::string base64_seed_data = local_prefs->GetString(prefs::kVariationsSeed);
578  if (base64_seed_data.empty()) {
579    UMA_HISTOGRAM_BOOLEAN("Variations.SeedEmpty", true);
580    return false;
581  }
582
583  // If the decode process fails, assume the pref value is corrupt and clear it.
584  std::string seed_data;
585  if (!base::Base64Decode(base64_seed_data, &seed_data) ||
586      !seed->ParseFromString(seed_data)) {
587    VLOG(1) << "Variations Seed data in local pref is corrupt, clearing the "
588            << "pref.";
589    local_prefs->ClearPref(prefs::kVariationsSeed);
590    return false;
591  }
592  variations_serial_number_ = seed->serial_number();
593  return true;
594}
595
596void VariationsService::CreateTrialFromStudy(const Study& study,
597                                             const base::Time& reference_date) {
598  base::FieldTrial::Probability total_probability = 0;
599  if (!ValidateStudyAndComputeTotalProbability(study, &total_probability))
600    return;
601
602  // The trial is created without specifying an expiration date because the
603  // expiration check in field_trial.cc is based on the build date. Instead,
604  // the expiration check using |reference_date| is done explicitly below.
605  scoped_refptr<base::FieldTrial> trial(
606      base::FieldTrialList::FactoryGetFieldTrial(
607          study.name(), total_probability, study.default_experiment_name(),
608          base::FieldTrialList::kNoExpirationYear, 1, 1, NULL));
609
610  if (study.has_consistency() &&
611      study.consistency() == Study_Consistency_PERMANENT) {
612    trial->UseOneTimeRandomization();
613  }
614
615  for (int i = 0; i < study.experiment_size(); ++i) {
616    const Study_Experiment& experiment = study.experiment(i);
617    if (experiment.name() != study.default_experiment_name())
618      trial->AppendGroup(experiment.name(), experiment.probability_weight());
619
620    if (experiment.has_google_web_experiment_id()) {
621      const VariationID variation_id =
622          static_cast<VariationID>(experiment.google_web_experiment_id());
623      AssociateGoogleVariationIDForce(GOOGLE_WEB_PROPERTIES,
624                                      study.name(),
625                                      experiment.name(),
626                                      variation_id);
627    }
628    if (experiment.has_google_update_experiment_id()) {
629      const VariationID variation_id =
630          static_cast<VariationID>(experiment.google_update_experiment_id());
631      AssociateGoogleVariationIDForce(GOOGLE_UPDATE_SERVICE,
632                                      study.name(),
633                                      experiment.name(),
634                                      variation_id);
635    }
636  }
637
638  trial->SetForced();
639  if (IsStudyExpired(study, reference_date))
640    trial->Disable();
641}
642
643void VariationsService::RecordLastFetchTime() {
644  // local_state_ is NULL in tests, so check it first.
645  if (local_state_) {
646    local_state_->SetInt64(prefs::kVariationsLastFetchTime,
647                           base::Time::Now().ToInternalValue());
648  }
649}
650
651}  // namespace chrome_variations
652