blob: fea0ee0327af3d45bc525ac54da3d5c90b734b23 [file] [log] [blame]
fhorschigcb5d7fc02016-12-20 13:14:551// 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
treib9de525a2017-01-19 12:20:375#include "components/ntp_snippets/remote/json_request.h"
fhorschigcb5d7fc02016-12-20 13:14:556
7#include <algorithm>
8#include <utility>
9#include <vector>
10
11#include "base/command_line.h"
12#include "base/json/json_writer.h"
Ilya Sherman1edb6f182017-12-12 04:00:4213#include "base/metrics/histogram_functions.h"
fhorschigcb5d7fc02016-12-20 13:14:5514#include "base/metrics/histogram_macros.h"
fhorschigcb5d7fc02016-12-20 13:14:5515#include "base/strings/stringprintf.h"
markusheintz05b1e882017-02-15 14:38:1916#include "base/time/clock.h"
fhorschigcb5d7fc02016-12-20 13:14:5517#include "base/values.h"
18#include "components/data_use_measurement/core/data_use_user_data.h"
19#include "components/ntp_snippets/category_info.h"
20#include "components/ntp_snippets/features.h"
treib9de525a2017-01-19 12:20:3721#include "components/ntp_snippets/remote/request_params.h"
fhorschigcb5d7fc02016-12-20 13:14:5522#include "components/ntp_snippets/user_classifier.h"
thakisfe8fa0a2017-02-23 19:46:3623#include "components/strings/grit/components_strings.h"
fhorschigcb5d7fc02016-12-20 13:14:5524#include "components/variations/net/variations_http_headers.h"
25#include "components/variations/variations_associated_data.h"
fhorschigcb5d7fc02016-12-20 13:14:5526#include "net/base/load_flags.h"
27#include "net/http/http_response_headers.h"
28#include "net/http/http_status_code.h"
rhalavatic0ea6f042017-02-27 15:58:0629#include "net/traffic_annotation/network_traffic_annotation.h"
fhorschigcb5d7fc02016-12-20 13:14:5530#include "net/url_request/url_fetcher.h"
31#include "net/url_request/url_request_context_getter.h"
32#include "third_party/icu/source/common/unicode/uloc.h"
33#include "third_party/icu/source/common/unicode/utypes.h"
34#include "ui/base/l10n/l10n_util.h"
35
Michael Martis98cd8732017-07-14 03:26:1936using language::UrlLanguageHistogram;
fhorschigcb5d7fc02016-12-20 13:14:5537using net::URLFetcher;
38using net::URLRequestContextGetter;
39using net::HttpRequestHeaders;
40using net::URLRequestStatus;
fhorschigcb5d7fc02016-12-20 13:14:5541
42namespace ntp_snippets {
43
44namespace internal {
45
46namespace {
47
48// Variation parameter for disabling the retry.
49const char kBackground5xxRetriesName[] = "background_5xx_retries_count";
50
Michael Martis98cd8732017-07-14 03:26:1951// Variation parameter for sending UrlLanguageHistogram info to the server.
fhorschigcb5d7fc02016-12-20 13:14:5552const char kSendTopLanguagesName[] = "send_top_languages";
53
54// Variation parameter for sending UserClassifier info to the server.
55const char kSendUserClassName[] = "send_user_class";
56
57int Get5xxRetryCount(bool interactive_request) {
58 if (interactive_request) {
59 return 2;
60 }
61 return std::max(0, variations::GetVariationParamByFeatureAsInt(
62 ntp_snippets::kArticleSuggestionsFeature,
63 kBackground5xxRetriesName, 0));
64}
65
66bool IsSendingTopLanguagesEnabled() {
67 return variations::GetVariationParamByFeatureAsBool(
68 ntp_snippets::kArticleSuggestionsFeature, kSendTopLanguagesName,
jkrcald1625e22017-01-18 16:54:3869 /*default_value=*/true);
fhorschigcb5d7fc02016-12-20 13:14:5570}
71
72bool IsSendingUserClassEnabled() {
73 return variations::GetVariationParamByFeatureAsBool(
74 ntp_snippets::kArticleSuggestionsFeature, kSendUserClassName,
Tim Schumann7e4602982017-08-28 13:34:5375 /*default_value=*/true);
fhorschigcb5d7fc02016-12-20 13:14:5576}
77
78// Translate the BCP 47 |language_code| into a posix locale string.
79std::string PosixLocaleFromBCP47Language(const std::string& language_code) {
80 char locale[ULOC_FULLNAME_CAPACITY];
81 UErrorCode error = U_ZERO_ERROR;
82 // Translate the input to a posix locale.
83 uloc_forLanguageTag(language_code.c_str(), locale, ULOC_FULLNAME_CAPACITY,
84 nullptr, &error);
85 if (error != U_ZERO_ERROR) {
86 DLOG(WARNING) << "Error in translating language code to a locale string: "
87 << error;
88 return std::string();
89 }
90 return locale;
91}
92
93std::string ISO639FromPosixLocale(const std::string& locale) {
94 char language[ULOC_LANG_CAPACITY];
95 UErrorCode error = U_ZERO_ERROR;
96 uloc_getLanguage(locale.c_str(), language, ULOC_LANG_CAPACITY, &error);
97 if (error != U_ZERO_ERROR) {
98 DLOG(WARNING)
99 << "Error in translating locale string to a ISO639 language code: "
100 << error;
101 return std::string();
102 }
103 return language;
104}
105
106void AppendLanguageInfoToList(base::ListValue* list,
Michael Martis98cd8732017-07-14 03:26:19107 const UrlLanguageHistogram::LanguageInfo& info) {
Jinho Bangedffb4ee2018-01-02 15:38:30108 auto lang = std::make_unique<base::DictionaryValue>();
fhorschigcb5d7fc02016-12-20 13:14:55109 lang->SetString("language", info.language_code);
110 lang->SetDouble("frequency", info.frequency);
111 list->Append(std::move(lang));
112}
113
114std::string GetUserClassString(UserClassifier::UserClass user_class) {
115 switch (user_class) {
116 case UserClassifier::UserClass::RARE_NTP_USER:
117 return "RARE_NTP_USER";
118 case UserClassifier::UserClass::ACTIVE_NTP_USER:
119 return "ACTIVE_NTP_USER";
120 case UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER:
121 return "ACTIVE_SUGGESTIONS_CONSUMER";
122 }
123 NOTREACHED();
124 return std::string();
125}
126
127} // namespace
128
treib9de525a2017-01-19 12:20:37129JsonRequest::JsonRequest(
fhorschigcb5d7fc02016-12-20 13:14:55130 base::Optional<Category> exclusive_category,
markusheintz05b1e882017-02-15 14:38:19131 base::Clock* clock, // Needed until destruction of the request.
fhorschigcb5d7fc02016-12-20 13:14:55132 const ParseJSONCallback& callback)
133 : exclusive_category_(exclusive_category),
markusheintz05b1e882017-02-15 14:38:19134 clock_(clock),
fhorschigcb5d7fc02016-12-20 13:14:55135 parse_json_callback_(callback),
136 weak_ptr_factory_(this) {
markusheintz05b1e882017-02-15 14:38:19137 creation_time_ = clock_->Now();
fhorschigcb5d7fc02016-12-20 13:14:55138}
139
treib9de525a2017-01-19 12:20:37140JsonRequest::~JsonRequest() {
fhorschigcb5d7fc02016-12-20 13:14:55141 LOG_IF(DFATAL, !request_completed_callback_.is_null())
142 << "The CompletionCallback was never called!";
143}
144
treib9de525a2017-01-19 12:20:37145void JsonRequest::Start(CompletedCallback callback) {
fhorschigcb5d7fc02016-12-20 13:14:55146 request_completed_callback_ = std::move(callback);
147 url_fetcher_->Start();
148}
149
treib9de525a2017-01-19 12:20:37150base::TimeDelta JsonRequest::GetFetchDuration() const {
markusheintz05b1e882017-02-15 14:38:19151 return clock_->Now() - creation_time_;
fhorschigcb5d7fc02016-12-20 13:14:55152}
153
treib9de525a2017-01-19 12:20:37154std::string JsonRequest::GetResponseString() const {
fhorschigcb5d7fc02016-12-20 13:14:55155 std::string response;
156 url_fetcher_->GetResponseAsString(&response);
157 return response;
158}
159
160////////////////////////////////////////////////////////////////////////////////
161// URLFetcherDelegate overrides
treib9de525a2017-01-19 12:20:37162void JsonRequest::OnURLFetchComplete(const net::URLFetcher* source) {
fhorschigcb5d7fc02016-12-20 13:14:55163 DCHECK_EQ(url_fetcher_.get(), source);
164 const URLRequestStatus& status = url_fetcher_->GetStatus();
165 int response = url_fetcher_->GetResponseCode();
Ilya Sherman1edb6f182017-12-12 04:00:42166 base::UmaHistogramSparse("NewTabPage.Snippets.FetchHttpResponseOrErrorCode",
167 status.is_success() ? response : status.error());
fhorschigcb5d7fc02016-12-20 13:14:55168
169 if (!status.is_success()) {
170 std::move(request_completed_callback_)
171 .Run(/*result=*/nullptr, FetchResult::URL_REQUEST_STATUS_ERROR,
172 /*error_details=*/base::StringPrintf(" %d", status.error()));
173 } else if (response != net::HTTP_OK) {
174 // TODO(jkrcal): https://crbug.com/609084
175 // We need to deal with the edge case again where the auth
176 // token expires just before we send the request (in which case we need to
177 // fetch a new auth token). We should extract that into a common class
178 // instead of adding it to every single class that uses auth tokens.
179 std::move(request_completed_callback_)
180 .Run(/*result=*/nullptr, FetchResult::HTTP_ERROR,
181 /*error_details=*/base::StringPrintf(" %d", response));
182 } else {
183 ParseJsonResponse();
184 }
185}
186
treib9de525a2017-01-19 12:20:37187void JsonRequest::ParseJsonResponse() {
fhorschigcb5d7fc02016-12-20 13:14:55188 std::string json_string;
189 bool stores_result_to_string =
190 url_fetcher_->GetResponseAsString(&json_string);
191 DCHECK(stores_result_to_string);
192
treib9de525a2017-01-19 12:20:37193 parse_json_callback_.Run(
194 json_string,
195 base::Bind(&JsonRequest::OnJsonParsed, weak_ptr_factory_.GetWeakPtr()),
196 base::Bind(&JsonRequest::OnJsonError, weak_ptr_factory_.GetWeakPtr()));
fhorschigcb5d7fc02016-12-20 13:14:55197}
198
treib9de525a2017-01-19 12:20:37199void JsonRequest::OnJsonParsed(std::unique_ptr<base::Value> result) {
fhorschigcb5d7fc02016-12-20 13:14:55200 std::move(request_completed_callback_)
201 .Run(std::move(result), FetchResult::SUCCESS,
202 /*error_details=*/std::string());
203}
204
treib9de525a2017-01-19 12:20:37205void JsonRequest::OnJsonError(const std::string& error) {
fhorschigcb5d7fc02016-12-20 13:14:55206 std::string json_string;
207 url_fetcher_->GetResponseAsString(&json_string);
208 LOG(WARNING) << "Received invalid JSON (" << error << "): " << json_string;
209 std::move(request_completed_callback_)
210 .Run(/*result=*/nullptr, FetchResult::JSON_PARSE_ERROR,
211 /*error_details=*/base::StringPrintf(" (error %s)", error.c_str()));
212}
213
Michael Martis98cd8732017-07-14 03:26:19214JsonRequest::Builder::Builder() : language_histogram_(nullptr) {}
treib9de525a2017-01-19 12:20:37215JsonRequest::Builder::Builder(JsonRequest::Builder&&) = default;
216JsonRequest::Builder::~Builder() = default;
fhorschigcb5d7fc02016-12-20 13:14:55217
treib9de525a2017-01-19 12:20:37218std::unique_ptr<JsonRequest> JsonRequest::Builder::Build() const {
fhorschigcb5d7fc02016-12-20 13:14:55219 DCHECK(!url_.is_empty());
220 DCHECK(url_request_context_getter_);
markusheintz05b1e882017-02-15 14:38:19221 DCHECK(clock_);
Jinho Bangedffb4ee2018-01-02 15:38:30222 auto request = std::make_unique<JsonRequest>(params_.exclusive_category,
markusheintz05b1e882017-02-15 14:38:19223 clock_, parse_json_callback_);
fhorschigcb5d7fc02016-12-20 13:14:55224 std::string body = BuildBody();
225 std::string headers = BuildHeaders();
226 request->url_fetcher_ = BuildURLFetcher(request.get(), headers, body);
227
228 // Log the request for debugging network issues.
229 VLOG(1) << "Sending a NTP snippets request to " << url_ << ":\n"
230 << headers << "\n"
231 << body;
232
233 return request;
234}
235
treib9de525a2017-01-19 12:20:37236JsonRequest::Builder& JsonRequest::Builder::SetAuthentication(
fhorschigcb5d7fc02016-12-20 13:14:55237 const std::string& account_id,
238 const std::string& auth_header) {
239 obfuscated_gaia_id_ = account_id;
240 auth_header_ = auth_header;
241 return *this;
242}
243
Michael Martis98cd8732017-07-14 03:26:19244JsonRequest::Builder& JsonRequest::Builder::SetLanguageHistogram(
245 const language::UrlLanguageHistogram* language_histogram) {
246 language_histogram_ = language_histogram;
fhorschigcb5d7fc02016-12-20 13:14:55247 return *this;
248}
249
treib9de525a2017-01-19 12:20:37250JsonRequest::Builder& JsonRequest::Builder::SetParams(
251 const RequestParams& params) {
fhorschigcb5d7fc02016-12-20 13:14:55252 params_ = params;
253 return *this;
254}
255
treib9de525a2017-01-19 12:20:37256JsonRequest::Builder& JsonRequest::Builder::SetParseJsonCallback(
fhorschigcb5d7fc02016-12-20 13:14:55257 ParseJSONCallback callback) {
258 parse_json_callback_ = callback;
259 return *this;
260}
261
markusheintz05b1e882017-02-15 14:38:19262JsonRequest::Builder& JsonRequest::Builder::SetClock(base::Clock* clock) {
263 clock_ = clock;
fhorschigcb5d7fc02016-12-20 13:14:55264 return *this;
265}
266
treib9de525a2017-01-19 12:20:37267JsonRequest::Builder& JsonRequest::Builder::SetUrl(const GURL& url) {
fhorschigcb5d7fc02016-12-20 13:14:55268 url_ = url;
269 return *this;
270}
271
treib9de525a2017-01-19 12:20:37272JsonRequest::Builder& JsonRequest::Builder::SetUrlRequestContextGetter(
fhorschigcb5d7fc02016-12-20 13:14:55273 const scoped_refptr<net::URLRequestContextGetter>& context_getter) {
274 url_request_context_getter_ = context_getter;
275 return *this;
276}
277
treib9de525a2017-01-19 12:20:37278JsonRequest::Builder& JsonRequest::Builder::SetUserClassifier(
fhorschigcb5d7fc02016-12-20 13:14:55279 const UserClassifier& user_classifier) {
280 if (IsSendingUserClassEnabled()) {
281 user_class_ = GetUserClassString(user_classifier.GetUserClass());
282 }
283 return *this;
284}
285
treib9de525a2017-01-19 12:20:37286std::string JsonRequest::Builder::BuildHeaders() const {
fhorschigcb5d7fc02016-12-20 13:14:55287 net::HttpRequestHeaders headers;
288 headers.SetHeader("Content-Type", "application/json; charset=UTF-8");
289 if (!auth_header_.empty()) {
290 headers.SetHeader("Authorization", auth_header_);
291 }
292 // Add X-Client-Data header with experiment IDs from field trials.
Mark Pearsoncea91cf2017-12-13 20:45:58293 // Note: It's OK to pass SignedIn::kNo if it's unknown, as it does not affect
294 // transmission of experiments coming from the variations server.
295 variations::AppendVariationHeaders(url_, variations::InIncognito::kNo,
296 variations::SignedIn::kNo, &headers);
fhorschigcb5d7fc02016-12-20 13:14:55297 return headers.ToString();
298}
299
treib9de525a2017-01-19 12:20:37300std::string JsonRequest::Builder::BuildBody() const {
Jinho Bangedffb4ee2018-01-02 15:38:30301 auto request = std::make_unique<base::DictionaryValue>();
fhorschigcb5d7fc02016-12-20 13:14:55302 std::string user_locale = PosixLocaleFromBCP47Language(params_.language_code);
treiba57f51e2017-03-23 14:47:52303 if (!user_locale.empty()) {
304 request->SetString("uiLanguage", user_locale);
305 }
fhorschigcb5d7fc02016-12-20 13:14:55306
treiba57f51e2017-03-23 14:47:52307 request->SetString("priority", params_.interactive_request
308 ? "USER_ACTION"
309 : "BACKGROUND_PREFETCH");
fhorschigcb5d7fc02016-12-20 13:14:55310
Jinho Bangedffb4ee2018-01-02 15:38:30311 auto excluded = std::make_unique<base::ListValue>();
treiba57f51e2017-03-23 14:47:52312 for (const auto& id : params_.excluded_ids) {
313 excluded->AppendString(id);
fhorschigcb5d7fc02016-12-20 13:14:55314 }
treiba57f51e2017-03-23 14:47:52315 request->Set("excludedSuggestionIds", std::move(excluded));
316
317 if (!user_class_.empty()) {
318 request->SetString("userActivenessClass", user_class_);
319 }
320
Michael Martis98cd8732017-07-14 03:26:19321 language::UrlLanguageHistogram::LanguageInfo ui_language;
322 language::UrlLanguageHistogram::LanguageInfo other_top_language;
treiba57f51e2017-03-23 14:47:52323 PrepareLanguages(&ui_language, &other_top_language);
324 if (ui_language.frequency != 0 || other_top_language.frequency != 0) {
Jinho Bangedffb4ee2018-01-02 15:38:30325 auto language_list = std::make_unique<base::ListValue>();
treiba57f51e2017-03-23 14:47:52326 if (ui_language.frequency > 0) {
327 AppendLanguageInfoToList(language_list.get(), ui_language);
328 }
329 if (other_top_language.frequency > 0) {
330 AppendLanguageInfoToList(language_list.get(), other_top_language);
331 }
332 request->Set("topLanguages", std::move(language_list));
333 }
334
Vitalii Iarko3fdb9f912017-09-21 08:28:25335 // TODO(vitaliii): Support count_to_fetch without requiring
336 // |exclusive_category|.
337 if (params_.exclusive_category.has_value()) {
338 base::DictionaryValue exclusive_category_parameters;
339 exclusive_category_parameters.SetInteger(
340 "id", params_.exclusive_category->remote_id());
341 exclusive_category_parameters.SetInteger("numSuggestions",
342 params_.count_to_fetch);
343 base::ListValue category_parameters;
344 category_parameters.GetList().push_back(
345 std::move(exclusive_category_parameters));
346 request->SetKey("categoryParameters", std::move(category_parameters));
347 }
fhorschigcb5d7fc02016-12-20 13:14:55348
349 std::string request_json;
350 bool success = base::JSONWriter::WriteWithOptions(
351 *request, base::JSONWriter::OPTIONS_PRETTY_PRINT, &request_json);
352 DCHECK(success);
353 return request_json;
354}
355
treib9de525a2017-01-19 12:20:37356std::unique_ptr<net::URLFetcher> JsonRequest::Builder::BuildURLFetcher(
fhorschigcb5d7fc02016-12-20 13:14:55357 net::URLFetcherDelegate* delegate,
358 const std::string& headers,
359 const std::string& body) const {
rhalavatic0ea6f042017-02-27 15:58:06360 net::NetworkTrafficAnnotationTag traffic_annotation =
361 net::DefineNetworkTrafficAnnotation("ntp_snippets_fetch", R"(
362 semantics {
363 sender: "New Tab Page Content Suggestions Fetch"
364 description:
365 "Chromium can show content suggestions (e.g. news articles) on the "
366 "New Tab page. For signed-in users, these may be personalized "
367 "based on the user's synced browsing history."
368 trigger:
369 "Triggered periodically in the background, or upon explicit user "
370 "request."
371 data:
372 "The Chromium UI language, as well as a second language the user "
Michael Martis98cd8732017-07-14 03:26:19373 "understands, based on language::UrlLanguageHistogram. For "
374 "signed-in users, the requests is authenticated."
rhalavatic0ea6f042017-02-27 15:58:06375 destination: GOOGLE_OWNED_SERVICE
376 }
377 policy {
Ramin Halavati3b979782017-07-21 11:40:26378 cookies_allowed: NO
rhalavatic0ea6f042017-02-27 15:58:06379 setting:
380 "This feature cannot be disabled by settings now (but is requested "
381 "to be implemented in crbug.com/695129)."
rhalavatieaa64e92017-04-03 09:36:43382 chrome_policy {
rhalavatic0ea6f042017-02-27 15:58:06383 NTPContentSuggestionsEnabled {
384 policy_options {mode: MANDATORY}
rhalavatieaa64e92017-04-03 09:36:43385 NTPContentSuggestionsEnabled: false
rhalavatic0ea6f042017-02-27 15:58:06386 }
387 }
388 })");
389 std::unique_ptr<net::URLFetcher> url_fetcher = net::URLFetcher::Create(
390 url_, net::URLFetcher::POST, delegate, traffic_annotation);
fhorschigcb5d7fc02016-12-20 13:14:55391 url_fetcher->SetRequestContext(url_request_context_getter_.get());
392 url_fetcher->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
393 net::LOAD_DO_NOT_SAVE_COOKIES);
394 data_use_measurement::DataUseUserData::AttachToFetcher(
jkrcalcd18da62017-02-10 07:53:50395 url_fetcher.get(),
396 data_use_measurement::DataUseUserData::NTP_SNIPPETS_SUGGESTIONS);
fhorschigcb5d7fc02016-12-20 13:14:55397
398 url_fetcher->SetExtraRequestHeaders(headers);
399 url_fetcher->SetUploadData("application/json", body);
400
401 // Fetchers are sometimes cancelled because a network change was detected.
402 url_fetcher->SetAutomaticallyRetryOnNetworkChanges(3);
403 url_fetcher->SetMaxRetriesOn5xx(
404 Get5xxRetryCount(params_.interactive_request));
405 return url_fetcher;
406}
407
treib9de525a2017-01-19 12:20:37408void JsonRequest::Builder::PrepareLanguages(
Michael Martis98cd8732017-07-14 03:26:19409 language::UrlLanguageHistogram::LanguageInfo* ui_language,
410 language::UrlLanguageHistogram::LanguageInfo* other_top_language) const {
fhorschigcb5d7fc02016-12-20 13:14:55411 // TODO(jkrcal): Add language model factory for iOS and add fakes to tests so
Michael Martis98cd8732017-07-14 03:26:19412 // that |language_histogram| is never nullptr. Remove this check and add a
413 // DCHECK into the constructor.
414 if (!language_histogram_ || !IsSendingTopLanguagesEnabled()) {
fhorschigcb5d7fc02016-12-20 13:14:55415 return;
416 }
417
418 // TODO(jkrcal): Is this back-and-forth converting necessary?
419 ui_language->language_code = ISO639FromPosixLocale(
420 PosixLocaleFromBCP47Language(params_.language_code));
421 ui_language->frequency =
Michael Martis98cd8732017-07-14 03:26:19422 language_histogram_->GetLanguageFrequency(ui_language->language_code);
fhorschigcb5d7fc02016-12-20 13:14:55423
Michael Martis98cd8732017-07-14 03:26:19424 std::vector<UrlLanguageHistogram::LanguageInfo> top_languages =
425 language_histogram_->GetTopLanguages();
426 for (const UrlLanguageHistogram::LanguageInfo& info : top_languages) {
fhorschigcb5d7fc02016-12-20 13:14:55427 if (info.language_code != ui_language->language_code) {
428 *other_top_language = info;
429
430 // Report to UMA how important the UI language is.
431 DCHECK_GT(other_top_language->frequency, 0)
432 << "GetTopLanguages() should not return languages with 0 frequency";
433 float ratio_ui_in_both_languages =
434 ui_language->frequency /
435 (ui_language->frequency + other_top_language->frequency);
436 UMA_HISTOGRAM_PERCENTAGE(
437 "NewTabPage.Languages.UILanguageRatioInTwoTopLanguages",
438 ratio_ui_in_both_languages * 100);
439 break;
440 }
441 }
442}
443
444} // namespace internal
445
446} // namespace ntp_snippets