on_device_clustering_backend_unittest.cc 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  1. // Copyright 2021 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "components/history_clusters/core/on_device_clustering_backend.h"
  5. #include "base/containers/flat_set.h"
  6. #include "base/run_loop.h"
  7. #include "base/test/metrics/histogram_tester.h"
  8. #include "base/test/scoped_feature_list.h"
  9. #include "base/test/task_environment.h"
  10. #include "components/history_clusters/core/clustering_test_utils.h"
  11. #include "components/history_clusters/core/config.h"
  12. #include "components/history_clusters/core/on_device_clustering_features.h"
  13. #include "components/optimization_guide/core/entity_metadata_provider.h"
  14. #include "components/optimization_guide/core/new_optimization_guide_decider.h"
  15. #include "components/site_engagement/core/site_engagement_score_provider.h"
  16. #include "testing/gmock/include/gmock/gmock.h"
  17. #include "testing/gtest/include/gtest/gtest.h"
  18. namespace history_clusters {
  19. namespace {
  20. using ::testing::ElementsAre;
  21. using ::testing::FloatEq;
  22. using ::testing::UnorderedElementsAre;
  23. class TestSiteEngagementScoreProvider
  24. : public site_engagement::SiteEngagementScoreProvider {
  25. public:
  26. TestSiteEngagementScoreProvider() = default;
  27. ~TestSiteEngagementScoreProvider() = default;
  28. double GetScore(const GURL& url) const override {
  29. ++count_get_score_invocations_;
  30. return 0;
  31. }
  32. double GetTotalEngagementPoints() const override { return 1; }
  33. size_t count_get_score_invocations() const {
  34. return count_get_score_invocations_;
  35. }
  36. private:
  37. mutable size_t count_get_score_invocations_ = 0;
  38. };
  39. class TestEntityMetadataProvider
  40. : public optimization_guide::EntityMetadataProvider {
  41. public:
  42. explicit TestEntityMetadataProvider(
  43. scoped_refptr<base::SingleThreadTaskRunner> main_thread_task_runner)
  44. : main_thread_task_runner_(main_thread_task_runner) {}
  45. ~TestEntityMetadataProvider() override = default;
  46. // EntityMetadataProvider:
  47. void GetMetadataForEntityId(
  48. const std::string& entity_id,
  49. optimization_guide::EntityMetadataRetrievedCallback callback) override {
  50. main_thread_task_runner_->PostTask(
  51. FROM_HERE,
  52. base::BindOnce(
  53. [](const std::string& entity_id,
  54. optimization_guide::EntityMetadataRetrievedCallback callback) {
  55. optimization_guide::EntityMetadata metadata;
  56. metadata.human_readable_name = "rewritten-" + entity_id;
  57. // Add it in twice to verify that a category only gets added once
  58. // and it takes the max.
  59. metadata.human_readable_categories.insert(
  60. {"category-" + entity_id, 0.6});
  61. metadata.human_readable_categories.insert(
  62. {"category-" + entity_id, 0.5});
  63. metadata.human_readable_categories.insert(
  64. {"toolow-" + entity_id, 0.01});
  65. metadata.human_readable_aliases.push_back("alias-" + entity_id);
  66. std::move(callback).Run(entity_id == "nometadata"
  67. ? absl::nullopt
  68. : absl::make_optional(metadata));
  69. },
  70. entity_id, std::move(callback)));
  71. }
  72. private:
  73. scoped_refptr<base::SingleThreadTaskRunner> main_thread_task_runner_;
  74. };
  75. class TestOptimizationGuideDecider
  76. : public optimization_guide::NewOptimizationGuideDecider {
  77. public:
  78. TestOptimizationGuideDecider() = default;
  79. ~TestOptimizationGuideDecider() override = default;
  80. void RegisterOptimizationTypes(
  81. const std::vector<optimization_guide::proto::OptimizationType>&
  82. optimization_types) override {
  83. ASSERT_EQ(optimization_types.size(), 1u);
  84. ASSERT_EQ(optimization_guide::proto::HISTORY_CLUSTERS,
  85. optimization_types[0]);
  86. }
  87. void CanApplyOptimization(
  88. const GURL& url,
  89. optimization_guide::proto::OptimizationType optimization_type,
  90. optimization_guide::OptimizationGuideDecisionCallback callback) override {
  91. NOTREACHED();
  92. }
  93. optimization_guide::OptimizationGuideDecision CanApplyOptimization(
  94. const GURL& url,
  95. optimization_guide::proto::OptimizationType optimization_type,
  96. optimization_guide::OptimizationMetadata* optimization_metadata)
  97. override {
  98. DCHECK_EQ(optimization_guide::proto::HISTORY_CLUSTERS, optimization_type);
  99. return url.host() == "shouldskip.com"
  100. ? optimization_guide::OptimizationGuideDecision::kFalse
  101. : optimization_guide::OptimizationGuideDecision::kTrue;
  102. }
  103. void CanApplyOptimizationOnDemand(
  104. const std::vector<GURL>& urls,
  105. const base::flat_set<optimization_guide::proto::OptimizationType>&
  106. optimization_types,
  107. optimization_guide::proto::RequestContext request_context,
  108. optimization_guide::OnDemandOptimizationGuideDecisionRepeatingCallback
  109. callback) override {}
  110. };
  111. class OnDeviceClusteringWithoutContentBackendTest : public ::testing::Test {
  112. public:
  113. OnDeviceClusteringWithoutContentBackendTest() {
  114. config_.content_clustering_enabled = false;
  115. config_.keyword_filter_on_categories = true;
  116. config_.keyword_filter_on_noisy_visits = true;
  117. config_.keyword_filter_on_entity_aliases = true;
  118. config_.max_entity_aliases_in_keywords = 100;
  119. config_.split_clusters_at_search_visits = false;
  120. config_.should_label_clusters = false;
  121. config_.entity_relevance_threshold = 60;
  122. config_.should_check_hosts_to_skip_clustering_for = true;
  123. SetConfigForTesting(config_);
  124. }
  125. void SetUp() override {
  126. clustering_backend_ = std::make_unique<OnDeviceClusteringBackend>(
  127. /*entity_metadata_provider=*/nullptr, &test_site_engagement_provider_,
  128. /*optimization_guide_decider_=*/nullptr,
  129. /*mid_blocklist_=*/base::flat_set<std::string>({"blockedentity"}));
  130. }
  131. void TearDown() override { clustering_backend_.reset(); }
  132. std::vector<history::Cluster> ClusterVisits(
  133. ClusteringRequestSource clustering_request_source,
  134. const std::vector<history::AnnotatedVisit>& visits) {
  135. std::vector<history::Cluster> clusters;
  136. base::RunLoop run_loop;
  137. clustering_backend_->GetClusters(
  138. clustering_request_source,
  139. base::BindOnce(
  140. [](base::RunLoop* run_loop,
  141. std::vector<history::Cluster>* out_clusters,
  142. std::vector<history::Cluster> clusters) {
  143. *out_clusters = std::move(clusters);
  144. run_loop->Quit();
  145. },
  146. &run_loop, &clusters),
  147. visits);
  148. run_loop.Run();
  149. return clusters;
  150. }
  151. size_t GetSiteEngagementGetScoreInvocationCount() const {
  152. return test_site_engagement_provider_.count_get_score_invocations();
  153. }
  154. protected:
  155. std::unique_ptr<OnDeviceClusteringBackend> clustering_backend_;
  156. base::test::TaskEnvironment task_environment_;
  157. private:
  158. Config config_;
  159. TestSiteEngagementScoreProvider test_site_engagement_provider_;
  160. };
  161. TEST_F(OnDeviceClusteringWithoutContentBackendTest, ClusterNoVisits) {
  162. EXPECT_TRUE(
  163. ClusterVisits(ClusteringRequestSource::kJourneysPage, {}).empty());
  164. }
  165. TEST_F(OnDeviceClusteringWithoutContentBackendTest, ClusterOneVisit) {
  166. std::vector<history::AnnotatedVisit> visits;
  167. // Fill in the visits vector with 1 visit.
  168. history::AnnotatedVisit visit =
  169. testing::CreateDefaultAnnotatedVisit(1, GURL("https://google.com/"));
  170. visits.push_back(visit);
  171. std::vector<history::Cluster> result_clusters =
  172. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  173. EXPECT_THAT(testing::ToVisitResults(result_clusters),
  174. ElementsAre(ElementsAre(testing::VisitResult(1, 1.0))));
  175. }
  176. TEST_F(OnDeviceClusteringWithoutContentBackendTest,
  177. ClusterTwoVisitsTiedByReferringVisit) {
  178. base::HistogramTester histogram_tester;
  179. std::vector<history::AnnotatedVisit> visits;
  180. // Visit2's referrer is visit 1 and are close together.
  181. history::AnnotatedVisit visit = testing::CreateDefaultAnnotatedVisit(
  182. 1, GURL("https://google.com/"), base::Time::FromTimeT(1));
  183. visit.content_annotations.model_annotations.categories = {
  184. {"google-category", 100}, {"com", 100}};
  185. visits.push_back(visit);
  186. history::AnnotatedVisit visit2 = testing::CreateDefaultAnnotatedVisit(
  187. 2, GURL("https://google.com/next"), base::Time::FromTimeT(2));
  188. visit2.content_annotations.model_annotations.entities = {
  189. {"google-entity", 100}, {"com", 100}};
  190. visit2.referring_visit_of_redirect_chain_start = 1;
  191. visits.push_back(visit2);
  192. std::vector<history::Cluster> result_clusters =
  193. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  194. EXPECT_THAT(testing::ToVisitResults(result_clusters),
  195. ElementsAre(ElementsAre(testing::VisitResult(2, 1.0),
  196. testing::VisitResult(1, 1.0))));
  197. ASSERT_EQ(result_clusters.size(), 1u);
  198. EXPECT_THAT(result_clusters.at(0).GetKeywords(),
  199. UnorderedElementsAre(std::u16string(u"google-category"),
  200. std::u16string(u"com"),
  201. std::u16string(u"google-entity")));
  202. EXPECT_FALSE(result_clusters[0].label.has_value());
  203. histogram_tester.ExpectUniqueSample(
  204. "History.Clusters.Backend.ClusterSize.Min", 2, 1);
  205. histogram_tester.ExpectUniqueSample(
  206. "History.Clusters.Backend.ClusterSize.Max", 2, 1);
  207. histogram_tester.ExpectUniqueSample(
  208. "History.Clusters.Backend.NumKeywordsPerCluster.Min", 3, 1);
  209. histogram_tester.ExpectUniqueSample(
  210. "History.Clusters.Backend.NumKeywordsPerCluster.Max", 3, 1);
  211. }
  212. TEST_F(OnDeviceClusteringWithoutContentBackendTest,
  213. ClusterTwoVisitsTiedByOpenerVisit) {
  214. base::HistogramTester histogram_tester;
  215. std::vector<history::AnnotatedVisit> visits;
  216. // Visit2's referrer is visit 1 and are close together.
  217. history::AnnotatedVisit visit = testing::CreateDefaultAnnotatedVisit(
  218. 1, GURL("https://google.com/"), base::Time::FromTimeT(1));
  219. visits.push_back(visit);
  220. history::AnnotatedVisit visit2 = testing::CreateDefaultAnnotatedVisit(
  221. 2, GURL("https://google.com/next"), base::Time::FromTimeT(2));
  222. visit2.opener_visit_of_redirect_chain_start = 1;
  223. visits.push_back(visit2);
  224. std::vector<history::Cluster> result_clusters =
  225. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  226. EXPECT_THAT(testing::ToVisitResults(result_clusters),
  227. ElementsAre(ElementsAre(testing::VisitResult(2, 1.0),
  228. testing::VisitResult(1, 1.0))));
  229. histogram_tester.ExpectUniqueSample(
  230. "History.Clusters.Backend.ClusterSize.Min", 2, 1);
  231. histogram_tester.ExpectUniqueSample(
  232. "History.Clusters.Backend.ClusterSize.Max", 2, 1);
  233. histogram_tester.ExpectUniqueSample(
  234. "History.Clusters.Backend.NumKeywordsPerCluster.Min", 0, 1);
  235. histogram_tester.ExpectUniqueSample(
  236. "History.Clusters.Backend.NumKeywordsPerCluster.Max", 0, 1);
  237. }
  238. TEST_F(OnDeviceClusteringWithoutContentBackendTest, ClusterTwoVisitsTiedByURL) {
  239. base::HistogramTester histogram_tester;
  240. std::vector<history::AnnotatedVisit> visits;
  241. // Visit2 has the same URL as Visit1.
  242. history::AnnotatedVisit visit = testing::CreateDefaultAnnotatedVisit(
  243. 1, GURL("https://google.com/"), base::Time::FromTimeT(1));
  244. visits.push_back(visit);
  245. history::AnnotatedVisit visit2 = testing::CreateDefaultAnnotatedVisit(
  246. 2, GURL("https://google.com/"), base::Time::FromTimeT(2));
  247. visits.push_back(visit2);
  248. std::vector<history::Cluster> result_clusters =
  249. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  250. EXPECT_THAT(testing::ToVisitResults(result_clusters),
  251. ElementsAre(ElementsAre(testing::VisitResult(
  252. 2, 1.0, {testing::VisitResult(1, 0.0)}))));
  253. histogram_tester.ExpectUniqueSample(
  254. "History.Clusters.Backend.ClusterSize.Min", 1, 1);
  255. histogram_tester.ExpectUniqueSample(
  256. "History.Clusters.Backend.ClusterSize.Max", 1, 1);
  257. histogram_tester.ExpectUniqueSample(
  258. "History.Clusters.Backend.NumKeywordsPerCluster.Min", 0, 1);
  259. histogram_tester.ExpectUniqueSample(
  260. "History.Clusters.Backend.NumKeywordsPerCluster.Max", 0, 1);
  261. }
  262. TEST_F(OnDeviceClusteringWithoutContentBackendTest, DedupeClusters) {
  263. std::vector<history::AnnotatedVisit> visits;
  264. // Visit2 has the same URL as Visit1.
  265. history::AnnotatedVisit visit = testing::CreateDefaultAnnotatedVisit(
  266. 1, GURL("https://google.com/"), base::Time::FromTimeT(1));
  267. visits.push_back(visit);
  268. history::AnnotatedVisit visit2 = testing::CreateDefaultAnnotatedVisit(
  269. 2, GURL("https://google.com/"), base::Time::FromTimeT(2));
  270. visits.push_back(visit2);
  271. std::vector<history::Cluster> result_clusters =
  272. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  273. EXPECT_THAT(testing::ToVisitResults(result_clusters),
  274. ElementsAre(ElementsAre(testing::VisitResult(
  275. 2, 1.0, {testing::VisitResult(1, 0.0)}))));
  276. }
  277. TEST_F(OnDeviceClusteringWithoutContentBackendTest,
  278. DedupeRespectsDifferentURLs) {
  279. std::vector<history::AnnotatedVisit> visits;
  280. // Visit2 has a different URL but is linked by referring id.
  281. history::AnnotatedVisit visit = testing::CreateDefaultAnnotatedVisit(
  282. 1, GURL("https://google.com/"), base::Time::FromTimeT(1));
  283. visits.push_back(visit);
  284. history::AnnotatedVisit visit2 = testing::CreateDefaultAnnotatedVisit(
  285. 2, GURL("https://foo.com/"), base::Time::FromTimeT(2));
  286. visit2.referring_visit_of_redirect_chain_start = 1;
  287. visits.push_back(visit2);
  288. std::vector<history::Cluster> result_clusters =
  289. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  290. EXPECT_THAT(testing::ToVisitResults(result_clusters),
  291. ElementsAre(ElementsAre(testing::VisitResult(2, 1.0),
  292. testing::VisitResult(1, 1.0))));
  293. }
  294. TEST_F(OnDeviceClusteringWithoutContentBackendTest, MultipleClusters) {
  295. base::HistogramTester histogram_tester;
  296. std::vector<history::AnnotatedVisit> visits;
  297. // Visit2's referrer is visit 1 and visit 4 is a back navigation from visit 2.
  298. // Visit 3 is a different journey altogether. Visit 10 is referring to a
  299. // missing visit and should be considered as in its own cluster.
  300. // Also, make sure these aren't sorted so we test that we are sorting the
  301. // visits by visit ID.
  302. history::AnnotatedVisit visit = testing::CreateDefaultAnnotatedVisit(
  303. 1, GURL("https://github.com/"), base::Time::FromTimeT(1));
  304. visits.push_back(visit);
  305. history::AnnotatedVisit visit2 = testing::CreateDefaultAnnotatedVisit(
  306. 2, GURL("https://google.com/"), base::Time::FromTimeT(2));
  307. visit2.referring_visit_of_redirect_chain_start = 1;
  308. // Set the visit duration to be 2x the default so it has the same duration
  309. // after |visit| and |visit4| are deduped.
  310. visit2.visit_row.visit_duration = base::Seconds(20);
  311. visits.push_back(visit2);
  312. history::AnnotatedVisit visit4 = testing::CreateDefaultAnnotatedVisit(
  313. 4, GURL("https://github.com/"), base::Time::FromTimeT(4));
  314. visits.push_back(visit4);
  315. history::AnnotatedVisit visit5 = testing::CreateDefaultAnnotatedVisit(
  316. 10, GURL("https://nonexistentreferrer.com/"), base::Time::FromTimeT(10));
  317. visit5.referring_visit_of_redirect_chain_start = 6;
  318. visits.push_back(visit5);
  319. // Although it says shouldskip, it should not be skipped since there is no
  320. // optimization guide decider.
  321. history::AnnotatedVisit visit3 = testing::CreateDefaultAnnotatedVisit(
  322. 3, GURL("https://shouldskip.com/butnotsincenodecider"),
  323. base::Time::FromTimeT(3));
  324. visits.push_back(visit3);
  325. std::vector<history::Cluster> result_clusters =
  326. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  327. EXPECT_THAT(
  328. testing::ToVisitResults(result_clusters),
  329. ElementsAre(ElementsAre(testing::VisitResult(10, 1.0)),
  330. ElementsAre(testing::VisitResult(
  331. 4, 1.0, {testing::VisitResult(1, 0.0)}),
  332. testing::VisitResult(2, 1.0)),
  333. ElementsAre(testing::VisitResult(3, 1.0))));
  334. histogram_tester.ExpectUniqueSample(
  335. "History.Clusters.Backend.ClusterSize.Min", 1, 1);
  336. histogram_tester.ExpectUniqueSample(
  337. "History.Clusters.Backend.ClusterSize.Max", 2, 1);
  338. histogram_tester.ExpectUniqueSample(
  339. "History.Clusters.Backend.NumKeywordsPerCluster.Min", 0, 1);
  340. histogram_tester.ExpectUniqueSample(
  341. "History.Clusters.Backend.NumKeywordsPerCluster.Max", 0, 1);
  342. // This is coming from the Journeys page so expect that the per-cluster
  343. // metrics are not collected.
  344. histogram_tester.ExpectTotalCount(
  345. "History.Clusters.Backend.ClusterContainsSearch", 0);
  346. histogram_tester.ExpectTotalCount(
  347. "History.Clusters.Backend.NumKeywordsPerCluster", 0);
  348. histogram_tester.ExpectTotalCount(
  349. "History.Clusters.Backend.NumVisitsPerCluster", 0);
  350. }
  351. TEST_F(OnDeviceClusteringWithoutContentBackendTest,
  352. SplitClusterOnNavigationTime) {
  353. base::HistogramTester histogram_tester;
  354. std::vector<history::AnnotatedVisit> visits;
  355. history::AnnotatedVisit visit =
  356. testing::CreateDefaultAnnotatedVisit(1, GURL("https://google.com/"));
  357. visit.visit_row.visit_time = base::Time::Now();
  358. visits.push_back(visit);
  359. // Visit2 has a different URL but is linked by referring id to visit.
  360. history::AnnotatedVisit visit2 =
  361. testing::CreateDefaultAnnotatedVisit(2, GURL("https://bar.com/"));
  362. visit2.referring_visit_of_redirect_chain_start = 1;
  363. visit2.visit_row.visit_time = base::Time::Now() + base::Minutes(5);
  364. visits.push_back(visit2);
  365. // Visit3 has a different URL but is linked by referring id to visit but the
  366. // cutoff has passed so it should be in a different cluster.
  367. history::AnnotatedVisit visit3 =
  368. testing::CreateDefaultAnnotatedVisit(3, GURL("https://foo.com/"));
  369. visit3.referring_visit_of_redirect_chain_start = 1;
  370. visit3.visit_row.visit_time = base::Time::Now() + base::Hours(2);
  371. visits.push_back(visit3);
  372. std::vector<history::Cluster> result_clusters =
  373. ClusterVisits(ClusteringRequestSource::kKeywordCacheGeneration, visits);
  374. EXPECT_THAT(testing::ToVisitResults(result_clusters),
  375. ElementsAre(ElementsAre(testing::VisitResult(3, 1.0)),
  376. ElementsAre(testing::VisitResult(2, 1.0),
  377. testing::VisitResult(1, 1.0))));
  378. histogram_tester.ExpectUniqueSample(
  379. "History.Clusters.Backend.ClusterSize.Min", 1, 1);
  380. histogram_tester.ExpectUniqueSample(
  381. "History.Clusters.Backend.ClusterSize.Max", 2, 1);
  382. histogram_tester.ExpectUniqueSample(
  383. "History.Clusters.Backend.NumKeywordsPerCluster.Min", 0, 1);
  384. histogram_tester.ExpectUniqueSample(
  385. "History.Clusters.Backend.NumKeywordsPerCluster.Max", 0, 1);
  386. // This is coming from the keyword cache generation so expect that the
  387. // per-cluster metrics are collected.
  388. histogram_tester.ExpectUniqueSample(
  389. "History.Clusters.Backend.ClusterContainsSearch", false, 2);
  390. histogram_tester.ExpectUniqueSample(
  391. "History.Clusters.Backend.NumKeywordsPerCluster", 0, 2);
  392. histogram_tester.ExpectTotalCount(
  393. "History.Clusters.Backend.NumVisitsPerCluster", 2);
  394. histogram_tester.ExpectBucketCount(
  395. "History.Clusters.Backend.NumVisitsPerCluster", 1, 1);
  396. histogram_tester.ExpectBucketCount(
  397. "History.Clusters.Backend.NumVisitsPerCluster", 2, 1);
  398. }
  399. class OnDeviceClusteringWithContentBackendTest
  400. : public OnDeviceClusteringWithoutContentBackendTest {
  401. public:
  402. OnDeviceClusteringWithContentBackendTest() {
  403. config_.content_clustering_enabled = true;
  404. config_.keyword_filter_on_categories = true;
  405. config_.keyword_filter_on_noisy_visits = true;
  406. config_.keyword_filter_on_entity_aliases = true;
  407. config_.should_check_hosts_to_skip_clustering_for = false;
  408. SetConfigForTesting(config_);
  409. }
  410. private:
  411. Config config_;
  412. };
  413. TEST_F(OnDeviceClusteringWithContentBackendTest, ClusterOnContent) {
  414. std::vector<history::AnnotatedVisit> visits;
  415. // Visit2's referrer is visit 1 and visit 4 is a back navigation from visit 2.
  416. // Visit 3 is a different journey altogether. Visit 10 is referring to a
  417. // missing visit and should be considered as in its own cluster.
  418. // Also, make sure these aren't sorted so we test that we are sorting the
  419. // visits by visit ID.
  420. history::AnnotatedVisit visit = testing::CreateDefaultAnnotatedVisit(
  421. 1, GURL("https://github.com/"), base::Time::FromTimeT(1));
  422. visit.content_annotations.model_annotations.entities = {{"github", 100}};
  423. visit.content_annotations.model_annotations.categories = {{"category", 100}};
  424. visits.push_back(visit);
  425. history::AnnotatedVisit visit2 = testing::CreateDefaultAnnotatedVisit(
  426. 2, GURL("https://google.com/"), base::Time::FromTimeT(2));
  427. visit2.content_annotations.model_annotations.entities = {{"github", 100}};
  428. visit2.content_annotations.model_annotations.categories = {{"category", 100}};
  429. visit2.referring_visit_of_redirect_chain_start = 1;
  430. // Set the visit duration to be 2x the default so it has the same duration
  431. // after |visit| and |visit4| are deduped.
  432. visit2.visit_row.visit_duration = base::Seconds(20);
  433. visits.push_back(visit2);
  434. history::AnnotatedVisit visit4 = testing::CreateDefaultAnnotatedVisit(
  435. 4, GURL("https://github.com/"), base::Time::FromTimeT(4));
  436. visit4.content_annotations.model_annotations.entities = {{"github", 100}};
  437. visit4.content_annotations.model_annotations.categories = {
  438. {"category", 100}, {"category2", 100}};
  439. visits.push_back(visit4);
  440. // After the context clustering, visit5 will not be in the same cluster as
  441. // visit, visit2, and visit4 but all of the visits have the same entities
  442. // and categories so they will be clustered in the content pass.
  443. history::AnnotatedVisit visit5 = testing::CreateDefaultAnnotatedVisit(
  444. 10,
  445. GURL("https://shouldskip.com/butnotsincehostcheckingisfalse/"
  446. "andhasnonexistentreferrer"),
  447. base::Time::FromTimeT(10));
  448. visit5.content_annotations.model_annotations.entities = {{"github", 100}};
  449. visit5.content_annotations.model_annotations.categories = {
  450. {"category", 100}, {"category2", 100}};
  451. visit5.referring_visit_of_redirect_chain_start = 6;
  452. visits.push_back(visit5);
  453. std::vector<history::Cluster> result_clusters =
  454. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  455. EXPECT_THAT(
  456. testing::ToVisitResults(result_clusters),
  457. ElementsAre(ElementsAre(
  458. testing::VisitResult(4, 1.0, {testing::VisitResult(1, 0.0)}),
  459. testing::VisitResult(2, 1.0), testing::VisitResult(10, 0.5))));
  460. }
  461. TEST_F(OnDeviceClusteringWithContentBackendTest,
  462. ClusterOnContentBelowThreshold) {
  463. base::HistogramTester histogram_tester;
  464. std::vector<history::AnnotatedVisit> visits;
  465. // Visit2's referrer is visit 1 and visit 4 is a back navigation from visit 2.
  466. // Visit 3 is a different journey altogether. Visit 10 is referring to a
  467. // missing visit and should be considered as in its own cluster.
  468. // Also, make sure these aren't sorted so we test that we are sorting the
  469. // visits by visit ID.
  470. history::AnnotatedVisit visit = testing::CreateDefaultAnnotatedVisit(
  471. 1, GURL("https://github.com/"), base::Time::FromTimeT(1));
  472. visit.content_annotations.model_annotations.entities = {{"github", 100}};
  473. visit.content_annotations.model_annotations.categories = {{"category", 100}};
  474. visits.push_back(visit);
  475. history::AnnotatedVisit visit2 = testing::CreateDefaultAnnotatedVisit(
  476. 2, GURL("https://google.com/"), base::Time::FromTimeT(2));
  477. visit2.referring_visit_of_redirect_chain_start = 1;
  478. // Set the visit duration to be 2x the default so it has the same duration
  479. // after |visit| and |visit4| are deduped.
  480. visit2.visit_row.visit_duration = base::Seconds(20);
  481. visits.push_back(visit2);
  482. // After the context clustering, visit4 will not be in the same cluster as
  483. // visit and visit2 but should be clustered together since they have the same
  484. // title.
  485. history::AnnotatedVisit visit4 = testing::CreateDefaultAnnotatedVisit(
  486. 4, GURL("https://github.com/"), base::Time::FromTimeT(4));
  487. visit4.content_annotations.model_annotations.entities = {{"github", 100}};
  488. visit4.content_annotations.model_annotations.categories = {{"category", 100}};
  489. visits.push_back(visit4);
  490. // This visit has a different title and shouldn't be grouped with the others.
  491. history::AnnotatedVisit visit5 = testing::CreateDefaultAnnotatedVisit(
  492. 10, GURL("https://nonexistentreferrer.com/"), base::Time::FromTimeT(10));
  493. visit5.referring_visit_of_redirect_chain_start = 6;
  494. visit5.content_annotations.model_annotations.entities = {{"irrelevant", 100}};
  495. visits.push_back(visit5);
  496. std::vector<history::Cluster> result_clusters =
  497. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  498. EXPECT_THAT(
  499. testing::ToVisitResults(result_clusters),
  500. ElementsAre(ElementsAre(testing::VisitResult(10, 1.0)),
  501. ElementsAre(testing::VisitResult(
  502. 4, 1.0, {testing::VisitResult(1, 0.0)}),
  503. testing::VisitResult(2, 1.0))));
  504. histogram_tester.ExpectUniqueSample(
  505. "History.Clusters.Backend.ClusterSize.Min", 1, 1);
  506. histogram_tester.ExpectUniqueSample(
  507. "History.Clusters.Backend.ClusterSize.Max", 2, 1);
  508. histogram_tester.ExpectUniqueSample(
  509. "History.Clusters.Backend.NumKeywordsPerCluster.Min", 1, 1);
  510. histogram_tester.ExpectUniqueSample(
  511. "History.Clusters.Backend.NumKeywordsPerCluster.Max", 2, 1);
  512. }
  513. class OnDeviceClusteringWithAllTheBackendsTest
  514. : public OnDeviceClusteringWithoutContentBackendTest {
  515. public:
  516. void SetUp() override {
  517. entity_metadata_provider_ = std::make_unique<TestEntityMetadataProvider>(
  518. task_environment_.GetMainThreadTaskRunner());
  519. optimization_guide_decider_ =
  520. std::make_unique<TestOptimizationGuideDecider>();
  521. clustering_backend_ = std::make_unique<OnDeviceClusteringBackend>(
  522. entity_metadata_provider_.get(),
  523. /*engagement_score_provider=*/nullptr,
  524. optimization_guide_decider_.get(),
  525. /*mid_blocklist_=*/base::flat_set<std::string>({"blockedentity"}));
  526. }
  527. private:
  528. std::unique_ptr<TestEntityMetadataProvider> entity_metadata_provider_;
  529. std::unique_ptr<TestOptimizationGuideDecider> optimization_guide_decider_;
  530. };
  531. TEST_F(OnDeviceClusteringWithAllTheBackendsTest, EntityOnMidBlocklist) {
  532. base::HistogramTester histogram_tester;
  533. std::vector<history::AnnotatedVisit> visits;
  534. // Visit 2 refers to visit 1 and will be clustered. Visit 3 refers to a
  535. // missing visit and should be considered as in its own cluster.
  536. // Goal is to test the mid blocklist
  537. history::AnnotatedVisit visit =
  538. testing::CreateDefaultAnnotatedVisit(1, GURL("https://github.com/"));
  539. visit.content_annotations.model_annotations.entities = {
  540. {"blockedentity", 100}};
  541. visits.push_back(visit);
  542. history::AnnotatedVisit visit2 =
  543. testing::CreateDefaultAnnotatedVisit(2, GURL("https://google.com/"));
  544. visit2.referring_visit_of_redirect_chain_start = 1;
  545. visit2.content_annotations.model_annotations.entities = {{"unblocked", 100}};
  546. // Set the visit duration to be 2x the default so it has the same duration
  547. // after |visit| and |visit4| are deduped.
  548. visit2.visit_row.visit_duration = base::Seconds(20);
  549. visits.push_back(visit2);
  550. // This visit has a different title and shouldn't be grouped with the others.
  551. history::AnnotatedVisit visit3 = testing::CreateDefaultAnnotatedVisit(
  552. 10, GURL("https://nonexistentreferrer.com/"));
  553. visit3.referring_visit_of_redirect_chain_start = 6;
  554. visit3.content_annotations.model_annotations.entities = {{"irrelevant", 100}};
  555. visits.push_back(visit3);
  556. std::vector<history::Cluster> result_clusters =
  557. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  558. histogram_tester.ExpectUniqueSample(
  559. "History.Clusters.Backend.ClusterSize.Min", 1, 1);
  560. histogram_tester.ExpectUniqueSample(
  561. "History.Clusters.Backend.ClusterSize.Max", 2, 1);
  562. histogram_tester.ExpectUniqueSample(
  563. "History.Clusters.Backend.NumKeywordsPerCluster.Min", 3, 1);
  564. histogram_tester.ExpectUniqueSample(
  565. "History.Clusters.Backend.NumKeywordsPerCluster.Max", 3, 1);
  566. }
  567. TEST_F(OnDeviceClusteringWithAllTheBackendsTest,
  568. DedupeSimilarUrlSameSearchQuery) {
  569. base::HistogramTester histogram_tester;
  570. std::vector<history::AnnotatedVisit> visits;
  571. // Visit2 has the same search URL as Visit1.
  572. history::AnnotatedVisit visit = testing::CreateDefaultAnnotatedVisit(
  573. 1, GURL("http://default-engine.com/?q=foo&otherstuff"),
  574. base::Time::FromTimeT(1));
  575. visit.content_annotations.model_annotations.visibility_score = 0.5;
  576. visit.content_annotations.search_terms = u"foo";
  577. visit.content_annotations.search_normalized_url =
  578. GURL("http://default-engine.com/?q=foo");
  579. visits.push_back(visit);
  580. history::AnnotatedVisit visit2 = testing::CreateDefaultAnnotatedVisit(
  581. 2, GURL("http://default-engine.com/?q=foo"), base::Time::FromTimeT(2));
  582. visit2.content_annotations.model_annotations.entities = {
  583. history::VisitContentModelAnnotations::Category("foo", 70),
  584. history::VisitContentModelAnnotations::Category("nometadata", 100),
  585. history::VisitContentModelAnnotations::Category("toolow", 1),
  586. };
  587. visit2.content_annotations.model_annotations.visibility_score = 0.5;
  588. visit2.content_annotations.search_terms = u"foo";
  589. visit2.content_annotations.search_normalized_url =
  590. GURL("http://default-engine.com/?q=foo");
  591. visits.push_back(visit2);
  592. history::AnnotatedVisit visit3 = testing::CreateDefaultAnnotatedVisit(
  593. 3, GURL("http://non-default-engine.com/?q=nometadata#whatever"),
  594. base::Time::FromTimeT(3));
  595. visit3.content_annotations.model_annotations.entities = {
  596. history::VisitContentModelAnnotations::Category("nometadata", 100),
  597. // This is too low and should not be added as a keyword despite it
  598. // being a valid entity for a different visit.
  599. history::VisitContentModelAnnotations::Category("foo", 10),
  600. };
  601. visit3.content_annotations.search_terms = u"nometadata";
  602. visit3.content_annotations.search_normalized_url =
  603. GURL("http://non-default-engine.com/?q=nometadata");
  604. visit3.content_annotations.model_annotations.visibility_score = 0.5;
  605. visits.push_back(visit3);
  606. history::AnnotatedVisit should_skip = testing::CreateDefaultAnnotatedVisit(
  607. 11, GURL("https://shouldskip.com/whatever"), base::Time::FromTimeT(11));
  608. visits.push_back(should_skip);
  609. std::vector<history::Cluster> result_clusters =
  610. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  611. EXPECT_THAT(
  612. testing::ToVisitResults(result_clusters),
  613. ElementsAre(
  614. ElementsAre(testing::VisitResult(3, 1.0, {}, u"nometadata")),
  615. ElementsAre(testing::VisitResult(
  616. 2, 1.0, {testing::VisitResult(1, 0.0, {}, u"foo")}, u"foo"))));
  617. // Make sure visits are normalized.
  618. history::Cluster cluster = result_clusters.at(0);
  619. ASSERT_EQ(cluster.visits.size(), 1u);
  620. // The third visit should have its original URL as the normalized URL and
  621. // also have its entities rewritten.
  622. history::ClusterVisit third_result_visit = cluster.visits.at(0);
  623. EXPECT_EQ(third_result_visit.normalized_url,
  624. GURL("http://non-default-engine.com/?q=nometadata"));
  625. EXPECT_TRUE(third_result_visit.annotated_visit.content_annotations
  626. .model_annotations.entities.empty());
  627. EXPECT_TRUE(third_result_visit.annotated_visit.content_annotations
  628. .model_annotations.categories.empty());
  629. EXPECT_TRUE(cluster.keyword_to_data_map.empty());
  630. history::Cluster cluster2 = result_clusters.at(1);
  631. ASSERT_EQ(cluster2.visits.size(), 1u);
  632. // The first visit should have its original URL as the normalized URL and
  633. // also have its entities rewritten.
  634. history::ClusterVisit better_visit = cluster2.visits.at(0);
  635. EXPECT_EQ(better_visit.normalized_url,
  636. GURL("http://default-engine.com/?q=foo"));
  637. std::vector<history::VisitContentModelAnnotations::Category> entities =
  638. better_visit.annotated_visit.content_annotations.model_annotations
  639. .entities;
  640. ASSERT_EQ(entities.size(), 1u);
  641. EXPECT_EQ(entities.at(0).id, "rewritten-foo");
  642. std::vector<history::VisitContentModelAnnotations::Category> categories =
  643. better_visit.annotated_visit.content_annotations.model_annotations
  644. .categories;
  645. ASSERT_EQ(categories.size(), 1u);
  646. EXPECT_EQ(categories.at(0).id, "category-foo");
  647. EXPECT_EQ(categories.at(0).weight, /*70*0.6=*/42);
  648. EXPECT_THAT(better_visit.annotated_visit.content_annotations.model_annotations
  649. .visibility_score,
  650. FloatEq(0.5));
  651. // The second visit should have a normalized URL, but be the worse duplicate.
  652. EXPECT_EQ(cluster2.visits.at(0).duplicate_visits.at(0).normalized_url,
  653. GURL("http://default-engine.com/?q=foo"));
  654. EXPECT_THAT(cluster2.visits.at(0)
  655. .duplicate_visits.at(0)
  656. .annotated_visit.content_annotations.model_annotations
  657. .visibility_score,
  658. FloatEq(0.5));
  659. // Cluster should have 3 keywords.
  660. EXPECT_THAT(
  661. cluster2.GetKeywords(),
  662. UnorderedElementsAre(u"rewritten-foo", u"category-foo", u"alias-foo"));
  663. histogram_tester.ExpectUniqueSample(
  664. "History.Clusters.Backend.ClusterSize.Min", 1, 1);
  665. histogram_tester.ExpectUniqueSample(
  666. "History.Clusters.Backend.ClusterSize.Max", 1, 1);
  667. histogram_tester.ExpectUniqueSample(
  668. "History.Clusters.Backend.NumKeywordsPerCluster.Min", 0, 1);
  669. histogram_tester.ExpectUniqueSample(
  670. "History.Clusters.Backend.NumKeywordsPerCluster.Max", 3, 1);
  671. histogram_tester.ExpectTotalCount(
  672. "History.Clusters.Backend.BatchEntityLookupLatency2", 1);
  673. histogram_tester.ExpectUniqueSample(
  674. "History.Clusters.Backend.BatchEntityLookupSize", 2, 1);
  675. }
  676. TEST_F(OnDeviceClusteringWithoutContentBackendTest, EngagementScoreCache) {
  677. base::HistogramTester histogram_tester;
  678. std::vector<history::AnnotatedVisit> visits;
  679. // Add 2 different hosts to |visits|.
  680. history::AnnotatedVisit visit1 =
  681. testing::CreateDefaultAnnotatedVisit(1, GURL("https://github.com/"));
  682. visits.push_back(visit1);
  683. history::AnnotatedVisit visit2 =
  684. testing::CreateDefaultAnnotatedVisit(2, GURL("https://github.com/"));
  685. visits.push_back(visit2);
  686. history::AnnotatedVisit visit3 =
  687. testing::CreateDefaultAnnotatedVisit(4, GURL("https://github.com/"));
  688. visits.push_back(visit3);
  689. history::AnnotatedVisit visit4 =
  690. testing::CreateDefaultAnnotatedVisit(10, GURL("https://github.com/"));
  691. visits.push_back(visit4);
  692. history::AnnotatedVisit visit5 =
  693. testing::CreateDefaultAnnotatedVisit(3, GURL("https://github2.com/"));
  694. visits.push_back(visit5);
  695. std::vector<history::Cluster> result_clusters_1 =
  696. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  697. EXPECT_EQ(2u, GetSiteEngagementGetScoreInvocationCount());
  698. // No new queries should be issued when cache store is enabled.
  699. std::vector<history::Cluster> result_clusters_2 =
  700. ClusterVisits(ClusteringRequestSource::kJourneysPage, visits);
  701. EXPECT_EQ(2u, GetSiteEngagementGetScoreInvocationCount());
  702. }
  703. } // namespace
  704. } // namespace history_clusters