Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(84)

Side by Side Diff: components/ntp_snippets/remote/ntp_snippets_json_request.cc

Issue 2578173002: NTP: Extract JSON requests from Fetcher. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 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/ntp_snippets/remote/ntp_snippets_json_request.h"
6
7 #include <algorithm>
8 #include <utility>
9 #include <vector>
10
11 #include "base/command_line.h"
12 #include "base/json/json_writer.h"
13 #include "base/memory/ptr_util.h"
14 #include "base/metrics/histogram_macros.h"
15 #include "base/metrics/sparse_histogram.h"
16 #include "base/strings/stringprintf.h"
17 #include "base/time/tick_clock.h"
18 #include "base/time/time.h"
19 #include "base/values.h"
20 #include "components/data_use_measurement/core/data_use_user_data.h"
21 #include "components/ntp_snippets/category_info.h"
22 #include "components/ntp_snippets/features.h"
23 #include "components/ntp_snippets/remote/ntp_snippets_request_params.h"
24 #include "components/ntp_snippets/user_classifier.h"
25 #include "components/signin/core/browser/profile_oauth2_token_service.h"
26 #include "components/signin/core/browser/signin_manager.h"
27 #include "components/signin/core/browser/signin_manager_base.h"
28 #include "components/variations/net/variations_http_headers.h"
29 #include "components/variations/variations_associated_data.h"
30 #include "grit/components_strings.h"
31 #include "net/base/load_flags.h"
32 #include "net/http/http_response_headers.h"
33 #include "net/http/http_status_code.h"
34 #include "net/url_request/url_fetcher.h"
35 #include "net/url_request/url_request_context_getter.h"
36 #include "third_party/icu/source/common/unicode/uloc.h"
37 #include "third_party/icu/source/common/unicode/utypes.h"
38 #include "ui/base/l10n/l10n_util.h"
39
40 using net::URLFetcher;
41 using net::URLRequestContextGetter;
42 using net::HttpRequestHeaders;
43 using net::URLRequestStatus;
44 using translate::LanguageModel;
45
46 namespace ntp_snippets {
47
48 namespace internal {
49
50 namespace {
51
52 // Variation parameter for disabling the retry.
53 const char kBackground5xxRetriesName[] = "background_5xx_retries_count";
54
55 const int kMaxExcludedIds = 100;
56
57 // Variation parameter for sending LanguageModel info to the server.
58 const char kSendTopLanguagesName[] = "send_top_languages";
59
60 // Variation parameter for sending UserClassifier info to the server.
61 const char kSendUserClassName[] = "send_user_class";
62
63 const char kBooleanParameterEnabled[] = "true";
64 const char kBooleanParameterDisabled[] = "false";
65
66 bool IsBooleanParameterEnabled(const std::string& param_name,
67 bool default_value) {
68 std::string param_value = variations::GetVariationParamValueByFeature(
69 ntp_snippets::kArticleSuggestionsFeature, param_name);
70 if (param_value == kBooleanParameterEnabled) {
71 return true;
72 }
73 if (param_value == kBooleanParameterDisabled) {
74 return false;
75 }
76 if (!param_value.empty()) {
77 LOG(WARNING) << "Invalid value \"" << param_value
78 << "\" for variation parameter " << param_name;
79 }
80 return default_value;
81 }
82
83 int Get5xxRetryCount(bool interactive_request) {
84 if (interactive_request) {
85 return 2;
86 }
87 return std::max(0, variations::GetVariationParamByFeatureAsInt(
88 ntp_snippets::kArticleSuggestionsFeature,
89 kBackground5xxRetriesName, 0));
90 }
91
92 bool IsSendingTopLanguagesEnabled() {
93 return IsBooleanParameterEnabled(kSendTopLanguagesName,
94 /*default_value=*/false);
95 }
96
97 bool IsSendingUserClassEnabled() {
98 return IsBooleanParameterEnabled(kSendUserClassName,
99 /*default_value=*/false);
100 }
101
102 // Translate the BCP 47 |language_code| into a posix locale string.
103 std::string PosixLocaleFromBCP47Language(const std::string& language_code) {
104 char locale[ULOC_FULLNAME_CAPACITY];
105 UErrorCode error = U_ZERO_ERROR;
106 // Translate the input to a posix locale.
107 uloc_forLanguageTag(language_code.c_str(), locale, ULOC_FULLNAME_CAPACITY,
108 nullptr, &error);
109 if (error != U_ZERO_ERROR) {
110 DLOG(WARNING) << "Error in translating language code to a locale string: "
111 << error;
112 return std::string();
113 }
114 return locale;
115 }
116
117 std::string ISO639FromPosixLocale(const std::string& locale) {
118 char language[ULOC_LANG_CAPACITY];
119 UErrorCode error = U_ZERO_ERROR;
120 uloc_getLanguage(locale.c_str(), language, ULOC_LANG_CAPACITY, &error);
121 if (error != U_ZERO_ERROR) {
122 DLOG(WARNING)
123 << "Error in translating locale string to a ISO639 language code: "
124 << error;
125 return std::string();
126 }
127 return language;
128 }
129
130 void AppendLanguageInfoToList(base::ListValue* list,
131 const LanguageModel::LanguageInfo& info) {
132 auto lang = base::MakeUnique<base::DictionaryValue>();
133 lang->SetString("language", info.language_code);
134 lang->SetDouble("frequency", info.frequency);
135 list->Append(std::move(lang));
136 }
137
138 std::string GetUserClassString(UserClassifier::UserClass user_class) {
139 switch (user_class) {
140 case UserClassifier::UserClass::RARE_NTP_USER:
141 return "RARE_NTP_USER";
142 case UserClassifier::UserClass::ACTIVE_NTP_USER:
143 return "ACTIVE_NTP_USER";
144 case UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER:
145 return "ACTIVE_SUGGESTIONS_CONSUMER";
146 }
147 NOTREACHED();
148 return std::string();
149 }
150
151 } // namespace
152
153 CategoryInfo BuildArticleCategoryInfo(
154 const base::Optional<base::string16>& title) {
155 return CategoryInfo(
156 title.has_value() ? title.value()
157 : l10n_util::GetStringUTF16(
158 IDS_NTP_ARTICLE_SUGGESTIONS_SECTION_HEADER),
159 ContentSuggestionsCardLayout::FULL_CARD,
160 // TODO(dgn): merge has_more_action and has_reload_action when we remove
161 // the kFetchMoreFeature flag. See https://crbug.com/667752
162 /*has_more_action=*/base::FeatureList::IsEnabled(kFetchMoreFeature),
163 /*has_reload_action=*/true,
164 /*has_view_all_action=*/false,
165 /*show_if_empty=*/true,
166 l10n_util::GetStringUTF16(IDS_NTP_ARTICLE_SUGGESTIONS_SECTION_EMPTY));
167 }
168
169 CategoryInfo BuildRemoteCategoryInfo(const base::string16& title,
170 bool allow_fetching_more_results) {
171 return CategoryInfo(
172 title, ContentSuggestionsCardLayout::FULL_CARD,
173 // TODO(dgn): merge has_more_action and has_reload_action when we remove
174 // the kFetchMoreFeature flag. See https://crbug.com/667752
175 /*has_more_action=*/allow_fetching_more_results &&
176 base::FeatureList::IsEnabled(kFetchMoreFeature),
177 /*has_reload_action=*/allow_fetching_more_results,
178 /*has_view_all_action=*/false,
179 /*show_if_empty=*/false,
180 // TODO(tschumann): The message for no-articles is likely wrong
181 // and needs to be added to the stubby protocol if we want to
182 // support it.
183 l10n_util::GetStringUTF16(IDS_NTP_ARTICLE_SUGGESTIONS_SECTION_EMPTY));
184 }
185
186 NTPSnippetsJsonRequest::NTPSnippetsJsonRequest(
187 base::Optional<Category> exclusive_category,
188 base::TickClock* tick_clock, // Needed until destruction of the request.
189 const ParseJSONCallback& callback)
190 : exclusive_category_(exclusive_category),
191 tick_clock_(tick_clock),
192 parse_json_callback_(callback),
193 weak_ptr_factory_(this) {
194 creation_time_ = tick_clock_->NowTicks();
195 }
196
197 NTPSnippetsJsonRequest::~NTPSnippetsJsonRequest() {
198 LOG_IF(DFATAL, !request_completed_callback_.is_null())
199 << "The CompletionCallback was never called!";
200 }
201
202 void NTPSnippetsJsonRequest::Start(CompletedCallback callback) {
203 request_completed_callback_ = std::move(callback);
204 url_fetcher_->Start();
205 }
206
207 base::TimeDelta NTPSnippetsJsonRequest::GetFetchDuration() const {
208 return tick_clock_->NowTicks() - creation_time_;
209 }
210
211 std::string NTPSnippetsJsonRequest::GetResponseString() const {
212 std::string response;
213 url_fetcher_->GetResponseAsString(&response);
214 return response;
215 }
216
217 ////////////////////////////////////////////////////////////////////////////////
218 // URLFetcherDelegate overrides
219 void NTPSnippetsJsonRequest::OnURLFetchComplete(const net::URLFetcher* source) {
220 DCHECK_EQ(url_fetcher_.get(), source);
221 const URLRequestStatus& status = url_fetcher_->GetStatus();
222 int response = url_fetcher_->GetResponseCode();
223 UMA_HISTOGRAM_SPARSE_SLOWLY(
224 "NewTabPage.Snippets.FetchHttpResponseOrErrorCode",
225 status.is_success() ? response : status.error());
226
227 if (!status.is_success()) {
228 std::move(request_completed_callback_)
229 .Run(/*result=*/nullptr, FetchResult::URL_REQUEST_STATUS_ERROR,
230 /*error_details=*/base::StringPrintf(" %d", status.error()));
231 } else if (response != net::HTTP_OK) {
232 // TODO(jkrcal): https://crbug.com/609084
233 // We need to deal with the edge case again where the auth
234 // token expires just before we send the request (in which case we need to
235 // fetch a new auth token). We should extract that into a common class
236 // instead of adding it to every single class that uses auth tokens.
237 std::move(request_completed_callback_)
238 .Run(/*result=*/nullptr, FetchResult::HTTP_ERROR,
239 /*error_details=*/base::StringPrintf(" %d", response));
240 } else {
241 ParseJsonResponse();
242 }
243 }
244
245 void NTPSnippetsJsonRequest::ParseJsonResponse() {
246 std::string json_string;
247 bool stores_result_to_string =
248 url_fetcher_->GetResponseAsString(&json_string);
249 DCHECK(stores_result_to_string);
250
251 parse_json_callback_.Run(json_string,
252 base::Bind(&NTPSnippetsJsonRequest::OnJsonParsed,
253 weak_ptr_factory_.GetWeakPtr()),
254 base::Bind(&NTPSnippetsJsonRequest::OnJsonError,
255 weak_ptr_factory_.GetWeakPtr()));
256 }
257
258 void NTPSnippetsJsonRequest::OnJsonParsed(std::unique_ptr<base::Value> result) {
259 std::move(request_completed_callback_)
260 .Run(std::move(result), FetchResult::SUCCESS,
261 /*error_details=*/std::string());
262 }
263
264 void NTPSnippetsJsonRequest::OnJsonError(const std::string& error) {
265 std::string json_string;
266 url_fetcher_->GetResponseAsString(&json_string);
267 LOG(WARNING) << "Received invalid JSON (" << error << "): " << json_string;
268 std::move(request_completed_callback_)
269 .Run(/*result=*/nullptr, FetchResult::JSON_PARSE_ERROR,
270 /*error_details=*/base::StringPrintf(" (error %s)", error.c_str()));
271 }
272
273 NTPSnippetsJsonRequest::Builder::Builder()
274 : fetch_api_(CHROME_READER_API),
275 personalization_(Personalization::kBoth),
276 language_model_(nullptr) {}
277 NTPSnippetsJsonRequest::Builder::Builder(NTPSnippetsJsonRequest::Builder&&) =
278 default;
279 NTPSnippetsJsonRequest::Builder::~Builder() = default;
280
281 std::unique_ptr<NTPSnippetsJsonRequest> NTPSnippetsJsonRequest::Builder::Build()
282 const {
283 DCHECK(!url_.is_empty());
284 DCHECK(url_request_context_getter_);
285 DCHECK(tick_clock_);
286 auto request = base::MakeUnique<NTPSnippetsJsonRequest>(
287 params_.exclusive_category, tick_clock_, parse_json_callback_);
288 std::string body = BuildBody();
289 std::string headers = BuildHeaders();
290 request->url_fetcher_ = BuildURLFetcher(request.get(), headers, body);
291
292 // Log the request for debugging network issues.
293 VLOG(1) << "Sending a NTP snippets request to " << url_ << ":\n"
294 << headers << "\n"
295 << body;
296
297 return request;
298 }
299
300 NTPSnippetsJsonRequest::Builder&
301 NTPSnippetsJsonRequest::Builder::SetAuthentication(
302 const std::string& account_id,
303 const std::string& auth_header) {
304 obfuscated_gaia_id_ = account_id;
305 auth_header_ = auth_header;
306 return *this;
307 }
308
309 NTPSnippetsJsonRequest::Builder& NTPSnippetsJsonRequest::Builder::SetFetchAPI(
310 FetchAPI fetch_api) {
311 fetch_api_ = fetch_api;
312 return *this;
313 }
314
315 NTPSnippetsJsonRequest::Builder&
316 NTPSnippetsJsonRequest::Builder::SetLanguageModel(
317 const translate::LanguageModel* language_model) {
318 language_model_ = language_model;
319 return *this;
320 }
321
322 NTPSnippetsJsonRequest::Builder& NTPSnippetsJsonRequest::Builder::SetParams(
323 const NTPSnippetsRequestParams& params) {
324 params_ = params;
325 return *this;
326 }
327
328 NTPSnippetsJsonRequest::Builder&
329 NTPSnippetsJsonRequest::Builder::SetParseJsonCallback(
330 ParseJSONCallback callback) {
331 parse_json_callback_ = callback;
332 return *this;
333 }
334
335 NTPSnippetsJsonRequest::Builder&
336 NTPSnippetsJsonRequest::Builder::SetPersonalization(
337 Personalization personalization) {
338 personalization_ = personalization;
339 return *this;
340 }
341
342 NTPSnippetsJsonRequest::Builder& NTPSnippetsJsonRequest::Builder::SetTickClock(
343 base::TickClock* tick_clock) {
344 tick_clock_ = tick_clock;
345 return *this;
346 }
347
348 NTPSnippetsJsonRequest::Builder& NTPSnippetsJsonRequest::Builder::SetUrl(
349 const GURL& url) {
350 url_ = url;
351 return *this;
352 }
353
354 NTPSnippetsJsonRequest::Builder&
355 NTPSnippetsJsonRequest::Builder::SetUrlRequestContextGetter(
356 const scoped_refptr<net::URLRequestContextGetter>& context_getter) {
357 url_request_context_getter_ = context_getter;
358 return *this;
359 }
360
361 NTPSnippetsJsonRequest::Builder&
362 NTPSnippetsJsonRequest::Builder::SetUserClassifier(
363 const UserClassifier& user_classifier) {
364 if (IsSendingUserClassEnabled()) {
365 user_class_ = GetUserClassString(user_classifier.GetUserClass());
366 }
367 return *this;
368 }
369
370 std::string NTPSnippetsJsonRequest::Builder::BuildHeaders() const {
371 net::HttpRequestHeaders headers;
372 headers.SetHeader("Content-Type", "application/json; charset=UTF-8");
373 if (!auth_header_.empty()) {
374 headers.SetHeader("Authorization", auth_header_);
375 }
376 // Add X-Client-Data header with experiment IDs from field trials.
377 // Note: It's fine to pass in |is_signed_in| false, which does not affect
378 // transmission of experiment ids coming from the variations server.
379 bool is_signed_in = false;
380 variations::AppendVariationHeaders(url_,
381 false, // incognito
382 false, // uma_enabled
383 is_signed_in, &headers);
384 return headers.ToString();
385 }
386
387 std::string NTPSnippetsJsonRequest::Builder::BuildBody() const {
388 auto request = base::MakeUnique<base::DictionaryValue>();
389 std::string user_locale = PosixLocaleFromBCP47Language(params_.language_code);
390 switch (fetch_api_) {
391 case CHROME_READER_API: {
392 auto content_params = base::MakeUnique<base::DictionaryValue>();
393 content_params->SetBoolean("only_return_personalized_results",
394 ReturnOnlyPersonalizedResults());
395
396 auto content_restricts = base::MakeUnique<base::ListValue>();
397 for (const auto* metadata : {"TITLE", "SNIPPET", "THUMBNAIL"}) {
398 auto entry = base::MakeUnique<base::DictionaryValue>();
399 entry->SetString("type", "METADATA");
400 entry->SetString("value", metadata);
401 content_restricts->Append(std::move(entry));
402 }
403
404 auto local_scoring_params = base::MakeUnique<base::DictionaryValue>();
405 local_scoring_params->Set("content_params", std::move(content_params));
406 local_scoring_params->Set("content_restricts",
407 std::move(content_restricts));
408
409 auto global_scoring_params = base::MakeUnique<base::DictionaryValue>();
410 global_scoring_params->SetInteger("num_to_return",
411 params_.count_to_fetch);
412 global_scoring_params->SetInteger("sort_type", 1);
413
414 auto advanced = base::MakeUnique<base::DictionaryValue>();
415 advanced->Set("local_scoring_params", std::move(local_scoring_params));
416 advanced->Set("global_scoring_params", std::move(global_scoring_params));
417
418 request->SetString("response_detail_level", "STANDARD");
419 request->Set("advanced_options", std::move(advanced));
420 if (!obfuscated_gaia_id_.empty()) {
421 request->SetString("obfuscated_gaia_id", obfuscated_gaia_id_);
422 }
423 if (!user_locale.empty()) {
424 request->SetString("user_locale", user_locale);
425 }
426 break;
427 }
428
429 case CHROME_CONTENT_SUGGESTIONS_API: {
430 if (!user_locale.empty()) {
431 request->SetString("uiLanguage", user_locale);
432 }
433
434 request->SetString("priority", params_.interactive_request
435 ? "USER_ACTION"
436 : "BACKGROUND_PREFETCH");
437
438 auto excluded = base::MakeUnique<base::ListValue>();
439 for (const auto& id : params_.excluded_ids) {
440 excluded->AppendString(id);
441 if (excluded->GetSize() >= kMaxExcludedIds) {
442 break;
443 }
444 }
445 request->Set("excludedSuggestionIds", std::move(excluded));
446
447 if (!user_class_.empty()) {
448 request->SetString("userActivenessClass", user_class_);
449 }
450
451 translate::LanguageModel::LanguageInfo ui_language;
452 translate::LanguageModel::LanguageInfo other_top_language;
453 PrepareLanguages(&ui_language, &other_top_language);
454
455 if (ui_language.frequency == 0 && other_top_language.frequency == 0) {
456 break;
457 }
458
459 auto language_list = base::MakeUnique<base::ListValue>();
460 if (ui_language.frequency > 0) {
461 AppendLanguageInfoToList(language_list.get(), ui_language);
462 }
463 if (other_top_language.frequency > 0) {
464 AppendLanguageInfoToList(language_list.get(), other_top_language);
465 }
466 request->Set("topLanguages", std::move(language_list));
467
468 // TODO(sfiera): Support only_return_personalized_results.
469 // TODO(sfiera): Support count_to_fetch.
470 break;
471 }
472 }
473
474 std::string request_json;
475 bool success = base::JSONWriter::WriteWithOptions(
476 *request, base::JSONWriter::OPTIONS_PRETTY_PRINT, &request_json);
477 DCHECK(success);
478 return request_json;
479 }
480
481 std::unique_ptr<net::URLFetcher>
482 NTPSnippetsJsonRequest::Builder::BuildURLFetcher(
483 net::URLFetcherDelegate* delegate,
484 const std::string& headers,
485 const std::string& body) const {
486 std::unique_ptr<net::URLFetcher> url_fetcher =
487 net::URLFetcher::Create(url_, net::URLFetcher::POST, delegate);
488 url_fetcher->SetRequestContext(url_request_context_getter_.get());
489 url_fetcher->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
490 net::LOAD_DO_NOT_SAVE_COOKIES);
491 data_use_measurement::DataUseUserData::AttachToFetcher(
492 url_fetcher.get(), data_use_measurement::DataUseUserData::NTP_SNIPPETS);
493
494 url_fetcher->SetExtraRequestHeaders(headers);
495 url_fetcher->SetUploadData("application/json", body);
496
497 // Fetchers are sometimes cancelled because a network change was detected.
498 url_fetcher->SetAutomaticallyRetryOnNetworkChanges(3);
499 url_fetcher->SetMaxRetriesOn5xx(
500 Get5xxRetryCount(params_.interactive_request));
501 return url_fetcher;
502 }
503
504 void NTPSnippetsJsonRequest::Builder::PrepareLanguages(
505 translate::LanguageModel::LanguageInfo* ui_language,
506 translate::LanguageModel::LanguageInfo* other_top_language) const {
507 // TODO(jkrcal): Add language model factory for iOS and add fakes to tests so
508 // that |language_model| is never nullptr. Remove this check and add a DCHECK
509 // into the constructor.
510 if (!language_model_ || !IsSendingTopLanguagesEnabled()) {
511 return;
512 }
513
514 // TODO(jkrcal): Is this back-and-forth converting necessary?
515 ui_language->language_code = ISO639FromPosixLocale(
516 PosixLocaleFromBCP47Language(params_.language_code));
517 ui_language->frequency =
518 language_model_->GetLanguageFrequency(ui_language->language_code);
519
520 std::vector<LanguageModel::LanguageInfo> top_languages =
521 language_model_->GetTopLanguages();
522 for (const LanguageModel::LanguageInfo& info : top_languages) {
523 if (info.language_code != ui_language->language_code) {
524 *other_top_language = info;
525
526 // Report to UMA how important the UI language is.
527 DCHECK_GT(other_top_language->frequency, 0)
528 << "GetTopLanguages() should not return languages with 0 frequency";
529 float ratio_ui_in_both_languages =
530 ui_language->frequency /
531 (ui_language->frequency + other_top_language->frequency);
532 UMA_HISTOGRAM_PERCENTAGE(
533 "NewTabPage.Languages.UILanguageRatioInTwoTopLanguages",
534 ratio_ui_in_both_languages * 100);
535 break;
536 }
537 }
538 }
539
540 } // namespace internal
541
542 } // namespace ntp_snippets
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698