diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2021-03-12 09:13:00 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2021-03-16 09:58:26 +0000 |
commit | 03561cae90f1d99b5c54b1ef3be69f10e882b25e (patch) | |
tree | cc5f0958e823c044e7ae51cc0117fe51432abe5e /chromium/components/feed | |
parent | fa98118a45f7e169f8846086dc2c22c49a8ba310 (diff) | |
download | qtwebengine-chromium-03561cae90f1d99b5c54b1ef3be69f10e882b25e.tar.gz |
BASELINE: Update Chromium to 88.0.4324.208
Change-Id: I3ae87d23e4eff4b4a469685658740a213600c667
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/components/feed')
55 files changed, 1469 insertions, 122 deletions
diff --git a/chromium/components/feed/core/common/pref_names.cc b/chromium/components/feed/core/common/pref_names.cc index a50a96ff852..70cd1594940 100644 --- a/chromium/components/feed/core/common/pref_names.cc +++ b/chromium/components/feed/core/common/pref_names.cc @@ -36,6 +36,9 @@ const char kHostOverrideBlessNonce[] = "feed.host_override.bless_nonce"; const char kHasReachedClickAndViewActionsUploadConditions[] = "feed.clicks_and_views_upload_conditions_reached"; const char kLastFetchHadNoticeCard[] = "feed.last_fetch_had_notice_card"; +const char kLastRefreshWasSignedIn[] = "feed.last_refresh_was_signed_in"; +const char kNoticeCardViewsCount[] = "feed.notice_card_views_count"; +const char kNoticeCardClicksCount[] = "feed.notice_card_clicks_count"; const char kThrottlerRequestCountListPrefName[] = "feedv2.request_throttler.request_counts"; @@ -68,6 +71,9 @@ void RegisterProfilePrefs(PrefRegistrySimple* registry) { registry->RegisterBooleanPref( feed::prefs::kHasReachedClickAndViewActionsUploadConditions, false); registry->RegisterBooleanPref(feed::prefs::kLastFetchHadNoticeCard, true); + registry->RegisterBooleanPref(feed::prefs::kLastRefreshWasSignedIn, false); + registry->RegisterIntegerPref(feed::prefs::kNoticeCardViewsCount, 0); + registry->RegisterIntegerPref(feed::prefs::kNoticeCardClicksCount, 0); UserClassifier::RegisterProfilePrefs(registry); } diff --git a/chromium/components/feed/core/common/pref_names.h b/chromium/components/feed/core/common/pref_names.h index 9c381483e65..93116f51d2e 100644 --- a/chromium/components/feed/core/common/pref_names.h +++ b/chromium/components/feed/core/common/pref_names.h @@ -46,16 +46,26 @@ extern const char kHostOverrideBlessNonce[]; // to enable the upload of click and view actions in the feed with the notice // card when using the feature kInterestFeedConditionalClickAndViewActionUpload. // This is for when the privacy notice card is at the second position in the -// feed. Currently only used in V1. +// feed. extern const char kHasReachedClickAndViewActionsUploadConditions[]; // The pref name for the bit that determines whether the notice card was present // in the feed in the last fetch of content. The notice card is considered as // present by default to make sure that the upload of click and view actions // doesn't take place when the notice card is present but has not yet been -// detected. Currently only used in V1. +// detected. extern const char kLastFetchHadNoticeCard[]; +// The pref name for the bit that determines whether the last refresh request +// was signed in. Currently only used in V1. +extern const char kLastRefreshWasSignedIn[]; + +// The pref name for the counter for the number of views on the notice card. +extern const char kNoticeCardViewsCount[]; + +// The pref name for the counter for the number of clicks on the notice card. +extern const char kNoticeCardClicksCount[]; + // The following prefs are used only by v2. // The pref name for the request throttler counts. diff --git a/chromium/components/feed/core/feed_networking_host.cc b/chromium/components/feed/core/feed_networking_host.cc index e6ab4ac9321..b6104f523d7 100644 --- a/chromium/components/feed/core/feed_networking_host.cc +++ b/chromium/components/feed/core/feed_networking_host.cc @@ -345,7 +345,8 @@ void NetworkFetch::OnSimpleLoaderComplete( static_cast<int>(response_body.size() / 1024)); } - std::move(done_callback_).Run(status_code, std::move(response_body)); + std::move(done_callback_) + .Run(status_code, std::move(response_body), !access_token_.empty()); } FeedNetworkingHost::FeedNetworkingHost( @@ -388,12 +389,13 @@ void FeedNetworkingHost::NetworkFetchFinished( NetworkFetch* fetch, ResponseCallback callback, int32_t http_code, - std::vector<uint8_t> response_body) { + std::vector<uint8_t> response_body, + bool is_signed_in) { auto fetch_iterator = pending_requests_.find(fetch); CHECK(fetch_iterator != pending_requests_.end()); pending_requests_.erase(fetch_iterator); - std::move(callback).Run(http_code, std::move(response_body)); + std::move(callback).Run(http_code, std::move(response_body), is_signed_in); } } // namespace feed diff --git a/chromium/components/feed/core/feed_networking_host.h b/chromium/components/feed/core/feed_networking_host.h index f20873554e7..dcfec632efd 100644 --- a/chromium/components/feed/core/feed_networking_host.h +++ b/chromium/components/feed/core/feed_networking_host.h @@ -45,7 +45,8 @@ class FeedNetworkingHost { // status code(if the request succeeded in reaching the server). using ResponseCallback = base::OnceCallback<void(int32_t status_code, - std::vector<uint8_t> response_bytes)>; + std::vector<uint8_t> response_bytes, + bool is_signed_in)>; FeedNetworkingHost( signin::IdentityManager* identity_manager, @@ -74,7 +75,8 @@ class FeedNetworkingHost { void NetworkFetchFinished(NetworkFetch* fetch, ResponseCallback callback, int32_t http_code, - std::vector<uint8_t> response_body); + std::vector<uint8_t> response_body, + bool isSignedIn); base::flat_set<std::unique_ptr<NetworkFetch>, base::UniquePtrComparator> pending_requests_; diff --git a/chromium/components/feed/core/feed_networking_host_unittest.cc b/chromium/components/feed/core/feed_networking_host_unittest.cc index 9361ec2dd7d..da5af8defdc 100644 --- a/chromium/components/feed/core/feed_networking_host_unittest.cc +++ b/chromium/components/feed/core/feed_networking_host_unittest.cc @@ -9,7 +9,7 @@ #include "base/bind.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_split.h" -#include "base/test/bind_test_util.h" +#include "base/test/bind.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/scoped_feature_list.h" #include "base/test/simple_test_tick_clock.h" @@ -46,16 +46,20 @@ class MockResponseDoneCallback { public: MockResponseDoneCallback() : has_run(false), code(0) {} - void Done(int32_t http_code, std::vector<uint8_t> response) { + void Done(int32_t http_code, + std::vector<uint8_t> response, + bool is_signed_in) { EXPECT_FALSE(has_run); has_run = true; code = http_code; response_bytes = std::move(response); + is_signed_in_result = is_signed_in; } bool has_run; int32_t code; std::vector<uint8_t> response_bytes; + bool is_signed_in_result; }; } // namespace @@ -266,6 +270,15 @@ TEST_F(FeedNetworkingHostTest, ShouldSetHeadersCorrectly) { EXPECT_EQ(authorization, "Bearer access_token"); } +TEST_F(FeedNetworkingHostTest, ProvideIsSignedInBitInResult) { + MockResponseDoneCallback done_callback; + SendRequestAndRespond("http://foobar.com/feed", "POST", "body", "", + net::HTTP_OK, network::URLLoaderCompletionStatus(), + &done_callback); + + EXPECT_TRUE(done_callback.is_signed_in_result); +} + TEST_F(FeedNetworkingHostTest, ShouldNotSendContentEncodingForEmptyBody) { MockResponseDoneCallback done_callback; net::HttpRequestHeaders headers; diff --git a/chromium/components/feed/core/proto/BUILD.gn b/chromium/components/feed/core/proto/BUILD.gn index 2ff52bed0fc..9244b9aebc5 100644 --- a/chromium/components/feed/core/proto/BUILD.gn +++ b/chromium/components/feed/core/proto/BUILD.gn @@ -28,7 +28,9 @@ proto_library("proto_v2") { "v2/ui.proto", "v2/wire/action_payload.proto", "v2/wire/capability.proto", + "v2/wire/chrome_client_info.proto", "v2/wire/chrome_feed_response_metadata.proto", + "v2/wire/chrome_fulfillment_info.proto", "v2/wire/client_info.proto", "v2/wire/consistency_token.proto", "v2/wire/content_id.proto", @@ -92,6 +94,7 @@ if (is_android) { "wire/action_request.proto", "wire/action_type.proto", "wire/capability.proto", + "wire/chrome_fulfillment_info.proto", "wire/client_info.proto", "wire/consistency_token.proto", "wire/content_id.proto", diff --git a/chromium/components/feed/core/proto/v2/store.proto b/chromium/components/feed/core/proto/v2/store.proto index 29ba828c4ea..206d90596db 100644 --- a/chromium/components/feed/core/proto/v2/store.proto +++ b/chromium/components/feed/core/proto/v2/store.proto @@ -51,15 +51,24 @@ message StreamData { bool logging_enabled = 8; // Has the privacy notice been fulfilled? bool privacy_notice_fulfilled = 9; + reserved 3, 5; } // Data that doesn't belong to a stream. message Metadata { + // Session identifier used for signed-out feed requests and interactions. + message SessionID { + string token = 1; + int64 expiry_time_ms = 2; + } + // Token used to read or write to the same storage. bytes consistency_token = 1; // ID for the next pending action. int32 next_action_id = 2; + // The most recent session identifier. + SessionID session_id = 3; } // A set of StreamStructures that should be applied to a stream. diff --git a/chromium/components/feed/core/proto/v2/wire/chrome_client_info.proto b/chromium/components/feed/core/proto/v2/wire/chrome_client_info.proto new file mode 100644 index 00000000000..26f71a2b690 --- /dev/null +++ b/chromium/components/feed/core/proto/v2/wire/chrome_client_info.proto @@ -0,0 +1,17 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +syntax = "proto2"; + +package feedwire; + +option optimize_for = LITE_RUNTIME; + +// Information about the client performing the request, relevant only to Chrome +// surfaces. +message ChromeClientInfo { + // The signed-out session identifier (Zwieback) token, for clients which embed + // this information in-band instead of using an HTTP Cookie. + optional string session_id = 3; +} diff --git a/chromium/components/feed/core/proto/v2/wire/chrome_feed_response_metadata.proto b/chromium/components/feed/core/proto/v2/wire/chrome_feed_response_metadata.proto index c6ec3fcdbe7..2eb9cad7263 100644 --- a/chromium/components/feed/core/proto/v2/wire/chrome_feed_response_metadata.proto +++ b/chromium/components/feed/core/proto/v2/wire/chrome_feed_response_metadata.proto @@ -11,4 +11,5 @@ option optimize_for = LITE_RUNTIME; message ChromeFeedResponseMetadata { optional bool privacy_notice_fulfilled = 1; optional bool logging_enabled = 2; + optional string session_id = 3; } diff --git a/chromium/components/feed/core/proto/v2/wire/chrome_fulfillment_info.proto b/chromium/components/feed/core/proto/v2/wire/chrome_fulfillment_info.proto new file mode 100644 index 00000000000..22ea6991dcb --- /dev/null +++ b/chromium/components/feed/core/proto/v2/wire/chrome_fulfillment_info.proto @@ -0,0 +1,17 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +syntax = "proto2"; + +package feedwire; + +option optimize_for = LITE_RUNTIME; + +// Information on how to do content fulfillment for Chrome. +message ChromeFulfillmentInfo { + // Whether the notice card has already been acknowledged by the user based on + // their views and clicks on the card. This is different from when the user + // explicitly dismiss the notice card by touching the button. + optional bool notice_card_acknowledged = 1; +} diff --git a/chromium/components/feed/core/proto/v2/wire/client_info.proto b/chromium/components/feed/core/proto/v2/wire/client_info.proto index 40eaca72c48..a54ae75ec48 100644 --- a/chromium/components/feed/core/proto/v2/wire/client_info.proto +++ b/chromium/components/feed/core/proto/v2/wire/client_info.proto @@ -8,6 +8,7 @@ package feedwire; option optimize_for = LITE_RUNTIME; +import "components/feed/core/proto/v2/wire/chrome_client_info.proto"; import "components/feed/core/proto/v2/wire/display_info.proto"; import "components/feed/core/proto/v2/wire/version.proto"; @@ -58,4 +59,8 @@ message ClientInfo { // this comes from GServices check-in which uses the SIM card MCC (mobile // country code), with fallback to IP geo lookup. optional string device_country = 9; + + // Information about the client performing the request, relevant only to + // Chrome surfaces. + optional ChromeClientInfo chrome_client_info = 338478298; } diff --git a/chromium/components/feed/core/proto/v2/wire/feed_query.proto b/chromium/components/feed/core/proto/v2/wire/feed_query.proto index e69e919fae6..0cc218f07bb 100644 --- a/chromium/components/feed/core/proto/v2/wire/feed_query.proto +++ b/chromium/components/feed/core/proto/v2/wire/feed_query.proto @@ -8,6 +8,7 @@ package feedwire; option optimize_for = LITE_RUNTIME; +import "components/feed/core/proto/v2/wire/chrome_fulfillment_info.proto"; import "components/feed/core/proto/v2/wire/token.proto"; message FeedQuery { @@ -48,5 +49,8 @@ message FeedQuery { Tokens in_place_update_tokens = 5; } + // Information on how to do content fulfillment for Chrome. + optional ChromeFulfillmentInfo chrome_fulfillment_info = 341477699; + reserved 2; } diff --git a/chromium/components/feed/core/proto/wire/chrome_fulfillment_info.proto b/chromium/components/feed/core/proto/wire/chrome_fulfillment_info.proto new file mode 100644 index 00000000000..e78acbaedcb --- /dev/null +++ b/chromium/components/feed/core/proto/wire/chrome_fulfillment_info.proto @@ -0,0 +1,20 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +syntax = "proto2"; + +package feedwire1; + +option optimize_for = LITE_RUNTIME; + +option java_package = "org.chromium.components.feed.core.proto.wire"; +option java_outer_classname = "ChromeFulfillmentInfoProto"; + +// Information on how to do content fulfillment for Chrome. +message ChromeFulfillmentInfo { + // Whether the notice card has already been acknowledged by the user based on + // their views and clicks on the card. This is different from when the user + // explicitly dismiss the notice card by touching the button. + optional bool notice_card_acknowledged = 1; +} diff --git a/chromium/components/feed/core/proto/wire/feed_query.proto b/chromium/components/feed/core/proto/wire/feed_query.proto index 2620819c328..e105058af93 100644 --- a/chromium/components/feed/core/proto/wire/feed_query.proto +++ b/chromium/components/feed/core/proto/wire/feed_query.proto @@ -11,6 +11,8 @@ option optimize_for = LITE_RUNTIME; option java_package = "org.chromium.components.feed.core.proto.wire"; option java_outer_classname = "FeedQueryProto"; +import "components/feed/core/proto/wire/chrome_fulfillment_info.proto"; + message FeedQuery { enum RequestReason { // Bucket for any not listed. Should not be used (prefer adding a new @@ -49,4 +51,7 @@ message FeedQuery { // Used to fetch the next page when scrolling copied from // Token.next_page_token optional bytes page_token = 2; + + // Information on how to do content fulfillment for Chrome. + optional ChromeFulfillmentInfo chrome_fulfillment_info = 3; } diff --git a/chromium/components/feed/core/v2/BUILD.gn b/chromium/components/feed/core/v2/BUILD.gn index 3de0962ba05..6331e2eceda 100644 --- a/chromium/components/feed/core/v2/BUILD.gn +++ b/chromium/components/feed/core/v2/BUILD.gn @@ -27,6 +27,8 @@ source_set("feed_core_v2") { "image_fetcher.h", "metrics_reporter.cc", "metrics_reporter.h", + "notice_card_tracker.cc", + "notice_card_tracker.h", "offline_page_spy.cc", "offline_page_spy.h", "prefs.cc", @@ -63,6 +65,8 @@ source_set("feed_core_v2") { "tasks/load_stream_from_store_task.h", "tasks/load_stream_task.cc", "tasks/load_stream_task.h", + "tasks/prefetch_images_task.cc", + "tasks/prefetch_images_task.h", "tasks/upload_actions_task.cc", "tasks/upload_actions_task.h", "tasks/wait_for_store_initialize_task.cc", @@ -71,6 +75,7 @@ source_set("feed_core_v2") { "types.h", ] deps = [ + ":common", "//components/feed/core:feed_core", "//components/feed/core/common:feed_core_common", "//components/history/core/browser", @@ -97,6 +102,11 @@ source_set("feed_core_v2") { ] } +source_set("common") { + sources = [ "common_enums.h" ] + deps = [] +} + source_set("core_unit_tests") { testonly = true sources = [ @@ -106,6 +116,7 @@ source_set("core_unit_tests") { "feed_stream_unittest.cc", "image_fetcher_unittest.cc", "metrics_reporter_unittest.cc", + "notice_card_tracker_unittest.cc", "proto_util_unittest.cc", "protocol_translator_unittest.cc", "public/feed_service_unittest.cc", @@ -122,6 +133,7 @@ source_set("core_unit_tests") { ] deps = [ + ":common", ":feed_core_v2", ":unit_tests_bundle_data", "//base", diff --git a/chromium/components/feed/core/v2/common_enums.h b/chromium/components/feed/core/v2/common_enums.h new file mode 100644 index 00000000000..070dfc83916 --- /dev/null +++ b/chromium/components/feed/core/v2/common_enums.h @@ -0,0 +1,86 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEED_CORE_V2_COMMON_ENUMS_H_ +#define COMPONENTS_FEED_CORE_V2_COMMON_ENUMS_H_ + +// Unlike most code from feed/core, these enums are used by both iOS and +// Android. +namespace feed { + +// Values for the UMA ContentSuggestions.Feed.EngagementType +// histogram. These values are persisted to logs. Entries should not be +// renumbered and numeric values should never be reused. This must be kept +// in sync with FeedEngagementType in enums.xml. +enum class FeedEngagementType { + kFeedEngaged = 0, + kFeedEngagedSimple = 1, + kFeedInteracted = 2, + kDeprecatedFeedScrolled = 3, + kFeedScrolled = 4, + kMaxValue = kFeedScrolled, +}; + +// Values for the UMA ContentSuggestions.Feed.UserActions +// histogram. These values are persisted to logs. Entries should not be +// renumbered and numeric values should never be reused. This must be kept +// in sync with FeedUserActionType in enums.xml. +// Note: Most of these have a corresponding UserMetricsAction reported here. +// Exceptions are described below. +enum class FeedUserActionType { + // User tapped on card, opening the article in the same tab. + kTappedOnCard = 0, + // This is not an actual user action, so there will be no UserMetricsAction + // reported for this. + kShownCard = 1, + // User tapped on 'Send Feedback' in the back of card menu. + kTappedSendFeedback = 2, + // Discover feed header menu 'Learn More' tapped. + kTappedLearnMore = 3, + // User Tapped Hide Story in the back of card menu. + kTappedHideStory = 4, + // User Tapped Not Interested In X in the back of card menu. + kTappedNotInterestedIn = 5, + // Discover feed header menu 'Manage Interests' tapped. + kTappedManageInterests = 6, + kTappedDownload = 7, + // User opened the article in a new tab from the back of card menu. + kTappedOpenInNewTab = 8, + // User opened the back of card menu. + kOpenedContextMenu = 9, + // User action not reported here. See Suggestions.SurfaceVisible. + kOpenedFeedSurface = 10, + // User opened the article in an incognito tab from the back of card menu. + kTappedOpenInNewIncognitoTab = 11, + // Ephemeral change, likely due to hide story or not interested in. + kEphemeralChange = 12, + // Ephemeral change undone, likely due to pressing 'undo' on the snackbar. + kEphemeralChangeRejected = 13, + // Discover feed visibility toggled from header menu. + kTappedTurnOn = 14, + kTappedTurnOff = 15, + // Discover feed header menu 'Manage Activity' tapped. + kTappedManageActivity = 16, + // User added article to 'Read Later' list. + kAddedToReadLater = 17, + // User closed the back of card menu. + kClosedContextMenu = 18, + // Ephemeral change committed, likely due to dismissing an 'undo' snackbar. + kEphemeralChangeCommited = 19, + // User opened a Dialog. e.g. Report content Dialog. User action not reported + // here. + kOpenedDialog = 20, + // User closed a Dialog. e.g. Report content Dialog. + kClosedDialog = 21, + // User action caused a snackbar to be shown. User action not reported here. + kShowSnackbar = 22, + // User opened the native back of card menu. + kOpenedNativeContextMenu = 23, + // Highest enumerator. Recommended by Histogram metrics best practices. + kMaxValue = kOpenedNativeContextMenu, +}; + +} // namespace feed + +#endif // COMPONENTS_FEED_CORE_V2_COMMON_ENUMS_H_ diff --git a/chromium/components/feed/core/v2/config.cc b/chromium/components/feed/core/v2/config.cc index 753f275f00b..309e4583b89 100644 --- a/chromium/components/feed/core/v2/config.cc +++ b/chromium/components/feed/core/v2/config.cc @@ -76,6 +76,21 @@ void OverrideWithFinch(Config* config) { kInterestFeedV2, "upload_actions_on_enter_background", config->upload_actions_on_enter_background); + config->send_signed_out_session_logs = + base::GetFieldTrialParamByFeatureAsBool( + kInterestFeedV2, "send_signed_out_session_logs", + config->send_signed_out_session_logs); + + config->session_id_max_age = + base::TimeDelta::FromDays(base::GetFieldTrialParamByFeatureAsInt( + kInterestFeedV2, "session_id_max_age_days", + config->session_id_max_age.InDays())); + + config->max_prefetch_image_requests_per_refresh = + base::GetFieldTrialParamByFeatureAsInt( + kInterestFeedV2, "max_prefetch_image_requests_per_refresh", + config->max_prefetch_image_requests_per_refresh); + // Erase any capabilities with "enable_CAPABILITY = false" set. base::EraseIf(config->experimental_capabilities, CapabilityDisabled); } diff --git a/chromium/components/feed/core/v2/config.h b/chromium/components/feed/core/v2/config.h index 2789d0cd219..1eb0d612c12 100644 --- a/chromium/components/feed/core/v2/config.h +++ b/chromium/components/feed/core/v2/config.h @@ -38,6 +38,12 @@ struct Config { int load_more_trigger_lookahead = 5; // Whether to attempt uploading actions when Chrome is hidden. bool upload_actions_on_enter_background = true; + // Whether to send (pseudonymous) logs for signed-out sessions. + bool send_signed_out_session_logs = false; + // The max age of a signed-out session token. + base::TimeDelta session_id_max_age = base::TimeDelta::FromDays(30); + // Maximum number of images prefetched per refresh. + int max_prefetch_image_requests_per_refresh = 50; // Set of optional capabilities included in requests. See // CreateFeedQueryRequest() for required capabilities. base::flat_set<feedwire::Capability> experimental_capabilities = { diff --git a/chromium/components/feed/core/v2/feed_network.h b/chromium/components/feed/core/v2/feed_network.h index b0836218f4f..a5676de45a4 100644 --- a/chromium/components/feed/core/v2/feed_network.h +++ b/chromium/components/feed/core/v2/feed_network.h @@ -30,6 +30,8 @@ class FeedNetwork { NetworkResponseInfo response_info; // Response body if one was received. std::unique_ptr<feedwire::Response> response_body; + // Whether the request was signed in. + bool was_signed_in; }; // Result of SendActionRequest. diff --git a/chromium/components/feed/core/v2/feed_network_impl.cc b/chromium/components/feed/core/v2/feed_network_impl.cc index 4cc86a94c45..5f2c68ad471 100644 --- a/chromium/components/feed/core/v2/feed_network_impl.cc +++ b/chromium/components/feed/core/v2/feed_network_impl.cc @@ -12,6 +12,7 @@ #include "base/containers/flat_set.h" #include "base/metrics/histogram_functions.h" #include "base/metrics/histogram_macros.h" +#include "base/strings/strcat.h" #include "base/time/tick_clock.h" #include "base/time/time.h" #include "components/feed/core/common/pref_names.h" @@ -39,6 +40,9 @@ #include "third_party/protobuf/src/google/protobuf/io/coded_stream.h" #include "third_party/zlib/google/compression_utils.h" +// Token override for Feedv2NewTabPageCardInstrumentationTest.java: +// #define TOKEN_OVERRIDE_FOR_TESTING "put-test-token-here" + namespace feed { namespace { constexpr char kApplicationXProtobuf[] = "application/x-protobuf"; @@ -312,8 +316,12 @@ class FeedNetworkImpl::NetworkFetch { variations::SignedIn signed_in_status = variations::SignedIn::kNo; if (!access_token_.empty()) { + base::StringPiece token = access_token_; +#ifdef TOKEN_OVERRIDE_FOR_TESTING + token = TOKEN_OVERRIDE_FOR_TESTING; +#endif request.headers.SetHeader(net::HttpRequestHeaders::kAuthorization, - "Bearer " + access_token_); + base::StrCat({"Bearer ", token})); signed_in_status = variations::SignedIn::kYes; } @@ -329,6 +337,7 @@ class FeedNetworkImpl::NetworkFetch { tick_clock_->NowTicks() - entire_send_start_ticks_; response_info.fetch_time = base::Time::Now(); response_info.base_request_url = GetUrlWithoutQuery(url_); + response_info.was_signed_in = !access_token_.empty(); // If overriding the feed host, try to grab the Bless nonce. This is // strictly informational, and only displayed in snippets-internals. diff --git a/chromium/components/feed/core/v2/feed_network_impl_unittest.cc b/chromium/components/feed/core/v2/feed_network_impl_unittest.cc index 6d759773629..c6ad6e97a6e 100644 --- a/chromium/components/feed/core/v2/feed_network_impl_unittest.cc +++ b/chromium/components/feed/core/v2/feed_network_impl_unittest.cc @@ -10,7 +10,7 @@ #include "base/bind.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_split.h" -#include "base/test/bind_test_util.h" +#include "base/test/bind.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/simple_test_tick_clock.h" #include "base/test/task_environment.h" @@ -277,6 +277,7 @@ TEST_F(FeedNetworkTest, SendQueryRequestReceivesResponse) { "https://www.google.com/httpservice/retry/TrellisClankService/FeedQuery", result.response_info.base_request_url); EXPECT_NE(base::Time(), result.response_info.fetch_time); + EXPECT_TRUE(result.response_info.was_signed_in); EXPECT_EQ(GetTestFeedResponse().response_version(), result.response_body->response_version()); } diff --git a/chromium/components/feed/core/v2/feed_store.cc b/chromium/components/feed/core/v2/feed_store.cc index bb0add49a8e..65eafb55d54 100644 --- a/chromium/components/feed/core/v2/feed_store.cc +++ b/chromium/components/feed/core/v2/feed_store.cc @@ -7,7 +7,7 @@ #include <utility> #include "base/bind.h" -#include "base/bind_helpers.h" +#include "base/callback_helpers.h" #include "base/containers/flat_set.h" #include "base/files/file_path.h" #include "base/memory/ptr_util.h" @@ -501,7 +501,7 @@ void FeedStore::RemoveActions(std::vector<LocalActionId> ids, database_->UpdateEntries( /*entries_to_save=*/std::make_unique< std::vector<std::pair<std::string, feedstore::Record>>>(), - /*key_to_remove=*/std::move(keys), std::move(callback)); + /*keys_to_remove=*/std::move(keys), std::move(callback)); } void FeedStore::Write(std::vector<feedstore::Record> records, diff --git a/chromium/components/feed/core/v2/feed_store_unittest.cc b/chromium/components/feed/core/v2/feed_store_unittest.cc index db7473c2d58..fe2a1ecd19c 100644 --- a/chromium/components/feed/core/v2/feed_store_unittest.cc +++ b/chromium/components/feed/core/v2/feed_store_unittest.cc @@ -11,7 +11,7 @@ #include "base/strings/strcat.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_piece.h" -#include "base/test/bind_test_util.h" +#include "base/test/bind.h" #include "base/test/task_environment.h" #include "components/feed/core/proto/v2/wire/content_id.pb.h" #include "components/feed/core/v2/protocol_translator.h" diff --git a/chromium/components/feed/core/v2/feed_stream.cc b/chromium/components/feed/core/v2/feed_stream.cc index 022579bcb33..a2794afa866 100644 --- a/chromium/components/feed/core/v2/feed_stream.cc +++ b/chromium/components/feed/core/v2/feed_stream.cc @@ -34,6 +34,7 @@ #include "components/feed/core/v2/tasks/clear_all_task.h" #include "components/feed/core/v2/tasks/get_prefetch_suggestions_task.h" #include "components/feed/core/v2/tasks/load_stream_task.h" +#include "components/feed/core/v2/tasks/prefetch_images_task.h" #include "components/feed/core/v2/tasks/upload_actions_task.h" #include "components/feed/core/v2/tasks/wait_for_store_initialize_task.h" #include "components/feed/feed_feature_list.h" @@ -113,7 +114,7 @@ void FeedStream::Metadata::Populate(feedstore::Metadata metadata) { metadata_ = std::move(metadata); } -std::string FeedStream::Metadata::GetConsistencyToken() const { +const std::string& FeedStream::Metadata::GetConsistencyToken() const { return metadata_.consistency_token(); } @@ -122,6 +123,36 @@ void FeedStream::Metadata::SetConsistencyToken(std::string consistency_token) { store_->WriteMetadata(metadata_, base::DoNothing()); } +const std::string& FeedStream::Metadata::GetSessionIdToken() const { + return metadata_.session_id().token(); +} + +base::Time FeedStream::Metadata::GetSessionIdExpiryTime() const { + return base::Time::FromDeltaSinceWindowsEpoch( + base::TimeDelta::FromMilliseconds( + metadata_.session_id().expiry_time_ms())); +} + +void FeedStream::Metadata::SetSessionId(std::string token, + base::Time expiry_time) { + feedstore::Metadata::SessionID* session_id = metadata_.mutable_session_id(); + session_id->set_token(std::move(token)); + session_id->set_expiry_time_ms( + expiry_time.ToDeltaSinceWindowsEpoch().InMilliseconds()); + store_->WriteMetadata(metadata_, base::DoNothing()); +} + +void FeedStream::Metadata::MaybeUpdateSessionId( + base::Optional<std::string> token, + const base::Clock* clock) { + if (token && metadata_.session_id().token() != *token) { + base::Time expiry_time = + token->empty() ? base::Time() + : clock->Now() + GetFeedConfig().session_id_max_age; + SetSessionId(*token, expiry_time); + } +} + LocalActionId FeedStream::Metadata::GetNextActionId() { uint32_t id = metadata_.next_action_id(); // Never use 0, as that's an invalid LocalActionId. @@ -157,7 +188,8 @@ FeedStream::FeedStream(RefreshTaskScheduler* refresh_task_scheduler, chrome_info_(chrome_info), task_queue_(this), request_throttler_(profile_prefs, clock), - metadata_(feed_store) { + metadata_(feed_store), + notice_card_tracker_(profile_prefs) { static WireResponseTranslator default_translator; wire_response_translator_ = &default_translator; @@ -238,13 +270,32 @@ bool FeedStream::IsActivityLoggingEnabled() const { void FeedStream::UpdateIsActivityLoggingEnabled() { is_activity_logging_enabled_ = - model_ && model_->signed_in() && model_->logging_enabled(); + model_ && + ((model_->signed_in() && model_->logging_enabled()) || + (!model_->signed_in() && GetFeedConfig().send_signed_out_session_logs)); +} + +std::string FeedStream::GetSessionId() const { + return GetMetadata()->GetSessionIdToken(); +} + +void FeedStream::PrefetchImage(const GURL& url) { + delegate_->PrefetchImage(url); } void FeedStream::AttachSurface(SurfaceInterface* surface) { metrics_reporter_->SurfaceOpened(surface->GetSurfaceId()); + + // Skip normal processing when overriding stream data from the internals page. + if (forced_stream_update_for_debugging_.updated_slices_size() > 0) { + surface_updater_->SurfaceAdded(surface); + surface->StreamUpdate(forced_stream_update_for_debugging_); + return; + } + TriggerStreamLoad(); surface_updater_->SurfaceAdded(surface); + // Cancel any scheduled model unload task. ++unload_on_detach_sequence_number_; UpdateCanUploadActionsWithNoticeCard(); @@ -292,7 +343,7 @@ bool FeedStream::IsArticlesListVisible() { return profile_prefs_->GetBoolean(prefs::kArticlesListVisible); } -std::string FeedStream::GetClientInstanceId() { +std::string FeedStream::GetClientInstanceId() const { return prefs::GetClientInstanceId(*profile_prefs_); } @@ -455,6 +506,11 @@ std::string FeedStream::DumpStateForDebugging() { return ss.str(); } +void FeedStream::SetForcedStreamUpdateForDebugging( + const feedui::StreamUpdate& stream_update) { + forced_stream_update_for_debugging_ = stream_update; +} + base::Time FeedStream::GetLastFetchTime() { const base::Time fetch_time = profile_prefs_->GetTime(feed::prefs::kLastFetchAttemptTime); @@ -556,12 +612,38 @@ bool FeedStream::ShouldForceSignedOutFeedQueryRequest() const { return base::TimeTicks::Now() < signed_out_refreshes_until_; } -RequestMetadata FeedStream::GetRequestMetadata() { +RequestMetadata FeedStream::GetRequestMetadata(bool is_for_next_page) const { RequestMetadata result; result.chrome_info = chrome_info_; result.display_metrics = delegate_->GetDisplayMetrics(); result.language_tag = delegate_->GetLanguageTag(); - result.client_instance_id = GetClientInstanceId(); + result.notice_card_acknowledged = + notice_card_tracker_.HasAcknowledgedNoticeCard(); + + if (is_for_next_page) { + // If we are continuing an existing feed, use whatever session continuity + // mechanism is currently associated with the stream: client-instance-id + // for signed-in feed, session_id token for signed-out. + DCHECK(model_); + if (model_->signed_in()) { + result.client_instance_id = GetClientInstanceId(); + } else { + result.session_id = GetMetadata()->GetSessionIdToken(); + } + } else { + // The request is for the first page of the feed. Use client_instance_id + // for signed in requests and session_id token (if any, and not expired) + // for signed-out. + if (delegate_->IsSignedIn() && !ShouldForceSignedOutFeedQueryRequest()) { + result.client_instance_id = GetClientInstanceId(); + } else if (!GetMetadata()->GetSessionIdToken().empty() && + GetMetadata()->GetSessionIdExpiryTime() > clock_->Now()) { + result.session_id = GetMetadata()->GetSessionIdToken(); + } + } + + DCHECK(result.session_id.empty() || result.client_instance_id.empty()); + return result; } @@ -626,6 +708,10 @@ void FeedStream::BackgroundRefreshComplete(LoadStreamTask::Result result) { if (result.loaded_new_content_from_network && prefetch_service_) prefetch_service_->NewSuggestionsAvailable(); + // Add prefetch images to task queue without waiting to finish + // since we treat them as best-effort. + task_queue_.AddTask(std::make_unique<PrefetchImagesTask>(this)); + refresh_task_scheduler_->RefreshTaskComplete(); } @@ -693,19 +779,22 @@ void FeedStream::UnloadModel() { surface_updater_->SetModel(nullptr); model_.reset(); } - void FeedStream::ReportOpenAction(const std::string& slice_id) { int index = surface_updater_->GetSliceIndexFromSliceId(slice_id); - if (index >= 0) - metrics_reporter_->OpenAction(index); + if (index < 0) + index = MetricsReporter::kUnknownCardIndex; + metrics_reporter_->OpenAction(index); + notice_card_tracker_.OnOpenAction(index); } void FeedStream::ReportOpenVisitComplete(base::TimeDelta visit_time) { metrics_reporter_->OpenVisitComplete(visit_time); } void FeedStream::ReportOpenInNewTabAction(const std::string& slice_id) { int index = surface_updater_->GetSliceIndexFromSliceId(slice_id); - if (index >= 0) - metrics_reporter_->OpenInNewTabAction(index); + if (index < 0) + index = MetricsReporter::kUnknownCardIndex; + metrics_reporter_->OpenInNewTabAction(index); + notice_card_tracker_.OnOpenAction(index); } void FeedStream::ReportOpenInNewIncognitoTabAction() { metrics_reporter_->OpenInNewIncognitoTabAction(); @@ -715,9 +804,11 @@ void FeedStream::ReportSliceViewed(SurfaceId surface_id, int index = surface_updater_->GetSliceIndexFromSliceId(slice_id); if (index >= 0) { UpdateShownSlicesUploadCondition(index); + notice_card_tracker_.OnSliceViewed(index); metrics_reporter_->ContentSliceViewed(surface_id, index); } } +// TODO(crbug/1147237): Rename this method and related members? bool FeedStream::CanUploadActions() const { return can_upload_actions_with_notice_card_ || !prefs::GetLastFetchHadNoticeCard(*profile_prefs_); @@ -746,11 +837,16 @@ void FeedStream::DeclareHasReachedConditionsToUploadActionsWithNoticeCard() { void FeedStream::UpdateShownSlicesUploadCondition(int viewed_slice_index) { constexpr int kShownSlicesThreshold = 2; + DCHECK(model_) << "Model was unloaded while handling a viewed slice"; + // Don't take shown slices into consideration when the upload conditions has // already been reached. if (HasReachedConditionsToUploadActionsWithNoticeCard()) return; + if (!model_->signed_in()) + return; + if (viewed_slice_index + 1 >= kShownSlicesThreshold) DeclareHasReachedConditionsToUploadActionsWithNoticeCard(); } diff --git a/chromium/components/feed/core/v2/feed_stream.h b/chromium/components/feed/core/v2/feed_stream.h index 0b3082a4211..7b7fd04e6e7 100644 --- a/chromium/components/feed/core/v2/feed_stream.h +++ b/chromium/components/feed/core/v2/feed_stream.h @@ -16,8 +16,10 @@ #include "base/version.h" #include "components/feed/core/common/enums.h" #include "components/feed/core/common/user_classifier.h" +#include "components/feed/core/proto/v2/ui.pb.h" #include "components/feed/core/proto/v2/wire/response.pb.h" #include "components/feed/core/v2/enums.h" +#include "components/feed/core/v2/notice_card_tracker.h" #include "components/feed/core/v2/protocol_translator.h" #include "components/feed/core/v2/public/feed_stream_api.h" #include "components/feed/core/v2/request_throttler.h" @@ -68,6 +70,7 @@ class FeedStream : public FeedStreamApi, virtual std::string GetLanguageTag() = 0; virtual void ClearAll() = 0; virtual bool IsSignedIn() = 0; + virtual void PrefetchImage(const GURL& url) = 0; }; // Forwards to |feed::TranslateWireResponse()| by default. Can be overridden @@ -90,9 +93,15 @@ class FeedStream : public FeedStreamApi, void Populate(feedstore::Metadata metadata); - std::string GetConsistencyToken() const; + const std::string& GetConsistencyToken() const; void SetConsistencyToken(std::string consistency_token); + const std::string& GetSessionIdToken() const; + base::Time GetSessionIdExpiryTime() const; + void SetSessionId(std::string token, base::Time expiry_time); + void MaybeUpdateSessionId(base::Optional<std::string> token, + const base::Clock* clock); + LocalActionId GetNextActionId(); private: @@ -123,11 +132,12 @@ class FeedStream : public FeedStreamApi, // FeedStreamApi. bool IsActivityLoggingEnabled() const override; + std::string GetSessionId() const override; void AttachSurface(SurfaceInterface*) override; void DetachSurface(SurfaceInterface*) override; void SetArticlesListVisible(bool is_visible) override; bool IsArticlesListVisible() override; - std::string GetClientInstanceId() override; + std::string GetClientInstanceId() const override; void ExecuteRefreshTask() override; ImageFetchId FetchImage( const GURL& url, @@ -148,6 +158,8 @@ class FeedStream : public FeedStreamApi, DebugStreamData GetDebugStreamData() override; void ForceRefreshForDebugging() override; std::string DumpStateForDebugging() override; + void SetForcedStreamUpdateForDebugging( + const feedui::StreamUpdate& stream_update) override; void ReportSliceViewed(SurfaceId surface_id, const std::string& slice_id) override; @@ -211,8 +223,11 @@ class FeedStream : public FeedStreamApi, FeedStore* GetStore() { return store_; } RequestThrottler* GetRequestThrottler() { return &request_throttler_; } Metadata* GetMetadata() { return &metadata_; } + const Metadata* GetMetadata() const { return &metadata_; } MetricsReporter* GetMetricsReporter() const { return metrics_reporter_; } + void PrefetchImage(const GURL& url); + // Returns the time of the last content fetch. base::Time GetLastFetchTime(); @@ -252,7 +267,7 @@ class FeedStream : public FeedStreamApi, const base::Clock* GetClock() const { return clock_; } const base::TickClock* GetTickClock() const { return tick_clock_; } - RequestMetadata GetRequestMetadata(); + RequestMetadata GetRequestMetadata(bool is_for_next_page) const; const WireResponseTranslator* GetWireResponseTranslator() const { return wire_response_translator_; @@ -354,6 +369,12 @@ class FeedStream : public FeedStreamApi, // To allow tests to wait on task queue idle. base::RepeatingClosure idle_callback_; + // Stream update forced to use for new surfaces. This is provided in feed + // internals page for debugging purpose. + feedui::StreamUpdate forced_stream_update_for_debugging_; + + NoticeCardTracker notice_card_tracker_; + base::WeakPtrFactory<FeedStream> weak_ptr_factory_{this}; }; diff --git a/chromium/components/feed/core/v2/feed_stream_unittest.cc b/chromium/components/feed/core/v2/feed_stream_unittest.cc index ffbb96cc550..43831c0aa58 100644 --- a/chromium/components/feed/core/v2/feed_stream_unittest.cc +++ b/chromium/components/feed/core/v2/feed_stream_unittest.cc @@ -18,7 +18,7 @@ #include "base/path_service.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" -#include "base/test/bind_test_util.h" +#include "base/test/bind.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/scoped_feature_list.h" #include "base/test/scoped_run_loop_timeout.h" @@ -29,6 +29,7 @@ #include "components/feed/core/common/pref_names.h" #include "components/feed/core/proto/v2/store.pb.h" #include "components/feed/core/proto/v2/ui.pb.h" +#include "components/feed/core/proto/v2/wire/chrome_client_info.pb.h" #include "components/feed/core/proto/v2/wire/request.pb.h" #include "components/feed/core/proto/v2/wire/there_and_back_again_data.pb.h" #include "components/feed/core/proto/v2/xsurface.pb.h" @@ -303,6 +304,7 @@ class TestFeedNetwork : public FeedNetwork { result.response_info.status_code = 200; result.response_info.response_body_bytes = 100; result.response_info.fetch_duration = base::TimeDelta::FromMilliseconds(42); + result.response_info.was_signed_in = true; if (injected_response_) { result.response_body = std::make_unique<feedwire::Response>( std::move(injected_response_.value())); @@ -387,9 +389,12 @@ class TestWireResponseTranslator : public FeedStream::WireResponseTranslator { return FeedStream::WireResponseTranslator::TranslateWireResponse( std::move(response), source, was_signed_in_request, current_time); } - void InjectResponse(std::unique_ptr<StreamModelUpdateRequest> response) { + void InjectResponse(std::unique_ptr<StreamModelUpdateRequest> response, + base::Optional<std::string> session_id = base::nullopt) { + DCHECK(!response->stream_data.signed_in() || !session_id); RefreshResponseData data; data.model_update_request = std::move(response); + data.session_id = std::move(session_id); InjectResponse(std::move(data)); } void InjectResponse(RefreshResponseData response_data) { @@ -573,10 +578,7 @@ class FeedStreamTest : public testing::Test, public FeedStream::Delegate { CreateStream(); } - virtual void SetupFeatures() { - scoped_feature_list_.InitAndDisableFeature( - feed::kInterestFeedV2ClicksAndViewsConditionalUpload); - } + virtual void SetupFeatures() {} void TearDown() override { // Ensure the task queue can return to idle. Failure to do so may be due @@ -600,6 +602,10 @@ class FeedStreamTest : public testing::Test, public FeedStream::Delegate { std::string GetLanguageTag() override { return "en-US"; } void ClearAll() override {} bool IsSignedIn() override { return is_signed_in_; } + void PrefetchImage(const GURL& url) override { + prefetched_images_.push_back(url); + prefetch_image_call_count_++; + } // For tests. @@ -690,6 +696,8 @@ class FeedStreamTest : public testing::Test, public FeedStream::Delegate { bool is_offline_ = false; bool is_signed_in_ = true; base::test::ScopedFeatureList scoped_feature_list_; + int prefetch_image_call_count_ = 0; + std::vector<GURL> prefetched_images_; }; class FeedStreamConditionalActionsUploadTest : public FeedStreamTest { @@ -742,6 +750,21 @@ TEST_F(FeedStreamTest, BackgroundRefreshSuccess) { EXPECT_EQ(1, prefetch_service_.NewSuggestionsAvailableCallCount()); } +TEST_F(FeedStreamTest, BackgroundRefreshPrefetchesImages) { + // Trigger a background refresh. + response_translator_.InjectResponse(MakeTypicalInitialModelState()); + stream_->ExecuteRefreshTask(); + EXPECT_EQ(0, prefetch_image_call_count_); + WaitForIdleTaskQueue(); + + std::vector<GURL> expected_fetches( + {GURL("http://image0/"), GURL("http://favicon0/"), GURL("http://image1/"), + GURL("http://favicon1/")}); + // Verify that images were prefetched. + EXPECT_EQ(4, prefetch_image_call_count_); + EXPECT_EQ(expected_fetches, prefetched_images_); +} + TEST_F(FeedStreamTest, BackgroundRefreshNotAttemptedWhenModelIsLoading) { response_translator_.InjectResponse(MakeTypicalInitialModelState()); TestSurface surface(stream_.get()); @@ -1089,14 +1112,78 @@ TEST_F(FeedStreamTest, LoadStreamAfterEulaIsAccepted) { TEST_F(FeedStreamTest, ForceSignedOutRequestAfterHistoryIsDeleted) { stream_->OnAllHistoryDeleted(); + + const std::string kSessionId = "session-id"; + + // This test injects response post translation/parsing. We have to configure + // the response data that should come out of the translator, which should + // mark the request/response as having been made from the signed-out state. + StreamModelUpdateRequestGenerator model_generator; + model_generator.signed_in = false; + + // Advance the clock, but not past the end of the forced-signed-out period. task_environment_.FastForwardBy(kSuppressRefreshDuration - base::TimeDelta::FromSeconds(1)); - response_translator_.InjectResponse(MakeTypicalInitialModelState()); + + // Refresh the feed, queuing up a signed-out response. + response_translator_.InjectResponse(model_generator.MakeFirstPage(), + kSessionId); TestSurface surface(stream_.get()); WaitForIdleTaskQueue(); + // Validate that the network request was sent as signed out. + ASSERT_EQ(1, network_.send_query_call_count); + EXPECT_TRUE(network_.forced_signed_out_request); + EXPECT_TRUE(network_.query_request_sent->feed_request() + .client_info() + .chrome_client_info() + .session_id() + .empty()); + + // Validate the downstream consumption of the response. EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates()); + EXPECT_EQ(kSessionId, stream_->GetMetadata()->GetSessionIdToken()); + EXPECT_FALSE(stream_->GetModel()->signed_in()); + + // Advance the clock beyond the forced signed out period. + task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(2)); + EXPECT_FALSE(stream_->GetModel()->signed_in()); + + // Requests for subsequent pages continue the use existing session. + // Subsequent responses may omit the session id. + response_translator_.InjectResponse(model_generator.MakeNextPage()); + stream_->LoadMore(surface.GetSurfaceId(), base::DoNothing()); + WaitForIdleTaskQueue(); + + // Validate that the network request was sent as signed out and + // contained the session id. + ASSERT_EQ(2, network_.send_query_call_count); EXPECT_TRUE(network_.forced_signed_out_request); + EXPECT_EQ(kSessionId, stream_->GetMetadata()->GetSessionIdToken()); + EXPECT_EQ(network_.query_request_sent->feed_request() + .client_info() + .chrome_client_info() + .session_id(), + kSessionId); + + // The model should still be in the signed-out state. + EXPECT_FALSE(stream_->GetModel()->signed_in()); + + // Force a refresh of the feed by clearing the cache. The request for the + // first page should revert back to signed-in. The response data will denote + // a signed-in response with no session_id. + model_generator.signed_in = true; + response_translator_.InjectResponse(model_generator.MakeFirstPage()); + stream_->OnCacheDataCleared(); + WaitForIdleTaskQueue(); + + // Validate that a signed-in request was sent. + ASSERT_EQ(3, network_.send_query_call_count); + EXPECT_FALSE(network_.forced_signed_out_request); + + // The model should now be in the signed-in state. + EXPECT_TRUE(stream_->GetModel()->signed_in()); + EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdToken().empty()); } TEST_F(FeedStreamTest, AllowSignedInRequestAfterHistoryIsDeletedAfterDelay) { @@ -1109,6 +1196,7 @@ TEST_F(FeedStreamTest, AllowSignedInRequestAfterHistoryIsDeletedAfterDelay) { EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates()); EXPECT_FALSE(network_.forced_signed_out_request); + EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdToken().empty()); } TEST_F(FeedStreamTest, ShouldMakeFeedQueryRequestConsumesQuota) { @@ -1809,6 +1897,29 @@ TEST_F(FeedStreamTest, LoadStreamUpdateNoticeCardFulfillmentHistogram) { 1, 1); } +TEST_F(FeedStreamConditionalActionsUploadTest, + DontTriggerActionsUploadWhenWasNotSignedIn) { + auto update_request = MakeTypicalInitialModelState(); + update_request->stream_data.set_signed_in(false); + response_translator_.InjectResponse(std::move(update_request)); + TestSurface surface(stream_.get()); + WaitForIdleTaskQueue(); + + // Try to reach conditions. + stream_->ReportSliceViewed( + surface.GetSurfaceId(), + surface.initial_state->updated_slices(1).slice().slice_id()); + + // Try to trigger an upload through a query. + stream_->OnEnterBackground(); + WaitForIdleTaskQueue(); + + // Verify that even if the conditions were reached, the pref that enables the + // upload wasn't set to true because the latest refresh request wasn't signed + // in. + ASSERT_FALSE(stream_->CanUploadActions()); +} + TEST_F(FeedStreamTest, LoadStreamFromNetworkUploadsActions) { stream_->UploadAction(MakeFeedAction(99ul), false, base::DoNothing()); WaitForIdleTaskQueue(); @@ -1893,7 +2004,14 @@ TEST_F(FeedStreamTest, LoadMoreUpdatesIsActivityLoggingEnabled) { CallbackReceiver<bool> callback; stream_->LoadMore(surface.GetSurfaceId(), callback.Bind()); WaitForIdleTaskQueue(); - EXPECT_EQ(stream_->IsActivityLoggingEnabled(), signed_in && waa_on); + EXPECT_EQ( + stream_->IsActivityLoggingEnabled(), + (signed_in && waa_on) || + (!signed_in && GetFeedConfig().send_signed_out_session_logs)) + << "signed_in=" << signed_in << " waa_on=" << waa_on + << " privacy_notice_fulfilled=" << privacy_notice_fulfilled + << " send_signed_out_session_logs=" + << GetFeedConfig().send_signed_out_session_logs; } } } @@ -1911,7 +2029,6 @@ TEST_F(FeedStreamConditionalActionsUploadTest, response_translator_.InjectResponse(MakeTypicalNextPageState( /* first_cluster_id= */ 0, /* last_added_time= */ kTestTimeEpoch, - /* signed_in= */ true, /* logging_enabled= */ true, /* privacy_notice_fulfilled= */ false)); @@ -2055,7 +2172,8 @@ TEST_F(FeedStreamTest, UploadActionsErasesStaleActionsByAttempts) { TEST_F(FeedStreamTest, MetadataLoadedWhenDatabaseInitialized) { ASSERT_TRUE(stream_->GetMetadata()); - // Set the token and increment next action ID. + const auto kExpiry = kTestTimeEpoch + base::TimeDelta::FromDays(1234); + stream_->GetMetadata()->SetSessionId("session-id", kExpiry); stream_->GetMetadata()->SetConsistencyToken("token"); EXPECT_EQ(1, stream_->GetMetadata()->GetNextActionId().GetUnsafeValue()); @@ -2063,6 +2181,8 @@ TEST_F(FeedStreamTest, MetadataLoadedWhenDatabaseInitialized) { CreateStream(); ASSERT_TRUE(stream_->GetMetadata()); + EXPECT_EQ("session-id", stream_->GetMetadata()->GetSessionIdToken()); + EXPECT_EQ(kExpiry, stream_->GetMetadata()->GetSessionIdExpiryTime()); EXPECT_EQ("token", stream_->GetMetadata()->GetConsistencyToken()); EXPECT_EQ(2, stream_->GetMetadata()->GetNextActionId().GetUnsafeValue()); } @@ -2335,6 +2455,13 @@ TEST_F(FeedStreamTest, SendsClientInstanceId) { .client_instance_id(); EXPECT_NE("", first_instance_id); + // No signed-out session id was in the request. + EXPECT_TRUE(network_.query_request_sent->feed_request() + .client_info() + .chrome_client_info() + .session_id() + .empty()); + // LoadMore, and verify the same token is used. response_translator_.InjectResponse(MakeTypicalNextPageState(2)); stream_->LoadMore(surface.GetSurfaceId(), base::DoNothing()); @@ -2345,15 +2472,311 @@ TEST_F(FeedStreamTest, SendsClientInstanceId) { .client_info() .client_instance_id()); + // No signed-out session id was in the request. + EXPECT_TRUE(network_.query_request_sent->feed_request() + .client_info() + .chrome_client_info() + .session_id() + .empty()); + // Trigger a ClearAll to verify the instance ID changes. stream_->OnSignedOut(); WaitForIdleTaskQueue(); + EXPECT_FALSE(stream_->GetModel()); + const bool is_for_next_page = false; // No model so no first page yet. const std::string new_instance_id = - stream_->GetRequestMetadata().client_instance_id; + stream_->GetRequestMetadata(is_for_next_page).client_instance_id; ASSERT_NE("", new_instance_id); ASSERT_NE(first_instance_id, new_instance_id); } +TEST_F(FeedStreamTest, LoadStreamSendsNoticeCardAcknowledgement) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature( + feed::kInterestFeedNoticeCardAutoDismiss); + + store_->OverwriteStream(MakeTypicalInitialModelState(), base::DoNothing()); + TestSurface surface(stream_.get()); + WaitForIdleTaskQueue(); + + // Generate 3 view actions and 1 click action to trigger the acknowledgement + // of the notice card. + const int notice_card_index = 0; + std::string slice_id = + surface.initial_state->updated_slices(notice_card_index) + .slice() + .slice_id(); + stream_->ReportSliceViewed(surface.GetSurfaceId(), slice_id); + stream_->ReportSliceViewed(surface.GetSurfaceId(), slice_id); + stream_->ReportSliceViewed(surface.GetSurfaceId(), slice_id); + stream_->ReportOpenAction(slice_id); + + response_translator_.InjectResponse(MakeTypicalInitialModelState()); + refresh_scheduler_.Clear(); + stream_->UnloadModel(); + stream_->ExecuteRefreshTask(); + WaitForIdleTaskQueue(); + + EXPECT_TRUE(network_.query_request_sent->feed_request() + .feed_query() + .chrome_fulfillment_info() + .notice_card_acknowledged()); +} + +TEST_F(FeedStreamTest, GetSetAndUpdateSessionId) { + const std::string kToken1 = "token1"; + const std::string kToken2 = "token2"; + const base::Time kExpiryTime1 = + kTestTimeEpoch + base::TimeDelta::FromHours(2); + const base::Time kExpiryTime2 = + kTestTimeEpoch + GetFeedConfig().session_id_max_age; + ASSERT_NE(kExpiryTime1, kExpiryTime2); + + // The stream metadata is initialized with an empty token and expiry time. + EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdToken().empty()); + EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdExpiryTime().is_null()); + + // Verify that directly calling SetSessionId works as expected. + stream_->GetMetadata()->SetSessionId(kToken1, kExpiryTime1); + EXPECT_EQ(kToken1, stream_->GetMetadata()->GetSessionIdToken()); + EXPECT_EQ(kExpiryTime1, stream_->GetMetadata()->GetSessionIdExpiryTime()); + + // Updating the token with nullopt is a NOP. + stream_->GetMetadata()->MaybeUpdateSessionId(base::nullopt, + stream_->GetClock()); + EXPECT_EQ(kToken1, stream_->GetMetadata()->GetSessionIdToken()); + EXPECT_EQ(kExpiryTime1, stream_->GetMetadata()->GetSessionIdExpiryTime()); + + // Updating the token with the same value is a NOP. + stream_->GetMetadata()->MaybeUpdateSessionId(kToken1, stream_->GetClock()); + EXPECT_EQ(kToken1, stream_->GetMetadata()->GetSessionIdToken()); + EXPECT_EQ(kExpiryTime1, stream_->GetMetadata()->GetSessionIdExpiryTime()); + + // Updating the token with a different value resets the token and assigns a + // new expiry time. + stream_->GetMetadata()->MaybeUpdateSessionId(kToken2, stream_->GetClock()); + EXPECT_EQ(kToken2, stream_->GetMetadata()->GetSessionIdToken()); + EXPECT_EQ(kExpiryTime2, stream_->GetMetadata()->GetSessionIdExpiryTime()); + + // Updating the token with the empty string clears its value. + stream_->GetMetadata()->MaybeUpdateSessionId("", stream_->GetClock()); + EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdToken().empty()); + EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdExpiryTime().is_null()); +} + +TEST_F(FeedStreamTest, SignedOutSessionIdConsistency) { + const std::string kSessionToken1("session-token-1"); + const std::string kSessionToken2("session-token-2"); + + is_signed_in_ = false; + + StreamModelUpdateRequestGenerator model_generator; + model_generator.signed_in = false; + + // (1) Do an initial load of the store + // - this should trigger a network request + // - the request should not include client-instance-id + // - the request should not include a session-id + // - the stream should capture the session-id token from the response + response_translator_.InjectResponse(model_generator.MakeFirstPage(), + kSessionToken1); + TestSurface surface(stream_.get()); + WaitForIdleTaskQueue(); + ASSERT_EQ(1, network_.send_query_call_count); + EXPECT_TRUE(network_.query_request_sent->feed_request() + .client_info() + .client_instance_id() + .empty()); + EXPECT_FALSE(network_.query_request_sent->feed_request() + .client_info() + .has_chrome_client_info()); + EXPECT_EQ(kSessionToken1, stream_->GetMetadata()->GetSessionIdToken()); + const base::Time kSessionToken1ExpiryTime = + stream_->GetMetadata()->GetSessionIdExpiryTime(); + + // (2) LoadMore: the server returns the same session-id token + // - this should trigger a network request + // - the request should not include client-instance-id + // - the request should include the first session-id + // - the stream should retain the first session-id + // - the session-id's expiry time should be unchanged + task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(1)); + response_translator_.InjectResponse(model_generator.MakeNextPage(2), + kSessionToken1); + stream_->LoadMore(surface.GetSurfaceId(), base::DoNothing()); + WaitForIdleTaskQueue(); + ASSERT_EQ(2, network_.send_query_call_count); + EXPECT_TRUE(network_.query_request_sent->feed_request() + .client_info() + .client_instance_id() + .empty()); + EXPECT_EQ(kSessionToken1, network_.query_request_sent->feed_request() + .client_info() + .chrome_client_info() + .session_id()); + EXPECT_EQ(kSessionToken1, stream_->GetMetadata()->GetSessionIdToken()); + EXPECT_EQ(kSessionToken1ExpiryTime, + stream_->GetMetadata()->GetSessionIdExpiryTime()); + + // (3) LoadMore: the server omits returning a session-id token + // - this should trigger a network request + // - the request should not include client-instance-id + // - the request should include the first session-id + // - the stream should retain the first session-id + // - the session-id's expiry time should be unchanged + task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(1)); + response_translator_.InjectResponse(model_generator.MakeNextPage(3)); + stream_->LoadMore(surface.GetSurfaceId(), base::DoNothing()); + WaitForIdleTaskQueue(); + ASSERT_EQ(3, network_.send_query_call_count); + EXPECT_TRUE(network_.query_request_sent->feed_request() + .client_info() + .client_instance_id() + .empty()); + EXPECT_EQ(kSessionToken1, network_.query_request_sent->feed_request() + .client_info() + .chrome_client_info() + .session_id()); + EXPECT_EQ(kSessionToken1, stream_->GetMetadata()->GetSessionIdToken()); + EXPECT_EQ(kSessionToken1ExpiryTime, + stream_->GetMetadata()->GetSessionIdExpiryTime()); + + // (4) LoadMore: the server returns new session id. + // - this should trigger a network request + // - the request should not include client-instance-id + // - the request should include the first session-id + // - the stream should retain the second session-id + // - the new session-id's expiry time should be updated + task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(1)); + response_translator_.InjectResponse(model_generator.MakeNextPage(4), + kSessionToken2); + stream_->LoadMore(surface.GetSurfaceId(), base::DoNothing()); + WaitForIdleTaskQueue(); + ASSERT_EQ(4, network_.send_query_call_count); + EXPECT_TRUE(network_.query_request_sent->feed_request() + .client_info() + .client_instance_id() + .empty()); + EXPECT_EQ(kSessionToken1, network_.query_request_sent->feed_request() + .client_info() + .chrome_client_info() + .session_id()); + EXPECT_EQ(kSessionToken2, stream_->GetMetadata()->GetSessionIdToken()); + EXPECT_EQ(kSessionToken1ExpiryTime + base::TimeDelta::FromSeconds(3), + stream_->GetMetadata()->GetSessionIdExpiryTime()); +} + +TEST_F(FeedStreamTest, ClearAllResetsSessionId) { + is_signed_in_ = false; + + // Initialize a session id. + stream_->GetMetadata()->MaybeUpdateSessionId("session-id", + stream_->GetClock()); + ASSERT_FALSE(stream_->GetMetadata()->GetSessionIdToken().empty()); + ASSERT_FALSE(stream_->GetMetadata()->GetSessionIdExpiryTime().is_null()); + + // Trigger a ClearAll. + stream_->OnCacheDataCleared(); + WaitForIdleTaskQueue(); + + // Session-ID should be wiped. + EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdToken().empty()); + EXPECT_TRUE(stream_->GetMetadata()->GetSessionIdExpiryTime().is_null()); +} + +TEST_F(FeedStreamTest, SignedOutSessionIdExpiry) { + const std::string kSessionToken1("session-token-1"); + const std::string kSessionToken2("session-token-2"); + + is_signed_in_ = false; + + StreamModelUpdateRequestGenerator model_generator; + model_generator.signed_in = false; + + // (1) Do an initial load of the store + // - this should trigger a network request + // - the request should not include a session-id + // - the stream should capture the session-id token from the response + response_translator_.InjectResponse(model_generator.MakeFirstPage(), + kSessionToken1); + TestSurface surface(stream_.get()); + WaitForIdleTaskQueue(); + ASSERT_EQ(1, network_.send_query_call_count); + EXPECT_FALSE(network_.query_request_sent->feed_request() + .client_info() + .has_chrome_client_info()); + EXPECT_EQ(kSessionToken1, stream_->GetMetadata()->GetSessionIdToken()); + + // (2) Reload the stream from the network: + // - Detach the surface, advance the clock beyond the stale content + // threshold, re-attach the surface. + // - this should trigger a network request + // - the request should include kSessionToken1 + // - the stream should retain the original session-id + surface.Detach(); + task_environment_.FastForwardBy(GetFeedConfig().stale_content_threshold + + base::TimeDelta::FromSeconds(1)); + response_translator_.InjectResponse(model_generator.MakeFirstPage()); + surface.Attach(stream_.get()); + WaitForIdleTaskQueue(); + ASSERT_EQ(2, network_.send_query_call_count); + EXPECT_EQ(kSessionToken1, network_.query_request_sent->feed_request() + .client_info() + .chrome_client_info() + .session_id()); + EXPECT_EQ(kSessionToken1, stream_->GetMetadata()->GetSessionIdToken()); + + // (3) Reload the stream from the network: + // - Detach the surface, advance the clock beyond the session id max age + // threshold, re-attach the surface. + // - this should trigger a network request + // - the request should not include a session-id + // - the stream should get a new session-id + surface.Detach(); + task_environment_.FastForwardBy(GetFeedConfig().session_id_max_age - + GetFeedConfig().stale_content_threshold); + ASSERT_LT(stream_->GetMetadata()->GetSessionIdExpiryTime(), + task_environment_.GetMockClock()->Now()); + response_translator_.InjectResponse(model_generator.MakeFirstPage(), + kSessionToken2); + surface.Attach(stream_.get()); + WaitForIdleTaskQueue(); + ASSERT_EQ(3, network_.send_query_call_count); + EXPECT_FALSE(network_.query_request_sent->feed_request() + .client_info() + .has_chrome_client_info()); + EXPECT_EQ(kSessionToken2, stream_->GetMetadata()->GetSessionIdToken()); +} + +TEST_F(FeedStreamTest, SessionIdPersistsAcrossStreamLoads) { + const std::string kSessionToken("session-token-ftw"); + const base::Time kExpiryTime = + kTestTimeEpoch + GetFeedConfig().session_id_max_age; + + StreamModelUpdateRequestGenerator model_generator; + model_generator.signed_in = false; + is_signed_in_ = false; + + // (1) Do an initial load of the store + // - this should trigger a network request + // - the stream should capture the session-id token from the response + response_translator_.InjectResponse(model_generator.MakeFirstPage(), + kSessionToken); + TestSurface surface(stream_.get()); + WaitForIdleTaskQueue(); + ASSERT_EQ(1, network_.send_query_call_count); + + // (2) Reload the metadata from disk. + // - the network query call count should be unchanged + // - the session token and expiry time should have been reloaded. + surface.Detach(); + CreateStream(); + WaitForIdleTaskQueue(); + ASSERT_EQ(1, network_.send_query_call_count); + EXPECT_EQ(kSessionToken, stream_->GetMetadata()->GetSessionIdToken()); + EXPECT_EQ(kExpiryTime, stream_->GetMetadata()->GetSessionIdExpiryTime()); +} + } // namespace } // namespace feed diff --git a/chromium/components/feed/core/v2/image_fetcher_unittest.cc b/chromium/components/feed/core/v2/image_fetcher_unittest.cc index ba2dbe4f548..185945c7d89 100644 --- a/chromium/components/feed/core/v2/image_fetcher_unittest.cc +++ b/chromium/components/feed/core/v2/image_fetcher_unittest.cc @@ -10,7 +10,7 @@ #include "base/bind.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_split.h" -#include "base/test/bind_test_util.h" +#include "base/test/bind.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/task_environment.h" #include "components/feed/core/v2/public/types.h" diff --git a/chromium/components/feed/core/v2/metrics_reporter.cc b/chromium/components/feed/core/v2/metrics_reporter.cc index d23a99f17b9..df136e67a40 100644 --- a/chromium/components/feed/core/v2/metrics_reporter.cc +++ b/chromium/components/feed/core/v2/metrics_reporter.cc @@ -11,12 +11,13 @@ #include "base/threading/thread_task_runner_handle.h" #include "base/time/tick_clock.h" #include "base/time/time.h" +#include "components/feed/core/v2/common_enums.h" #include "components/feed/core/v2/prefs.h" namespace feed { namespace { -using feed::internal::FeedEngagementType; -using feed::internal::FeedUserActionType; +using feed::FeedEngagementType; +using feed::FeedUserActionType; const int kMaxSuggestionsTotal = 50; // Maximum time to wait before declaring a load operation failed. // For both ContentSuggestions.Feed.UserJourney.OpenFeed diff --git a/chromium/components/feed/core/v2/metrics_reporter.h b/chromium/components/feed/core/v2/metrics_reporter.h index 31489456ead..21515c8697f 100644 --- a/chromium/components/feed/core/v2/metrics_reporter.h +++ b/chromium/components/feed/core/v2/metrics_reporter.h @@ -5,6 +5,7 @@ #ifndef COMPONENTS_FEED_CORE_V2_METRICS_REPORTER_H_ #define COMPONENTS_FEED_CORE_V2_METRICS_REPORTER_H_ +#include <climits> #include <map> #include "base/memory/weak_ptr.h" @@ -17,50 +18,16 @@ namespace base { class TickClock; } // namespace base namespace feed { -namespace internal { -// This enum is used for a UMA histogram. Keep in sync with FeedEngagementType -// in enums.xml. -enum class FeedEngagementType { - kFeedEngaged = 0, - kFeedEngagedSimple = 1, - kFeedInteracted = 2, - kDeprecatedFeedScrolled = 3, - kFeedScrolled = 4, - kMaxValue = FeedEngagementType::kFeedScrolled, -}; - -// This enum must match FeedUserActionType in enums.xml. -// Note that most of these have a corresponding UserMetricsAction reported here. -// Exceptions are described below. -enum class FeedUserActionType { - kTappedOnCard = 0, - // This is not an actual user action, so there will be no UserMetricsAction - // reported for this. - kShownCard = 1, - kTappedSendFeedback = 2, - kTappedLearnMore = 3, - kTappedHideStory = 4, - kTappedNotInterestedIn = 5, - kTappedManageInterests = 6, - kTappedDownload = 7, - kTappedOpenInNewTab = 8, - kOpenedContextMenu = 9, - // User action not reported here. See Suggestions.SurfaceVisible. - kOpenedFeedSurface = 10, - kTappedOpenInNewIncognitoTab = 11, - kEphemeralChange = 12, - kEphemeralChangeRejected = 13, - kTappedTurnOn = 14, - kTappedTurnOff = 15, - kMaxValue = kTappedTurnOff, -}; - -} // namespace internal // Reports UMA metrics for feed. // Note this is inherited only for testing. class MetricsReporter { public: + // For 'index_in_stream' parameters, when the card index is unknown. + // This is most likely to happen when the action originates from the bottom + // sheet. + static const int kUnknownCardIndex = INT_MAX; + explicit MetricsReporter(const base::TickClock* clock, PrefService* profile_prefs); virtual ~MetricsReporter(); diff --git a/chromium/components/feed/core/v2/metrics_reporter_unittest.cc b/chromium/components/feed/core/v2/metrics_reporter_unittest.cc index c38bdb43cad..e768420fa4c 100644 --- a/chromium/components/feed/core/v2/metrics_reporter_unittest.cc +++ b/chromium/components/feed/core/v2/metrics_reporter_unittest.cc @@ -12,12 +12,11 @@ #include "base/test/task_environment.h" #include "components/feed/core/common/pref_names.h" #include "components/feed/core/shared_prefs/pref_names.h" +#include "components/feed/core/v2/common_enums.h" #include "components/prefs/testing_pref_service.h" #include "testing/gtest/include/gtest/gtest.h" namespace feed { -using feed::internal::FeedEngagementType; -using feed::internal::FeedUserActionType; constexpr SurfaceId kSurfaceId = SurfaceId(5); const base::TimeDelta kEpsilon = base::TimeDelta::FromMilliseconds(1); diff --git a/chromium/components/feed/core/v2/notice_card_tracker.cc b/chromium/components/feed/core/v2/notice_card_tracker.cc new file mode 100644 index 00000000000..cb318c8678e --- /dev/null +++ b/chromium/components/feed/core/v2/notice_card_tracker.cc @@ -0,0 +1,97 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feed/core/v2/notice_card_tracker.h" + +#include "components/feed/core/v2/prefs.h" +#include "components/feed/feed_feature_list.h" +#include "components/prefs/pref_service.h" + +namespace feed { +namespace { + +int GetNoticeCardIndex() { + // Infer that the notice card is at the 2nd position when the feature related + // to putting the notice card at the second position is enabled. + if (base::FeatureList::IsEnabled( + feed::kInterestFeedV2ClicksAndViewsConditionalUpload)) { + return 1; + } + return 0; +} + +} // namespace + +NoticeCardTracker::NoticeCardTracker(PrefService* profile_prefs) + : profile_prefs_(profile_prefs) { + DCHECK(profile_prefs_); +} + +void NoticeCardTracker::OnSliceViewed(int index) { + MaybeUpdateNoticeCardViewsCount(index); +} + +void NoticeCardTracker::OnOpenAction(int index) { + MaybeUpdateNoticeCardClicksCount(index); +} + +bool NoticeCardTracker::HasAcknowledgedNoticeCard() const { + if (!base::FeatureList::IsEnabled(feed::kInterestFeedNoticeCardAutoDismiss)) + return false; + + int views_count_threshold = base::GetFieldTrialParamByFeatureAsInt( + feed::kInterestFeedNoticeCardAutoDismiss, + kNoticeCardViewsCountThresholdParamName, 3); + DCHECK(views_count_threshold >= 0); + int clicks_count_threshold = base::GetFieldTrialParamByFeatureAsInt( + feed::kInterestFeedNoticeCardAutoDismiss, + kNoticeCardClicksCountThresholdParamName, 1); + DCHECK(clicks_count_threshold >= 0); + + DCHECK(views_count_threshold > 0 || clicks_count_threshold > 0) + << "all notice card auto-dismiss thresholds are set to 0 when there " + "should be at least one threshold above 0"; + + if (views_count_threshold > 0 && + prefs::GetNoticeCardViewsCount(*profile_prefs_) >= + views_count_threshold) { + return true; + } + + if (clicks_count_threshold > 0 && + prefs::GetNoticeCardClicksCount(*profile_prefs_) >= + clicks_count_threshold) { + return true; + } + + return false; +} + +bool NoticeCardTracker::HasNoticeCardActionsCountPrerequisites(int index) { + if (!base::FeatureList::IsEnabled(feed::kInterestFeedNoticeCardAutoDismiss)) + return false; + + if (!prefs::GetLastFetchHadNoticeCard(*profile_prefs_)) { + return false; + } + + if (index != GetNoticeCardIndex()) { + return false; + } + return true; +} +void NoticeCardTracker::MaybeUpdateNoticeCardViewsCount(int index) { + if (!HasNoticeCardActionsCountPrerequisites(index)) + return; + + prefs::IncrementNoticeCardViewsCount(*profile_prefs_); +} +void NoticeCardTracker::MaybeUpdateNoticeCardClicksCount(int index) { + if (!HasNoticeCardActionsCountPrerequisites(index)) + return; + + prefs::IncrementNoticeCardClicksCount(*profile_prefs_); +} + +} // namespace feed
\ No newline at end of file diff --git a/chromium/components/feed/core/v2/notice_card_tracker.h b/chromium/components/feed/core/v2/notice_card_tracker.h new file mode 100644 index 00000000000..db9f71815d1 --- /dev/null +++ b/chromium/components/feed/core/v2/notice_card_tracker.h @@ -0,0 +1,47 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEED_CORE_V2_NOTICE_CARD_TRACKER_H_ +#define COMPONENTS_FEED_CORE_V2_NOTICE_CARD_TRACKER_H_ + +class PrefService; + +namespace feed { + +constexpr char kNoticeCardViewsCountThresholdParamName[] = + "notice-card-views-count-threshold"; +constexpr char kNoticeCardClicksCountThresholdParamName[] = + "notice-card-clicks-count-threshold"; + +// Tracker for the notice card related actions that also provide signals based +// on those. +class NoticeCardTracker { + public: + explicit NoticeCardTracker(PrefService* profile_prefs); + + NoticeCardTracker(const NoticeCardTracker&) = delete; + NoticeCardTracker& operator=(const NoticeCardTracker&) = delete; + + // Capture the actions. + + void OnSliceViewed(int index); + void OnOpenAction(int index); + + // Get signals based on the actions. + + // Indicates whether there were enough views or clicks done on the notice + // card to consider it as acknowledged by the user. + bool HasAcknowledgedNoticeCard() const; + + private: + bool HasNoticeCardActionsCountPrerequisites(int index); + void MaybeUpdateNoticeCardViewsCount(int index); + void MaybeUpdateNoticeCardClicksCount(int index); + + PrefService* profile_prefs_; +}; + +} // namespace feed + +#endif // COMPONENTS_FEED_CORE_V2_NOTICE_CARD_TRACKER_H_
\ No newline at end of file diff --git a/chromium/components/feed/core/v2/notice_card_tracker_unittest.cc b/chromium/components/feed/core/v2/notice_card_tracker_unittest.cc new file mode 100644 index 00000000000..60efb9d10aa --- /dev/null +++ b/chromium/components/feed/core/v2/notice_card_tracker_unittest.cc @@ -0,0 +1,151 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feed/core/v2/notice_card_tracker.h" + +#include "base/test/scoped_feature_list.h" +#include "components/feed/core/common/pref_names.h" +#include "components/feed/core/v2/prefs.h" +#include "components/feed/feed_feature_list.h" +#include "components/prefs/pref_registry_simple.h" +#include "components/prefs/testing_pref_service.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feed { +namespace { + +class NoticeCardTrackerTest : public testing::Test { + public: + void SetUp() override { + feed::RegisterProfilePrefs(profile_prefs_.registry()); + ; + } + + protected: + TestingPrefServiceSimple profile_prefs_; +}; + +TEST_F(NoticeCardTrackerTest, + TrackingNoticeCardActionsDoesntUpdateCountsWhenNoNoticeCard) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature( + feed::kInterestFeedNoticeCardAutoDismiss); + NoticeCardTracker tracker(&profile_prefs_); + + prefs::SetLastFetchHadNoticeCard(profile_prefs_, false); + + // Generate enough views to reach the acknowlegement threshold, but there was + // no notice card in the feed. + const int notice_card_index = 0; + tracker.OnSliceViewed(notice_card_index); + tracker.OnSliceViewed(notice_card_index); + tracker.OnSliceViewed(notice_card_index); + + EXPECT_FALSE(tracker.HasAcknowledgedNoticeCard()); +} + +TEST_F(NoticeCardTrackerTest, + TrackingNoticeCardActionsDoesntUpdateCountsForNonNoticeCard) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature( + feed::kInterestFeedNoticeCardAutoDismiss); + NoticeCardTracker tracker(&profile_prefs_); + + // Generate enough views to reach the acknowlegement threshold, but the views + // were not on the notice card. + const int non_notice_card_index = 1; + tracker.OnSliceViewed(non_notice_card_index); + tracker.OnSliceViewed(non_notice_card_index); + tracker.OnSliceViewed(non_notice_card_index); + + EXPECT_FALSE(tracker.HasAcknowledgedNoticeCard()); +} + +TEST_F(NoticeCardTrackerTest, + AcknowledgedNoticeCardWhenEnoughViewsAndNoticeCardAt1stPos) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature( + feed::kInterestFeedNoticeCardAutoDismiss); + NoticeCardTracker tracker(&profile_prefs_); + + const int notice_card_index = 0; + tracker.OnSliceViewed(notice_card_index); + tracker.OnSliceViewed(notice_card_index); + tracker.OnSliceViewed(notice_card_index); + + EXPECT_TRUE(tracker.HasAcknowledgedNoticeCard()); +} + +TEST_F(NoticeCardTrackerTest, + AcknowledgedNoticeCardWhenEnoughViewsAndNoticeCardAt2ndPos) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures( + /*enabled_features=*/{feed::kInterestFeedNoticeCardAutoDismiss, + feed:: + kInterestFeedV2ClicksAndViewsConditionalUpload}, + /*disabled_features=*/{}); + NoticeCardTracker tracker(&profile_prefs_); + + const int notice_card_index = 1; + tracker.OnSliceViewed(notice_card_index); + tracker.OnSliceViewed(notice_card_index); + tracker.OnSliceViewed(notice_card_index); + + EXPECT_TRUE(tracker.HasAcknowledgedNoticeCard()); +} + +TEST_F(NoticeCardTrackerTest, + DontAcknowledgedNoticeCardWhenNotEnoughViewsNorClicks) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeature( + feed::kInterestFeedNoticeCardAutoDismiss); + NoticeCardTracker tracker(&profile_prefs_); + + // Generate views but not enough to reach the threshold. + const int notice_card_index = 0; + tracker.OnSliceViewed(notice_card_index); + tracker.OnSliceViewed(notice_card_index); + + EXPECT_FALSE(tracker.HasAcknowledgedNoticeCard()); +} + +TEST_F(NoticeCardTrackerTest, DontAcknowledgedNoticeCardWhenFeatureDisabled) { + // Generate enough views and clicks on the notice card to reach the threshold, + // but the feature is disabled. + prefs::IncrementNoticeCardClicksCount(profile_prefs_); + prefs::IncrementNoticeCardViewsCount(profile_prefs_); + prefs::IncrementNoticeCardViewsCount(profile_prefs_); + prefs::IncrementNoticeCardViewsCount(profile_prefs_); + + NoticeCardTracker tracker(&profile_prefs_); + EXPECT_FALSE(tracker.HasAcknowledgedNoticeCard()); +} + +TEST_F(NoticeCardTrackerTest, + DontAcknowledgedNoticeCardFromViewsCountWhenThresholdIsZero) { + base::FieldTrialParams params; + params[kNoticeCardViewsCountThresholdParamName] = "0"; + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeatureWithParameters( + feed::kInterestFeedNoticeCardAutoDismiss, params); + + NoticeCardTracker tracker(&profile_prefs_); + EXPECT_FALSE(tracker.HasAcknowledgedNoticeCard()); +} + +TEST_F(NoticeCardTrackerTest, + DontAcknowledgedNoticeCardFromClicksCountWhenThresholdIsZero) { + base::FieldTrialParams params; + params[kNoticeCardClicksCountThresholdParamName] = "0"; + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitAndEnableFeatureWithParameters( + feed::kInterestFeedNoticeCardAutoDismiss, params); + + NoticeCardTracker tracker(&profile_prefs_); + EXPECT_FALSE(tracker.HasAcknowledgedNoticeCard()); +} + +} // namespace +} // namespace feed
\ No newline at end of file diff --git a/chromium/components/feed/core/v2/prefs.cc b/chromium/components/feed/core/v2/prefs.cc index 562ff7d9c4f..49ef37fd3fe 100644 --- a/chromium/components/feed/core/v2/prefs.cc +++ b/chromium/components/feed/core/v2/prefs.cc @@ -106,6 +106,24 @@ bool GetHasReachedClickAndViewActionsUploadConditions( feed::prefs::kHasReachedClickAndViewActionsUploadConditions); } +void IncrementNoticeCardViewsCount(PrefService& pref_service) { + int count = pref_service.GetInteger(feed::prefs::kNoticeCardViewsCount); + pref_service.SetInteger(feed::prefs::kNoticeCardViewsCount, count + 1); +} + +int GetNoticeCardViewsCount(const PrefService& pref_service) { + return pref_service.GetInteger(feed::prefs::kNoticeCardViewsCount); +} + +void IncrementNoticeCardClicksCount(PrefService& pref_service) { + int count = pref_service.GetInteger(feed::prefs::kNoticeCardClicksCount); + pref_service.SetInteger(feed::prefs::kNoticeCardClicksCount, count + 1); +} + +int GetNoticeCardClicksCount(const PrefService& pref_service) { + return pref_service.GetInteger(feed::prefs::kNoticeCardClicksCount); +} + } // namespace prefs } // namespace feed diff --git a/chromium/components/feed/core/v2/prefs.h b/chromium/components/feed/core/v2/prefs.h index 634bddb16c6..e16befa3e47 100644 --- a/chromium/components/feed/core/v2/prefs.h +++ b/chromium/components/feed/core/v2/prefs.h @@ -52,6 +52,16 @@ void SetHasReachedClickAndViewActionsUploadConditions(PrefService& pref_service, bool GetHasReachedClickAndViewActionsUploadConditions( const PrefService& pref_service); +// Increment the stored notice card views count by 1. +void IncrementNoticeCardViewsCount(PrefService& pref_service); + +int GetNoticeCardViewsCount(const PrefService& pref_service); + +// Increment the stored notice card clicks count by 1. +void IncrementNoticeCardClicksCount(PrefService& pref_service); + +int GetNoticeCardClicksCount(const PrefService& pref_service); + } // namespace prefs } // namespace feed diff --git a/chromium/components/feed/core/v2/proto_util.cc b/chromium/components/feed/core/v2/proto_util.cc index aa21f4a1b93..d680ecfcf3f 100644 --- a/chromium/components/feed/core/v2/proto_util.cc +++ b/chromium/components/feed/core/v2/proto_util.cc @@ -13,10 +13,12 @@ #include "build/build_config.h" #include "components/feed/core/proto/v2/store.pb.h" #include "components/feed/core/proto/v2/wire/capability.pb.h" +#include "components/feed/core/proto/v2/wire/chrome_client_info.pb.h" #include "components/feed/core/proto/v2/wire/feed_request.pb.h" #include "components/feed/core/proto/v2/wire/request.pb.h" #include "components/feed/core/v2/config.h" #include "components/feed/core/v2/feed_stream.h" +#include "components/feed/feed_feature_list.h" #if defined(OS_ANDROID) #include "base/android/build_info.h" @@ -124,9 +126,15 @@ feedwire::Request CreateFeedQueryRequest( *feed_request.mutable_client_info() = CreateClientInfo(request_metadata); feedwire::FeedQuery& query = *feed_request.mutable_feed_query(); query.set_reason(request_reason); - if (!consistency_token.empty()) { + + // |consistency_token|, for action reporting, is only applicable to signed-in + // requests. The presence of |client_instance_id|, also signed-in only, can be + // used a proxy for checking if we're creating a signed-in request. + if (!consistency_token.empty() && + !request_metadata.client_instance_id.empty()) { feed_request.mutable_consistency_token()->set_token(consistency_token); } + if (!next_page_token.empty()) { DCHECK_EQ(request_reason, feedwire::FeedQuery::NEXT_PAGE_SCROLL); query.mutable_next_page_token() @@ -136,6 +144,16 @@ feedwire::Request CreateFeedQueryRequest( return request; } +void SetNoticeCardAcknowledged(feedwire::Request* request, + const RequestMetadata& request_metadata) { + if (request_metadata.notice_card_acknowledged) { + request->mutable_feed_request() + ->mutable_feed_query() + ->mutable_chrome_fulfillment_info() + ->set_notice_card_acknowledged(true); + } +} + } // namespace std::string ContentIdString(const feedwire::ContentId& content_id) { @@ -176,7 +194,6 @@ bool CompareContent(const feedstore::Content& a, const feedstore::Content& b) { feedwire::ClientInfo CreateClientInfo(const RequestMetadata& request_metadata) { feedwire::ClientInfo client_info; - client_info.set_client_instance_id(request_metadata.client_instance_id); feedwire::DisplayInfo& display_info = *client_info.add_display_info(); display_info.set_screen_density(request_metadata.display_metrics.density); @@ -196,6 +213,19 @@ feedwire::ClientInfo CreateClientInfo(const RequestMetadata& request_metadata) { *client_info.mutable_platform_version() = GetPlatformVersionMessage(); *client_info.mutable_app_version() = GetAppVersionMessage(request_metadata.chrome_info); + + // client_instance_id and session_id should not both be set at the same time. + DCHECK(request_metadata.client_instance_id.empty() || + request_metadata.session_id.empty()); + + // Populate client_instance_id, session_id, or neither. + if (!request_metadata.client_instance_id.empty()) { + client_info.set_client_instance_id(request_metadata.client_instance_id); + } else if (!request_metadata.session_id.empty()) { + client_info.mutable_chrome_client_info()->set_session_id( + request_metadata.session_id); + } + return client_info; } @@ -203,8 +233,10 @@ feedwire::Request CreateFeedQueryRefreshRequest( feedwire::FeedQuery::RequestReason request_reason, const RequestMetadata& request_metadata, const std::string& consistency_token) { - return CreateFeedQueryRequest(request_reason, request_metadata, - consistency_token, std::string()); + feedwire::Request request = CreateFeedQueryRequest( + request_reason, request_metadata, consistency_token, std::string()); + SetNoticeCardAcknowledged(&request, request_metadata); + return request; } feedwire::Request CreateFeedQueryLoadMoreRequest( diff --git a/chromium/components/feed/core/v2/proto_util_unittest.cc b/chromium/components/feed/core/v2/proto_util_unittest.cc index 53eecb56451..8d53ebe00fa 100644 --- a/chromium/components/feed/core/v2/proto_util_unittest.cc +++ b/chromium/components/feed/core/v2/proto_util_unittest.cc @@ -108,5 +108,31 @@ TEST(ProtoUtilTest, DisableCapabilitiesWithFinch) { EXPECT_TRUE(HasCapability(request, feedwire::Capability::PREFETCH_METADATA)); } +TEST(ProtoUtilTest, NoticeCardAcknowledged) { + RequestMetadata request_metadata; + request_metadata.notice_card_acknowledged = true; + feedwire::Request request = CreateFeedQueryRefreshRequest( + feedwire::FeedQuery::MANUAL_REFRESH, request_metadata, + /*consistency_token=*/std::string()); + + EXPECT_TRUE(request.feed_request() + .feed_query() + .chrome_fulfillment_info() + .notice_card_acknowledged()); +} + +TEST(ProtoUtilTest, NoticeCardNotAcknowledged) { + RequestMetadata request_metadata; + request_metadata.notice_card_acknowledged = false; + feedwire::Request request = CreateFeedQueryRefreshRequest( + feedwire::FeedQuery::MANUAL_REFRESH, request_metadata, + /*consistency_token=*/std::string()); + + EXPECT_FALSE(request.feed_request() + .feed_query() + .chrome_fulfillment_info() + .notice_card_acknowledged()); +} + } // namespace } // namespace feed diff --git a/chromium/components/feed/core/v2/protocol_translator.cc b/chromium/components/feed/core/v2/protocol_translator.cc index db128746859..ea69442f531 100644 --- a/chromium/components/feed/core/v2/protocol_translator.cc +++ b/chromium/components/feed/core/v2/protocol_translator.cc @@ -306,6 +306,18 @@ RefreshResponseData TranslateWireResponse( result->stream_data.set_privacy_notice_fulfilled( response_metadata.privacy_notice_fulfilled()); + base::Optional<std::string> session_id = base::nullopt; + if (was_signed_in_request) { + // Signed-in requests don't use session_id tokens; set an empty value to + // ensure that there are no old session_id tokens left hanging around. + session_id = std::string(); + } else if (response_metadata.has_session_id()) { + // Signed-out requests can set a new session token; otherwise, we leave + // the default base::nullopt value to keep whatever token is already in + // play. + session_id = response_metadata.session_id(); + } + MetricsReporter::ActivityLoggingEnabled(response_metadata.logging_enabled()); MetricsReporter::NoticeCardFulfilledObsolete( response_metadata.privacy_notice_fulfilled()); @@ -313,6 +325,7 @@ RefreshResponseData TranslateWireResponse( RefreshResponseData response_data; response_data.model_update_request = std::move(result); response_data.request_schedule = std::move(global_data.request_schedule); + response_data.session_id = std::move(session_id); return response_data; } diff --git a/chromium/components/feed/core/v2/protocol_translator.h b/chromium/components/feed/core/v2/protocol_translator.h index 3728e1e5dc4..b83a23fdecf 100644 --- a/chromium/components/feed/core/v2/protocol_translator.h +++ b/chromium/components/feed/core/v2/protocol_translator.h @@ -64,6 +64,9 @@ struct RefreshResponseData { // Server-defined request schedule, if provided. base::Optional<RequestSchedule> request_schedule; + + // Server-defined session id token, if provided. + base::Optional<std::string> session_id; }; base::Optional<feedstore::DataOperation> TranslateDataOperation( diff --git a/chromium/components/feed/core/v2/public/feed_service.cc b/chromium/components/feed/core/v2/public/feed_service.cc index 7e7a59776ee..b5836074240 100644 --- a/chromium/components/feed/core/v2/public/feed_service.cc +++ b/chromium/components/feed/core/v2/public/feed_service.cc @@ -121,6 +121,9 @@ class FeedService::StreamDelegateImpl : public FeedStream::Delegate { return service_delegate_->GetLanguageTag(); } void ClearAll() override { service_delegate_->ClearAll(); } + void PrefetchImage(const GURL& url) override { + service_delegate_->PrefetchImage(url); + } bool IsSignedIn() override { return identity_manager_->HasPrimaryAccount(); } private: diff --git a/chromium/components/feed/core/v2/public/feed_service.h b/chromium/components/feed/core/v2/public/feed_service.h index ae3b5ddefb0..d8c1f2aa222 100644 --- a/chromium/components/feed/core/v2/public/feed_service.h +++ b/chromium/components/feed/core/v2/public/feed_service.h @@ -67,6 +67,8 @@ class FeedService : public KeyedService { virtual DisplayMetrics GetDisplayMetrics() = 0; // Clear all stored data. virtual void ClearAll() = 0; + // Fetch the image and store it in the disk cache. + virtual void PrefetchImage(const GURL& url) = 0; }; // Construct a FeedService given an already constructed FeedStream. diff --git a/chromium/components/feed/core/v2/public/feed_stream_api.h b/chromium/components/feed/core/v2/public/feed_stream_api.h index d3bfabc34de..b33d9e87239 100644 --- a/chromium/components/feed/core/v2/public/feed_stream_api.h +++ b/chromium/components/feed/core/v2/public/feed_stream_api.h @@ -63,9 +63,14 @@ class FeedStreamApi { // as the feed is refreshed or the user signs in/out. virtual bool IsActivityLoggingEnabled() const = 0; - // Returns the client_instance_id. This value is reset whenever the feed - // stream is cleared (on sign-in, sign-out, and some data clear events). - virtual std::string GetClientInstanceId() = 0; + // Returns the signed-in client_instance_id. This value is reset whenever the + // feed stream is cleared (on sign-in, sign-out, and some data clear events). + virtual std::string GetClientInstanceId() const = 0; + + // Returns the client's signed-out session id. This value is reset whenever + // the feed stream is cleared (on sign-in, sign-out, and some data clear + // events). + virtual std::string GetSessionId() const = 0; // Invoked by RefreshTaskScheduler's scheduled task. virtual void ExecuteRefreshTask() = 0; @@ -170,6 +175,9 @@ class FeedStreamApi { virtual void ForceRefreshForDebugging() = 0; // Dumps some state information for debugging. virtual std::string DumpStateForDebugging() = 0; + // Forces to render a StreamUpdate on all subsequent surface attaches. + virtual void SetForcedStreamUpdateForDebugging( + const feedui::StreamUpdate& stream_update) = 0; }; } // namespace feed diff --git a/chromium/components/feed/core/v2/public/types.h b/chromium/components/feed/core/v2/public/types.h index 7680ef75813..0bdc72e7f07 100644 --- a/chromium/components/feed/core/v2/public/types.h +++ b/chromium/components/feed/core/v2/public/types.h @@ -48,6 +48,7 @@ struct NetworkResponseInfo { std::string bless_nonce; GURL base_request_url; size_t response_body_bytes = 0; + bool was_signed_in = false; }; struct NetworkResponse { diff --git a/chromium/components/feed/core/v2/tasks/load_more_task.cc b/chromium/components/feed/core/v2/tasks/load_more_task.cc index 8c6dc6370aa..6afb90d4780 100644 --- a/chromium/components/feed/core/v2/tasks/load_more_task.cc +++ b/chromium/components/feed/core/v2/tasks/load_more_task.cc @@ -7,7 +7,7 @@ #include <memory> #include <utility> -#include "base/bind_helpers.h" +#include "base/callback_helpers.h" #include "base/check.h" #include "base/time/clock.h" #include "base/time/tick_clock.h" @@ -48,22 +48,34 @@ void LoadMoreTask::Run() { } void LoadMoreTask::UploadActionsComplete(UploadActionsTask::Result result) { + StreamModel* model = stream_->GetModel(); + DCHECK(model) << "Model was unloaded outside of a Task"; + + // Determine whether the load more request should be forced signed-out + // regardless of the live sign-in state of the client. + // + // The signed-in state of the model is used instead of using + // FeedStream#ShouldForceSignedOutFeedQueryRequest because the load more + // requests should be in the same signed-in state as the prior requests that + // filled the model to have consistent data. + // + // The sign-in state of the load stream request that brings the initial + // content determines the sign-in state of the subsequent load more requests. + // This avoids a possible situation where there would be a mix of signed-in + // and signed-out content, which we don't want. + bool force_signed_out_request = !model->signed_in(); // Send network request. - bool force_signed_out_request = - stream_->ShouldForceSignedOutFeedQueryRequest(); fetch_start_time_ = stream_->GetTickClock()->NowTicks(); stream_->GetNetwork()->SendQueryRequest( CreateFeedQueryLoadMoreRequest( - stream_->GetRequestMetadata(), + stream_->GetRequestMetadata(/*is_for_next_page=*/true), stream_->GetMetadata()->GetConsistencyToken(), stream_->GetModel()->GetNextPageToken()), force_signed_out_request, - base::BindOnce(&LoadMoreTask::QueryRequestComplete, GetWeakPtr(), - force_signed_out_request)); + base::BindOnce(&LoadMoreTask::QueryRequestComplete, GetWeakPtr())); } void LoadMoreTask::QueryRequestComplete( - bool was_forced_signed_out_request, FeedNetwork::QueryRequestResult result) { StreamModel* model = stream_->GetModel(); DCHECK(model) << "Model was unloaded outside of a Task"; @@ -71,20 +83,21 @@ void LoadMoreTask::QueryRequestComplete( if (!result.response_body) return Done(LoadStreamStatus::kNoResponseBody); - bool was_signed_in_request = - !was_forced_signed_out_request && stream_->IsSignedIn(); - RefreshResponseData translated_response = stream_->GetWireResponseTranslator()->TranslateWireResponse( *result.response_body, StreamModelUpdateRequest::Source::kNetworkLoadMore, - was_signed_in_request, stream_->GetClock()->Now()); + result.response_info.was_signed_in, stream_->GetClock()->Now()); if (!translated_response.model_update_request) return Done(LoadStreamStatus::kProtoTranslationFailed); loaded_new_content_from_network_ = !translated_response.model_update_request->stream_structures.empty(); + + stream_->GetMetadata()->MaybeUpdateSessionId(translated_response.session_id, + stream_->GetClock()); + model->Update(std::move(translated_response.model_update_request)); if (translated_response.request_schedule) diff --git a/chromium/components/feed/core/v2/tasks/load_more_task.h b/chromium/components/feed/core/v2/tasks/load_more_task.h index 397c4eb1cab..538fcf9c75d 100644 --- a/chromium/components/feed/core/v2/tasks/load_more_task.h +++ b/chromium/components/feed/core/v2/tasks/load_more_task.h @@ -44,8 +44,7 @@ class LoadMoreTask : public offline_pages::Task { } void UploadActionsComplete(UploadActionsTask::Result result); - void QueryRequestComplete(bool was_signed_in_request, - FeedNetwork::QueryRequestResult result); + void QueryRequestComplete(FeedNetwork::QueryRequestResult result); void Done(LoadStreamStatus status); FeedStream* stream_; // Unowned. diff --git a/chromium/components/feed/core/v2/tasks/load_stream_from_store_task.cc b/chromium/components/feed/core/v2/tasks/load_stream_from_store_task.cc index 71fff522a38..999d5f6fae9 100644 --- a/chromium/components/feed/core/v2/tasks/load_stream_from_store_task.cc +++ b/chromium/components/feed/core/v2/tasks/load_stream_from_store_task.cc @@ -126,8 +126,6 @@ void LoadStreamFromStoreTask::Complete(LoadStreamStatus status) { if (status == LoadStreamStatus::kLoadedFromStore && load_type_ == LoadType::kFullLoad) { task_result.update_request = std::move(update_request_); - } else { - task_result.consistency_token = consistency_token_; } std::move(result_callback_).Run(std::move(task_result)); TaskComplete(); diff --git a/chromium/components/feed/core/v2/tasks/load_stream_from_store_task.h b/chromium/components/feed/core/v2/tasks/load_stream_from_store_task.h index 61dee09d676..fef672dc9ac 100644 --- a/chromium/components/feed/core/v2/tasks/load_stream_from_store_task.h +++ b/chromium/components/feed/core/v2/tasks/load_stream_from_store_task.h @@ -76,7 +76,6 @@ class LoadStreamFromStoreTask : public offline_pages::Task { // Data to be stuffed into the Result when the task is complete. std::unique_ptr<StreamModelUpdateRequest> update_request_; - std::string consistency_token_; std::vector<feedstore::StoredAction> pending_actions_; base::WeakPtrFactory<LoadStreamFromStoreTask> weak_ptr_factory_{this}; diff --git a/chromium/components/feed/core/v2/tasks/load_stream_task.cc b/chromium/components/feed/core/v2/tasks/load_stream_task.cc index c061f9d91bf..9b4f04b1e69 100644 --- a/chromium/components/feed/core/v2/tasks/load_stream_task.cc +++ b/chromium/components/feed/core/v2/tasks/load_stream_task.cc @@ -7,7 +7,7 @@ #include <memory> #include <utility> -#include "base/bind_helpers.h" +#include "base/callback_helpers.h" #include "base/check.h" #include "base/time/clock.h" #include "base/time/tick_clock.h" @@ -123,15 +123,14 @@ void LoadStreamTask::UploadActionsComplete(UploadActionsTask::Result result) { latencies_->StepComplete(LoadLatencyTimes::kUploadActions); stream_->GetNetwork()->SendQueryRequest( CreateFeedQueryRefreshRequest( - GetRequestReason(load_type_), stream_->GetRequestMetadata(), + GetRequestReason(load_type_), + stream_->GetRequestMetadata(/*is_for_next_page=*/false), stream_->GetMetadata()->GetConsistencyToken()), force_signed_out_request, - base::BindOnce(&LoadStreamTask::QueryRequestComplete, GetWeakPtr(), - force_signed_out_request)); + base::BindOnce(&LoadStreamTask::QueryRequestComplete, GetWeakPtr())); } void LoadStreamTask::QueryRequestComplete( - bool was_forced_signed_out_request, FeedNetwork::QueryRequestResult result) { latencies_->StepComplete(LoadLatencyTimes::kQueryRequest); @@ -149,14 +148,11 @@ void LoadStreamTask::QueryRequestComplete( return Done(LoadStreamStatus::kNoResponseBody); } - bool was_signed_in_request = - !was_forced_signed_out_request && stream_->IsSignedIn(); - RefreshResponseData response_data = stream_->GetWireResponseTranslator()->TranslateWireResponse( *result.response_body, StreamModelUpdateRequest::Source::kNetworkUpdate, - was_signed_in_request, stream_->GetClock()->Now()); + result.response_info.was_signed_in, stream_->GetClock()->Now()); if (!response_data.model_update_request) return Done(LoadStreamStatus::kProtoTranslationFailed); @@ -172,6 +168,9 @@ void LoadStreamTask::QueryRequestComplete( stream_->SetLastStreamLoadHadNoticeCard(isNoticeCardFulfilled); MetricsReporter::NoticeCardFulfilled(isNoticeCardFulfilled); + stream_->GetMetadata()->MaybeUpdateSessionId(response_data.session_id, + stream_->GetClock()); + if (load_type_ != LoadType::kBackgroundRefresh) { auto model = std::make_unique<StreamModel>(); model->Update(std::move(response_data.model_update_request)); diff --git a/chromium/components/feed/core/v2/tasks/load_stream_task.h b/chromium/components/feed/core/v2/tasks/load_stream_task.h index 6e8993959ac..dd4c210db73 100644 --- a/chromium/components/feed/core/v2/tasks/load_stream_task.h +++ b/chromium/components/feed/core/v2/tasks/load_stream_task.h @@ -74,8 +74,7 @@ class LoadStreamTask : public offline_pages::Task { void LoadFromStoreComplete(LoadStreamFromStoreTask::Result result); void UploadActionsComplete(UploadActionsTask::Result result); - void QueryRequestComplete(bool was_signed_in_request, - FeedNetwork::QueryRequestResult result); + void QueryRequestComplete(FeedNetwork::QueryRequestResult result); void Done(LoadStreamStatus status); LoadType load_type_; diff --git a/chromium/components/feed/core/v2/tasks/prefetch_images_task.cc b/chromium/components/feed/core/v2/tasks/prefetch_images_task.cc new file mode 100644 index 00000000000..f8a927ba6af --- /dev/null +++ b/chromium/components/feed/core/v2/tasks/prefetch_images_task.cc @@ -0,0 +1,100 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feed/core/v2/tasks/prefetch_images_task.h" + +#include <utility> + +#include "base/callback.h" +#include "base/logging.h" +#include "components/feed/core/proto/v2/wire/stream_structure.pb.h" +#include "components/feed/core/v2/config.h" +#include "components/feed/core/v2/feed_store.h" +#include "components/feed/core/v2/feed_stream.h" +#include "components/feed/core/v2/stream_model.h" +#include "components/feed/core/v2/tasks/load_stream_from_store_task.h" + +namespace feed { +namespace { + +// Converts a URL string into a GURL. If the string is not a valid URL, returns +// an empty GURL. Since GURL::spec() asserts on invalid URLs, this is necessary +// to scrub the incoming data from the wire. +GURL SpecToGURL(const std::string& url_string) { + GURL url(url_string); + if (!url.is_valid()) + url = GURL(); + return url; +} + +} // namespace + +PrefetchImagesTask::PrefetchImagesTask(FeedStream* stream) : stream_(stream) { + max_images_per_refresh_ = + GetFeedConfig().max_prefetch_image_requests_per_refresh; +} + +PrefetchImagesTask::~PrefetchImagesTask() = default; + +void PrefetchImagesTask::Run() { + if (stream_->GetModel()) { + PrefetchImagesFromModel(*stream_->GetModel()); + return; + } + + load_from_store_task_ = std::make_unique<LoadStreamFromStoreTask>( + LoadStreamFromStoreTask::LoadType::kFullLoad, stream_->GetStore(), + stream_->GetClock(), + base::BindOnce(&PrefetchImagesTask::LoadStreamComplete, + base::Unretained(this))); + + load_from_store_task_->Execute(base::DoNothing()); +} + +void PrefetchImagesTask::LoadStreamComplete( + LoadStreamFromStoreTask::Result result) { + if (!result.update_request) { + TaskComplete(); + return; + } + + // It is a bit dangerous to retain the model loaded here. The normal + // LoadStreamTask flow has various considerations for metrics and signalling + // surfaces to update. For this reason, we're not going to retain the loaded + // model for use outside of this task. + StreamModel model; + model.Update(std::move(result.update_request)); + PrefetchImagesFromModel(model); +} + +void PrefetchImagesTask::PrefetchImagesFromModel(const StreamModel& model) { + for (ContentRevision rev : model.GetContentList()) { + const feedstore::Content* content = model.FindContent(rev); + if (!content) + continue; + for (const feedwire::PrefetchMetadata& metadata : + content->prefetch_metadata()) { + MaybePrefetchImage(SpecToGURL(metadata.image_url())); + MaybePrefetchImage(SpecToGURL(metadata.favicon_url())); + for (const std::string& url : metadata.additional_image_urls()) { + MaybePrefetchImage(SpecToGURL(url)); + } + } + } + + TaskComplete(); +} + +void PrefetchImagesTask::MaybePrefetchImage(const GURL& gurl) { + // If we've already fetched this url, or we've hit the max number of fetches, + // then don't send a fetch request. + if (!gurl.is_valid() || + (previously_fetched_.find(gurl.spec()) != previously_fetched_.end()) || + previously_fetched_.size() >= max_images_per_refresh_) + return; + previously_fetched_.insert(gurl.spec()); + stream_->PrefetchImage(gurl); +} + +} // namespace feed diff --git a/chromium/components/feed/core/v2/tasks/prefetch_images_task.h b/chromium/components/feed/core/v2/tasks/prefetch_images_task.h new file mode 100644 index 00000000000..ddc24a6cc76 --- /dev/null +++ b/chromium/components/feed/core/v2/tasks/prefetch_images_task.h @@ -0,0 +1,51 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEED_CORE_V2_TASKS_PREFETCH_IMAGES_TASK_H_ +#define COMPONENTS_FEED_CORE_V2_TASKS_PREFETCH_IMAGES_TASK_H_ + +#include <memory> +#include <vector> + +#include "base/memory/weak_ptr.h" +#include "components/feed/core/v2/tasks/load_stream_from_store_task.h" +#include "components/offline_pages/task/task.h" + +namespace feed { +class FeedStream; +class StreamModel; + +// Prefetch the images in the model. +class PrefetchImagesTask : public offline_pages::Task { + public: + explicit PrefetchImagesTask(FeedStream* stream); + ~PrefetchImagesTask() override; + PrefetchImagesTask(const PrefetchImagesTask&) = delete; + PrefetchImagesTask& operator=(const PrefetchImagesTask&) = delete; + + private: + base::WeakPtr<PrefetchImagesTask> GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); + } + + // offline_pages::Task. + void Run() override; + + void LoadStreamComplete(LoadStreamFromStoreTask::Result result); + + void PrefetchImagesFromModel(const StreamModel& model); + + void MaybePrefetchImage(const GURL& gurl); + + FeedStream* stream_; + std::unordered_set<std::string> previously_fetched_; + unsigned long max_images_per_refresh_; + + std::unique_ptr<LoadStreamFromStoreTask> load_from_store_task_; + + base::WeakPtrFactory<PrefetchImagesTask> weak_ptr_factory_{this}; +}; +} // namespace feed + +#endif // COMPONENTS_FEED_CORE_V2_TASKS_PREFETCH_IMAGES_TASK_H_ diff --git a/chromium/components/feed/core/v2/types.h b/chromium/components/feed/core/v2/types.h index 883effa2bfb..508e56c98e4 100644 --- a/chromium/components/feed/core/v2/types.h +++ b/chromium/components/feed/core/v2/types.h @@ -39,7 +39,9 @@ struct RequestMetadata { ChromeInfo chrome_info; std::string language_tag; std::string client_instance_id; + std::string session_id; DisplayMetrics display_metrics; + bool notice_card_acknowledged = false; }; // Data internal to MetricsReporter which is persisted to Prefs. diff --git a/chromium/components/feed/features.gni b/chromium/components/feed/features.gni index 022f876896e..627f7d7bf52 100644 --- a/chromium/components/feed/features.gni +++ b/chromium/components/feed/features.gni @@ -2,12 +2,28 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import("//chrome/android/channel.gni") + # Chrome can be built with any combination of v1/v2 enabled. # When both are enabled, we use the InterestFeedV2 feature to select which # one is used at runtime. # If both are false, some of the Feed V2 UI classes are still used to display # the NTP. declare_args() { + # Whether version 1 of the NTP feed is enabled in the build. enable_feed_v1 = is_android + + # Whether version 2 of the NTP feed is enabled in the build. enable_feed_v2 = is_android + + # Whether to include Feed V2 in ChromeModern builds. + enable_feed_v2_modern = true + + # Whether to include Feed V2 as a DFM in ChromeModern builds. + dfmify_feed_v2_modern = false +} + +if (is_android && android_channel != "default") { + # You probably don't want to build without either version in a Clank release. + assert(enable_feed_v1 || enable_feed_v2) } diff --git a/chromium/components/feed/feed_feature_list.cc b/chromium/components/feed/feed_feature_list.cc index 8065e083e04..f9cf11aba5e 100644 --- a/chromium/components/feed/feed_feature_list.cc +++ b/chromium/components/feed/feed_feature_list.cc @@ -30,9 +30,6 @@ const base::FeatureParam<bool> kOnlySetLastRefreshAttemptOnSuccess{ &kInterestFeedContentSuggestions, "only_set_last_refresh_attempt_on_success", true}; -const base::Feature kInterestFeedFeedback{"InterestFeedFeedback", - base::FEATURE_DISABLED_BY_DEFAULT}; - const base::Feature kReportFeedUserActions{"ReportFeedUserActions", base::FEATURE_DISABLED_BY_DEFAULT}; @@ -47,6 +44,15 @@ const base::Feature kInterestFeedV2ClicksAndViewsConditionalUpload{ "InterestFeedV2ClickAndViewActionsConditionalUpload", base::FEATURE_DISABLED_BY_DEFAULT}; +// Feature that allows the client to automatically dismiss the notice card based +// on the clicks and views on the notice card. +const base::Feature kInterestFeedNoticeCardAutoDismiss{ + "InterestFeedNoticeCardAutoDismiss", base::FEATURE_DISABLED_BY_DEFAULT}; + +// Used for A:B testing of a bug fix (crbug.com/1151391). +const base::Feature kInterestFeedSpinnerAlwaysAnimate{ + "InterestFeedSpinnerAlwaysAnimate", base::FEATURE_DISABLED_BY_DEFAULT}; + const char kDefaultReferrerUrl[] = "https://www.googleapis.com/auth/chrome-content-suggestions"; diff --git a/chromium/components/feed/feed_feature_list.h b/chromium/components/feed/feed_feature_list.h index 3fd0e3cb300..432ba988ff8 100644 --- a/chromium/components/feed/feed_feature_list.h +++ b/chromium/components/feed/feed_feature_list.h @@ -24,8 +24,6 @@ extern const base::FeatureParam<int> kTimeoutDurationSeconds; extern const base::FeatureParam<bool> kThrottleBackgroundFetches; extern const base::FeatureParam<bool> kOnlySetLastRefreshAttemptOnSuccess; -extern const base::Feature kInterestFeedFeedback; - // Indicates if user card clicks and views in Chrome's feed should be reported // for personalization. Also enables the feed header menu to manage the feed. extern const base::Feature kReportFeedUserActions; @@ -33,6 +31,10 @@ extern const base::Feature kReportFeedUserActions; extern const base::Feature kInterestFeedV1ClicksAndViewsConditionalUpload; extern const base::Feature kInterestFeedV2ClicksAndViewsConditionalUpload; +extern const base::Feature kInterestFeedNoticeCardAutoDismiss; + +extern const base::Feature kInterestFeedSpinnerAlwaysAnimate; + std::string GetFeedReferrerUrl(); } // namespace feed |