OLD | NEW |
| (Empty) |
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_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/version.h" | |
16 #include "chrome/browser/browser_process.h" | |
17 #include "chrome/browser/metrics/proto/trials_seed.pb.h" | |
18 #include "chrome/browser/prefs/pref_service.h" | |
19 #include "chrome/common/chrome_switches.h" | |
20 #include "chrome/common/metrics/variations_util.h" | |
21 #include "chrome/common/pref_names.h" | |
22 #include "content/public/browser/browser_thread.h" | |
23 #include "content/public/common/url_fetcher.h" | |
24 #include "googleurl/src/gurl.h" | |
25 #include "net/base/load_flags.h" | |
26 #include "net/base/network_change_notifier.h" | |
27 #include "net/http/http_response_headers.h" | |
28 #include "net/url_request/url_fetcher.h" | |
29 #include "net/url_request/url_request_status.h" | |
30 | |
31 namespace chrome_variations { | |
32 | |
33 namespace { | |
34 | |
35 // Default server of Variations seed info. | |
36 const char kDefaultVariationsServerURL[] = | |
37 "https://clients4.google.com/chrome-variations/seed"; | |
38 const int kMaxRetrySeedFetch = 5; | |
39 | |
40 // Time between seed fetches, in hours. | |
41 const int kSeedFetchPeriodHours = 5; | |
42 | |
43 // Maps Study_Channel enum values to corresponding chrome::VersionInfo::Channel | |
44 // enum values. | |
45 chrome::VersionInfo::Channel ConvertStudyChannelToVersionChannel( | |
46 Study_Channel study_channel) { | |
47 switch (study_channel) { | |
48 case Study_Channel_CANARY: | |
49 return chrome::VersionInfo::CHANNEL_CANARY; | |
50 case Study_Channel_DEV: | |
51 return chrome::VersionInfo::CHANNEL_DEV; | |
52 case Study_Channel_BETA: | |
53 return chrome::VersionInfo::CHANNEL_BETA; | |
54 case Study_Channel_STABLE: | |
55 return chrome::VersionInfo::CHANNEL_STABLE; | |
56 } | |
57 // All enum values of |study_channel| were handled above. | |
58 NOTREACHED(); | |
59 return chrome::VersionInfo::CHANNEL_UNKNOWN; | |
60 } | |
61 | |
62 Study_Platform GetCurrentPlatform() { | |
63 #if defined(OS_WIN) | |
64 return Study_Platform_PLATFORM_WINDOWS; | |
65 #elif defined(OS_MACOSX) | |
66 return Study_Platform_PLATFORM_MAC; | |
67 #elif defined(OS_CHROMEOS) | |
68 return Study_Platform_PLATFORM_CHROMEOS; | |
69 #elif defined(OS_ANDROID) | |
70 return Study_Platform_PLATFORM_ANDROID; | |
71 #elif defined(OS_IOS) | |
72 return Study_Platform_PLATFORM_IOS; | |
73 #elif defined(OS_LINUX) || defined(OS_BSD) || defined(OS_SOLARIS) | |
74 // Default BSD and SOLARIS to Linux to not break those builds, although these | |
75 // platforms are not officially supported by Chrome. | |
76 return Study_Platform_PLATFORM_LINUX; | |
77 #else | |
78 #error Unknown platform | |
79 #endif | |
80 } | |
81 | |
82 // Converts |date_time| in Study date format to base::Time. | |
83 base::Time ConvertStudyDateToBaseTime(int64 date_time) { | |
84 return base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(date_time); | |
85 } | |
86 | |
87 // Determine and return the variations server URL. | |
88 GURL GetVariationsServerURL() { | |
89 std::string server_url(CommandLine::ForCurrentProcess()->GetSwitchValueASCII( | |
90 switches::kVariationsServerURL)); | |
91 if (server_url.empty()) | |
92 server_url = kDefaultVariationsServerURL; | |
93 GURL url_as_gurl = GURL(server_url); | |
94 DCHECK(url_as_gurl.is_valid()); | |
95 return url_as_gurl; | |
96 } | |
97 | |
98 } // namespace | |
99 | |
100 VariationsService::VariationsService() | |
101 : variations_server_url_(GetVariationsServerURL()), | |
102 create_trials_from_seed_called_(false) { | |
103 } | |
104 | |
105 VariationsService::~VariationsService() {} | |
106 | |
107 bool VariationsService::CreateTrialsFromSeed(PrefService* local_prefs) { | |
108 create_trials_from_seed_called_ = true; | |
109 | |
110 TrialsSeed seed; | |
111 if (!LoadTrialsSeedFromPref(local_prefs, &seed)) | |
112 return false; | |
113 | |
114 const int64 date_value = local_prefs->GetInt64(prefs::kVariationsSeedDate); | |
115 const base::Time seed_date = base::Time::FromInternalValue(date_value); | |
116 const base::Time build_time = base::GetBuildTime(); | |
117 // Use the build time for date checks if either the seed date is invalid or | |
118 // the build time is newer than the seed date. | |
119 base::Time reference_date = seed_date; | |
120 if (seed_date.is_null() || seed_date < build_time) | |
121 reference_date = build_time; | |
122 | |
123 const chrome::VersionInfo current_version_info; | |
124 if (!current_version_info.is_valid()) | |
125 return false; | |
126 | |
127 for (int i = 0; i < seed.study_size(); ++i) { | |
128 if (ShouldAddStudy(seed.study(i), current_version_info, reference_date)) | |
129 CreateTrialFromStudy(seed.study(i), reference_date); | |
130 } | |
131 | |
132 return true; | |
133 } | |
134 | |
135 void VariationsService::StartRepeatedVariationsSeedFetch() { | |
136 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); | |
137 | |
138 // Check that |CreateTrialsFromSeed| was called, which is necessary to | |
139 // retrieve the serial number that will be sent to the server. | |
140 DCHECK(create_trials_from_seed_called_); | |
141 | |
142 // Perform the first fetch. | |
143 FetchVariationsSeed(); | |
144 | |
145 // Repeat this periodically. | |
146 timer_.Start(FROM_HERE, base::TimeDelta::FromHours(kSeedFetchPeriodHours), | |
147 this, &VariationsService::FetchVariationsSeed); | |
148 } | |
149 | |
150 void VariationsService::FetchVariationsSeed() { | |
151 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); | |
152 | |
153 const bool is_offline = net::NetworkChangeNotifier::IsOffline(); | |
154 UMA_HISTOGRAM_BOOLEAN("Variations.NetworkAvailability", !is_offline); | |
155 if (is_offline) { | |
156 DVLOG(1) << "Network was offline."; | |
157 return; | |
158 } | |
159 | |
160 pending_seed_request_.reset(net::URLFetcher::Create( | |
161 variations_server_url_, net::URLFetcher::GET, this)); | |
162 pending_seed_request_->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | | |
163 net::LOAD_DO_NOT_SAVE_COOKIES); | |
164 pending_seed_request_->SetRequestContext( | |
165 g_browser_process->system_request_context()); | |
166 pending_seed_request_->SetMaxRetries(kMaxRetrySeedFetch); | |
167 if (!variations_serial_number_.empty()) { | |
168 pending_seed_request_->AddExtraRequestHeader("If-Match:" + | |
169 variations_serial_number_); | |
170 } | |
171 pending_seed_request_->Start(); | |
172 } | |
173 | |
174 void VariationsService::OnURLFetchComplete(const net::URLFetcher* source) { | |
175 DCHECK_EQ(pending_seed_request_.get(), source); | |
176 // When we're done handling the request, the fetcher will be deleted. | |
177 scoped_ptr<const net::URLFetcher> request( | |
178 pending_seed_request_.release()); | |
179 if (request->GetStatus().status() != net::URLRequestStatus::SUCCESS) { | |
180 DVLOG(1) << "Variations server request failed."; | |
181 return; | |
182 } | |
183 | |
184 if (request->GetResponseCode() != 200) { | |
185 DVLOG(1) << "Variations server request returned non-200 response code: " | |
186 << request->GetResponseCode(); | |
187 return; | |
188 } | |
189 | |
190 std::string seed_data; | |
191 bool success = request->GetResponseAsString(&seed_data); | |
192 DCHECK(success); | |
193 | |
194 base::Time response_date; | |
195 success = request->GetResponseHeaders()->GetDateValue(&response_date); | |
196 DCHECK(success || response_date.is_null()); | |
197 | |
198 StoreSeedData(seed_data, response_date, g_browser_process->local_state()); | |
199 } | |
200 | |
201 // static | |
202 void VariationsService::RegisterPrefs(PrefService* prefs) { | |
203 prefs->RegisterStringPref(prefs::kVariationsSeed, std::string()); | |
204 prefs->RegisterInt64Pref(prefs::kVariationsSeedDate, | |
205 base::Time().ToInternalValue()); | |
206 } | |
207 | |
208 bool VariationsService::StoreSeedData(const std::string& seed_data, | |
209 const base::Time& seed_date, | |
210 PrefService* local_prefs) { | |
211 // Only store the seed data if it parses correctly. | |
212 TrialsSeed seed; | |
213 if (!seed.ParseFromString(seed_data)) { | |
214 VLOG(1) << "Variations Seed data from server is not in valid proto format, " | |
215 << "rejecting the seed."; | |
216 return false; | |
217 } | |
218 | |
219 std::string base64_seed_data; | |
220 if (!base::Base64Encode(seed_data, &base64_seed_data)) { | |
221 VLOG(1) << "Variations Seed data from server fails Base64Encode, rejecting " | |
222 << "the seed."; | |
223 return false; | |
224 } | |
225 | |
226 local_prefs->SetString(prefs::kVariationsSeed, base64_seed_data); | |
227 local_prefs->SetInt64(prefs::kVariationsSeedDate, | |
228 seed_date.ToInternalValue()); | |
229 variations_serial_number_ = seed.serial_number(); | |
230 return true; | |
231 } | |
232 | |
233 // static | |
234 bool VariationsService::ShouldAddStudy( | |
235 const Study& study, | |
236 const chrome::VersionInfo& version_info, | |
237 const base::Time& reference_date) { | |
238 if (study.has_filter()) { | |
239 if (!CheckStudyChannel(study.filter(), chrome::VersionInfo::GetChannel())) { | |
240 DVLOG(1) << "Filtered out study " << study.name() << " due to channel."; | |
241 return false; | |
242 } | |
243 | |
244 if (!CheckStudyLocale(study.filter(), | |
245 g_browser_process->GetApplicationLocale())) { | |
246 DVLOG(1) << "Filtered out study " << study.name() << " due to locale."; | |
247 return false; | |
248 } | |
249 | |
250 if (!CheckStudyPlatform(study.filter(), GetCurrentPlatform())) { | |
251 DVLOG(1) << "Filtered out study " << study.name() << " due to platform."; | |
252 return false; | |
253 } | |
254 | |
255 if (!CheckStudyVersion(study.filter(), version_info.Version())) { | |
256 DVLOG(1) << "Filtered out study " << study.name() << " due to version."; | |
257 return false; | |
258 } | |
259 | |
260 if (!CheckStudyStartDate(study.filter(), reference_date)) { | |
261 DVLOG(1) << "Filtered out study " << study.name() << | |
262 " due to start date."; | |
263 return false; | |
264 } | |
265 } | |
266 | |
267 DVLOG(1) << "Kept study " << study.name() << "."; | |
268 return true; | |
269 } | |
270 | |
271 // static | |
272 bool VariationsService::CheckStudyChannel( | |
273 const Study_Filter& filter, | |
274 chrome::VersionInfo::Channel channel) { | |
275 // An empty channel list matches all channels. | |
276 if (filter.channel_size() == 0) | |
277 return true; | |
278 | |
279 for (int i = 0; i < filter.channel_size(); ++i) { | |
280 if (ConvertStudyChannelToVersionChannel(filter.channel(i)) == channel) | |
281 return true; | |
282 } | |
283 return false; | |
284 } | |
285 | |
286 // static | |
287 bool VariationsService::CheckStudyLocale( | |
288 const chrome_variations::Study_Filter& filter, | |
289 const std::string& locale) { | |
290 // An empty locale list matches all locales. | |
291 if (filter.locale_size() == 0) | |
292 return true; | |
293 | |
294 for (int i = 0; i < filter.locale_size(); ++i) { | |
295 if (filter.locale(i) == locale) | |
296 return true; | |
297 } | |
298 return false; | |
299 } | |
300 | |
301 // static | |
302 bool VariationsService::CheckStudyPlatform( | |
303 const Study_Filter& filter, | |
304 Study_Platform platform) { | |
305 // An empty platform list matches all platforms. | |
306 if (filter.platform_size() == 0) | |
307 return true; | |
308 | |
309 for (int i = 0; i < filter.platform_size(); ++i) { | |
310 if (filter.platform(i) == platform) | |
311 return true; | |
312 } | |
313 return false; | |
314 } | |
315 | |
316 // static | |
317 bool VariationsService::CheckStudyVersion( | |
318 const Study_Filter& filter, | |
319 const std::string& version_string) { | |
320 const Version version(version_string); | |
321 if (!version.IsValid()) { | |
322 NOTREACHED(); | |
323 return false; | |
324 } | |
325 | |
326 if (filter.has_min_version()) { | |
327 if (version.CompareToWildcardString(filter.min_version()) < 0) | |
328 return false; | |
329 } | |
330 | |
331 if (filter.has_max_version()) { | |
332 if (version.CompareToWildcardString(filter.max_version()) > 0) | |
333 return false; | |
334 } | |
335 | |
336 return true; | |
337 } | |
338 | |
339 // static | |
340 bool VariationsService::CheckStudyStartDate( | |
341 const Study_Filter& filter, | |
342 const base::Time& date_time) { | |
343 if (filter.has_start_date()) { | |
344 const base::Time start_date = | |
345 ConvertStudyDateToBaseTime(filter.start_date()); | |
346 return date_time >= start_date; | |
347 } | |
348 | |
349 return true; | |
350 } | |
351 | |
352 bool VariationsService::IsStudyExpired(const Study& study, | |
353 const base::Time& date_time) { | |
354 if (study.has_expiry_date()) { | |
355 const base::Time expiry_date = | |
356 ConvertStudyDateToBaseTime(study.expiry_date()); | |
357 return date_time >= expiry_date; | |
358 } | |
359 | |
360 return false; | |
361 } | |
362 | |
363 // static | |
364 bool VariationsService::ValidateStudyAndComputeTotalProbability( | |
365 const Study& study, | |
366 base::FieldTrial::Probability* total_probability) { | |
367 // At the moment, a missing default_experiment_name makes the study invalid. | |
368 if (study.default_experiment_name().empty()) { | |
369 DVLOG(1) << study.name() << " has no default experiment defined."; | |
370 return false; | |
371 } | |
372 if (study.filter().has_min_version() && | |
373 !Version::IsValidWildcardString(study.filter().min_version())) { | |
374 DVLOG(1) << study.name() << " has invalid min version: " | |
375 << study.filter().min_version(); | |
376 return false; | |
377 } | |
378 if (study.filter().has_max_version() && | |
379 !Version::IsValidWildcardString(study.filter().max_version())) { | |
380 DVLOG(1) << study.name() << " has invalid max version: " | |
381 << study.filter().max_version(); | |
382 return false; | |
383 } | |
384 | |
385 const std::string& default_group_name = study.default_experiment_name(); | |
386 base::FieldTrial::Probability divisor = 0; | |
387 | |
388 bool found_default_group = false; | |
389 std::set<std::string> experiment_names; | |
390 for (int i = 0; i < study.experiment_size(); ++i) { | |
391 if (study.experiment(i).name().empty()) { | |
392 DVLOG(1) << study.name() << " is missing experiment " << i << " name"; | |
393 return false; | |
394 } | |
395 if (!experiment_names.insert(study.experiment(i).name()).second) { | |
396 DVLOG(1) << study.name() << " has a repeated experiment name " | |
397 << study.experiment(i).name(); | |
398 return false; | |
399 } | |
400 divisor += study.experiment(i).probability_weight(); | |
401 if (study.experiment(i).name() == default_group_name) | |
402 found_default_group = true; | |
403 } | |
404 | |
405 if (!found_default_group) { | |
406 DVLOG(1) << study.name() << " is missing default experiment in its " | |
407 << "experiment list"; | |
408 // The default group was not found in the list of groups. This study is not | |
409 // valid. | |
410 return false; | |
411 } | |
412 | |
413 *total_probability = divisor; | |
414 return true; | |
415 } | |
416 | |
417 bool VariationsService::LoadTrialsSeedFromPref(PrefService* local_prefs, | |
418 TrialsSeed* seed) { | |
419 std::string base64_seed_data = local_prefs->GetString(prefs::kVariationsSeed); | |
420 std::string seed_data; | |
421 | |
422 // If the decode process fails, assume the pref value is corrupt, and clear | |
423 // it. | |
424 if (!base::Base64Decode(base64_seed_data, &seed_data) || | |
425 !seed->ParseFromString(seed_data)) { | |
426 VLOG(1) << "Variations Seed data in local pref is corrupt, clearing the " | |
427 << "pref."; | |
428 local_prefs->ClearPref(prefs::kVariationsSeed); | |
429 return false; | |
430 } | |
431 variations_serial_number_ = seed->serial_number(); | |
432 return true; | |
433 } | |
434 | |
435 void VariationsService::CreateTrialFromStudy(const Study& study, | |
436 const base::Time& reference_date) { | |
437 base::FieldTrial::Probability total_probability = 0; | |
438 if (!ValidateStudyAndComputeTotalProbability(study, &total_probability)) | |
439 return; | |
440 | |
441 // The trial is created without specifying an expiration date because the | |
442 // expiration check in field_trial.cc is based on the build date. Instead, | |
443 // the expiration check using |reference_date| is done explicitly below. | |
444 scoped_refptr<base::FieldTrial> trial( | |
445 base::FieldTrialList::FactoryGetFieldTrial( | |
446 study.name(), total_probability, study.default_experiment_name(), | |
447 base::FieldTrialList::kExpirationYearInFuture, 1, 1, NULL)); | |
448 | |
449 if (study.has_consistency() && | |
450 study.consistency() == Study_Consistency_PERMANENT) { | |
451 trial->UseOneTimeRandomization(); | |
452 } | |
453 | |
454 for (int i = 0; i < study.experiment_size(); ++i) { | |
455 const Study_Experiment& experiment = study.experiment(i); | |
456 if (experiment.name() != study.default_experiment_name()) | |
457 trial->AppendGroup(experiment.name(), experiment.probability_weight()); | |
458 | |
459 if (experiment.has_experiment_id()) { | |
460 const VariationID variation_id = | |
461 static_cast<VariationID>(experiment.experiment_id()); | |
462 AssociateGoogleVariationIDForce(study.name(), | |
463 experiment.name(), | |
464 variation_id); | |
465 } | |
466 } | |
467 | |
468 trial->SetForced(); | |
469 if (IsStudyExpired(study, reference_date)) | |
470 trial->Disable(); | |
471 } | |
472 | |
473 } // namespace chrome_variations | |
OLD | NEW |