diff options
Diffstat (limited to 'chromium/components/feed')
51 files changed, 1087 insertions, 355 deletions
diff --git a/chromium/components/feed/core/common/pref_names.cc b/chromium/components/feed/core/common/pref_names.cc index a4736acee86..b3dc860cbb6 100644 --- a/chromium/components/feed/core/common/pref_names.cc +++ b/chromium/components/feed/core/common/pref_names.cc @@ -39,6 +39,7 @@ const char kThrottlerLastRequestTime[] = "feedv2.request_throttler.last_request_time"; const char kDebugStreamData[] = "feedv2.debug_stream_data"; const char kRequestSchedule[] = "feedv2.request_schedule"; +const char kMetricsData[] = "feedv2.metrics_data"; } // namespace prefs @@ -55,6 +56,7 @@ void RegisterProfilePrefs(PrefRegistrySimple* registry) { base::Time()); registry->RegisterStringPref(feed::prefs::kDebugStreamData, std::string()); registry->RegisterDictionaryPref(feed::prefs::kRequestSchedule); + registry->RegisterDictionaryPref(feed::prefs::kMetricsData); UserClassifier::RegisterProfilePrefs(registry); } diff --git a/chromium/components/feed/core/common/pref_names.h b/chromium/components/feed/core/common/pref_names.h index b23c1552bde..67a1d1ee502 100644 --- a/chromium/components/feed/core/common/pref_names.h +++ b/chromium/components/feed/core/common/pref_names.h @@ -52,6 +52,8 @@ extern const char kThrottlerLastRequestTime[]; extern const char kDebugStreamData[]; // The pref name for storing the request schedule. extern const char kRequestSchedule[]; +// The pref name for storing the persistent metrics data. +extern const char kMetricsData[]; } // namespace prefs diff --git a/chromium/components/feed/core/proto/BUILD.gn b/chromium/components/feed/core/proto/BUILD.gn index bede4090414..b2b79298464 100644 --- a/chromium/components/feed/core/proto/BUILD.gn +++ b/chromium/components/feed/core/proto/BUILD.gn @@ -19,6 +19,7 @@ proto_library("proto") { proto_library("proto_v2") { proto_in_dir = "../../../../" sources = [ + "v2/packing.proto", "v2/store.proto", "v2/ui.proto", "v2/wire/action_payload.proto", diff --git a/chromium/components/feed/core/proto/v2/packing.proto b/chromium/components/feed/core/proto/v2/packing.proto new file mode 100644 index 00000000000..692b15ca41f --- /dev/null +++ b/chromium/components/feed/core/proto/v2/packing.proto @@ -0,0 +1,19 @@ +// 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. + +// These are pseudo-wire types. We need these types to marshal data from the +// xsurface API. These messages can and should depend on wire types, and the +// Chrome UI code need not parse these. + +syntax = "proto3"; + +package feedpacking; + +import "components/feed/core/proto/v2/wire/data_operation.proto"; + +option optimize_for = LITE_RUNTIME; + +message DismissData { + repeated feedwire.DataOperation data_operations = 1; +} diff --git a/chromium/components/feed/core/proto/v2/wire/capability.proto b/chromium/components/feed/core/proto/v2/wire/capability.proto index f9da90c6a64..31dd85c1632 100644 --- a/chromium/components/feed/core/proto/v2/wire/capability.proto +++ b/chromium/components/feed/core/proto/v2/wire/capability.proto @@ -36,5 +36,7 @@ enum Capability { // Enable the card menu. CARD_MENU = 19; REQUEST_SCHEDULE = 20; + OPEN_IN_TAB = 27; + DOWNLOAD_LINK = 28; reserved 2 to 4, 6 to 8, 12; } 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 f21907add5c..554fa87711d 100644 --- a/chromium/components/feed/core/proto/v2/wire/client_info.proto +++ b/chromium/components/feed/core/proto/v2/wire/client_info.proto @@ -21,10 +21,9 @@ message ClientInfo { IOS = 2; } - enum AppType { - TEST_APP = 2; // For use with AGA endpoint for testing. - CHROME = 3; - } + // TODO(harringtond): This should be CLANK = 3, but the server isn't yet + // updated to support it. + enum AppType { CLANK = 2; } // The type of OS that the client is running. optional PlatformType platform_type = 1; diff --git a/chromium/components/feed/core/shared_prefs/pref_names.cc b/chromium/components/feed/core/shared_prefs/pref_names.cc index da75d63d4c4..7f4f7a91413 100644 --- a/chromium/components/feed/core/shared_prefs/pref_names.cc +++ b/chromium/components/feed/core/shared_prefs/pref_names.cc @@ -13,6 +13,9 @@ namespace feed { namespace prefs { const char kEnableSnippets[] = "ntp_snippets.enable"; +// A boolean pref set to true if Feed articles are visible. +// FEED_ARTICLES_LIST_VISIBLE in ChromePreferenceKeys.java is a pre-native cache +// and should be consistent with this pref. const char kArticlesListVisible[] = "ntp_snippets.list_visible"; void RegisterFeedSharedProfilePrefs(PrefRegistrySimple* registry) { diff --git a/chromium/components/feed/core/v2/BUILD.gn b/chromium/components/feed/core/v2/BUILD.gn index 929a554bd8d..e27fb66cfa0 100644 --- a/chromium/components/feed/core/v2/BUILD.gn +++ b/chromium/components/feed/core/v2/BUILD.gn @@ -82,6 +82,7 @@ source_set("feed_core_v2") { public_deps = [ "//base", + "//base/util/values:values_util", "//components/feed/core/common:feed_core_common", "//components/feed/core/proto:proto_v2", ] diff --git a/chromium/components/feed/core/v2/config.cc b/chromium/components/feed/core/v2/config.cc index 7f5279286f5..9bf890f534a 100644 --- a/chromium/components/feed/core/v2/config.cc +++ b/chromium/components/feed/core/v2/config.cc @@ -52,6 +52,11 @@ void OverrideWithFinch(Config* config) { config->max_action_upload_bytes = base::GetFieldTrialParamByFeatureAsInt( kInterestFeedV2, "max_action_upload_bytes", config->max_action_upload_bytes); + + config->model_unload_timeout = + base::TimeDelta::FromSecondsD(base::GetFieldTrialParamByFeatureAsDouble( + kInterestFeedV2, "model_unload_timeout_seconds", + config->model_unload_timeout.InSecondsF())); } } // namespace diff --git a/chromium/components/feed/core/v2/config.h b/chromium/components/feed/core/v2/config.h index 82c60a268ff..f4944aaf298 100644 --- a/chromium/components/feed/core/v2/config.h +++ b/chromium/components/feed/core/v2/config.h @@ -28,6 +28,9 @@ struct Config { base::TimeDelta max_action_age = base::TimeDelta::FromHours(24); // Maximum payload size for one action upload batch. size_t max_action_upload_bytes = 20000; + // If no surfaces are attached, the stream model is unloaded after this + // timeout. + base::TimeDelta model_unload_timeout = base::TimeDelta::FromSeconds(1); }; // Gets the current configuration. diff --git a/chromium/components/feed/core/v2/enums.cc b/chromium/components/feed/core/v2/enums.cc index bf4a3472b87..a675de4293c 100644 --- a/chromium/components/feed/core/v2/enums.cc +++ b/chromium/components/feed/core/v2/enums.cc @@ -47,6 +47,12 @@ std::ostream& operator<<(std::ostream& out, LoadStreamStatus value) { return out << "kCannotParseNetworkResponseBody"; case LoadStreamStatus::kLoadMoreModelIsNotLoaded: return out << "kLoadMoreModelIsNotLoaded"; + case LoadStreamStatus::kLoadNotAllowedDisabledByEnterprisePolicy: + return out << "kLoadNotAllowedDisabledByEnterprisePolicy"; + case LoadStreamStatus::kNetworkFetchFailed: + return out << "kNetworkFetchFailed"; + case LoadStreamStatus::kCannotLoadMoreNoNextPageToken: + return out << "kCannotLoadMoreNoNextPageToken"; } #else return out << (static_cast<int>(value)); diff --git a/chromium/components/feed/core/v2/enums.h b/chromium/components/feed/core/v2/enums.h index df2e2861341..ae0c95c5cd2 100644 --- a/chromium/components/feed/core/v2/enums.h +++ b/chromium/components/feed/core/v2/enums.h @@ -26,7 +26,6 @@ enum class LoadStreamStatus { kNoStreamDataInStore = 4, kModelAlreadyLoaded = 5, kNoResponseBody = 6, - // TODO(harringtond): Let's add more specific proto translation errors. kProtoTranslationFailed = 7, kDataInStoreIsStale = 8, // The timestamp for stored data is in the future, so we're treating stored @@ -37,10 +36,12 @@ enum class LoadStreamStatus { kCannotLoadFromNetworkThrottled = 12, kLoadNotAllowedEulaNotAccepted = 13, kLoadNotAllowedArticlesListHidden = 14, - // TODO(harringtond): Emit this status value. kCannotParseNetworkResponseBody = 15, kLoadMoreModelIsNotLoaded = 16, - kMaxValue = kLoadMoreModelIsNotLoaded, + kLoadNotAllowedDisabledByEnterprisePolicy = 17, + kNetworkFetchFailed = 18, + kCannotLoadMoreNoNextPageToken = 19, + kMaxValue = kCannotLoadMoreNoNextPageToken, }; std::ostream& operator<<(std::ostream& out, LoadStreamStatus value); diff --git a/chromium/components/feed/core/v2/feed_network_impl.cc b/chromium/components/feed/core/v2/feed_network_impl.cc index df1e4874b27..124592c159f 100644 --- a/chromium/components/feed/core/v2/feed_network_impl.cc +++ b/chromium/components/feed/core/v2/feed_network_impl.cc @@ -41,20 +41,48 @@ namespace feed { namespace { -constexpr char kAuthenticationScope[] = - "https://www.googleapis.com/auth/googlenow"; constexpr char kApplicationOctetStream[] = "application/octet-stream"; constexpr base::TimeDelta kNetworkTimeout = base::TimeDelta::FromSeconds(30); -// Add URLs for Bling when it is supported. -constexpr char kFeedQueryUrl[] = - "https://www.google.com/httpservice/retry/TrellisClankService/FeedQuery"; -constexpr char kNextPageQueryUrl[] = - "https://www.google.com/httpservice/retry/TrellisClankService/" - "NextPageQuery"; -constexpr char kBackgroundQueryUrl[] = - "https://www.google.com/httpservice/noretry/TrellisClankService/" - "FeedQuery"; +signin::ScopeSet GetAuthScopes() { + return {"https://www.googleapis.com/auth/googlenow"}; +} + +GURL GetFeedQueryURL(feedwire::FeedQuery::RequestReason reason) { + // Add URLs for Bling when it is supported. + switch (reason) { + case feedwire::FeedQuery::SCHEDULED_REFRESH: + case feedwire::FeedQuery::IN_PLACE_UPDATE: + return GURL( + "https://www.google.com/httpservice/noretry/TrellisClankService/" + "FeedQuery"); + case feedwire::FeedQuery::NEXT_PAGE_SCROLL: + return GURL( + "https://www.google.com/httpservice/retry/TrellisClankService/" + "NextPageQuery"); + case feedwire::FeedQuery::MANUAL_REFRESH: + return GURL( + "https://www.google.com/httpservice/retry/TrellisClankService/" + "FeedQuery"); + default: + return GURL(); + } +} + +GURL GetUploadActionURL(version_info::Channel channel) { + switch (channel) { + case version_info::Channel::BETA: + return GURL( + "https://staging-discover-pa.sandbox.googleapis.com/v1/" + "actions:upload"); + case version_info::Channel::STABLE: + return GURL("https://discover-pa.googleapis.com/v1/actions:upload"); + default: + return GURL( + "https://autopush-discover-pa.sandbox.googleapis.com/v1/" + "actions:upload"); + } +} GURL GetUrlWithoutQuery(const GURL& url) { GURL::Replacements replacements; @@ -102,12 +130,25 @@ void ParseAndForwardResponse(base::OnceCallback<void(RESULT)> result_callback, void AddMothershipPayloadQueryParams(bool is_post, const std::string& payload, const std::string& language_tag, - GURL* url) { + GURL& url) { if (!is_post) - *url = net::AppendQueryParameter(*url, "reqpld", payload); - *url = net::AppendQueryParameter(*url, "fmt", "bin"); + url = net::AppendQueryParameter(url, "reqpld", payload); + url = net::AppendQueryParameter(url, "fmt", "bin"); if (!language_tag.empty()) - *url = net::AppendQueryParameter(*url, "hl", language_tag); + url = net::AppendQueryParameter(url, "hl", language_tag); +} + +// Compresses and attaches |request_body| for upload if it's not empty. +// Returns the compressed size of the request. +int PopulateRequestBody(const std::string& request_body, + network::SimpleURLLoader* loader) { + if (request_body.empty()) + return 0; + std::string compressed_request_body; + compression::GzipCompress(request_body, &compressed_request_body); + loader->AttachStringForUpload(compressed_request_body, + kApplicationOctetStream); + return compressed_request_body.size(); } } // namespace @@ -167,11 +208,10 @@ class FeedNetworkImpl::NetworkFetch { private: void StartAccessTokenFetch() { - signin::ScopeSet scopes{kAuthenticationScope}; // It's safe to pass base::Unretained(this) since deleting the token fetcher // will prevent the callback from being completed. token_fetcher_ = std::make_unique<signin::PrimaryAccountAccessTokenFetcher>( - "feed", identity_manager_, scopes, + "feed", identity_manager_, GetAuthScopes(), base::BindOnce(&NetworkFetch::AccessTokenFetchFinished, base::Unretained(this), tick_clock_->NowTicks()), signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable); @@ -250,53 +290,41 @@ class FeedNetworkImpl::NetworkFetch { resource_request->site_for_cookies = net::SiteForCookies::FromUrl(url); } - SetRequestHeaders(!request_body_.empty(), resource_request.get()); + SetRequestHeaders(!request_body_.empty(), *resource_request); + DVLOG(1) << "Feed Request url=" << url; + DVLOG(1) << "Feed Request headers=" << resource_request->headers.ToString(); auto simple_loader = network::SimpleURLLoader::Create( std::move(resource_request), traffic_annotation); simple_loader->SetAllowHttpErrorResults(true); simple_loader->SetTimeoutDuration(kNetworkTimeout); - PopulateRequestBody(simple_loader.get()); + + const int compressed_size = + PopulateRequestBody(request_body_, simple_loader.get()); + UMA_HISTOGRAM_COUNTS_1M( + "ContentSuggestions.Feed.Network.RequestSizeKB.Compressed", + compressed_size / 1024); return simple_loader; } void SetRequestHeaders(bool has_request_body, - network::ResourceRequest* request) const { + network::ResourceRequest& request) const { if (has_request_body) { - request->headers.SetHeader(net::HttpRequestHeaders::kContentType, - kApplicationOctetStream); - request->headers.SetHeader("Content-Encoding", "gzip"); + request.headers.SetHeader(net::HttpRequestHeaders::kContentType, + kApplicationOctetStream); + request.headers.SetHeader("Content-Encoding", "gzip"); } variations::SignedIn signed_in_status = variations::SignedIn::kNo; if (!access_token_.empty()) { - request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization, - "Bearer " + access_token_); + request.headers.SetHeader(net::HttpRequestHeaders::kAuthorization, + "Bearer " + access_token_); signed_in_status = variations::SignedIn::kYes; } // Add X-Client-Data header with experiment IDs from field trials. variations::AppendVariationsHeader(url_, variations::InIncognito::kNo, - signed_in_status, request); - } - - void PopulateRequestBody(network::SimpleURLLoader* loader) { - std::string compressed_request_body; - if (!request_body_.empty()) { - std::string uncompressed_request_body( - reinterpret_cast<const char*>(request_body_.data()), - request_body_.size()); - - compression::GzipCompress(uncompressed_request_body, - &compressed_request_body); - - loader->AttachStringForUpload(compressed_request_body, - kApplicationOctetStream); - } - - UMA_HISTOGRAM_COUNTS_1M( - "ContentSuggestions.Feed.Network.RequestSizeKB.Compressed", - static_cast<int>(compressed_request_body.size() / 1024)); + signed_in_status, &request); } void OnSimpleLoaderComplete(std::unique_ptr<std::string> response) { @@ -329,14 +357,15 @@ class FeedNetworkImpl::NetworkFetch { if (response) { response_info.status_code = simple_loader_->ResponseInfo()->headers->response_code(); + response_info.response_body_bytes = response->size(); + response_body = std::move(*response); if (response_info.status_code == net::HTTP_UNAUTHORIZED) { - signin::ScopeSet scopes{kAuthenticationScope}; CoreAccountId account_id = identity_manager_->GetPrimaryAccountId(); if (!account_id.empty()) { - identity_manager_->RemoveAccessTokenFromCache(account_id, scopes, - access_token_); + identity_manager_->RemoveAccessTokenFromCache( + account_id, GetAuthScopes(), access_token_); } } } @@ -392,10 +421,12 @@ FeedNetworkImpl::FeedNetworkImpl( const std::string& api_key, scoped_refptr<network::SharedURLLoaderFactory> loader_factory, const base::TickClock* tick_clock, - PrefService* pref_service) + PrefService* pref_service, + version_info::Channel chrome_channel) : delegate_(delegate), identity_manager_(identity_manager), api_key_(api_key), + chrome_channel_(chrome_channel), loader_factory_(loader_factory), tick_clock_(tick_clock), pref_service_(pref_service) {} @@ -413,26 +444,13 @@ void FeedNetworkImpl::SendQueryRequest( // TODO(harringtond): Decide how we want to override these URLs for testing. // Should probably add a command-line flag. - GURL url; - switch (request.feed_request().feed_query().reason()) { - case feedwire::FeedQuery::SCHEDULED_REFRESH: - case feedwire::FeedQuery::IN_PLACE_UPDATE: - url = GURL(kBackgroundQueryUrl); - break; - case feedwire::FeedQuery::NEXT_PAGE_SCROLL: - url = GURL(kNextPageQueryUrl); - break; - case feedwire::FeedQuery::MANUAL_REFRESH: - url = GURL(kFeedQueryUrl); - break; - default: - std::move(callback).Run({}); - return; - } + GURL url = GetFeedQueryURL(request.feed_request().feed_query().reason()); + if (url.is_empty()) + return std::move(callback).Run({}); AddMothershipPayloadQueryParams(/*is_post=*/false, base64proto, - delegate_->GetLanguageTag(), &url); - Send(url, "GET", /*request_body=*/std::string(), + delegate_->GetLanguageTag(), url); + Send(url, "GET", /*request_body=*/{}, base::BindOnce(&ParseAndForwardResponse<QueryRequestResult, NetworkRequestType::kFeedQuery>, std::move(callback))); @@ -444,11 +462,10 @@ void FeedNetworkImpl::SendActionRequest( std::string binary_proto; request.SerializeToString(&binary_proto); - GURL url( - "https://www.google.com/httpservice/retry/ClankActionUploadService/" - "ClankActionUpload"); - AddMothershipPayloadQueryParams(/*is_post=*/true, /*payload=*/std::string(), - delegate_->GetLanguageTag(), &url); + GURL url = GetUploadActionURL(chrome_channel_); + AddMothershipPayloadQueryParams(/*is_post=*/true, + /*payload=*/{}, delegate_->GetLanguageTag(), + url); Send(url, "POST", std::move(binary_proto), base::BindOnce( &ParseAndForwardResponse<ActionRequestResult, diff --git a/chromium/components/feed/core/v2/feed_network_impl.h b/chromium/components/feed/core/v2/feed_network_impl.h index ba6e4dcb96e..2dc8e438d41 100644 --- a/chromium/components/feed/core/v2/feed_network_impl.h +++ b/chromium/components/feed/core/v2/feed_network_impl.h @@ -11,6 +11,7 @@ #include "base/containers/unique_ptr_adapters.h" #include "base/memory/scoped_refptr.h" #include "components/feed/core/v2/feed_network.h" +#include "components/version_info/channel.h" #include "url/gurl.h" class PrefService; @@ -43,7 +44,8 @@ class FeedNetworkImpl : public FeedNetwork { const std::string& api_key, scoped_refptr<network::SharedURLLoaderFactory> loader_factory, const base::TickClock* tick_clock, - PrefService* pref_service); + PrefService* pref_service, + version_info::Channel chrome_channel); ~FeedNetworkImpl() override; FeedNetworkImpl(const FeedNetworkImpl&) = delete; FeedNetworkImpl& operator=(FeedNetworkImpl&) = delete; @@ -79,6 +81,7 @@ class FeedNetworkImpl : public FeedNetwork { Delegate* delegate_; signin::IdentityManager* identity_manager_; const std::string api_key_; + const version_info::Channel chrome_channel_; scoped_refptr<network::SharedURLLoaderFactory> loader_factory_; const base::TickClock* tick_clock_; PrefService* pref_service_; 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 c931576ae71..990f791debf 100644 --- a/chromium/components/feed/core/v2/feed_network_impl_unittest.cc +++ b/chromium/components/feed/core/v2/feed_network_impl_unittest.cc @@ -94,7 +94,7 @@ class FeedNetworkTest : public testing::Test { feed_network_ = std::make_unique<FeedNetworkImpl>( &delegate_, identity_test_env_.identity_manager(), "dummy_api_key", shared_url_loader_factory_, task_environment_.GetMockTickClock(), - &profile_prefs_); + &profile_prefs_, version_info::Channel::STABLE); } FeedNetwork* feed_network() { return feed_network_.get(); } @@ -410,8 +410,8 @@ TEST_F(FeedNetworkTest, SendActionRequestSendsValidRequest) { RespondToActionRequest(GetTestActionResponse(), net::HTTP_OK); EXPECT_EQ( - GURL("https://www.google.com/httpservice/retry/ClankActionUploadService/" - "ClankActionUpload?fmt=bin&hl=en"), + GURL( + "https://discover-pa.googleapis.com/v1/actions:upload?fmt=bin&hl=en"), resource_request.url); EXPECT_EQ("POST", resource_request.method); diff --git a/chromium/components/feed/core/v2/feed_store.h b/chromium/components/feed/core/v2/feed_store.h index e0363db2cf5..c74ab1b4aaf 100644 --- a/chromium/components/feed/core/v2/feed_store.h +++ b/chromium/components/feed/core/v2/feed_store.h @@ -95,10 +95,6 @@ class FeedStore { void WriteMetadata(feedstore::Metadata metadata, base::OnceCallback<void(bool)> callback); - // TODO(iwells): implement this - // Deletes old records that are no longer needed - // void RemoveOldData(base::OnceCallback<void(bool)> callback); - bool IsInitializedForTesting() const; leveldb_proto::ProtoDatabase<feedstore::Record>* GetDatabaseForTesting() { @@ -151,11 +147,6 @@ class FeedStore { bool read_ok, std::unique_ptr<feedstore::Record> record); - // TODO(iwells): implement - // bool OldRecordFilter(const std::string& key); - // void OnRemoveOldDataFinished(base::OnceCallback<void(bool)> callback, - // bool success); - base::OnceClosure initialize_callback_; leveldb_proto::Enums::InitStatus database_status_; std::unique_ptr<leveldb_proto::ProtoDatabase<feedstore::Record>> database_; diff --git a/chromium/components/feed/core/v2/feed_stream.cc b/chromium/components/feed/core/v2/feed_stream.cc index 4544d65915c..d6f6ee104a7 100644 --- a/chromium/components/feed/core/v2/feed_stream.cc +++ b/chromium/components/feed/core/v2/feed_stream.cc @@ -10,12 +10,15 @@ #include "base/bind.h" #include "base/metrics/histogram_macros.h" +#include "base/threading/thread_task_runner_handle.h" #include "base/time/clock.h" #include "base/time/tick_clock.h" #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/there_and_back_again_data.pb.h" #include "components/feed/core/shared_prefs/pref_names.h" +#include "components/feed/core/v2/config.h" #include "components/feed/core/v2/enums.h" #include "components/feed/core/v2/feed_network.h" #include "components/feed/core/v2/feed_store.h" @@ -37,7 +40,7 @@ namespace feed { namespace { void PopulateDebugStreamData(const LoadStreamTask::Result& load_result, - PrefService* profile_prefs) { + PrefService& profile_prefs) { DebugStreamData debug_data = ::feed::prefs::GetDebugStreamData(profile_prefs); std::stringstream ss; ss << "Code: " << load_result.final_status; @@ -139,7 +142,7 @@ void FeedStream::TriggerStreamLoad() { } void FeedStream::InitialStreamLoadComplete(LoadStreamTask::Result result) { - PopulateDebugStreamData(result, profile_prefs_); + PopulateDebugStreamData(result, *profile_prefs_); metrics_reporter_->OnLoadStream(result.load_from_store_status, result.final_status); @@ -156,11 +159,36 @@ void FeedStream::AttachSurface(SurfaceInterface* surface) { metrics_reporter_->SurfaceOpened(surface->GetSurfaceId()); TriggerStreamLoad(); surface_updater_->SurfaceAdded(surface); + // Cancel any scheduled model unload task. + ++unload_on_detach_sequence_number_; } void FeedStream::DetachSurface(SurfaceInterface* surface) { metrics_reporter_->SurfaceClosed(surface->GetSurfaceId()); surface_updater_->SurfaceRemoved(surface); + if (!surface_updater_->HasSurfaceAttached()) { + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::BindOnce(&FeedStream::AddUnloadModelIfNoSurfacesAttachedTask, + GetWeakPtr(), unload_on_detach_sequence_number_), + GetFeedConfig().model_unload_timeout); + } +} + +void FeedStream::AddUnloadModelIfNoSurfacesAttachedTask(int sequence_number) { + // Don't continue if unload_on_detach_sequence_number_ has changed. + if (unload_on_detach_sequence_number_ != sequence_number) + return; + + task_queue_.AddTask(std::make_unique<offline_pages::ClosureTask>( + base::BindOnce(&FeedStream::UnloadModelIfNoSurfacesAttachedTask, + base::Unretained(this)))); +} + +void FeedStream::UnloadModelIfNoSurfacesAttachedTask() { + if (surface_updater_->HasSurfaceAttached()) + return; + UnloadModel(); } void FeedStream::SetArticlesListVisible(bool is_visible) { @@ -171,6 +199,10 @@ bool FeedStream::IsArticlesListVisible() { return profile_prefs_->GetBoolean(prefs::kArticlesListVisible); } +bool FeedStream::IsFeedEnabledByEnterprisePolicy() { + return profile_prefs_->GetBoolean(prefs::kEnableSnippets); +} + void FeedStream::LoadMore(SurfaceId surface_id, base::OnceCallback<void(bool)> callback) { metrics_reporter_->OnLoadMoreBegin(surface_id); @@ -178,7 +210,16 @@ void FeedStream::LoadMore(SurfaceId surface_id, DLOG(ERROR) << "Ignoring LoadMore() before the model is loaded"; return std::move(callback).Run(false); } + // We want to abort early to avoid showing a loading spinner if it's not + // necessary. + if (ShouldMakeFeedQueryRequest(/*is_load_more=*/true, + /*consume_quota=*/false) != + LoadStreamStatus::kNoStatus) { + return std::move(callback).Run(false); + } + surface_updater_->SetLoadingMore(true); + // Have at most one in-flight LoadMore() request. Send the result to all // requestors. load_more_complete_callbacks_.push_back(std::move(callback)); @@ -191,8 +232,6 @@ void FeedStream::LoadMore(SurfaceId surface_id, void FeedStream::LoadMoreComplete(LoadMoreTask::Result result) { metrics_reporter_->OnLoadMore(result.final_status); - // TODO(harringtond): In the case of failure, do we need to load an error - // message slice? surface_updater_->SetLoadingMore(false); std::vector<base::OnceCallback<void(bool)>> moved_callbacks = std::move(load_more_complete_callbacks_); @@ -220,6 +259,13 @@ EphemeralChangeId FeedStream::CreateEphemeralChange( return model_->CreateEphemeralChange(std::move(operations)); } +EphemeralChangeId FeedStream::CreateEphemeralChangeFromPackedData( + base::StringPiece data) { + feedpacking::DismissData msg; + msg.ParseFromArray(data.data(), data.size()); + return CreateEphemeralChange(TranslateDismissData(clock_->Now(), msg)); +} + bool FeedStream::CommitEphemeralChange(EphemeralChangeId id) { if (!model_) return false; @@ -232,8 +278,18 @@ bool FeedStream::RejectEphemeralChange(EphemeralChangeId id) { return model_->RejectEphemeralChange(id); } +void FeedStream::ProcessThereAndBackAgain(base::StringPiece data) { + feedwire::ThereAndBackAgainData msg; + msg.ParseFromArray(data.data(), data.size()); + if (msg.has_action_payload()) { + feedwire::FeedAction action_msg; + *action_msg.mutable_action_payload() = std::move(msg.action_payload()); + UploadAction(std::move(action_msg), /*upload_now=*/true, base::DoNothing()); + } +} + DebugStreamData FeedStream::GetDebugStreamData() { - return ::feed::prefs::GetDebugStreamData(profile_prefs_); + return ::feed::prefs::GetDebugStreamData(*profile_prefs_); } void FeedStream::ForceRefreshForDebugging() { @@ -315,13 +371,17 @@ LoadStreamStatus FeedStream::ShouldAttemptLoad(bool model_loading) { if (!IsArticlesListVisible()) return LoadStreamStatus::kLoadNotAllowedArticlesListHidden; + if (!IsFeedEnabledByEnterprisePolicy()) + return LoadStreamStatus::kLoadNotAllowedDisabledByEnterprisePolicy; + if (!delegate_->IsEulaAccepted()) return LoadStreamStatus::kLoadNotAllowedEulaNotAccepted; return LoadStreamStatus::kNoStatus; } -LoadStreamStatus FeedStream::ShouldMakeFeedQueryRequest(bool is_load_more) { +LoadStreamStatus FeedStream::ShouldMakeFeedQueryRequest(bool is_load_more, + bool consume_quota) { if (!is_load_more) { // Time has passed since calling |ShouldAttemptLoad()|, call it again to // confirm we should still attempt loading. @@ -330,6 +390,11 @@ LoadStreamStatus FeedStream::ShouldMakeFeedQueryRequest(bool is_load_more) { if (should_not_attempt_reason != LoadStreamStatus::kNoStatus) { return should_not_attempt_reason; } + } else { + // LoadMore requires a next page token. + if (!model_ || model_->GetNextPageToken().empty()) { + return LoadStreamStatus::kCannotLoadMoreNoNextPageToken; + } } // TODO(harringtond): |suppress_refreshes_until_| was historically used @@ -346,7 +411,8 @@ LoadStreamStatus FeedStream::ShouldMakeFeedQueryRequest(bool is_load_more) { return LoadStreamStatus::kCannotLoadFromNetworkOffline; } - if (!request_throttler_.RequestQuota(NetworkRequestType::kFeedQuery)) { + if (consume_quota && + !request_throttler_.RequestQuota(NetworkRequestType::kFeedQuery)) { return LoadStreamStatus::kCannotLoadFromNetworkThrottled; } @@ -390,7 +456,7 @@ void FeedStream::OnSignedOut() { void FeedStream::ExecuteRefreshTask() { // Schedule the next refresh attempt. If a new refresh schedule is returned // through this refresh, it will be overwritten. - SetRequestSchedule(feed::prefs::GetRequestSchedule(profile_prefs_)); + SetRequestSchedule(feed::prefs::GetRequestSchedule(*profile_prefs_)); LoadStreamStatus do_not_attempt_reason = ShouldAttemptLoad(); if (do_not_attempt_reason != LoadStreamStatus::kNoStatus) { @@ -438,7 +504,7 @@ void FeedStream::SetRequestSchedule(RequestSchedule schedule) { } else { refresh_task_scheduler_->Cancel(); } - feed::prefs::SetRequestSchedule(schedule, profile_prefs_); + feed::prefs::SetRequestSchedule(schedule, *profile_prefs_); } void FeedStream::UnloadModel() { @@ -500,5 +566,8 @@ void FeedStream::ReportContextMenuOpened() { void FeedStream::ReportStreamScrolled(int distance_dp) { metrics_reporter_->StreamScrolled(distance_dp); } +void FeedStream::ReportStreamScrollStart() { + metrics_reporter_->StreamScrollStart(); +} } // namespace feed diff --git a/chromium/components/feed/core/v2/feed_stream.h b/chromium/components/feed/core/v2/feed_stream.h index d92d889563e..7ddc42d6aa2 100644 --- a/chromium/components/feed/core/v2/feed_stream.h +++ b/chromium/components/feed/core/v2/feed_stream.h @@ -119,8 +119,11 @@ class FeedStream : public FeedStreamApi, std::vector<feedstore::DataOperation> operations) override; EphemeralChangeId CreateEphemeralChange( std::vector<feedstore::DataOperation> operations) override; + EphemeralChangeId CreateEphemeralChangeFromPackedData( + base::StringPiece data) override; bool CommitEphemeralChange(EphemeralChangeId id) override; bool RejectEphemeralChange(EphemeralChangeId id) override; + void ProcessThereAndBackAgain(base::StringPiece data) override; DebugStreamData GetDebugStreamData() override; void ForceRefreshForDebugging() override; std::string DumpStateForDebugging() override; @@ -140,6 +143,7 @@ class FeedStream : public FeedStreamApi, void ReportManageInterestsAction() override; void ReportContextMenuOpened() override; void ReportStreamScrolled(int distance_dp) override; + void ReportStreamScrollStart() override; // offline_pages::TaskQueue::Delegate. void OnTaskQueueIsIdle() override; @@ -194,8 +198,11 @@ class FeedStream : public FeedStreamApi, // Determines if a FeedQuery request can be made. If successful, // returns |LoadStreamStatus::kNoStatus| and acquires throttler quota. - // Otherwise returns the reason. - LoadStreamStatus ShouldMakeFeedQueryRequest(bool is_load_more = false); + // Otherwise returns the reason. If |consume_quota| is false, no quota is + // consumed. This can be used to predict the likely result on a subsequent + // call. + LoadStreamStatus ShouldMakeFeedQueryRequest(bool is_load_more = false, + bool consume_quota = true); // Unloads the model. Surfaces are not updated, and will remain frozen until a // model load is requested. @@ -229,16 +236,26 @@ class FeedStream : public FeedStreamApi, private: class ModelStoreChangeMonitor; + + base::WeakPtr<FeedStream> GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); + } + // A single function task to delete stored feed data and force a refresh. // To only be called from within a |Task|. void ForceRefreshForDebuggingTask(); + void AddUnloadModelIfNoSurfacesAttachedTask(int sequence_number); + void UnloadModelIfNoSurfacesAttachedTask(); + void InitialStreamLoadComplete(LoadStreamTask::Result result); void LoadMoreComplete(LoadMoreTask::Result result); void BackgroundRefreshComplete(LoadStreamTask::Result result); void ClearAll(); + bool IsFeedEnabledByEnterprisePolicy(); + // Unowned. RefreshTaskScheduler* refresh_task_scheduler_; @@ -268,9 +285,12 @@ class FeedStream : public FeedStreamApi, base::TimeTicks suppress_refreshes_until_; std::vector<base::OnceCallback<void(bool)>> load_more_complete_callbacks_; Metadata metadata_; + int unload_on_detach_sequence_number_ = 0; // To allow tests to wait on task queue idle. base::RepeatingClosure idle_callback_; + + base::WeakPtrFactory<FeedStream> weak_ptr_factory_{this}; }; } // namespace feed diff --git a/chromium/components/feed/core/v2/feed_stream_unittest.cc b/chromium/components/feed/core/v2/feed_stream_unittest.cc index d0325fbd69e..46a25b3191c 100644 --- a/chromium/components/feed/core/v2/feed_stream_unittest.cc +++ b/chromium/components/feed/core/v2/feed_stream_unittest.cc @@ -28,6 +28,7 @@ #include "components/feed/core/proto/v2/ui.pb.h" #include "components/feed/core/proto/v2/wire/action_request.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/shared_prefs/pref_names.h" #include "components/feed/core/v2/config.h" #include "components/feed/core/v2/feed_network.h" @@ -239,6 +240,8 @@ class TestFeedNetwork : public FeedNetwork { // time we want to inject a translated response for ease of test-writing. query_request_sent = request; QueryRequestResult result; + result.response_info.status_code = 200; + result.response_info.response_body_bytes = 100; result.response_info.fetch_duration = base::TimeDelta::FromMilliseconds(42); if (injected_response_) { result.response_body = std::make_unique<feedwire::Response>( @@ -359,8 +362,8 @@ class FakeRefreshTaskScheduler : public RefreshTaskScheduler { class TestMetricsReporter : public MetricsReporter { public: - explicit TestMetricsReporter(const base::TickClock* clock) - : MetricsReporter(clock) {} + explicit TestMetricsReporter(const base::TickClock* clock, PrefService* prefs) + : MetricsReporter(clock, prefs) {} // MetricsReporter. void ContentSliceViewed(SurfaceId surface_id, int index_in_stream) override { @@ -400,10 +403,15 @@ class TestMetricsReporter : public MetricsReporter { class FeedStreamTest : public testing::Test, public FeedStream::Delegate { public: void SetUp() override { + // Reset to default config, since tests can change it. + SetFeedConfigForTesting(Config()); + feed::prefs::RegisterFeedSharedProfilePrefs(profile_prefs_.registry()); feed::RegisterProfilePrefs(profile_prefs_.registry()); - CHECK_EQ(kTestTimeEpoch, task_environment_.GetMockClock()->Now()); + metrics_reporter_ = std::make_unique<TestMetricsReporter>( + task_environment_.GetMockTickClock(), &profile_prefs_); + CHECK_EQ(kTestTimeEpoch, task_environment_.GetMockClock()->Now()); CreateStream(); } @@ -436,7 +444,7 @@ class FeedStreamTest : public testing::Test, public FeedStream::Delegate { chrome_info.channel = version_info::Channel::STABLE; chrome_info.version = base::Version({99, 1, 9911, 2}); stream_ = std::make_unique<FeedStream>( - &refresh_scheduler_, &metrics_reporter_, this, &profile_prefs_, + &refresh_scheduler_, metrics_reporter_.get(), this, &profile_prefs_, &network_, store_.get(), task_environment_.GetMockClock(), task_environment_.GetMockTickClock(), chrome_info); @@ -495,8 +503,8 @@ class FeedStreamTest : public testing::Test, public FeedStream::Delegate { protected: base::test::TaskEnvironment task_environment_{ base::test::TaskEnvironment::TimeSource::MOCK_TIME}; - TestMetricsReporter metrics_reporter_{task_environment_.GetMockTickClock()}; TestingPrefServiceSimple profile_prefs_; + std::unique_ptr<TestMetricsReporter> metrics_reporter_; TestFeedNetwork network_; TestWireResponseTranslator response_translator_; @@ -531,7 +539,7 @@ TEST_F(FeedStreamTest, DoNotRefreshIfArticlesListIsHidden) { stream_->ExecuteRefreshTask(); EXPECT_TRUE(refresh_scheduler_.refresh_task_complete); EXPECT_EQ(LoadStreamStatus::kLoadNotAllowedArticlesListHidden, - metrics_reporter_.background_refresh_status); + metrics_reporter_->background_refresh_status); } TEST_F(FeedStreamTest, BackgroundRefreshSuccess) { @@ -544,7 +552,7 @@ TEST_F(FeedStreamTest, BackgroundRefreshSuccess) { // network. ASSERT_TRUE(refresh_scheduler_.refresh_task_complete); EXPECT_EQ(LoadStreamStatus::kLoadedFromNetwork, - metrics_reporter_.background_refresh_status); + metrics_reporter_->background_refresh_status); EXPECT_TRUE(response_translator_.InjectedResponseConsumed()); EXPECT_FALSE(stream_->GetModel()); TestSurface surface(stream_.get()); @@ -558,7 +566,7 @@ TEST_F(FeedStreamTest, BackgroundRefreshNotAttemptedWhenModelIsLoading) { stream_->ExecuteRefreshTask(); WaitForIdleTaskQueue(); - EXPECT_EQ(metrics_reporter_.background_refresh_status, + EXPECT_EQ(metrics_reporter_->background_refresh_status, LoadStreamStatus::kModelAlreadyLoaded); } @@ -570,7 +578,7 @@ TEST_F(FeedStreamTest, BackgroundRefreshNotAttemptedAfterModelIsLoaded) { stream_->ExecuteRefreshTask(); WaitForIdleTaskQueue(); - EXPECT_EQ(metrics_reporter_.background_refresh_status, + EXPECT_EQ(metrics_reporter_->background_refresh_status, LoadStreamStatus::kModelAlreadyLoaded); } @@ -837,7 +845,7 @@ TEST_F(FeedStreamTest, LoadFromNetworkFailsDueToProtoTranslation) { WaitForIdleTaskQueue(); EXPECT_EQ(LoadStreamStatus::kProtoTranslationFailed, - metrics_reporter_.load_stream_status); + metrics_reporter_->load_stream_status); } TEST_F(FeedStreamTest, DoNotLoadFromNetworkWhenOffline) { @@ -847,7 +855,7 @@ TEST_F(FeedStreamTest, DoNotLoadFromNetworkWhenOffline) { WaitForIdleTaskQueue(); EXPECT_EQ(LoadStreamStatus::kCannotLoadFromNetworkOffline, - metrics_reporter_.load_stream_status); + metrics_reporter_->load_stream_status); EXPECT_EQ("loading -> cant-refresh", surface.DescribeUpdates()); } @@ -858,7 +866,7 @@ TEST_F(FeedStreamTest, DoNotLoadStreamWhenArticleListIsHidden) { WaitForIdleTaskQueue(); EXPECT_EQ(LoadStreamStatus::kLoadNotAllowedArticlesListHidden, - metrics_reporter_.load_stream_status); + metrics_reporter_->load_stream_status); EXPECT_EQ("no-cards", surface.DescribeUpdates()); } @@ -869,7 +877,7 @@ TEST_F(FeedStreamTest, DoNotLoadStreamWhenEulaIsNotAccepted) { WaitForIdleTaskQueue(); EXPECT_EQ(LoadStreamStatus::kLoadNotAllowedEulaNotAccepted, - metrics_reporter_.load_stream_status); + metrics_reporter_->load_stream_status); EXPECT_EQ("no-cards", surface.DescribeUpdates()); } @@ -900,7 +908,7 @@ TEST_F(FeedStreamTest, DoNotLoadFromNetworkAfterHistoryIsDeleted) { EXPECT_EQ("loading -> no-cards", surface.DescribeUpdates()); EXPECT_EQ(LoadStreamStatus::kCannotLoadFromNetworkSupressedForHistoryDelete, - metrics_reporter_.load_stream_status); + metrics_reporter_->load_stream_status); surface.Detach(); task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(2)); @@ -1028,7 +1036,7 @@ TEST_F(FeedStreamTest, ReportSliceViewedIdentifiesCorrectIndex) { stream_->ReportSliceViewed( surface.GetSurfaceId(), surface.initial_state->updated_slices(1).slice().slice_id()); - EXPECT_EQ(1, metrics_reporter_.slice_viewed_index); + EXPECT_EQ(1, metrics_reporter_->slice_viewed_index); } TEST_F(FeedStreamTest, LoadMoreAppendsContent) { @@ -1145,6 +1153,26 @@ TEST_F(FeedStreamTest, LoadMoreSendsTokens) { .next_page_token()); } +TEST_F(FeedStreamTest, LoadMoreAbortsIfNoNextPageToken) { + { + std::unique_ptr<StreamModelUpdateRequest> initial_state = + MakeTypicalInitialModelState(); + initial_state->stream_data.clear_next_page_token(); + response_translator_.InjectResponse(std::move(initial_state)); + } + TestSurface surface(stream_.get()); + WaitForIdleTaskQueue(); + + CallbackReceiver<bool> callback; + stream_->LoadMore(surface.GetSurfaceId(), callback.Bind()); + WaitForIdleTaskQueue(); + + // LoadMore fails, and does not make an additional request. + EXPECT_EQ(base::Optional<bool>(false), callback.GetResult()); + ASSERT_EQ(1, network_.send_query_call_count); + EXPECT_EQ("loading -> 2 slices", surface.DescribeUpdates()); +} + TEST_F(FeedStreamTest, LoadMoreFail) { response_translator_.InjectResponse(MakeTypicalInitialModelState()); TestSurface surface(stream_.get()); @@ -1252,15 +1280,17 @@ TEST_F(FeedStreamTest, StorePendingAction) { TEST_F(FeedStreamTest, StorePendingActionAndUploadNow) { network_.consistency_token = "token-11"; - CallbackReceiver<UploadActionsTask::Result> cr; - stream_->UploadAction(MakeFeedAction(42ul), true, cr.Bind()); + // Call |ProcessThereAndBackAgain()|, which triggers Upload() with + // upload_now=true. + { + feedwire::ThereAndBackAgainData msg; + *msg.mutable_action_payload() = MakeFeedAction(42ul).action_payload(); + stream_->ProcessThereAndBackAgain(msg.SerializeAsString()); + } WaitForIdleTaskQueue(); - ASSERT_TRUE(cr.GetResult()); - EXPECT_EQ(1ul, cr.GetResult()->upload_attempt_count); - EXPECT_EQ(UploadActionsStatus::kUpdatedConsistencyToken, - cr.GetResult()->status); - + // Verify the action was uploaded. + EXPECT_EQ(1, network_.action_request_call_count); std::vector<feedstore::StoredAction> result = ReadStoredActions(stream_->GetStore()); ASSERT_EQ(0ul, result.size()); @@ -1425,5 +1455,75 @@ TEST_F(FeedStreamTest, MetadataLoadedWhenDatabaseInitialized) { EXPECT_EQ(1, stream_->GetMetadata()->GetNextActionId().GetUnsafeValue()); } +TEST_F(FeedStreamTest, ModelUnloadsAfterTimeout) { + Config config; + config.model_unload_timeout = base::TimeDelta::FromSeconds(1); + SetFeedConfigForTesting(config); + + response_translator_.InjectResponse(MakeTypicalInitialModelState()); + TestSurface surface(stream_.get()); + WaitForIdleTaskQueue(); + + surface.Detach(); + + task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(999)); + WaitForIdleTaskQueue(); + EXPECT_TRUE(stream_->GetModel()); + + task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(2)); + WaitForIdleTaskQueue(); + EXPECT_FALSE(stream_->GetModel()); +} + +TEST_F(FeedStreamTest, ModelDoesNotUnloadIfSurfaceIsAttached) { + Config config; + config.model_unload_timeout = base::TimeDelta::FromSeconds(1); + SetFeedConfigForTesting(config); + + response_translator_.InjectResponse(MakeTypicalInitialModelState()); + TestSurface surface(stream_.get()); + WaitForIdleTaskQueue(); + + surface.Detach(); + + task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(999)); + WaitForIdleTaskQueue(); + EXPECT_TRUE(stream_->GetModel()); + + surface.Attach(stream_.get()); + + task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(2)); + WaitForIdleTaskQueue(); + EXPECT_TRUE(stream_->GetModel()); +} + +TEST_F(FeedStreamTest, ModelUnloadsAfterSecondTimeout) { + Config config; + config.model_unload_timeout = base::TimeDelta::FromSeconds(1); + SetFeedConfigForTesting(config); + + response_translator_.InjectResponse(MakeTypicalInitialModelState()); + TestSurface surface(stream_.get()); + WaitForIdleTaskQueue(); + + surface.Detach(); + + task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(999)); + WaitForIdleTaskQueue(); + EXPECT_TRUE(stream_->GetModel()); + + // Attaching another surface will prolong the unload time for another second. + surface.Attach(stream_.get()); + surface.Detach(); + + task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(999)); + WaitForIdleTaskQueue(); + EXPECT_TRUE(stream_->GetModel()); + + task_environment_.FastForwardBy(base::TimeDelta::FromMilliseconds(2)); + WaitForIdleTaskQueue(); + EXPECT_FALSE(stream_->GetModel()); +} + } // namespace } // namespace feed diff --git a/chromium/components/feed/core/v2/metrics_reporter.cc b/chromium/components/feed/core/v2/metrics_reporter.cc index 975db91a92c..8c367a66a18 100644 --- a/chromium/components/feed/core/v2/metrics_reporter.cc +++ b/chromium/components/feed/core/v2/metrics_reporter.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "components/feed/core/v2/metrics_reporter.h" +#include <algorithm> #include <cmath> #include "base/metrics/histogram_functions.h" @@ -10,6 +11,7 @@ #include "base/threading/thread_task_runner_handle.h" #include "base/time/tick_clock.h" #include "base/time/time.h" +#include "components/feed/core/v2/prefs.h" namespace feed { namespace { @@ -23,6 +25,12 @@ constexpr base::TimeDelta kLoadTimeout = base::TimeDelta::FromSeconds(15); // Maximum time to wait before declaring opening a card a failure. // For ContentSuggestions.Feed.UserJourney.OpenCard. constexpr base::TimeDelta kOpenTimeout = base::TimeDelta::FromSeconds(20); +// For ContentSuggestions.Feed.TimeSpentInFeed, we want to get a measure +// of how much time the user is spending with the Feed. If the user stops +// interacting with the Feed, we stop counting it as time spent after this +// timeout. +constexpr base::TimeDelta kTimeSpentInFeedInteractionTimeout = + base::TimeDelta::FromSeconds(30); void ReportEngagementTypeHistogram(FeedEngagementType engagement_type) { base::UmaHistogramEnumeration("ContentSuggestions.Feed.EngagementType", @@ -41,10 +49,16 @@ void ReportUserActionHistogram(FeedUserActionType action_type) { } // namespace -MetricsReporter::MetricsReporter(const base::TickClock* clock) - : clock_(clock) {} +MetricsReporter::MetricsReporter(const base::TickClock* clock, + PrefService* profile_prefs) + : clock_(clock), profile_prefs_(profile_prefs) { + persistent_data_ = prefs::GetPersistentMetricsData(*profile_prefs_); + ReportPersistentDataIfDayIsDone(); +} -MetricsReporter::~MetricsReporter() = default; +MetricsReporter::~MetricsReporter() { + FinalizeMetrics(); +} void MetricsReporter::OnEnterBackground() { FinalizeMetrics(); @@ -57,6 +71,28 @@ void MetricsReporter::RecordInteraction() { ReportEngagementTypeHistogram(FeedEngagementType::kFeedInteracted); } +void MetricsReporter::TrackTimeSpentInFeed(bool interacted_or_scrolled) { + if (time_in_feed_start_) { + ReportPersistentDataIfDayIsDone(); + persistent_data_.accumulated_time_spent_in_feed += + std::min(kTimeSpentInFeedInteractionTimeout, + clock_->NowTicks() - *time_in_feed_start_); + time_in_feed_start_ = base::nullopt; + } + + if (interacted_or_scrolled) { + time_in_feed_start_ = clock_->NowTicks(); + } +} + +void MetricsReporter::FinalizeVisit() { + if (!engaged_simple_reported_) + return; + engaged_reported_ = false; + engaged_simple_reported_ = false; + TrackTimeSpentInFeed(false); +} + void MetricsReporter::RecordEngagement(int scroll_distance_dp, bool interacted) { scroll_distance_dp = std::abs(scroll_distance_dp); @@ -64,12 +100,13 @@ void MetricsReporter::RecordEngagement(int scroll_distance_dp, auto now = clock_->NowTicks(); const base::TimeDelta kVisitTimeout = base::TimeDelta::FromMinutes(5); if (now - visit_start_time_ > kVisitTimeout) { - engaged_reported_ = false; - engaged_simple_reported_ = false; + FinalizeVisit(); } // Reset the last active time for session measurement. visit_start_time_ = now; + TrackTimeSpentInFeed(true); + // Report the user as engaged-simple if they have scrolled any amount or // interacted with the card, and we have not already reported it for this // chrome run. @@ -89,6 +126,13 @@ void MetricsReporter::RecordEngagement(int scroll_distance_dp, } } +void MetricsReporter::StreamScrollStart() { + // Note that |TrackTimeSpentInFeed()| is called as a result of + // |StreamScrolled()| as well. Tracking the start of scroll events ensures we + // don't miss out on long and slow scrolling. + TrackTimeSpentInFeed(true); +} + void MetricsReporter::StreamScrolled(int distance_dp) { RecordEngagement(distance_dp, /*interacted=*/false); @@ -188,6 +232,8 @@ void MetricsReporter::ContextMenuOpened() { } void MetricsReporter::SurfaceOpened(SurfaceId surface_id) { + ReportPersistentDataIfDayIsDone(); + surfaces_waiting_for_content_.emplace(surface_id, clock_->NowTicks()); ReportUserActionHistogram(FeedUserActionType::kOpenedFeedSurface); base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( @@ -203,6 +249,7 @@ void MetricsReporter::SurfaceClosed(SurfaceId surface_id) { } void MetricsReporter::FinalizeMetrics() { + FinalizeVisit(); ReportCardOpenEndIfNeeded(false); for (auto iter = surfaces_waiting_for_content_.begin(); iter != surfaces_waiting_for_content_.end();) { @@ -212,6 +259,7 @@ void MetricsReporter::FinalizeMetrics() { iter != surfaces_waiting_for_more_content_.end();) { ReportGetMoreIfNeeded((iter++)->first, false); } + prefs::SetPersistentMetricsData(persistent_data_, *profile_prefs_); } void MetricsReporter::ReportOpenFeedIfNeeded(SurfaceId surface_id, @@ -332,6 +380,18 @@ void MetricsReporter::OnLoadMore(LoadStreamStatus status) { "ContentSuggestions.Feed.LoadStreamStatus.LoadMore", status); } +void MetricsReporter::OnUploadActionsBatch(UploadActionsBatchStatus status) { + DVLOG(1) << "UploadActionsBatchStatus: " << status; + base::UmaHistogramEnumeration( + "ContentSuggestions.Feed.UploadActionsBatchStatus", status); +} + +void MetricsReporter::OnUploadActions(UploadActionsStatus status) { + DVLOG(1) << "UploadActionsTask finished with status " << status; + base::UmaHistogramEnumeration("ContentSuggestions.Feed.UploadActionsStatus", + status); +} + void MetricsReporter::SurfaceReceivedContent(SurfaceId surface_id) { ReportGetMoreIfNeeded(surface_id, true); } @@ -344,4 +404,34 @@ void MetricsReporter::OnClearAll(base::TimeDelta time_since_last_clear) { /*bucket_count=*/50); } +void MetricsReporter::ReportPersistentDataIfDayIsDone() { + // Reset the persistent data if 24 hours have elapsed, or if it has never + // been initialized. + bool reset_data = false; + if (persistent_data_.current_day_start.is_null()) { + reset_data = true; + } else { + // Report metrics if 24 hours have passed since the day started. + const base::TimeDelta since_day_start = + (base::Time::Now() - persistent_data_.current_day_start); + if (since_day_start > base::TimeDelta::FromDays(1) + // Allow up to 1 hour of negative delta, for expected clock changes. + || since_day_start < -base::TimeDelta::FromHours(1)) { + if (persistent_data_.accumulated_time_spent_in_feed > base::TimeDelta()) { + base::UmaHistogramLongTimes( + "ContentSuggestions.Feed.TimeSpentInFeed", + persistent_data_.accumulated_time_spent_in_feed); + } + + reset_data = true; + } + } + + if (reset_data) { + persistent_data_ = PersistentMetricsData(); + persistent_data_.current_day_start = base::Time::Now().LocalMidnight(); + prefs::SetPersistentMetricsData(persistent_data_, *profile_prefs_); + } +} + } // namespace feed diff --git a/chromium/components/feed/core/v2/metrics_reporter.h b/chromium/components/feed/core/v2/metrics_reporter.h index d680f1e3916..e42a05e2ce1 100644 --- a/chromium/components/feed/core/v2/metrics_reporter.h +++ b/chromium/components/feed/core/v2/metrics_reporter.h @@ -5,6 +5,8 @@ #ifndef COMPONENTS_FEED_CORE_V2_METRICS_REPORTER_H_ #define COMPONENTS_FEED_CORE_V2_METRICS_REPORTER_H_ +#include <map> + #include "base/memory/weak_ptr.h" #include "base/optional.h" #include "base/time/time.h" @@ -54,7 +56,8 @@ enum class FeedUserActionType { // Note this is inherited only for testing. class MetricsReporter { public: - explicit MetricsReporter(const base::TickClock* clock); + explicit MetricsReporter(const base::TickClock* clock, + PrefService* profile_prefs); virtual ~MetricsReporter(); MetricsReporter(const MetricsReporter&) = delete; MetricsReporter& operator=(const MetricsReporter&) = delete; @@ -77,6 +80,7 @@ class MetricsReporter { // Indicates the user scrolled the feed by |distance_dp| and then stopped // scrolling. void StreamScrolled(int distance_dp); + void StreamScrollStart(); // Called when the Feed surface is opened and closed. void SurfaceOpened(SurfaceId surface_id); @@ -100,20 +104,31 @@ class MetricsReporter { // Called when Chrome is entering the background. void OnEnterBackground(); + // Actions upload. + static void OnUploadActionsBatch(UploadActionsBatchStatus status); + static void OnUploadActions(UploadActionsStatus status); + private: base::WeakPtr<MetricsReporter> GetWeakPtr() { return weak_ptr_factory_.GetWeakPtr(); } + void ReportPersistentDataIfDayIsDone(); void CardOpenBegin(); void CardOpenTimeout(base::TimeTicks start_ticks); void ReportCardOpenEndIfNeeded(bool success); void RecordEngagement(int scroll_distance_dp, bool interacted); + void TrackTimeSpentInFeed(bool interacted_or_scrolled); void RecordInteraction(); void ReportOpenFeedIfNeeded(SurfaceId surface_id, bool success); void ReportGetMoreIfNeeded(SurfaceId surface_id, bool success); void FinalizeMetrics(); + void FinalizeVisit(); const base::TickClock* clock_; + PrefService* profile_prefs_; + // Persistent data stored in prefs. Data is read in the constructor, and then + // written back to prefs on backgrounding. + PersistentMetricsData persistent_data_; base::TimeTicks visit_start_time_; bool engaged_simple_reported_ = false; @@ -132,6 +147,11 @@ class MetricsReporter { // loading the page succeeds, the open is considered successful. base::Optional<base::TimeTicks> pending_open_; + // For tracking time spent in the Feed. + base::Optional<base::TimeTicks> time_in_feed_start_; + // For TimeSpentOnFeed. + base::TimeDelta tracked_visit_time_in_feed_; + base::WeakPtrFactory<MetricsReporter> weak_ptr_factory_{this}; }; } // namespace feed diff --git a/chromium/components/feed/core/v2/metrics_reporter_unittest.cc b/chromium/components/feed/core/v2/metrics_reporter_unittest.cc index 7300a16b8fc..58e9143c091 100644 --- a/chromium/components/feed/core/v2/metrics_reporter_unittest.cc +++ b/chromium/components/feed/core/v2/metrics_reporter_unittest.cc @@ -5,10 +5,14 @@ #include "components/feed/core/v2/metrics_reporter.h" #include <map> +#include <memory> #include "base/test/metrics/histogram_tester.h" #include "base/test/metrics/user_action_tester.h" #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/prefs/testing_pref_service.h" #include "testing/gtest/include/gtest/gtest.h" namespace feed { @@ -19,6 +23,17 @@ const base::TimeDelta kEpsilon = base::TimeDelta::FromMilliseconds(1); class MetricsReporterTest : public testing::Test { protected: + void SetUp() override { + feed::prefs::RegisterFeedSharedProfilePrefs(profile_prefs_.registry()); + feed::RegisterProfilePrefs(profile_prefs_.registry()); + + // Tests start at the beginning of a day. + task_environment_.AdvanceClock( + (base::Time::Now().LocalMidnight() + base::TimeDelta::FromDays(1)) - + base::Time::Now() + base::TimeDelta::FromSeconds(1)); + + RecreateMetricsReporter(); + } std::map<FeedEngagementType, int> ReportedEngagementType() { std::map<FeedEngagementType, int> result; for (const auto& bucket : @@ -28,21 +43,27 @@ class MetricsReporterTest : public testing::Test { return result; } + void RecreateMetricsReporter() { + reporter_ = std::make_unique<MetricsReporter>( + task_environment_.GetMockTickClock(), &profile_prefs_); + } + protected: base::test::TaskEnvironment task_environment_{ base::test::TaskEnvironment::TimeSource::MOCK_TIME}; - MetricsReporter reporter_{task_environment_.GetMockTickClock()}; + TestingPrefServiceSimple profile_prefs_; + std::unique_ptr<MetricsReporter> reporter_; base::HistogramTester histogram_; base::UserActionTester user_actions_; }; TEST_F(MetricsReporterTest, SliceViewedReportsSuggestionShown) { - reporter_.ContentSliceViewed(kSurfaceId, 5); + reporter_->ContentSliceViewed(kSurfaceId, 5); histogram_.ExpectUniqueSample("NewTabPage.ContentSuggestions.Shown", 5, 1); } TEST_F(MetricsReporterTest, ScrollingSmall) { - reporter_.StreamScrolled(100); + reporter_->StreamScrolled(100); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedScrolled, 1}, @@ -52,7 +73,7 @@ TEST_F(MetricsReporterTest, ScrollingSmall) { } TEST_F(MetricsReporterTest, ScrollingCanTriggerEngaged) { - reporter_.StreamScrolled(161); + reporter_->StreamScrolled(161); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedScrolled, 1}, @@ -63,7 +84,7 @@ TEST_F(MetricsReporterTest, ScrollingCanTriggerEngaged) { } TEST_F(MetricsReporterTest, OpeningContentIsInteracting) { - reporter_.OpenAction(5); + reporter_->OpenAction(5); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -74,7 +95,7 @@ TEST_F(MetricsReporterTest, OpeningContentIsInteracting) { } TEST_F(MetricsReporterTest, RemovingContentIsInteracting) { - reporter_.RemoveAction(); + reporter_->RemoveAction(); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -85,7 +106,7 @@ TEST_F(MetricsReporterTest, RemovingContentIsInteracting) { } TEST_F(MetricsReporterTest, NotInterestedInIsInteracting) { - reporter_.NotInterestedInAction(); + reporter_->NotInterestedInAction(); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -96,7 +117,7 @@ TEST_F(MetricsReporterTest, NotInterestedInIsInteracting) { } TEST_F(MetricsReporterTest, ManageInterestsInIsInteracting) { - reporter_.ManageInterestsAction(); + reporter_->ManageInterestsAction(); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -107,11 +128,11 @@ TEST_F(MetricsReporterTest, ManageInterestsInIsInteracting) { } TEST_F(MetricsReporterTest, VisitsCanLastMoreThanFiveMinutes) { - reporter_.StreamScrolled(1); + reporter_->StreamScrolled(1); task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(5) - kEpsilon); - reporter_.OpenAction(0); + reporter_->OpenAction(0); task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(5) - kEpsilon); - reporter_.StreamScrolled(1); + reporter_->StreamScrolled(1); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -123,11 +144,11 @@ TEST_F(MetricsReporterTest, VisitsCanLastMoreThanFiveMinutes) { } TEST_F(MetricsReporterTest, NewVisitAfterInactivity) { - reporter_.OpenAction(0); - reporter_.StreamScrolled(1); + reporter_->OpenAction(0); + reporter_->StreamScrolled(1); task_environment_.FastForwardBy(base::TimeDelta::FromMinutes(5) + kEpsilon); - reporter_.OpenAction(0); - reporter_.StreamScrolled(1); + reporter_->OpenAction(0); + reporter_->StreamScrolled(1); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 2}, @@ -139,8 +160,8 @@ TEST_F(MetricsReporterTest, NewVisitAfterInactivity) { } TEST_F(MetricsReporterTest, ReportsLoadStreamStatus) { - reporter_.OnLoadStream(LoadStreamStatus::kDataInStoreIsStale, - LoadStreamStatus::kLoadedFromNetwork); + reporter_->OnLoadStream(LoadStreamStatus::kDataInStoreIsStale, + LoadStreamStatus::kLoadedFromNetwork); histogram_.ExpectUniqueSample( "ContentSuggestions.Feed.LoadStreamStatus.Initial", @@ -151,8 +172,8 @@ TEST_F(MetricsReporterTest, ReportsLoadStreamStatus) { } TEST_F(MetricsReporterTest, ReportsLoadStreamStatusIgnoresNoStatusFromStore) { - reporter_.OnLoadStream(LoadStreamStatus::kNoStatus, - LoadStreamStatus::kLoadedFromNetwork); + reporter_->OnLoadStream(LoadStreamStatus::kNoStatus, + LoadStreamStatus::kLoadedFromNetwork); histogram_.ExpectUniqueSample( "ContentSuggestions.Feed.LoadStreamStatus.Initial", @@ -162,7 +183,7 @@ TEST_F(MetricsReporterTest, ReportsLoadStreamStatusIgnoresNoStatusFromStore) { } TEST_F(MetricsReporterTest, ReportsLoadMoreStatus) { - reporter_.OnLoadMore(LoadStreamStatus::kLoadedFromNetwork); + reporter_->OnLoadMore(LoadStreamStatus::kLoadedFromNetwork); histogram_.ExpectUniqueSample( "ContentSuggestions.Feed.LoadStreamStatus.LoadMore", @@ -170,7 +191,7 @@ TEST_F(MetricsReporterTest, ReportsLoadMoreStatus) { } TEST_F(MetricsReporterTest, ReportsBackgroundRefreshStatus) { - reporter_.OnBackgroundRefresh(LoadStreamStatus::kLoadedFromNetwork); + reporter_->OnBackgroundRefresh(LoadStreamStatus::kLoadedFromNetwork); histogram_.ExpectUniqueSample( "ContentSuggestions.Feed.LoadStreamStatus.BackgroundRefresh", @@ -178,7 +199,7 @@ TEST_F(MetricsReporterTest, ReportsBackgroundRefreshStatus) { } TEST_F(MetricsReporterTest, OpenAction) { - reporter_.OpenAction(5); + reporter_->OpenAction(5); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -194,7 +215,7 @@ TEST_F(MetricsReporterTest, OpenAction) { } TEST_F(MetricsReporterTest, OpenInNewTabAction) { - reporter_.OpenInNewTabAction(5); + reporter_->OpenInNewTabAction(5); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -210,7 +231,7 @@ TEST_F(MetricsReporterTest, OpenInNewTabAction) { } TEST_F(MetricsReporterTest, OpenInNewIncognitoTabAction) { - reporter_.OpenInNewIncognitoTabAction(); + reporter_->OpenInNewIncognitoTabAction(); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -227,7 +248,7 @@ TEST_F(MetricsReporterTest, OpenInNewIncognitoTabAction) { } TEST_F(MetricsReporterTest, SendFeedbackAction) { - reporter_.SendFeedbackAction(); + reporter_->SendFeedbackAction(); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -242,7 +263,7 @@ TEST_F(MetricsReporterTest, SendFeedbackAction) { } TEST_F(MetricsReporterTest, DownloadAction) { - reporter_.DownloadAction(); + reporter_->DownloadAction(); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -257,7 +278,7 @@ TEST_F(MetricsReporterTest, DownloadAction) { } TEST_F(MetricsReporterTest, LearnMoreAction) { - reporter_.LearnMoreAction(); + reporter_->LearnMoreAction(); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -272,7 +293,7 @@ TEST_F(MetricsReporterTest, LearnMoreAction) { } TEST_F(MetricsReporterTest, RemoveAction) { - reporter_.RemoveAction(); + reporter_->RemoveAction(); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -287,7 +308,7 @@ TEST_F(MetricsReporterTest, RemoveAction) { } TEST_F(MetricsReporterTest, NotInterestedInAction) { - reporter_.NotInterestedInAction(); + reporter_->NotInterestedInAction(); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -302,7 +323,7 @@ TEST_F(MetricsReporterTest, NotInterestedInAction) { } TEST_F(MetricsReporterTest, ManageInterestsAction) { - reporter_.ManageInterestsAction(); + reporter_->ManageInterestsAction(); std::map<FeedEngagementType, int> want({ {FeedEngagementType::kFeedEngaged, 1}, @@ -317,7 +338,7 @@ TEST_F(MetricsReporterTest, ManageInterestsAction) { } TEST_F(MetricsReporterTest, ContextMenuOpened) { - reporter_.ContextMenuOpened(); + reporter_->ContextMenuOpened(); std::map<FeedEngagementType, int> want_empty; EXPECT_EQ(want_empty, ReportedEngagementType()); @@ -328,7 +349,7 @@ TEST_F(MetricsReporterTest, ContextMenuOpened) { } TEST_F(MetricsReporterTest, SurfaceOpened) { - reporter_.SurfaceOpened(kSurfaceId); + reporter_->SurfaceOpened(kSurfaceId); std::map<FeedEngagementType, int> want_empty; EXPECT_EQ(want_empty, ReportedEngagementType()); @@ -337,9 +358,9 @@ TEST_F(MetricsReporterTest, SurfaceOpened) { } TEST_F(MetricsReporterTest, OpenFeedSuccessDuration) { - reporter_.SurfaceOpened(kSurfaceId); + reporter_->SurfaceOpened(kSurfaceId); task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(9)); - reporter_.ContentSliceViewed(kSurfaceId, 0); + reporter_->ContentSliceViewed(kSurfaceId, 0); histogram_.ExpectUniqueTimeSample( "ContentSuggestions.Feed.UserJourney.OpenFeed.SuccessDuration", @@ -347,7 +368,7 @@ TEST_F(MetricsReporterTest, OpenFeedSuccessDuration) { } TEST_F(MetricsReporterTest, OpenFeedLoadTimeout) { - reporter_.SurfaceOpened(kSurfaceId); + reporter_->SurfaceOpened(kSurfaceId); task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(16)); histogram_.ExpectUniqueTimeSample( @@ -358,9 +379,9 @@ TEST_F(MetricsReporterTest, OpenFeedLoadTimeout) { } TEST_F(MetricsReporterTest, OpenFeedCloseBeforeLoad) { - reporter_.SurfaceOpened(kSurfaceId); + reporter_->SurfaceOpened(kSurfaceId); task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(14)); - reporter_.SurfaceClosed(kSurfaceId); + reporter_->SurfaceClosed(kSurfaceId); histogram_.ExpectUniqueTimeSample( "ContentSuggestions.Feed.UserJourney.OpenFeed.FailureDuration", @@ -370,21 +391,19 @@ TEST_F(MetricsReporterTest, OpenFeedCloseBeforeLoad) { } TEST_F(MetricsReporterTest, OpenCardSuccessDuration) { - reporter_.OpenAction(0); + reporter_->OpenAction(0); task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(19)); - reporter_.PageLoaded(); + reporter_->PageLoaded(); - histogram_.ExpectTotalCount( - "ContentSuggestions.Feed.UserJourney.OpenCard.SuccessDuration", 1); histogram_.ExpectUniqueTimeSample( "ContentSuggestions.Feed.UserJourney.OpenCard.SuccessDuration", base::TimeDelta::FromSeconds(19), 1); } TEST_F(MetricsReporterTest, OpenCardTimeout) { - reporter_.OpenAction(0); + reporter_->OpenAction(0); task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(21)); - reporter_.PageLoaded(); + reporter_->PageLoaded(); histogram_.ExpectUniqueSample( "ContentSuggestions.Feed.UserJourney.OpenCard.Failure", 1, 1); @@ -393,10 +412,10 @@ TEST_F(MetricsReporterTest, OpenCardTimeout) { } TEST_F(MetricsReporterTest, OpenCardFailureTwiceAndThenSucceed) { - reporter_.OpenAction(0); - reporter_.OpenAction(1); - reporter_.OpenAction(2); - reporter_.PageLoaded(); + reporter_->OpenAction(0); + reporter_->OpenAction(1); + reporter_->OpenAction(2); + reporter_->PageLoaded(); histogram_.ExpectUniqueSample( "ContentSuggestions.Feed.UserJourney.OpenCard.Failure", 1, 2); @@ -405,8 +424,8 @@ TEST_F(MetricsReporterTest, OpenCardFailureTwiceAndThenSucceed) { } TEST_F(MetricsReporterTest, OpenCardCloseChromeFailure) { - reporter_.OpenAction(0); - reporter_.OnEnterBackground(); + reporter_->OpenAction(0); + reporter_->OnEnterBackground(); histogram_.ExpectUniqueSample( "ContentSuggestions.Feed.UserJourney.OpenCard.Failure", 1, 1); @@ -414,4 +433,77 @@ TEST_F(MetricsReporterTest, OpenCardCloseChromeFailure) { "ContentSuggestions.Feed.UserJourney.OpenCard.SuccessDuration", 0); } +TEST_F(MetricsReporterTest, TimeSpentInFeedCountsOnlyForegroundTime) { + reporter_->OpenAction(0); + task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(1)); + reporter_->OnEnterBackground(); + task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(2)); + reporter_->OpenAction(0); + task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(3)); + reporter_->OnEnterBackground(); + + // Trigger reporting the persistent metrics the next day. + task_environment_.FastForwardBy(base::TimeDelta::FromDays(1)); + RecreateMetricsReporter(); + + histogram_.ExpectUniqueTimeSample("ContentSuggestions.Feed.TimeSpentInFeed", + base::TimeDelta::FromSeconds(4), 1); +} + +TEST_F(MetricsReporterTest, TimeSpentInFeedLimitsIdleTime) { + reporter_->OpenAction(0); + task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(31)); + reporter_->OnEnterBackground(); + + // Trigger reporting the persistent metrics the next day. + task_environment_.FastForwardBy(base::TimeDelta::FromDays(1)); + RecreateMetricsReporter(); + + histogram_.ExpectUniqueTimeSample("ContentSuggestions.Feed.TimeSpentInFeed", + base::TimeDelta::FromSeconds(30), 1); +} + +TEST_F(MetricsReporterTest, TimeSpentInFeedIsPerDay) { + // One interaction every hour for 2 days. Should be reported at 30 seconds per + // interaction due to the interaction timeout. The 49th |OpenAction()| call + // triggers reporting the UMA for the previous day. + for (int i = 0; i < 49; ++i) { + reporter_->OpenAction(0); + task_environment_.FastForwardBy(base::TimeDelta::FromHours(1)); + } + + histogram_.ExpectUniqueTimeSample("ContentSuggestions.Feed.TimeSpentInFeed", + base::TimeDelta::FromSeconds(30) * 24, 2); +} + +TEST_F(MetricsReporterTest, TimeSpentIsPersisted) { + // Verify that the previous test also works when |MetricsReporter| is + // destroyed and recreated. The 49th |OpenAction()| call triggers reporting + // the UMA for the previous day. + for (int i = 0; i < 49; ++i) { + reporter_->OpenAction(0); + task_environment_.FastForwardBy(base::TimeDelta::FromHours(1)); + reporter_->OnEnterBackground(); + RecreateMetricsReporter(); + } + + histogram_.ExpectUniqueTimeSample("ContentSuggestions.Feed.TimeSpentInFeed", + base::TimeDelta::FromSeconds(30) * 24, 2); +} + +TEST_F(MetricsReporterTest, TimeSpentInFeedTracksWholeScrollTime) { + reporter_->StreamScrollStart(); + task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(2)); + reporter_->StreamScrolled(1); + task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(1)); + reporter_->OnEnterBackground(); + + // Trigger reporting the persistent metrics the next day. + task_environment_.FastForwardBy(base::TimeDelta::FromDays(1)); + RecreateMetricsReporter(); + + histogram_.ExpectUniqueTimeSample("ContentSuggestions.Feed.TimeSpentInFeed", + base::TimeDelta::FromSeconds(3), 1); +} + } // namespace feed diff --git a/chromium/components/feed/core/v2/prefs.cc b/chromium/components/feed/core/v2/prefs.cc index 589e445686d..41dc55cf43b 100644 --- a/chromium/components/feed/core/v2/prefs.cc +++ b/chromium/components/feed/core/v2/prefs.cc @@ -6,7 +6,6 @@ #include <utility> -#include "base/value_conversions.h" #include "base/values.h" #include "components/feed/core/common/pref_names.h" #include "components/feed/core/v2/scheduling.h" @@ -16,10 +15,10 @@ namespace feed { namespace prefs { -std::vector<int> GetThrottlerRequestCounts(PrefService* pref_service) { +std::vector<int> GetThrottlerRequestCounts(PrefService& pref_service) { std::vector<int> result; const auto& value_list = - pref_service->GetList(kThrottlerRequestCountListPrefName)->GetList(); + pref_service.GetList(kThrottlerRequestCountListPrefName)->GetList(); for (const base::Value& value : value_list) { result.push_back(value.is_int() ? value.GetInt() : 0); } @@ -27,41 +26,50 @@ std::vector<int> GetThrottlerRequestCounts(PrefService* pref_service) { } void SetThrottlerRequestCounts(std::vector<int> request_counts, - PrefService* pref_service) { + PrefService& pref_service) { std::vector<base::Value> value_list; for (int count : request_counts) { value_list.push_back(base::Value(count)); } - pref_service->Set(kThrottlerRequestCountListPrefName, - base::Value(std::move(value_list))); + pref_service.Set(kThrottlerRequestCountListPrefName, + base::Value(std::move(value_list))); } -base::Time GetLastRequestTime(PrefService* pref_service) { - return pref_service->GetTime(kThrottlerLastRequestTime); +base::Time GetLastRequestTime(PrefService& pref_service) { + return pref_service.GetTime(kThrottlerLastRequestTime); } -void SetLastRequestTime(base::Time request_time, PrefService* pref_service) { - return pref_service->SetTime(kThrottlerLastRequestTime, request_time); +void SetLastRequestTime(base::Time request_time, PrefService& pref_service) { + return pref_service.SetTime(kThrottlerLastRequestTime, request_time); } -DebugStreamData GetDebugStreamData(PrefService* pref_service) { - return DeserializeDebugStreamData(pref_service->GetString(kDebugStreamData)) +DebugStreamData GetDebugStreamData(PrefService& pref_service) { + return DeserializeDebugStreamData(pref_service.GetString(kDebugStreamData)) .value_or(DebugStreamData()); } void SetDebugStreamData(const DebugStreamData& data, - PrefService* pref_service) { - pref_service->SetString(kDebugStreamData, SerializeDebugStreamData(data)); + PrefService& pref_service) { + pref_service.SetString(kDebugStreamData, SerializeDebugStreamData(data)); } void SetRequestSchedule(const RequestSchedule& schedule, - PrefService* pref_service) { - pref_service->Set(kRequestSchedule, RequestScheduleToValue(schedule)); + PrefService& pref_service) { + pref_service.Set(kRequestSchedule, RequestScheduleToValue(schedule)); } -RequestSchedule GetRequestSchedule(PrefService* pref_service) { - return RequestScheduleFromValue(*pref_service->Get(kRequestSchedule)); +RequestSchedule GetRequestSchedule(PrefService& pref_service) { + return RequestScheduleFromValue(*pref_service.Get(kRequestSchedule)); +} + +void SetPersistentMetricsData(const PersistentMetricsData& data, + PrefService& pref_service) { + pref_service.Set(kMetricsData, PersistentMetricsDataToValue(data)); +} + +PersistentMetricsData GetPersistentMetricsData(PrefService& pref_service) { + return PersistentMetricsDataFromValue(*pref_service.Get(kMetricsData)); } } // namespace prefs diff --git a/chromium/components/feed/core/v2/prefs.h b/chromium/components/feed/core/v2/prefs.h index 334372279ac..f24ab310b1f 100644 --- a/chromium/components/feed/core/v2/prefs.h +++ b/chromium/components/feed/core/v2/prefs.h @@ -9,6 +9,7 @@ #include "base/time/time.h" #include "components/feed/core/v2/public/types.h" +#include "components/feed/core/v2/types.h" class PrefService; @@ -20,21 +21,25 @@ namespace prefs { // For counting previously made requests, one integer for each // |NetworkRequestType|. -std::vector<int> GetThrottlerRequestCounts(PrefService* pref_service); +std::vector<int> GetThrottlerRequestCounts(PrefService& pref_service); void SetThrottlerRequestCounts(std::vector<int> request_counts, - PrefService* pref_service); + PrefService& pref_service); // Time of the last request. For determining whether the next day's quota should // be released. -base::Time GetLastRequestTime(PrefService* pref_service); -void SetLastRequestTime(base::Time request_time, PrefService* pref_service); +base::Time GetLastRequestTime(PrefService& pref_service); +void SetLastRequestTime(base::Time request_time, PrefService& pref_service); -DebugStreamData GetDebugStreamData(PrefService* pref_service); -void SetDebugStreamData(const DebugStreamData& data, PrefService* pref_service); +DebugStreamData GetDebugStreamData(PrefService& pref_service); +void SetDebugStreamData(const DebugStreamData& data, PrefService& pref_service); void SetRequestSchedule(const RequestSchedule& schedule, - PrefService* pref_service); -RequestSchedule GetRequestSchedule(PrefService* pref_service); + PrefService& pref_service); +RequestSchedule GetRequestSchedule(PrefService& pref_service); + +PersistentMetricsData GetPersistentMetricsData(PrefService& pref_service); +void SetPersistentMetricsData(const PersistentMetricsData& data, + 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 852666c263a..70ed71a2eb3 100644 --- a/chromium/components/feed/core/v2/proto_util.cc +++ b/chromium/components/feed/core/v2/proto_util.cc @@ -116,6 +116,10 @@ feedwire::Request CreateFeedQueryRequest( feedwire::FeedRequest& feed_request = *request.mutable_feed_request(); feed_request.add_client_capability(feedwire::Capability::BASE_UI); feed_request.add_client_capability(feedwire::Capability::REQUEST_SCHEDULE); + feed_request.add_client_capability(feedwire::Capability::OPEN_IN_TAB); + feed_request.add_client_capability(feedwire::Capability::DOWNLOAD_LINK); + feed_request.add_client_capability(feedwire::Capability::INFINITE_FEED); + *feed_request.mutable_client_info() = CreateClientInfo(request_metadata); feedwire::FeedQuery& query = *feed_request.mutable_feed_query(); query.set_reason(request_reason); @@ -127,7 +131,6 @@ feedwire::Request CreateFeedQueryRequest( query.mutable_next_page_token() ->mutable_next_page_token() ->set_next_page_token(next_page_token); - feed_request.mutable_consistency_token()->set_token(consistency_token); } return request; } @@ -156,11 +159,23 @@ bool CompareContentId(const feedwire::ContentId& a, std::tie(b.content_domain(), b_id, b_type); } +bool CompareContent(const feedstore::Content& a, const feedstore::Content& b) { + const ContentId& a_id = a.content_id(); + const ContentId& b_id = b.content_id(); + if (a_id.id() < b_id.id()) + return true; + if (a_id.id() > b_id.id()) + return false; + if (a_id.type() < b_id.type()) + return true; + if (a_id.type() > b_id.type()) + return false; + return a.frame() < b.frame(); +} + feedwire::ClientInfo CreateClientInfo(const RequestMetadata& request_metadata) { feedwire::ClientInfo client_info; // TODO(harringtond): Fill out client_instance_id. - // TODO(harringtond): Fill out advertising_id. - // TODO(harringtond): Fill out device_country. feedwire::DisplayInfo& display_info = *client_info.add_display_info(); display_info.set_screen_density(request_metadata.display_metrics.density); @@ -176,7 +191,7 @@ feedwire::ClientInfo CreateClientInfo(const RequestMetadata& request_metadata) { #elif defined(OS_IOS) client_info.set_platform_type(feedwire::ClientInfo::IOS); #endif - client_info.set_app_type(feedwire::ClientInfo::TEST_APP); + client_info.set_app_type(feedwire::ClientInfo::CLANK); *client_info.mutable_platform_version() = GetPlatformVersionMessage(); *client_info.mutable_app_version() = GetAppVersionMessage(request_metadata.chrome_info); @@ -203,8 +218,8 @@ feedwire::Request CreateFeedQueryLoadMoreRequest( } // namespace feed namespace feedstore { -void SetLastAddedTime(base::Time t, feedstore::StreamData* data) { - data->set_last_added_time_millis( +void SetLastAddedTime(base::Time t, feedstore::StreamData& data) { + data.set_last_added_time_millis( (t - base::Time::UnixEpoch()).InMilliseconds()); } diff --git a/chromium/components/feed/core/v2/proto_util.h b/chromium/components/feed/core/v2/proto_util.h index 89506b0be82..95fd50dc5fc 100644 --- a/chromium/components/feed/core/v2/proto_util.h +++ b/chromium/components/feed/core/v2/proto_util.h @@ -17,6 +17,7 @@ namespace feedwire { class Request; } // namespace feedwire namespace feedstore { +class Content; class StreamData; } // namespace feedstore @@ -29,6 +30,7 @@ std::string ContentIdString(const feedwire::ContentId&); bool Equal(const feedwire::ContentId& a, const feedwire::ContentId& b); bool CompareContentId(const feedwire::ContentId& a, const feedwire::ContentId& b); +bool CompareContent(const feedstore::Content& a, const feedstore::Content& b); class ContentIdCompareFunctor { public: @@ -38,6 +40,14 @@ class ContentIdCompareFunctor { } }; +class ContentCompareFunctor { + public: + bool operator()(const feedstore::Content& a, + const feedstore::Content& b) const { + return CompareContent(a, b); + } +}; + feedwire::ClientInfo CreateClientInfo(const RequestMetadata& request_metadata); feedwire::Request CreateFeedQueryRefreshRequest( @@ -54,7 +64,7 @@ feedwire::Request CreateFeedQueryLoadMoreRequest( namespace feedstore { -void SetLastAddedTime(base::Time t, feedstore::StreamData* data); +void SetLastAddedTime(base::Time t, feedstore::StreamData& data); base::Time GetLastAddedTime(const feedstore::StreamData& data); } // namespace feedstore diff --git a/chromium/components/feed/core/v2/proto_util_unittest.cc b/chromium/components/feed/core/v2/proto_util_unittest.cc index d405ee94d6d..e02772af600 100644 --- a/chromium/components/feed/core/v2/proto_util_unittest.cc +++ b/chromium/components/feed/core/v2/proto_util_unittest.cc @@ -23,8 +23,7 @@ TEST(ProtoUtilTest, CreateClientInfo) { request_metadata.language_tag = "en-US"; feedwire::ClientInfo result = CreateClientInfo(request_metadata); - // TODO(harringtond): change back to CHROME when it is supported. - EXPECT_EQ(feedwire::ClientInfo::TEST_APP, result.app_type()); + EXPECT_EQ(feedwire::ClientInfo::CLANK, result.app_type()); EXPECT_EQ(feedwire::Version::RELEASE, result.app_version().build_type()); EXPECT_EQ(1, result.app_version().major()); EXPECT_EQ(2, result.app_version().minor()); diff --git a/chromium/components/feed/core/v2/protocol_translator.cc b/chromium/components/feed/core/v2/protocol_translator.cc index 0b0b691ba98..f9c3c74bb32 100644 --- a/chromium/components/feed/core/v2/protocol_translator.cc +++ b/chromium/components/feed/core/v2/protocol_translator.cc @@ -4,10 +4,13 @@ #include "components/feed/core/v2/protocol_translator.h" +#include <string> #include <utility> +#include "base/logging.h" #include "base/optional.h" #include "base/time/time.h" +#include "components/feed/core/proto/v2/packing.pb.h" #include "components/feed/core/proto/v2/wire/data_operation.pb.h" #include "components/feed/core/proto/v2/wire/feature.pb.h" #include "components/feed/core/proto/v2/wire/feed_response.pb.h" @@ -85,10 +88,10 @@ struct ConvertedDataOperation { }; bool TranslateFeature(feedwire::Feature* feature, - ConvertedDataOperation* result) { + ConvertedDataOperation& result) { feedstore::StreamStructure::Type type = TranslateNodeType(feature->renderable_unit()); - result->stream_structure.set_type(type); + result.stream_structure.set_type(type); if (type == feedstore::StreamStructure::CONTENT) { feedwire::Content* wire_content = feature->mutable_content_extension(); @@ -99,10 +102,10 @@ bool TranslateFeature(feedwire::Feature* feature, // TODO(iwells): We still need score, availability_time_seconds, // offline_metadata, and representation_data to populate content_info. - result->content.emplace(); - *(result->content->mutable_content_id()) = - result->stream_structure.content_id(); - result->content->set_allocated_frame( + result.content.emplace(); + *(result.content->mutable_content_id()) = + result.stream_structure.content_id(); + result.content->set_allocated_frame( wire_content->mutable_xsurface_content()->release_xsurface_output()); } return true; @@ -110,26 +113,26 @@ bool TranslateFeature(feedwire::Feature* feature, base::Optional<feedstore::StreamSharedState> TranslateSharedState( feedwire::ContentId content_id, - feedwire::RenderData* wire_shared_state) { - if (wire_shared_state->render_data_type() != feedwire::RenderData::XSURFACE) { + feedwire::RenderData& wire_shared_state) { + if (wire_shared_state.render_data_type() != feedwire::RenderData::XSURFACE) { return base::nullopt; } feedstore::StreamSharedState shared_state; *shared_state.mutable_content_id() = std::move(content_id); shared_state.set_allocated_shared_state_data( - wire_shared_state->mutable_xsurface_container()->release_render_data()); + wire_shared_state.mutable_xsurface_container()->release_render_data()); return shared_state; } bool TranslatePayload(base::Time now, feedwire::DataOperation operation, ConvertedGlobalData* global_data, - ConvertedDataOperation* result) { + ConvertedDataOperation& result) { switch (operation.payload_case()) { case feedwire::DataOperation::kFeature: { feedwire::Feature* feature = operation.mutable_feature(); - result->stream_structure.set_allocated_parent_id( + result.stream_structure.set_allocated_parent_id( feature->release_parent_id()); if (!TranslateFeature(feature, result)) @@ -137,16 +140,16 @@ bool TranslatePayload(base::Time now, } break; case feedwire::DataOperation::kNextPageToken: { feedwire::Token* token = operation.mutable_next_page_token(); - result->stream_structure.set_allocated_parent_id( + result.stream_structure.set_allocated_parent_id( token->release_parent_id()); - result->next_page_token = std::move( + result.next_page_token = std::move( *token->mutable_next_page_token()->mutable_next_page_token()); } break; case feedwire::DataOperation::kRenderData: { - result->shared_state = - TranslateSharedState(result->stream_structure.content_id(), - operation.mutable_render_data()); - if (!result->shared_state) + result.shared_state = + TranslateSharedState(result.stream_structure.content_id(), + *operation.mutable_render_data()); + if (!result.shared_state) return false; } break; case feedwire::DataOperation::kRequestSchedule: { @@ -184,7 +187,7 @@ base::Optional<ConvertedDataOperation> TranslateDataOperationInternal( result.stream_structure.set_allocated_content_id( operation.mutable_metadata()->release_content_id()); - if (!TranslatePayload(now, std::move(operation), global_data, &result)) + if (!TranslatePayload(now, std::move(operation), global_data, result)) return base::nullopt; break; @@ -280,13 +283,15 @@ RefreshResponseData TranslateWireResponse( } } - // TODO(harringtond): If there's more than one shared state, record some - // sort of error. if (!result->shared_states.empty()) { + if (result->shared_states.size() > 1) { + DLOG(ERROR) + << "Receieved more than one shared state. Only the first is used."; + } *result->stream_data.mutable_shared_state_id() = result->shared_states.front().content_id(); } - feedstore::SetLastAddedTime(current_time, &result->stream_data); + feedstore::SetLastAddedTime(current_time, result->stream_data); RefreshResponseData response_data; response_data.model_update_request = std::move(result); @@ -295,4 +300,18 @@ RefreshResponseData TranslateWireResponse( return response_data; } +std::vector<feedstore::DataOperation> TranslateDismissData( + base::Time current_time, + feedpacking::DismissData data) { + std::vector<feedstore::DataOperation> result; + for (auto& operation : data.data_operations()) { + base::Optional<feedstore::DataOperation> translated_operation = + TranslateDataOperation(current_time, operation); + if (translated_operation) { + result.push_back(std::move(translated_operation.value())); + } + } + return result; +} + } // namespace feed diff --git a/chromium/components/feed/core/v2/protocol_translator.h b/chromium/components/feed/core/v2/protocol_translator.h index fdaaa60368c..ca72a108243 100644 --- a/chromium/components/feed/core/v2/protocol_translator.h +++ b/chromium/components/feed/core/v2/protocol_translator.h @@ -10,6 +10,7 @@ #include "base/optional.h" #include "base/time/time.h" +#include "components/feed/core/proto/v2/packing.pb.h" #include "components/feed/core/proto/v2/store.pb.h" #include "components/feed/core/proto/v2/wire/data_operation.pb.h" #include "components/feed/core/proto/v2/wire/response.pb.h" @@ -74,6 +75,10 @@ RefreshResponseData TranslateWireResponse( StreamModelUpdateRequest::Source source, base::Time current_time); +std::vector<feedstore::DataOperation> TranslateDismissData( + base::Time current_time, + feedpacking::DismissData data); + } // namespace feed #endif // COMPONENTS_FEED_CORE_V2_PROTOCOL_TRANSLATOR_H_ diff --git a/chromium/components/feed/core/v2/protocol_translator_unittest.cc b/chromium/components/feed/core/v2/protocol_translator_unittest.cc index 99a93a3d478..e7256ebdfaa 100644 --- a/chromium/components/feed/core/v2/protocol_translator_unittest.cc +++ b/chromium/components/feed/core/v2/protocol_translator_unittest.cc @@ -6,6 +6,7 @@ #include <sstream> #include <string> +#include <utility> #include "base/base_paths.h" #include "base/files/file_path.h" @@ -600,4 +601,40 @@ max_structure_sequence_number: 0 EXPECT_EQ(want, ss.str()); } +TEST(TranslateDismissData, Success) { + feedpacking::DismissData input; + *input.add_data_operations() = + MakeDataOperation(feedwire::DataOperation::CLEAR_ALL); + *input.add_data_operations() = + MakeDataOperationWithContent(feedwire::DataOperation::UPDATE_OR_APPEND); + std::vector<feedstore::DataOperation> result = + TranslateDismissData(kCurrentTime, input); + + ASSERT_EQ(2UL, result.size()); + EXPECT_EQ(R"({ + structure { + operation: 1 + } +} +)", + ToTextProto(result[0])); + EXPECT_EQ(R"({ + structure { + operation: 2 + content_id { + id: 42 + } + type: 3 + } + content { + content_id { + id: 42 + } + frame: "content" + } +} +)", + ToTextProto(result[1])); +} + } // namespace feed diff --git a/chromium/components/feed/core/v2/public/feed_service.cc b/chromium/components/feed/core/v2/public/feed_service.cc index 67202a0f3dc..7fd7c085842 100644 --- a/chromium/components/feed/core/v2/public/feed_service.cc +++ b/chromium/components/feed/core/v2/public/feed_service.cc @@ -9,14 +9,18 @@ #include "base/time/default_clock.h" #include "base/time/default_tick_clock.h" #include "build/build_config.h" +#include "components/feed/core/shared_prefs/pref_names.h" #include "components/feed/core/v2/feed_network_impl.h" #include "components/feed/core/v2/feed_store.h" #include "components/feed/core/v2/feed_stream.h" #include "components/feed/core/v2/metrics_reporter.h" #include "components/feed/core/v2/refresh_task_scheduler.h" +#include "components/feed/feed_feature_list.h" #include "components/history/core/browser/history_service.h" #include "components/history/core/browser/history_service_observer.h" #include "components/history/core/browser/history_types.h" +#include "components/prefs/pref_service.h" +#include "components/signin/public/identity_manager/identity_manager.h" #include "net/base/network_change_notifier.h" #include "services/network/public/cpp/shared_url_loader_factory.h" @@ -56,7 +60,9 @@ class FeedService::HistoryObserverImpl HistoryObserverImpl(history::HistoryService* history_service, FeedStream* feed_stream) : feed_stream_(feed_stream) { - history_service->AddObserver(this); + // May be null for some profiles. + if (history_service) + history_service->AddObserver(this); } HistoryObserverImpl(const HistoryObserverImpl&) = delete; HistoryObserverImpl& operator=(const HistoryObserverImpl&) = delete; @@ -118,6 +124,32 @@ class FeedService::StreamDelegateImpl : public FeedStream::Delegate { std::unique_ptr<HistoryObserverImpl> history_observer_; }; +class FeedService::IdentityManagerObserverImpl + : public signin::IdentityManager::Observer { + public: + IdentityManagerObserverImpl(signin::IdentityManager* identity_manager, + FeedStream* stream) + : identity_manager_(identity_manager), feed_stream_(stream) {} + IdentityManagerObserverImpl(const IdentityManagerObserverImpl&) = delete; + IdentityManagerObserverImpl& operator=(const IdentityManagerObserverImpl&) = + delete; + ~IdentityManagerObserverImpl() override { + identity_manager_->RemoveObserver(this); + } + void OnPrimaryAccountSet( + const CoreAccountInfo& primary_account_info) override { + feed_stream_->OnSignedIn(); + } + void OnPrimaryAccountCleared( + const CoreAccountInfo& previous_primary_account_info) override { + feed_stream_->OnSignedOut(); + } + + private: + signin::IdentityManager* identity_manager_; + FeedStream* feed_stream_; +}; + FeedService::FeedService(std::unique_ptr<FeedStream> stream) : stream_(std::move(stream)) {} @@ -138,11 +170,12 @@ FeedService::FeedService( stream_delegate_ = std::make_unique<StreamDelegateImpl>(local_state, delegate_.get()); network_delegate_ = std::make_unique<NetworkDelegateImpl>(delegate_.get()); - metrics_reporter_ = - std::make_unique<MetricsReporter>(base::DefaultTickClock::GetInstance()); + metrics_reporter_ = std::make_unique<MetricsReporter>( + base::DefaultTickClock::GetInstance(), profile_prefs); feed_network_ = std::make_unique<FeedNetworkImpl>( network_delegate_.get(), identity_manager, api_key, url_loader_factory, - base::DefaultTickClock::GetInstance(), profile_prefs); + base::DefaultTickClock::GetInstance(), profile_prefs, + chrome_info.channel); store_ = std::make_unique<FeedStore>(std::move(database)); stream_ = std::make_unique<FeedStream>( @@ -155,8 +188,10 @@ FeedService::FeedService( history_service, static_cast<FeedStream*>(stream_.get())); stream_delegate_->Initialize(static_cast<FeedStream*>(stream_.get())); -// TODO(harringtond): Call FeedStream::OnSignedIn() -// TODO(harringtond): Call FeedStream::OnSignedOut() + identity_manager_observer_ = std::make_unique<IdentityManagerObserverImpl>( + identity_manager, stream_.get()); + identity_manager->AddObserver(identity_manager_observer_.get()); + #if defined(OS_ANDROID) application_status_listener_ = base::android::ApplicationStatusListener::New(base::BindRepeating( @@ -174,6 +209,12 @@ void FeedService::ClearCachedData() { stream_->OnCacheDataCleared(); } +// static +bool FeedService::IsEnabled(const PrefService& pref_service) { + return base::FeatureList::IsEnabled(feed::kInterestFeedV2) && + pref_service.GetBoolean(feed::prefs::kEnableSnippets); +} + #if defined(OS_ANDROID) void FeedService::OnApplicationStateChange( base::android::ApplicationState state) { diff --git a/chromium/components/feed/core/v2/public/feed_service.h b/chromium/components/feed/core/v2/public/feed_service.h index 45fc8848abd..5fc285594ff 100644 --- a/chromium/components/feed/core/v2/public/feed_service.h +++ b/chromium/components/feed/core/v2/public/feed_service.h @@ -90,10 +90,14 @@ class FeedService : public KeyedService { return refresh_task_scheduler_.get(); } + // Whether Feedv2 is enabled. If false, the FeedService should not be created. + static bool IsEnabled(const PrefService& pref_service); + private: class StreamDelegateImpl; class NetworkDelegateImpl; class HistoryObserverImpl; + class IdentityManagerObserverImpl; #if defined(OS_ANDROID) void OnApplicationStateChange(base::android::ApplicationState state); #endif @@ -108,6 +112,7 @@ class FeedService : public KeyedService { std::unique_ptr<FeedStore> store_; std::unique_ptr<RefreshTaskScheduler> refresh_task_scheduler_; std::unique_ptr<HistoryObserverImpl> history_observer_; + std::unique_ptr<IdentityManagerObserverImpl> identity_manager_observer_; #if defined(OS_ANDROID) bool foregrounded_ = true; std::unique_ptr<base::android::ApplicationStatusListener> 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 6699db20184..9599b82e7e9 100644 --- a/chromium/components/feed/core/v2/public/feed_stream_api.h +++ b/chromium/components/feed/core/v2/public/feed_stream_api.h @@ -8,6 +8,7 @@ #include <string> #include <vector> +#include "base/callback_forward.h" #include "base/observer_list_types.h" #include "components/feed/core/v2/public/types.h" @@ -69,11 +70,19 @@ class FeedStreamApi { // nothing if the model is not yet loaded. virtual EphemeralChangeId CreateEphemeralChange( std::vector<feedstore::DataOperation> operations) = 0; + // Same as |CreateEphemeralChange()|, but data is a serialized + // |feedpacking::DismissData| message. + virtual EphemeralChangeId CreateEphemeralChangeFromPackedData( + base::StringPiece data) = 0; // Commits a change. Returns false if the change does not exist. virtual bool CommitEphemeralChange(EphemeralChangeId id) = 0; // Rejects a change. Returns false if the change does not exist. virtual bool RejectEphemeralChange(EphemeralChangeId id) = 0; + // Sends 'ThereAndBackAgainData' back to the server. |data| is a serialized + // |feedwire::ThereAndBackAgainData| message. + virtual void ProcessThereAndBackAgain(base::StringPiece data) = 0; + // User interaction reporting. These should have no side-effects other than // reporting metrics. @@ -109,6 +118,9 @@ class FeedStreamApi { virtual void ReportContextMenuOpened() = 0; // The user scrolled the feed by |distance_dp| and then stopped. virtual void ReportStreamScrolled(int distance_dp) = 0; + // The user started scrolling the feed. Typically followed by a call to + // |ReportStreamScrolled()|. + virtual void ReportStreamScrollStart() = 0; // The following methods are used for the internals page. diff --git a/chromium/components/feed/core/v2/public/types.h b/chromium/components/feed/core/v2/public/types.h index 0f2ec74f574..cffefa301a2 100644 --- a/chromium/components/feed/core/v2/public/types.h +++ b/chromium/components/feed/core/v2/public/types.h @@ -34,6 +34,11 @@ using EphemeralChangeId = util::IdTypeU32<class EphemeralChangeIdClass>; using SurfaceId = util::IdTypeU32<class SurfaceIdClass>; struct NetworkResponseInfo { + NetworkResponseInfo(); + ~NetworkResponseInfo(); + NetworkResponseInfo(const NetworkResponseInfo&); + NetworkResponseInfo& operator=(const NetworkResponseInfo&); + // A union of net::Error (if the request failed) and the http // status code(if the request succeeded in reaching the server). int32_t status_code = 0; @@ -41,6 +46,7 @@ struct NetworkResponseInfo { base::Time fetch_time; std::string bless_nonce; GURL base_request_url; + size_t response_body_bytes = 0; }; // For the snippets-internals page. diff --git a/chromium/components/feed/core/v2/request_throttler.cc b/chromium/components/feed/core/v2/request_throttler.cc index bfce9c98317..f45767770db 100644 --- a/chromium/components/feed/core/v2/request_throttler.cc +++ b/chromium/components/feed/core/v2/request_throttler.cc @@ -47,7 +47,7 @@ bool RequestThrottler::RequestQuota(NetworkRequestType request_type) { // Fetch request counts from prefs. There's an entry for each request type. // We may need to resize the list. std::vector<int> request_counts = - feed::prefs::GetThrottlerRequestCounts(pref_service_); + feed::prefs::GetThrottlerRequestCounts(*pref_service_); const size_t request_count_index = static_cast<size_t>(request_type); if (request_counts.size() <= request_count_index) request_counts.resize(request_count_index + 1); @@ -56,7 +56,7 @@ bool RequestThrottler::RequestQuota(NetworkRequestType request_type) { if (requests_already_made >= max_requests_per_day) return false; requests_already_made++; - feed::prefs::SetThrottlerRequestCounts(request_counts, pref_service_); + feed::prefs::SetThrottlerRequestCounts(request_counts, *pref_service_); return true; } @@ -65,12 +65,12 @@ void RequestThrottler::ResetCountersIfDayChanged() { // making un-throttled requests to server. const base::Time now = clock_->Now(); const bool day_changed = - DaysSinceOrigin(feed::prefs::GetLastRequestTime(pref_service_)) != + DaysSinceOrigin(feed::prefs::GetLastRequestTime(*pref_service_)) != DaysSinceOrigin(now); - feed::prefs::SetLastRequestTime(now, pref_service_); + feed::prefs::SetLastRequestTime(now, *pref_service_); if (day_changed) - feed::prefs::SetThrottlerRequestCounts({}, pref_service_); + feed::prefs::SetThrottlerRequestCounts({}, *pref_service_); } } // namespace feed diff --git a/chromium/components/feed/core/v2/scheduling.cc b/chromium/components/feed/core/v2/scheduling.cc index d65560e15d6..a1980628061 100644 --- a/chromium/components/feed/core/v2/scheduling.cc +++ b/chromium/components/feed/core/v2/scheduling.cc @@ -5,7 +5,7 @@ #include "components/feed/core/v2/scheduling.h" #include "base/time/time.h" -#include "base/value_conversions.h" +#include "base/util/values/values_util.h" #include "base/values.h" #include "components/feed/core/v2/config.h" @@ -15,7 +15,7 @@ namespace { base::Value VectorToValue(const std::vector<base::TimeDelta>& values) { base::Value result(base::Value::Type::LIST); for (base::TimeDelta delta : values) { - result.Append(CreateTimeDeltaValue(delta)); + result.Append(util::TimeDeltaToValue(delta)); } return result; } @@ -25,10 +25,10 @@ bool ValueToVector(const base::Value& value, if (!value.is_list()) return false; for (const base::Value& entry : value.GetList()) { - base::TimeDelta delta; - if (!GetValueAsTimeDelta(entry, &delta)) + base::Optional<base::TimeDelta> delta = util::ValueToTimeDelta(entry); + if (!delta) return false; - result->push_back(delta); + result->push_back(*delta); } return true; } @@ -41,7 +41,7 @@ RequestSchedule& RequestSchedule::operator=(const RequestSchedule&) = default; base::Value RequestScheduleToValue(const RequestSchedule& schedule) { base::Value result(base::Value::Type::DICTIONARY); - result.SetKey("anchor", CreateTimeValue(schedule.anchor_time)); + result.SetKey("anchor", util::TimeToValue(schedule.anchor_time)); result.SetKey("offsets", VectorToValue(schedule.refresh_offsets)); return result; } @@ -50,14 +50,13 @@ RequestSchedule RequestScheduleFromValue(const base::Value& value) { if (!value.is_dict()) return {}; RequestSchedule result; - const base::Value* anchor = value.FindKey("anchor"); + base::Optional<base::Time> anchor = + util::ValueToTime(value.FindKey("anchor")); const base::Value* offsets = value.FindKeyOfType("offsets", base::Value::Type::LIST); - if (!anchor || !offsets) - return {}; - if (!GetValueAsTime(*anchor, &result.anchor_time) || - !ValueToVector(*offsets, &result.refresh_offsets)) + if (!anchor || !offsets || !ValueToVector(*offsets, &result.refresh_offsets)) return {}; + result.anchor_time = *anchor; return result; } diff --git a/chromium/components/feed/core/v2/stream_model.cc b/chromium/components/feed/core/v2/stream_model.cc index eba0ab62f6c..f0645b5b087 100644 --- a/chromium/components/feed/core/v2/stream_model.cc +++ b/chromium/components/feed/core/v2/stream_model.cc @@ -116,9 +116,6 @@ void StreamModel::Update( } // Update non-tree data. - // TODO(harringtond): Once we start using StreamData.next_action_id, this line - // would be problematic. We should probably move next_action_id into a - // different record in FeedStore. stream_data_ = update_request->stream_data; if (has_clear_all) { @@ -142,11 +139,7 @@ void StreamModel::Update( } } - // TODO(harringtond): Some StreamData fields not yet used. - // next_action_id - do we need to load the model before uploading - // actions? If not, we probably will want to move this out of - // StreamData. - // content_id - probably just ignore for now + // TODO(harringtond): We're not using StreamData's content_id for anything. UpdateFlattenedTree(); } diff --git a/chromium/components/feed/core/v2/stream_model.h b/chromium/components/feed/core/v2/stream_model.h index f3e460b7966..b116eb0af87 100644 --- a/chromium/components/feed/core/v2/stream_model.h +++ b/chromium/components/feed/core/v2/stream_model.h @@ -134,8 +134,8 @@ class StreamModel { Observer* observer_ = nullptr; // Unowned. StoreObserver* store_observer_ = nullptr; // Unowned. - stream_model::ContentIdMap id_map_; - stream_model::FeatureTree base_feature_tree_{&id_map_}; + stream_model::ContentMap content_map_; + stream_model::FeatureTree base_feature_tree_{&content_map_}; // |base_feature_tree_| with |ephemeral_changes_| applied. // Null if there are no ephemeral changes. std::unique_ptr<stream_model::FeatureTree> feature_tree_after_changes_; diff --git a/chromium/components/feed/core/v2/stream_model/ephemeral_change.cc b/chromium/components/feed/core/v2/stream_model/ephemeral_change.cc index 96e8a65a3ef..91baf5e021c 100644 --- a/chromium/components/feed/core/v2/stream_model/ephemeral_change.cc +++ b/chromium/components/feed/core/v2/stream_model/ephemeral_change.cc @@ -41,7 +41,7 @@ std::unique_ptr<FeatureTree> ApplyEphemeralChanges( tree_with_changes->ApplyStreamStructure(operation.structure()); } if (operation.has_content()) { - tree_with_changes->AddContent(operation.content()); + tree_with_changes->CopyAndAddContent(operation.content()); } } } diff --git a/chromium/components/feed/core/v2/stream_model/feature_tree.cc b/chromium/components/feed/core/v2/stream_model/feature_tree.cc index 4e6fef564b7..583eac57da0 100644 --- a/chromium/components/feed/core/v2/stream_model/feature_tree.cc +++ b/chromium/components/feed/core/v2/stream_model/feature_tree.cc @@ -11,11 +11,22 @@ namespace feed { namespace stream_model { +namespace { +std::string ToAsciiForTesting(const std::string& s) { + std::string result = s; + for (size_t i = 0; i < result.size(); ++i) { + if (result[i] < 32 || result[i] > 126) { + result[i] = '?'; + } + } + return result; +} +} // namespace -ContentIdMap::ContentIdMap() = default; -ContentIdMap::~ContentIdMap() = default; +ContentMap::ContentMap() = default; +ContentMap::~ContentMap() = default; -ContentTag ContentIdMap::GetContentTag(const feedwire::ContentId& id) { +ContentTag ContentMap::GetContentTag(const feedwire::ContentId& id) { auto iter = mapping_.find(id); if (iter != mapping_.end()) return iter->second; @@ -24,8 +35,42 @@ ContentTag ContentIdMap::GetContentTag(const feedwire::ContentId& id) { return tag; } -ContentRevision ContentIdMap::NextContentRevision() { - return revision_generator_.GenerateNextId(); +const feedstore::Content* ContentMap::FindContent( + ContentRevision content_revision) { + const size_t index = content_revision.GetUnsafeValue(); + if (revision_to_content_.size() <= index) { + return nullptr; + } + return revision_to_content_[index]; +} + +ContentRevision ContentMap::LookupContentRevision( + const feedstore::Content& content) { + auto iter = content_.find(content); + return (iter != content_.end()) ? iter->second : ContentRevision(); +} + +ContentRevision ContentMap::AddContent(feedstore::Content content) { + auto result = content_.emplace(std::move(content), ContentRevision()); + // Already exists + if (!result.second) + return result.first->second; + + // Newly inserted. + const ContentRevision new_revision = revision_generator_.GenerateNextId(); + result.first->second = new_revision; + if (revision_to_content_.size() <= new_revision.GetUnsafeValue()) { + revision_to_content_.resize(new_revision.GetUnsafeValue() + 1); + } + revision_to_content_[new_revision.GetUnsafeValue()] = &result.first->first; + return new_revision; +} + +void ContentMap::Clear() { + // We don't clear the ID generators, so no IDs are re-used. + mapping_.clear(); + content_.clear(); + revision_to_content_.clear(); } StreamNode::StreamNode() = default; @@ -33,11 +78,11 @@ StreamNode::~StreamNode() = default; StreamNode::StreamNode(const StreamNode&) = default; StreamNode& StreamNode::operator=(const StreamNode&) = default; -FeatureTree::FeatureTree(ContentIdMap* id_map) : id_map_(id_map) {} +FeatureTree::FeatureTree(ContentMap* content_map) : content_map_(content_map) {} FeatureTree::FeatureTree(const FeatureTree* base) : base_(base), - id_map_(base->id_map_), + content_map_(base->content_map_), computed_root_(base->computed_root_), root_tag_(base->root_tag_), nodes_(base->nodes_) {} @@ -59,10 +104,7 @@ StreamNode* FeatureTree::FindNode(ContentTag id) { } const feedstore::Content* FeatureTree::FindContent(ContentRevision id) const { - auto iter = content_.find(id); - if (iter != content_.end()) - return &iter->second; - return base_ ? base_->FindContent(id) : nullptr; + return content_map_->FindContent(id); } void FeatureTree::ApplyStreamStructure( @@ -70,7 +112,11 @@ void FeatureTree::ApplyStreamStructure( switch (structure.operation()) { case feedstore::StreamStructure::CLEAR_ALL: nodes_.clear(); - content_.clear(); + // Clearing content is not required for correctness, but we can do it to + // free memory as long as there's no base feature tree that can reference + // the content. + if (!base_) + content_map_->Clear(); computed_root_ = false; break; case feedstore::StreamStructure::UPDATE_OR_APPEND: { @@ -126,19 +172,19 @@ void FeatureTree::ResizeNodesIfNeeded(ContentTag id) { } void FeatureTree::AddContent(feedstore::Content content) { - AddContent(id_map_->NextContentRevision(), std::move(content)); + const ContentTag tag = GetContentTag(content.content_id()); + const ContentRevision revision_id = + content_map_->AddContent(std::move(content)); + GetOrMakeNode(tag)->content_revision = revision_id; } -void FeatureTree::AddContent(ContentRevision revision_id, - feedstore::Content content) { - // TODO(harringtond): Consider de-duping content. - // Currently, we copy content for ephemeral changes. Both when the ephemeral - // change is created, and when it is committed. We should consider eliminating - // these copies. +void FeatureTree::CopyAndAddContent(const feedstore::Content& content) { + ContentRevision revision_id = content_map_->LookupContentRevision(content); + if (revision_id.is_null()) { + revision_id = content_map_->AddContent(content); + } const ContentTag tag = GetContentTag(content.content_id()); - DCHECK(!content_.count(revision_id)); GetOrMakeNode(tag)->content_revision = revision_id; - content_[revision_id] = std::move(content); } void FeatureTree::ResolveRoot() { @@ -211,7 +257,7 @@ std::string FeatureTree::DumpStateForTesting() { } if (!node->content_revision.is_null()) { const feedstore::Content* content = FindContent(node->content_revision); - ss << " content.frame=" << content->frame(); + ss << " content.frame=" << ToAsciiForTesting(content->frame()); } ss << '\n'; } diff --git a/chromium/components/feed/core/v2/stream_model/feature_tree.h b/chromium/components/feed/core/v2/stream_model/feature_tree.h index 5f5dee45c71..9a9fd2c4a95 100644 --- a/chromium/components/feed/core/v2/stream_model/feature_tree.h +++ b/chromium/components/feed/core/v2/stream_model/feature_tree.h @@ -18,25 +18,36 @@ namespace feed { namespace stream_model { -// Uniquely identifies a feedwire::ContentId. Provided by |ContentIdMap|. +// Uniquely identifies a feedwire::ContentId. Provided by |ContentMap|. using ContentTag = util::IdTypeU32<class ContentTagClass>; using ContentRevision = feed::ContentRevision; -// Maps ContentId into ContentTag, and generates ContentRevision IDs. -class ContentIdMap { +// Owns instances of feedstore::Content pointed to by the feature tree, and +// maps ContentId into ContentTag. +class ContentMap { public: - ContentIdMap(); - ~ContentIdMap(); - ContentIdMap(const ContentIdMap&) = delete; - ContentIdMap& operator=(const ContentIdMap&) = delete; + ContentMap(); + ~ContentMap(); + ContentMap(const ContentMap&) = delete; + ContentMap& operator=(const ContentMap&) = delete; ContentTag GetContentTag(const feedwire::ContentId& id); - ContentRevision NextContentRevision(); + + const feedstore::Content* FindContent(ContentRevision content_revision); + ContentRevision LookupContentRevision(const feedstore::Content& content); + ContentRevision AddContent(feedstore::Content content); + + void Clear(); private: ContentTag::Generator tag_generator_; ContentRevision::Generator revision_generator_; std::map<feedwire::ContentId, ContentTag, ContentIdCompareFunctor> mapping_; + + // These two containers work together to store and index content. + // Each unique piece of content is stored once, and never removed. + std::map<feedstore::Content, ContentRevision, ContentCompareFunctor> content_; + std::vector<const feedstore::Content*> revision_to_content_; }; // A node in FeatureTree. @@ -61,7 +72,7 @@ struct StreamNode { }; // The feature tree which underlies StreamModel. -// This tree is different that most, the rules are as follows: +// This tree is different than most, the rules are as follows: // * A node may or may not have a parent, so this is more of a forest than a // tree. // * When nodes are removed, their set of children are remembered. If the node @@ -78,7 +89,7 @@ class FeatureTree { public: // Constructor. |id_map| is retained by FeatureTree, and must have a greater // scope than FeatureTree. - explicit FeatureTree(ContentIdMap* id_map); + explicit FeatureTree(ContentMap* id_map); // Create a |FeatureTree| which starts as a copy of |base|. // Copies structure from |base|, and keeps a reference for content access. explicit FeatureTree(const FeatureTree* base); @@ -90,8 +101,11 @@ class FeatureTree { // Mutations. void ApplyStreamStructure(const feedstore::StreamStructure& structure); + // Adds |content| to the tree. void AddContent(feedstore::Content content); - void AddContent(ContentRevision revision_id, feedstore::Content content); + // Same as |AddContent()|, but can avoid a copy of |content| if it already + // exists. + void CopyAndAddContent(const feedstore::Content& content); // Data access. @@ -99,7 +113,7 @@ class FeatureTree { StreamNode* FindNode(ContentTag id); const feedstore::Content* FindContent(ContentRevision id) const; ContentTag GetContentTag(const feedwire::ContentId& id) { - return id_map_->GetContentTag(id); + return content_map_->GetContentTag(id); } // Returns the list of content that should be visible. @@ -115,7 +129,7 @@ class FeatureTree { bool RemoveFromParent(StreamNode* parent, ContentTag node_id); const FeatureTree* base_ = nullptr; // Unowned. - ContentIdMap* id_map_; // Unowned. + ContentMap* content_map_; // Unowned. // Finding the root: // We pick the root node as the last STREAM node which has no parent. // In most cases, we can identify the root as the tree is built. @@ -126,9 +140,6 @@ class FeatureTree { // All nodes in the forest, included removed nodes. // This datastructure was selected to make copies efficient. std::vector<StreamNode> nodes_; - // TODO(harringtond): It may be possible to remove old revisions of content - // to save memory. - std::map<ContentRevision, feedstore::Content> content_; }; } // namespace stream_model diff --git a/chromium/components/feed/core/v2/surface_updater.cc b/chromium/components/feed/core/v2/surface_updater.cc index 2ac92f10aa2..ff92bd98248 100644 --- a/chromium/components/feed/core/v2/surface_updater.cc +++ b/chromium/components/feed/core/v2/surface_updater.cc @@ -134,6 +134,7 @@ feedui::ZeroStateSlice::Type GetZeroStateType(LoadStreamStatus status) { case LoadStreamStatus::kProtoTranslationFailed: case LoadStreamStatus::kCannotLoadFromNetworkOffline: case LoadStreamStatus::kCannotLoadFromNetworkThrottled: + case LoadStreamStatus::kNetworkFetchFailed: return feedui::ZeroStateSlice::CANT_REFRESH; case LoadStreamStatus::kNoStatus: case LoadStreamStatus::kLoadedFromStore: @@ -148,6 +149,8 @@ feedui::ZeroStateSlice::Type GetZeroStateType(LoadStreamStatus status) { case LoadStreamStatus::kLoadNotAllowedArticlesListHidden: case LoadStreamStatus::kCannotParseNetworkResponseBody: case LoadStreamStatus::kLoadMoreModelIsNotLoaded: + case LoadStreamStatus::kLoadNotAllowedDisabledByEnterprisePolicy: + case LoadStreamStatus::kCannotLoadMoreNoNextPageToken: break; } return feedui::ZeroStateSlice::NO_CARDS_AVAILABLE; 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 7cd775c37b4..71fff522a38 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 @@ -70,8 +70,6 @@ void LoadStreamFromStoreTask::LoadStreamDone( } } - // TODO(harringtond): Add other failure cases? - std::vector<ContentId> referenced_content_ids; for (const feedstore::StreamStructureSet& structure_set : result.stream_structures) { 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 72bcb2d9bfd..0ca24120e21 100644 --- a/chromium/components/feed/core/v2/tasks/load_stream_task.cc +++ b/chromium/components/feed/core/v2/tasks/load_stream_task.cc @@ -123,9 +123,14 @@ void LoadStreamTask::QueryRequestComplete( network_response_info_ = result.response_info; + if (result.response_info.status_code != 200) + return Done(LoadStreamStatus::kNetworkFetchFailed); + if (!result.response_body) { - Done(LoadStreamStatus::kNoResponseBody); - return; + if (result.response_info.response_body_bytes > 0) + return Done(LoadStreamStatus::kCannotParseNetworkResponseBody); + else + return Done(LoadStreamStatus::kNoResponseBody); } RefreshResponseData response_data = @@ -133,10 +138,8 @@ void LoadStreamTask::QueryRequestComplete( *result.response_body, StreamModelUpdateRequest::Source::kNetworkUpdate, stream_->GetClock()->Now()); - if (!response_data.model_update_request) { - Done(LoadStreamStatus::kProtoTranslationFailed); - return; - } + if (!response_data.model_update_request) + return Done(LoadStreamStatus::kProtoTranslationFailed); stream_->GetStore()->OverwriteStream( std::make_unique<StreamModelUpdateRequest>( diff --git a/chromium/components/feed/core/v2/tasks/upload_actions_task.cc b/chromium/components/feed/core/v2/tasks/upload_actions_task.cc index adcc5c87fbb..bb479d1947b 100644 --- a/chromium/components/feed/core/v2/tasks/upload_actions_task.cc +++ b/chromium/components/feed/core/v2/tasks/upload_actions_task.cc @@ -6,6 +6,7 @@ #include <memory> #include <vector> +#include "base/metrics/histogram_functions.h" #include "base/time/time.h" #include "components/feed/core/proto/v2/store.pb.h" #include "components/feed/core/proto/v2/wire/action_request.pb.h" @@ -15,6 +16,7 @@ #include "components/feed/core/v2/feed_network.h" #include "components/feed/core/v2/feed_store.h" #include "components/feed/core/v2/feed_stream.h" +#include "components/feed/core/v2/metrics_reporter.h" #include "components/feed/core/v2/request_throttler.h" namespace feed { @@ -36,11 +38,6 @@ bool ShouldUpload(const StoredAction& action) { age < GetFeedConfig().max_action_age; } -void ReportBatchStatus(UploadActionsBatchStatus status) { - // TODO(iwells): Get rid of the DVLOG and record status to a histogram. - DVLOG(1) << "UploadActionsBatchStatus: " << status; -} - } // namespace class UploadActionsTask::Batch { @@ -196,19 +193,10 @@ void UploadActionsTask::UploadPendingActions() { } void UploadActionsTask::UpdateAndUploadNextBatch() { - // Finish if all pending actions have been visited. - if (pending_actions_.empty()) { - ReportBatchStatus(UploadActionsBatchStatus::kSuccessfullyUploadedBatch); - UpdateTokenAndFinish(); - return; - } - // Finish if there's no quota remaining for actions uploads. if (!stream_->GetRequestThrottler()->RequestQuota( NetworkRequestType::kUploadActions)) { - ReportBatchStatus(UploadActionsBatchStatus::kExhaustedUploadQuota); - UpdateTokenAndFinish(); - return; + return BatchComplete(UploadActionsBatchStatus::kExhaustedUploadQuota); } // Grab a few actions to be processed and erase them from pending_actions_. @@ -228,18 +216,13 @@ void UploadActionsTask::OnUpdateActionsFinished( std::unique_ptr<UploadActionsTask::Batch> batch, bool update_ok) { // Stop if there are no actions to upload. - if (batch->UploadCount() == 0ul) { - ReportBatchStatus(UploadActionsBatchStatus::kAllActionsWereStale); - UpdateTokenAndFinish(); - return; - } + if (batch->UploadCount() == 0ul) + return BatchComplete(UploadActionsBatchStatus::kAllActionsWereStale); + + // Skip uploading batch if these actions couldn't be updated in the store. + if (!update_ok) + return BatchComplete(UploadActionsBatchStatus::kFailedToUpdateStore); - // Skip uploading if these actions couldn't be updated in the store. - if (!update_ok) { - ReportBatchStatus(UploadActionsBatchStatus::kFailedToUpdateStore); - UpdateAndUploadNextBatch(); - return; - } upload_attempt_count_ += batch->UploadCount(); stale_count_ += batch->StaleCount(); @@ -264,11 +247,8 @@ void UploadActionsTask::OnUpdateActionsFinished( void UploadActionsTask::OnUploadFinished( std::unique_ptr<UploadActionsTask::Batch> batch, FeedNetwork::ActionRequestResult result) { - if (!result.response_body) { - ReportBatchStatus(UploadActionsBatchStatus::kFailedToUpload); - UpdateAndUploadNextBatch(); - return; - } + if (!result.response_body) + return BatchComplete(UploadActionsBatchStatus::kFailedToUpload); consistency_token_ = std::move(result.response_body->feed_response() .feed_response() @@ -282,22 +262,33 @@ void UploadActionsTask::OnUploadFinished( } void UploadActionsTask::OnUploadedActionsRemoved(bool remove_ok) { - ReportBatchStatus(UploadActionsBatchStatus::kFailedToRemoveUploadedActions); + if (remove_ok) + BatchComplete(UploadActionsBatchStatus::kSuccessfullyUploadedBatch); + else + BatchComplete(UploadActionsBatchStatus::kFailedToRemoveUploadedActions); +} + +void UploadActionsTask::BatchComplete(UploadActionsBatchStatus status) { + MetricsReporter::OnUploadActionsBatch(status); + + if (pending_actions_.empty() || + status == UploadActionsBatchStatus::kExhaustedUploadQuota || + status == UploadActionsBatchStatus::kAllActionsWereStale) { + return UpdateTokenAndFinish(); + } UpdateAndUploadNextBatch(); } void UploadActionsTask::UpdateTokenAndFinish() { - if (consistency_token_.empty()) { - Done(UploadActionsStatus::kFinishedWithoutUpdatingConsistencyToken); - return; - } + if (consistency_token_.empty()) + return Done(UploadActionsStatus::kFinishedWithoutUpdatingConsistencyToken); stream_->GetMetadata()->SetConsistencyToken(consistency_token_); Done(UploadActionsStatus::kUpdatedConsistencyToken); } void UploadActionsTask::Done(UploadActionsStatus status) { - DVLOG(1) << "UploadActionsTask finished with status " << status; + MetricsReporter::OnUploadActions(status); std::move(callback_).Run({status, upload_attempt_count_, stale_count_}); TaskComplete(); } diff --git a/chromium/components/feed/core/v2/tasks/upload_actions_task.h b/chromium/components/feed/core/v2/tasks/upload_actions_task.h index d543a22ec95..00d37ae09e2 100644 --- a/chromium/components/feed/core/v2/tasks/upload_actions_task.h +++ b/chromium/components/feed/core/v2/tasks/upload_actions_task.h @@ -87,6 +87,7 @@ class UploadActionsTask : public offline_pages::Task { FeedNetwork::ActionRequestResult result); void OnUploadedActionsRemoved(bool remove_ok); void UpdateTokenAndFinish(); + void BatchComplete(UploadActionsBatchStatus status); void Done(UploadActionsStatus status); FeedStream* stream_; diff --git a/chromium/components/feed/core/v2/types.cc b/chromium/components/feed/core/v2/types.cc index b3b0267e3ee..240a78903bb 100644 --- a/chromium/components/feed/core/v2/types.cc +++ b/chromium/components/feed/core/v2/types.cc @@ -9,6 +9,8 @@ #include "base/base64.h" #include "base/pickle.h" #include "base/strings/string_number_conversions.h" +#include "base/util/values/values_util.h" +#include "base/values.h" #include "components/feed/core/v2/public/types.h" // Note: This file contains implementation for both types.h and public/types.h. @@ -66,6 +68,12 @@ base::Optional<DebugStreamData> UnpickleDebugStreamData( } // namespace +NetworkResponseInfo::NetworkResponseInfo() = default; +NetworkResponseInfo::~NetworkResponseInfo() = default; +NetworkResponseInfo::NetworkResponseInfo(const NetworkResponseInfo&) = default; +NetworkResponseInfo& NetworkResponseInfo::operator=( + const NetworkResponseInfo&) = default; + std::string ToString(ContentRevision c) { return base::NumberToString(c.value()); } @@ -99,4 +107,30 @@ DebugStreamData::~DebugStreamData() = default; DebugStreamData::DebugStreamData(const DebugStreamData&) = default; DebugStreamData& DebugStreamData::operator=(const DebugStreamData&) = default; +base::Value PersistentMetricsDataToValue(const PersistentMetricsData& data) { + base::Value dict(base::Value::Type::DICTIONARY); + dict.SetKey("day_start", util::TimeToValue(data.current_day_start)); + dict.SetKey("time_spent_in_feed", + util::TimeDeltaToValue(data.accumulated_time_spent_in_feed)); + return dict; +} + +PersistentMetricsData PersistentMetricsDataFromValue(const base::Value& value) { + PersistentMetricsData result; + if (!value.is_dict()) + return result; + base::Optional<base::Time> day_start = + util::ValueToTime(value.FindKey("day_start")); + if (!day_start) + return result; + result.current_day_start = *day_start; + base::Optional<base::TimeDelta> time_spent_in_feed = + util::ValueToTimeDelta(value.FindKey("time_spent_in_feed")); + if (time_spent_in_feed) { + result.accumulated_time_spent_in_feed = *time_spent_in_feed; + } + + return result; +} + } // namespace feed diff --git a/chromium/components/feed/core/v2/types.h b/chromium/components/feed/core/v2/types.h index 4b6bf0b0238..02ecc6a2ac2 100644 --- a/chromium/components/feed/core/v2/types.h +++ b/chromium/components/feed/core/v2/types.h @@ -8,6 +8,7 @@ #include <string> #include "base/util/type_safety/id_type.h" +#include "base/values.h" #include "components/feed/core/v2/public/types.h" namespace feed { @@ -34,6 +35,17 @@ struct RequestMetadata { DisplayMetrics display_metrics; }; +// Data internal to MetricsReporter which is persisted to Prefs. +struct PersistentMetricsData { + // The midnight time for the day in which this metric was recorded. + base::Time current_day_start; + // The total recorded time spent on the Feed for the current day. + base::TimeDelta accumulated_time_spent_in_feed; +}; + +base::Value PersistentMetricsDataToValue(const PersistentMetricsData& data); +PersistentMetricsData PersistentMetricsDataFromValue(const base::Value& value); + } // namespace feed #endif // COMPONENTS_FEED_CORE_V2_TYPES_H_ diff --git a/chromium/components/feed/core/v2/types_unittest.cc b/chromium/components/feed/core/v2/types_unittest.cc index c7dcfd33eba..95938d4f2bf 100644 --- a/chromium/components/feed/core/v2/types_unittest.cc +++ b/chromium/components/feed/core/v2/types_unittest.cc @@ -4,10 +4,22 @@ #include "components/feed/core/v2/types.h" +#include "base/json/json_writer.h" +#include "base/strings/string_util.h" #include "testing/gtest/include/gtest/gtest.h" namespace feed { namespace { + +std::string ToJSON(const base::Value& value) { + std::string json; + CHECK(base::JSONWriter::WriteWithOptions( + value, base::JSONWriter::OPTIONS_PRETTY_PRINT, &json)); + // Don't use \r\n on windows. + base::RemoveChars(json, "\r", &json); + return json; +} + DebugStreamData MakeDebugStreamData() { NetworkResponseInfo fetch_info; fetch_info.status_code = 200; @@ -60,4 +72,24 @@ TEST(DebugStreamData, FailsDeserializationGracefully) { ASSERT_EQ(base::nullopt, DeserializeDebugStreamData({})); } +TEST(PersistentMetricsData, SerializesAndDeserializes) { + PersistentMetricsData data; + data.accumulated_time_spent_in_feed = base::TimeDelta::FromHours(2); + data.current_day_start = base::Time::UnixEpoch(); + + const base::Value serialized_value = PersistentMetricsDataToValue(data); + const PersistentMetricsData deserialized_value = + PersistentMetricsDataFromValue(serialized_value); + + EXPECT_EQ(R"({ + "day_start": "11644473600000000", + "time_spent_in_feed": "7200000000" +} +)", + ToJSON(serialized_value)); + EXPECT_EQ(data.accumulated_time_spent_in_feed, + deserialized_value.accumulated_time_spent_in_feed); + EXPECT_EQ(data.current_day_start, deserialized_value.current_day_start); +} + } // namespace feed diff --git a/chromium/components/feed/feed_feature_list.cc b/chromium/components/feed/feed_feature_list.cc index f6b51cb172a..f19e413151e 100644 --- a/chromium/components/feed/feed_feature_list.cc +++ b/chromium/components/feed/feed_feature_list.cc @@ -8,6 +8,9 @@ namespace feed { const base::Feature kInterestFeedContentSuggestions{ "InterestFeedContentSuggestions", base::FEATURE_ENABLED_BY_DEFAULT}; +// InterestFeedV2 takes precedence over InterestFeedContentSuggestions. +const base::Feature kInterestFeedV2{"InterestFeedV2", + base::FEATURE_DISABLED_BY_DEFAULT}; const base::FeatureParam<std::string> kDisableTriggerTypes{ &kInterestFeedContentSuggestions, "disable_trigger_types", ""}; @@ -30,8 +33,6 @@ const base::Feature kInterestFeedFeedback{"InterestFeedFeedback", const base::Feature kReportFeedUserActions{"ReportFeedUserActions", base::FEATURE_DISABLED_BY_DEFAULT}; -const base::Feature kInterestFeedV2{"InterestFeedV2", - base::FEATURE_DISABLED_BY_DEFAULT}; } // namespace feed diff --git a/chromium/components/feed/feed_feature_list.h b/chromium/components/feed/feed_feature_list.h index f26edc66376..432f56d98de 100644 --- a/chromium/components/feed/feed_feature_list.h +++ b/chromium/components/feed/feed_feature_list.h @@ -13,6 +13,7 @@ namespace feed { extern const base::Feature kInterestFeedContentSuggestions; +extern const base::Feature kInterestFeedV2; extern const base::FeatureParam<std::string> kDisableTriggerTypes; extern const base::FeatureParam<int> kSuppressRefreshDurationMinutes; @@ -28,7 +29,6 @@ extern const base::Feature kInterestFeedFeedback; // for personalization. Also enables the feed header menu to manage the feed. extern const base::Feature kReportFeedUserActions; -extern const base::Feature kInterestFeedV2; } // namespace feed |