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