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.
5 #include "chrome/browser/search/suggestions/suggestions_service.h"
7 #include "base/memory/scoped_ptr.h"
8 #include "base/metrics/field_trial.h"
9 #include "base/metrics/histogram.h"
10 #include "base/metrics/sparse_histogram.h"
11 #include "base/strings/string_number_conversions.h"
12 #include "base/strings/string_util.h"
13 #include "base/time/time.h"
14 #include "chrome/browser/browser_process.h"
15 #include "chrome/browser/history/history_types.h"
16 #include "chrome/browser/metrics/variations/variations_http_header_provider.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/search/suggestions/suggestions_store.h"
19 #include "components/pref_registry/pref_registry_syncable.h"
20 #include "components/variations/variations_associated_data.h"
21 #include "content/public/browser/browser_thread.h"
22 #include "net/base/escape.h"
23 #include "net/base/load_flags.h"
24 #include "net/base/net_errors.h"
25 #include "net/http/http_response_headers.h"
26 #include "net/http/http_status_code.h"
27 #include "net/http/http_util.h"
28 #include "net/url_request/url_fetcher.h"
29 #include "net/url_request/url_request_status.h"
32 using base::CancelableClosure;
33 using content::BrowserThread;
35 namespace suggestions {
39 // Used to UMA log the state of the last response from the server.
40 enum SuggestionsResponseState {
47 // Will log the supplied response |state|.
48 void LogResponseState(SuggestionsResponseState state) {
49 UMA_HISTOGRAM_ENUMERATION("Suggestions.ResponseState", state,
53 // Obtains the experiment parameter under the supplied |key|, or empty string
54 // if the parameter does not exist.
55 std::string GetExperimentParam(const std::string& key) {
56 return chrome_variations::GetVariationParamValue(kSuggestionsFieldTrialName,
60 // Runs each callback in |requestors| on |suggestions|, then deallocates
62 void DispatchRequestsAndClear(
63 const SuggestionsProfile& suggestions,
64 std::vector<SuggestionsService::ResponseCallback>* requestors) {
65 std::vector<SuggestionsService::ResponseCallback>::iterator it;
66 for (it = requestors->begin(); it != requestors->end(); ++it) {
69 std::vector<SuggestionsService::ResponseCallback>().swap(*requestors);
72 const int kDefaultRequestTimeoutMs = 200;
76 const char kSuggestionsFieldTrialName[] = "ChromeSuggestions";
77 const char kSuggestionsFieldTrialURLParam[] = "url";
78 const char kSuggestionsFieldTrialSuggestionsSuffixParam[] =
80 const char kSuggestionsFieldTrialBlacklistSuffixParam[] = "blacklist_suffix";
81 const char kSuggestionsFieldTrialStateParam[] = "state";
82 const char kSuggestionsFieldTrialControlParam[] = "control";
83 const char kSuggestionsFieldTrialStateEnabled[] = "enabled";
84 const char kSuggestionsFieldTrialTimeoutMs[] = "timeout_ms";
86 SuggestionsService::SuggestionsService(
87 Profile* profile, scoped_ptr<SuggestionsStore> suggestions_store)
88 : suggestions_store_(suggestions_store.Pass()),
89 thumbnail_manager_(new ThumbnailManager(profile)),
91 weak_ptr_factory_(this),
92 request_timeout_ms_(kDefaultRequestTimeoutMs) {
93 // Obtain various parameters from Variations.
95 GURL(GetExperimentParam(kSuggestionsFieldTrialURLParam) +
96 GetExperimentParam(kSuggestionsFieldTrialSuggestionsSuffixParam));
97 blacklist_url_prefix_ =
98 GetExperimentParam(kSuggestionsFieldTrialURLParam) +
99 GetExperimentParam(kSuggestionsFieldTrialBlacklistSuffixParam);
100 std::string timeout = GetExperimentParam(kSuggestionsFieldTrialTimeoutMs);
102 if (!timeout.empty() && base::StringToInt(timeout, &temp_timeout)) {
103 request_timeout_ms_ = temp_timeout;
107 SuggestionsService::~SuggestionsService() {}
110 bool SuggestionsService::IsEnabled() {
111 return GetExperimentParam(kSuggestionsFieldTrialStateParam) ==
112 kSuggestionsFieldTrialStateEnabled;
116 bool SuggestionsService::IsControlGroup() {
117 return GetExperimentParam(kSuggestionsFieldTrialControlParam) ==
118 kSuggestionsFieldTrialStateEnabled;
121 void SuggestionsService::FetchSuggestionsData(
122 SuggestionsService::ResponseCallback callback) {
123 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
125 FetchSuggestionsDataNoTimeout(callback);
127 // Post a task to serve the cached suggestions if the request hasn't completed
128 // after some time. Cancels the previous such task, if one existed.
129 pending_timeout_closure_.reset(new CancelableClosure(base::Bind(
130 &SuggestionsService::OnRequestTimeout, weak_ptr_factory_.GetWeakPtr())));
131 BrowserThread::PostDelayedTask(
132 BrowserThread::UI, FROM_HERE, pending_timeout_closure_->callback(),
133 base::TimeDelta::FromMilliseconds(request_timeout_ms_));
136 void SuggestionsService::FetchSuggestionsDataNoTimeout(
137 SuggestionsService::ResponseCallback callback) {
138 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
139 if (pending_request_.get()) {
140 // Request already exists, so just add requestor to queue.
141 waiting_requestors_.push_back(callback);
146 DCHECK(waiting_requestors_.empty());
147 waiting_requestors_.push_back(callback);
149 pending_request_.reset(CreateSuggestionsRequest(suggestions_url_));
150 pending_request_->Start();
151 last_request_started_time_ = base::TimeTicks::Now();
154 void SuggestionsService::GetPageThumbnail(
156 base::Callback<void(const GURL&, const SkBitmap*)> callback) {
157 thumbnail_manager_->GetPageThumbnail(url, callback);
160 void SuggestionsService::BlacklistURL(
161 const GURL& candidate_url, SuggestionsService::ResponseCallback callback) {
162 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
163 waiting_requestors_.push_back(callback);
165 if (pending_request_.get()) {
166 if (IsBlacklistRequest(pending_request_.get())) {
167 // Pending request is a blacklist request. Silently drop the new blacklist
168 // request. TODO - handle this case.
171 // Pending request is not a blacklist request - cancel it and go on to
172 // issuing a blacklist request. Also ensure the timeout closure does not
173 // run; instead we'll wait for the updated suggestions before servicing
175 pending_request_.reset(NULL);
176 pending_timeout_closure_.reset(NULL);
180 // Send blacklisting request.
181 // TODO(manzagop): make this a PUT request instead of a GET request.
182 GURL url(blacklist_url_prefix_ +
183 net::EscapeQueryParamValue(candidate_url.spec(), true));
184 pending_request_.reset(CreateSuggestionsRequest(url));
185 pending_request_->Start();
186 last_request_started_time_ = base::TimeTicks::Now();
190 void SuggestionsService::RegisterProfilePrefs(
191 user_prefs::PrefRegistrySyncable* registry) {
192 SuggestionsStore::RegisterProfilePrefs(registry);
195 void SuggestionsService::OnRequestTimeout() {
196 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
200 void SuggestionsService::OnURLFetchComplete(const net::URLFetcher* source) {
201 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
202 DCHECK_EQ(pending_request_.get(), source);
204 // We no longer need the timeout closure. Delete it whether or not it has run
205 // (if it hasn't, this cancels it).
206 pending_timeout_closure_.reset();
208 // The fetcher will be deleted when the request is handled.
209 scoped_ptr<const net::URLFetcher> request(pending_request_.release());
210 const net::URLRequestStatus& request_status = request->GetStatus();
211 if (request_status.status() != net::URLRequestStatus::SUCCESS) {
212 UMA_HISTOGRAM_SPARSE_SLOWLY("Suggestions.FailedRequestErrorCode",
213 -request_status.error());
214 DVLOG(1) << "Suggestions server request failed with error: "
215 << request_status.error() << ": "
216 << net::ErrorToString(request_status.error());
217 // Dispatch the cached profile on error.
222 // Log the response code.
223 const int response_code = request->GetResponseCode();
224 UMA_HISTOGRAM_SPARSE_SLOWLY("Suggestions.FetchResponseCode", response_code);
225 if (response_code != net::HTTP_OK) {
226 // Aggressively clear the store.
227 suggestions_store_->ClearSuggestions();
228 DispatchRequestsAndClear(SuggestionsProfile(), &waiting_requestors_);
232 const base::TimeDelta latency =
233 base::TimeTicks::Now() - last_request_started_time_;
234 UMA_HISTOGRAM_MEDIUM_TIMES("Suggestions.FetchSuccessLatency", latency);
236 std::string suggestions_data;
237 bool success = request->GetResponseAsString(&suggestions_data);
240 // Compute suggestions, and dispatch then to requestors. On error still
241 // dispatch empty suggestions.
242 SuggestionsProfile suggestions;
243 if (suggestions_data.empty()) {
244 LogResponseState(RESPONSE_EMPTY);
245 suggestions_store_->ClearSuggestions();
246 } else if (suggestions.ParseFromString(suggestions_data)) {
247 LogResponseState(RESPONSE_VALID);
248 thumbnail_manager_->InitializeThumbnailMap(suggestions);
249 suggestions_store_->StoreSuggestions(suggestions);
251 LogResponseState(RESPONSE_INVALID);
252 suggestions_store_->LoadSuggestions(&suggestions);
255 DispatchRequestsAndClear(suggestions, &waiting_requestors_);
258 void SuggestionsService::Shutdown() {
259 // Cancel pending request and timeout closure, then serve existing requestors
261 pending_request_.reset(NULL);
262 pending_timeout_closure_.reset(NULL);
266 bool SuggestionsService::IsBlacklistRequest(net::URLFetcher* request) const {
268 return StartsWithASCII(request->GetOriginalURL().spec(),
269 blacklist_url_prefix_, true);
272 net::URLFetcher* SuggestionsService::CreateSuggestionsRequest(const GURL& url) {
273 net::URLFetcher* request =
274 net::URLFetcher::Create(0, url, net::URLFetcher::GET, this);
275 request->SetLoadFlags(net::LOAD_DISABLE_CACHE);
276 request->SetRequestContext(profile_->GetRequestContext());
277 // Add Chrome experiment state to the request headers.
278 net::HttpRequestHeaders headers;
279 chrome_variations::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders(
280 request->GetOriginalURL(), profile_->IsOffTheRecord(), false, &headers);
281 request->SetExtraRequestHeaders(headers.ToString());
285 void SuggestionsService::ServeFromCache() {
286 SuggestionsProfile suggestions;
287 suggestions_store_->LoadSuggestions(&suggestions);
288 DispatchRequestsAndClear(suggestions, &waiting_requestors_);
291 } // namespace suggestions