[CherryPick][SH] Record metrics from JS response.

(cherry picked from commit 261f3da0c21934eb482969238cc3adf06a6be7c1)

Bug: 1131107
Change-Id: I2a0de73ed846b100a549429888dabeb8ffe2db6a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2461217
Commit-Queue: sebsg <[email protected]>
Reviewed-by: Eugene But <[email protected]>
Reviewed-by: Sebastien Lalancette <[email protected]>
Cr-Original-Commit-Position: refs/heads/master@{#817023}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2480622
Reviewed-by: sebsg <[email protected]>
Commit-Queue: Sebastien Lalancette <[email protected]>
Cr-Commit-Position: refs/branch-heads/4280@{#441}
Cr-Branched-From: ea420fb963f9658c9969b6513c56b8f47efa1a2a-refs/heads/master@{#812852}
diff --git a/ios/web/DEPS b/ios/web/DEPS
index c7a16a6..3eb9d1c 100644
--- a/ios/web/DEPS
+++ b/ios/web/DEPS
@@ -1,6 +1,7 @@
 include_rules = [
   "+components/url_formatter",
   "+components/leveldb_proto/public",
+  "+components/shared_highlighting/core/common",
   "+crypto",
   "+ios/net",
   "+ios/web",
diff --git a/ios/web/navigation/BUILD.gn b/ios/web/navigation/BUILD.gn
index 715ad5c4..4f0ce41 100644
--- a/ios/web/navigation/BUILD.gn
+++ b/ios/web/navigation/BUILD.gn
@@ -17,6 +17,7 @@
     ":core",
     ":navigation_manager_util",
     "//base",
+    "//components/shared_highlighting/core/common",
     "//ios/net",
     "//ios/web:core",
     "//ios/web/common",
diff --git a/ios/web/navigation/crw_text_fragments_handler.mm b/ios/web/navigation/crw_text_fragments_handler.mm
index f127c57..6496950 100644
--- a/ios/web/navigation/crw_text_fragments_handler.mm
+++ b/ios/web/navigation/crw_text_fragments_handler.mm
@@ -7,6 +7,7 @@
 #import "base/json/json_writer.h"
 #import "base/strings/string_util.h"
 #import "base/strings/utf_string_conversions.h"
+#import "components/shared_highlighting/core/common/shared_highlighting_metrics.h"
 #import "ios/web/common/features.h"
 #import "ios/web/navigation/text_fragments_utils.h"
 #import "ios/web/public/js_messaging/web_frame.h"
@@ -24,6 +25,9 @@
 const char kScriptCommandPrefix[] = "textFragments";
 const char kScriptResponseCommand[] = "textFragments.response";
 
+const double kMinSelectorCount = 0.0;
+const double kMaxSelectorCount = 200.0;
+
 }  // namespace
 
 @interface CRWTextFragmentsHandler () {
@@ -72,10 +76,14 @@
   base::Value parsedFragments =
       web::ParseTextFragments(self.webStateImpl->GetLastCommittedURL());
 
-  if (parsedFragments.type() == base::Value::Type::NONE)
+  if (parsedFragments.type() == base::Value::Type::NONE) {
     return;
+  }
 
-  // TODO (crbug.com/1099268): Log the origin and number of fragments metrics.
+  shared_highlighting::LogTextFragmentSelectorCount(
+      parsedFragments.GetList().size());
+  shared_highlighting::LogTextFragmentLinkOpenSource(referrer.url);
+
   std::string fragmentParam;
   base::JSONWriter::Write(parsedFragments, &fragmentParam);
 
@@ -91,8 +99,9 @@
 
 // Returns NO if fragments highlighting is not allowed in the current |context|.
 - (BOOL)areTextFragmentsAllowedInContext:(web::NavigationContext*)context {
-  if (!base::FeatureList::IsEnabled(web::features::kScrollToTextIOS))
+  if (!base::FeatureList::IsEnabled(web::features::kScrollToTextIOS)) {
     return NO;
+  }
 
   if (self.isBeingDestroyed) {
     return NO;
@@ -121,7 +130,34 @@
     return;
   }
 
-  // TODO (crbug.com/1099268): Log the success/failure metric.
+  // Log success metrics.
+  base::Optional<double> optionalFragmentCount =
+      response.FindDoublePath("result.fragmentsCount");
+  base::Optional<double> optionalSuccessCount =
+      response.FindDoublePath("result.successCount");
+
+  // Since the response can't be trusted, don't log metrics if the results look
+  // invalid.
+  if (!optionalFragmentCount ||
+      optionalFragmentCount.value() > kMaxSelectorCount ||
+      optionalFragmentCount.value() <= kMinSelectorCount) {
+    return;
+  }
+  if (!optionalSuccessCount ||
+      optionalSuccessCount.value() > kMaxSelectorCount ||
+      optionalSuccessCount.value() < kMinSelectorCount) {
+    return;
+  }
+  if (optionalSuccessCount.value() > optionalFragmentCount.value()) {
+    return;
+  }
+
+  int fragmentCount = static_cast<int>(optionalFragmentCount.value());
+  int successCount = static_cast<int>(optionalSuccessCount.value());
+
+  shared_highlighting::LogTextFragmentMatchRate(successCount, fragmentCount);
+  shared_highlighting::LogTextFragmentAmbiguousMatch(
+      /*ambiguous_match=*/successCount != fragmentCount);
 }
 
 @end
diff --git a/ios/web/navigation/crw_text_fragments_handler_unittest.mm b/ios/web/navigation/crw_text_fragments_handler_unittest.mm
index 463440c..e00544e 100644
--- a/ios/web/navigation/crw_text_fragments_handler_unittest.mm
+++ b/ios/web/navigation/crw_text_fragments_handler_unittest.mm
@@ -5,11 +5,13 @@
 #import "ios/web/navigation/crw_text_fragments_handler.h"
 
 #import "base/strings/utf_string_conversions.h"
+#import "base/test/metrics/histogram_tester.h"
 #import "base/test/scoped_feature_list.h"
 #import "ios/web/common/features.h"
 #import "ios/web/navigation/text_fragments_utils.h"
 #import "ios/web/public/navigation/referrer.h"
 #import "ios/web/public/test/fakes/fake_navigation_context.h"
+#import "ios/web/public/test/fakes/fake_web_frame.h"
 #import "ios/web/public/test/web_test.h"
 #import "ios/web/web_state/ui/crw_web_view_handler_delegate.h"
 #import "ios/web/web_state/web_state_impl.h"
@@ -29,11 +31,18 @@
 namespace {
 
 const char kValidFragmentsURL[] =
-    "https://chromium.org/#idFrag:~:text=text%201&text=text%202";
+    "https://chromium.org#idFrag:~:text=text%201&text=text%202";
 const char kScriptForValidFragmentsURL[] =
     "__gCrWeb.textFragments.handleTextFragments([{\"textStart\":\"text "
     "1\"},{\"textStart\":\"text 2\"}], true)";
 
+const char kSingleFragmentURL[] = "https://chromium.org#:~:text=text";
+const char kTwoFragmentsURL[] =
+    "https://chromium.org#:~:text=text&text=other%20text";
+
+const char kSearchEngineURL[] = "https://google.com";
+const char kNonSearchEngineURL[] = "https://notasearchengine.com";
+
 }  // namespace
 
 class MockWebStateImpl : public web::WebStateImpl {
@@ -107,6 +116,21 @@
         .WillOnce(ReturnRefOfCopy(last_url));
   }
 
+  Referrer GetSearchEngineReferrer() {
+    return Referrer(GURL(kSearchEngineURL), web::ReferrerPolicyDefault);
+  }
+
+  Referrer GetNonSearchEngineReferrer() {
+    return Referrer(GURL(kNonSearchEngineURL), web::ReferrerPolicyDefault);
+  }
+
+  void CreateHandlerAndProcessTextFragments() {
+    CRWTextFragmentsHandler* handler = CreateDefaultHandler();
+
+    [handler processTextFragmentsWithContext:&context_
+                                    referrer:GetSearchEngineReferrer()];
+  }
+
   web::FakeNavigationContext context_;
   MockWebStateImpl* web_state_;
   base::test::ScopedFeatureList feature_list_;
@@ -116,6 +140,7 @@
 // Tests that the handler will execute JavaScript if highlighting is allowed and
 // fragments are present.
 TEST_F(CRWTextFragmentsHandlerTest, ExecuteJavaScriptSuccess) {
+  base::HistogramTester histogram_tester;
   SetLastURL(GURL(kValidFragmentsURL));
 
   CRWTextFragmentsHandler* handler = CreateDefaultHandler();
@@ -125,7 +150,8 @@
       base::UTF8ToUTF16(kScriptForValidFragmentsURL);
   EXPECT_CALL(*web_state_, ExecuteJavaScript(expected_javascript)).Times(1);
 
-  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
 
   // Verify that a command callback was added with the right prefix.
   EXPECT_NE(web::WebState::ScriptCommandCallback(),
@@ -144,7 +170,8 @@
   EXPECT_CALL(*web_state_, ExecuteJavaScript(_)).Times(0);
   EXPECT_CALL(*web_state_, GetLastCommittedURL()).Times(0);
 
-  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
 
   // Verify that no callback was set when the flag is disabled.
   EXPECT_EQ(web::WebState::ScriptCommandCallback(),
@@ -162,7 +189,8 @@
   EXPECT_CALL(*web_state_, ExecuteJavaScript(_)).Times(0);
   EXPECT_CALL(*web_state_, GetLastCommittedURL()).Times(0);
 
-  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
 }
 
 // Tests that the handler will not execute JavaScript if the WebState has no
@@ -176,7 +204,8 @@
   EXPECT_CALL(*web_state_, ExecuteJavaScript(_)).Times(0);
   EXPECT_CALL(*web_state_, GetLastCommittedURL()).Times(0);
 
-  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
 }
 
 // Tests that the handler will not execute JavaScript if we navigated on the
@@ -190,7 +219,8 @@
   EXPECT_CALL(*web_state_, ExecuteJavaScript(_)).Times(0);
   EXPECT_CALL(*web_state_, GetLastCommittedURL()).Times(0);
 
-  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
 }
 
 // Tests that the handler will not execute JavaScript if there are no
@@ -205,7 +235,8 @@
 
   EXPECT_CALL(*web_state_, ExecuteJavaScript(_)).Times(0);
 
-  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
 }
 
 // Tests that any timing issue which would call the handle after it got closed
@@ -224,5 +255,204 @@
   EXPECT_CALL(*web_state_, ExecuteJavaScript(_)).Times(0);
   EXPECT_CALL(*web_state_, GetLastCommittedURL()).Times(0);
 
-  [handler processTextFragmentsWithContext:&context_ referrer:Referrer()];
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
+}
+
+// Tests that no metrics are recoded for an URL that doesn't contain text
+// fragments.
+TEST_F(CRWTextFragmentsHandlerTest, NoMetricsRecordedIfNoFragmentPresent) {
+  base::HistogramTester histogram_tester;
+
+  // Set a URL without text fragments.
+  SetLastURL(GURL("https://www.chromium.org/"));
+
+  CRWTextFragmentsHandler* handler = CreateDefaultHandler();
+
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
+
+  // Make sure no metrics were logged.
+  histogram_tester.ExpectTotalCount("TextFragmentAnchor.AmbiguousMatch", 0);
+  histogram_tester.ExpectTotalCount("TextFragmentAnchor.LinkOpenSource", 0);
+  histogram_tester.ExpectTotalCount("TextFragmentAnchor.MatchRate", 0);
+  histogram_tester.ExpectTotalCount("TextFragmentAnchor.SelectorCount", 0);
+}
+
+// Tests that no metrics are recoded for an URL that doesn't contain text
+// fragments, even if it contains a fragment id
+TEST_F(CRWTextFragmentsHandlerTest,
+       NoMetricsRecordedIfNoFragmentPresentWithFragmentId) {
+  base::HistogramTester histogram_tester;
+
+  // Set a URL without text fragments, but with an id fragment.
+  SetLastURL(GURL("https://www.chromium.org/#FragmentID"));
+
+  CRWTextFragmentsHandler* handler = CreateDefaultHandler();
+
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
+
+  // Make sure no metrics were logged.
+  histogram_tester.ExpectTotalCount("TextFragmentAnchor.AmbiguousMatch", 0);
+  histogram_tester.ExpectTotalCount("TextFragmentAnchor.LinkOpenSource", 0);
+  histogram_tester.ExpectTotalCount("TextFragmentAnchor.MatchRate", 0);
+  histogram_tester.ExpectTotalCount("TextFragmentAnchor.SelectorCount", 0);
+}
+
+// Tests that the LinkSource metric is recorded properly when the link comes
+// from a search engine.
+TEST_F(CRWTextFragmentsHandlerTest, LinkSourceMetricSearchEngine) {
+  base::HistogramTester histogram_tester;
+  SetLastURL(GURL(kValidFragmentsURL));
+
+  CRWTextFragmentsHandler* handler = CreateDefaultHandler();
+
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
+
+  histogram_tester.ExpectUniqueSample("TextFragmentAnchor.LinkOpenSource", 1,
+                                      1);
+}
+
+// Tests that the LinkSource metric is recorded properly when the link doesn't
+// come from a search engine.
+TEST_F(CRWTextFragmentsHandlerTest, LinkSourceMetricNonSearchEngine) {
+  base::HistogramTester histogram_tester;
+  SetLastURL(GURL(kValidFragmentsURL));
+
+  CRWTextFragmentsHandler* handler = CreateDefaultHandler();
+
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetNonSearchEngineReferrer()];
+
+  histogram_tester.ExpectUniqueSample("TextFragmentAnchor.LinkOpenSource", 0,
+                                      1);
+}
+
+// Tests that the SelectorCount metric is recorded properly when a single
+// selector is present.
+TEST_F(CRWTextFragmentsHandlerTest, SelectorCountMetricSingleSelector) {
+  base::HistogramTester histogram_tester;
+  SetLastURL(GURL(kSingleFragmentURL));
+
+  CRWTextFragmentsHandler* handler = CreateDefaultHandler();
+
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
+
+  histogram_tester.ExpectUniqueSample("TextFragmentAnchor.SelectorCount", 1, 1);
+}
+
+// Tests that the SelectorCount metric is recorded properly when two selectors
+// are present.
+TEST_F(CRWTextFragmentsHandlerTest, SelectorCountMetricTwoSelectors) {
+  base::HistogramTester histogram_tester;
+  SetLastURL(GURL(kTwoFragmentsURL));
+
+  CRWTextFragmentsHandler* handler = CreateDefaultHandler();
+
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
+
+  histogram_tester.ExpectUniqueSample("TextFragmentAnchor.SelectorCount", 2, 1);
+}
+
+// Tests that the AmbiguousMatch and MatchRate success metrics are recorded
+// properly in a variety of cases.
+TEST_F(CRWTextFragmentsHandlerTest,
+       DidReceiveJavaScriptResponseSuccessMetrics) {
+  SetLastURL(GURL(kTwoFragmentsURL));
+  CRWTextFragmentsHandler* handler = CreateDefaultHandler();
+
+  [handler processTextFragmentsWithContext:&context_
+                                  referrer:GetSearchEngineReferrer()];
+
+  web::WebState::ScriptCommandCallback parse_function =
+      web_state_->last_callback();
+  web::FakeWebFrame fake_main_frame(/*frame_id=*/"", /*is_main_frame=*/true,
+                                    GURL());
+
+  // 100% rate case.
+  {
+    base::HistogramTester histogram_tester;
+
+    base::DictionaryValue js_response = base::DictionaryValue();
+    js_response.SetKey("command", base::Value("textFragments.response"));
+    js_response.SetDoublePath("result.fragmentsCount", 2);
+    js_response.SetDoublePath("result.successCount", 2);
+
+    parse_function.Run(js_response, GURL("https://text.com"),
+                       /*interacted=*/true, &fake_main_frame);
+
+    histogram_tester.ExpectUniqueSample("TextFragmentAnchor.AmbiguousMatch", 0,
+                                        1);
+    histogram_tester.ExpectUniqueSample("TextFragmentAnchor.MatchRate", 100, 1);
+  }
+
+  // 50% rate case.
+  {
+    base::HistogramTester histogram_tester;
+
+    base::DictionaryValue js_response = base::DictionaryValue();
+    js_response.SetKey("command", base::Value("textFragments.response"));
+    js_response.SetDoublePath("result.fragmentsCount", 6);
+    js_response.SetDoublePath("result.successCount", 3);
+
+    parse_function.Run(js_response, GURL("https://text.com"),
+                       /*interacted=*/true, &fake_main_frame);
+
+    histogram_tester.ExpectUniqueSample("TextFragmentAnchor.AmbiguousMatch", 1,
+                                        1);
+    histogram_tester.ExpectUniqueSample("TextFragmentAnchor.MatchRate", 50, 1);
+  }
+
+  // 0% rate case.
+  {
+    base::HistogramTester histogram_tester;
+
+    base::DictionaryValue js_response = base::DictionaryValue();
+    js_response.SetKey("command", base::Value("textFragments.response"));
+    js_response.SetDoublePath("result.fragmentsCount", 2);
+    js_response.SetDoublePath("result.successCount", 0);
+
+    parse_function.Run(js_response, GURL("https://text.com"),
+                       /*interacted=*/true, &fake_main_frame);
+
+    histogram_tester.ExpectUniqueSample("TextFragmentAnchor.AmbiguousMatch", 1,
+                                        1);
+    histogram_tester.ExpectUniqueSample("TextFragmentAnchor.MatchRate", 0, 1);
+  }
+
+  // Invalid values case - negative numbers.
+  {
+    base::HistogramTester histogram_tester;
+
+    base::DictionaryValue js_response = base::DictionaryValue();
+    js_response.SetKey("command", base::Value("textFragments.response"));
+    js_response.SetDoublePath("result.fragmentsCount", -3);
+    js_response.SetDoublePath("result.successCount", 4);
+
+    parse_function.Run(js_response, GURL("https://text.com"),
+                       /*interacted=*/true, &fake_main_frame);
+
+    histogram_tester.ExpectTotalCount("TextFragmentAnchor.AmbiguousMatch", 0);
+    histogram_tester.ExpectTotalCount("TextFragmentAnchor.MatchRate", 0);
+  }
+
+  // Invalid values case - not numbers.
+  {
+    base::HistogramTester histogram_tester;
+
+    base::DictionaryValue js_response = base::DictionaryValue();
+    js_response.SetKey("command", base::Value("textFragments.response"));
+    js_response.SetStringPath("result.fragmentsCount", "a weird value");
+    js_response.SetDoublePath("result.successCount", 4);
+
+    parse_function.Run(js_response, GURL("https://text.com"),
+                       /*interacted=*/true, &fake_main_frame);
+
+    histogram_tester.ExpectTotalCount("TextFragmentAnchor.AmbiguousMatch", 0);
+    histogram_tester.ExpectTotalCount("TextFragmentAnchor.MatchRate", 0);
+  }
 }