| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/omnibox/browser/omnibox_metrics_provider.h" |
| |
| #include <memory> |
| #include <string> |
| |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "components/omnibox/browser/autocomplete_match.h" |
| #include "components/omnibox/browser/autocomplete_match_type.h" |
| #include "components/omnibox/browser/autocomplete_result.h" |
| #include "components/omnibox/browser/match_compare.h" |
| #include "components/omnibox/browser/omnibox_field_trial.h" |
| #include "components/omnibox/browser/omnibox_log.h" |
| #include "components/omnibox/browser/omnibox_popup_selection.h" |
| #include "components/omnibox/common/omnibox_features.h" |
| #include "components/ukm/test_ukm_recorder.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/metrics_proto/omnibox_event.pb.h" |
| #include "ui/base/window_open_disposition.h" |
| |
| using ScoringSignals = ::metrics::OmniboxEventProto::Suggestion::ScoringSignals; |
| |
| class OmniboxMetricsProviderTest : public testing::Test { |
| public: |
| OmniboxMetricsProviderTest() = default; |
| ~OmniboxMetricsProviderTest() override = default; |
| |
| void SetUp() override { |
| provider_ = std::make_unique<OmniboxMetricsProvider>(); |
| } |
| |
| void TearDown() override { provider_.reset(); } |
| |
| OmniboxLog BuildOmniboxLog(const AutocompleteResult& result, |
| size_t selected_index) { |
| return OmniboxLog( |
| u"my text", /*just_deleted_text=*/false, metrics::OmniboxInputType::URL, |
| /*in_keyword_mode=*/false, |
| metrics::OmniboxEventProto_KeywordModeEntryMethod_INVALID, |
| /*is_popup_open=*/false, |
| /*selection=*/OmniboxPopupSelection(selected_index), |
| WindowOpenDisposition::CURRENT_TAB, /*is_paste_and_go=*/false, |
| SessionID::NewUnique(), |
| metrics::OmniboxEventProto::PageClassification:: |
| OmniboxEventProto_PageClassification_NTP_REALBOX, |
| /*elapsed_time_since_user_first_modified_omnibox=*/base::TimeDelta(), |
| /*completed_length=*/0, |
| /*elapsed_time_since_last_change_to_default_match=*/base::TimeDelta(), |
| result, GURL("https://www.example.com/"), false); |
| } |
| |
| AutocompleteMatch BuildMatch(AutocompleteMatch::Type type) { |
| return AutocompleteMatch(nullptr, 0, false, type); |
| } |
| |
| void RecordLogAndVerifyClientSummarizedResultType( |
| const OmniboxLog& log, |
| int32_t expected_uma_sample, |
| int64_t expected_ukm_value) { |
| base::HistogramTester histogram_tester; |
| ukm::TestAutoSetUkmRecorder ukm_recorder; |
| provider_->RecordOmniboxOpenedURLClientSummarizedResultType(log); |
| |
| // Verify the UMA histogram. |
| histogram_tester.ExpectBucketCount( |
| "Omnibox.SuggestionUsed.ClientSummarizedResultType", |
| expected_uma_sample, |
| /*expected_count=*/1); |
| |
| // Verify the UKM event. |
| const char* entry_name = ukm::builders::Omnibox_SuggestionUsed::kEntryName; |
| if (log.ukm_source_id != ukm::kInvalidSourceId) { |
| EXPECT_EQ(ukm_recorder.GetEntriesByName(entry_name).size(), 1ul); |
| auto* entry = ukm_recorder.GetEntriesByName(entry_name)[0].get(); |
| ukm_recorder.ExpectEntryMetric( |
| entry, ukm::builders::Omnibox_SuggestionUsed::kResultTypeName, |
| expected_ukm_value); |
| } else { |
| EXPECT_EQ(ukm_recorder.GetEntriesByName(entry_name).size(), 0ul); |
| } |
| } |
| |
| void RecordLogAndVerifyScoringSignals( |
| const OmniboxLog& log, |
| ScoringSignals& expected_scoring_signals) { |
| // Clear the event cache so we start with a clean slate. |
| provider_->omnibox_events_cache.clear_omnibox_event(); |
| |
| provider_->RecordOmniboxOpenedURL(log); |
| |
| EXPECT_EQ(provider_->omnibox_events_cache.omnibox_event_size(), 1); |
| const metrics::OmniboxEventProto& omnibox_event = |
| provider_->omnibox_events_cache.omnibox_event(0); |
| |
| for (int i = 0; i < omnibox_event.suggestion_size(); i++) { |
| const metrics::OmniboxEventProto::Suggestion& suggestion = |
| omnibox_event.suggestion(i); |
| // Scoring signals should not be logged when in incognito/off-the-record |
| // mode, regardless of result type. |
| if (log.is_incognito) { |
| ASSERT_FALSE(suggestion.has_scoring_signals()); |
| continue; |
| } |
| |
| // When not in incognito, scoring signals should only be logged for URL |
| // (not search) types. Check that the signals are logged correctly for URL |
| // types, and not logged at all for search types. |
| if (suggestion.has_result_type() && |
| suggestion.result_type() != |
| metrics:: |
| OmniboxEventProto_Suggestion_ResultType_SEARCH_WHAT_YOU_TYPED) { |
| ASSERT_TRUE(suggestion.has_scoring_signals()); |
| ASSERT_EQ( |
| expected_scoring_signals.first_bookmark_title_match_position(), |
| suggestion.scoring_signals().first_bookmark_title_match_position()); |
| ASSERT_EQ(expected_scoring_signals.allowed_to_be_default_match(), |
| suggestion.scoring_signals().allowed_to_be_default_match()); |
| ASSERT_EQ(expected_scoring_signals.length_of_url(), |
| suggestion.scoring_signals().length_of_url()); |
| } else { |
| ASSERT_FALSE(suggestion.has_scoring_signals()); |
| } |
| } |
| |
| // Clear the event cache. |
| provider_->omnibox_events_cache.clear_omnibox_event(); |
| } |
| |
| protected: |
| base::test::TaskEnvironment task_environment_; |
| std::unique_ptr<OmniboxMetricsProvider> provider_; |
| }; |
| |
| TEST_F(OmniboxMetricsProviderTest, ClientSummarizedResultTypeSingleURL) { |
| AutocompleteResult result; |
| result.AppendMatches( |
| {BuildMatch(AutocompleteMatch::Type::URL_WHAT_YOU_TYPED)}); |
| OmniboxLog log = BuildOmniboxLog(result, /*selected_index=*/0); |
| log.ukm_source_id = ukm::NoURLSourceId(); |
| RecordLogAndVerifyClientSummarizedResultType(log, /*expected_uma_sample=*/0, |
| /*expected_ukm_value=*/0); |
| } |
| |
| TEST_F(OmniboxMetricsProviderTest, ClientSummarizedResultTypeSingleSearch) { |
| AutocompleteResult result; |
| result.AppendMatches({BuildMatch(AutocompleteMatch::Type::SEARCH_SUGGEST)}); |
| OmniboxLog log = BuildOmniboxLog(result, /*selected_index=*/0); |
| log.ukm_source_id = ukm::NoURLSourceId(); |
| RecordLogAndVerifyClientSummarizedResultType(log, /*expected_uma_sample=*/1, |
| /*expected_ukm_value=*/1); |
| } |
| |
| TEST_F(OmniboxMetricsProviderTest, ClientSummarizedResultTypeMultipleSearch) { |
| AutocompleteResult result; |
| result.AppendMatches( |
| {BuildMatch(AutocompleteMatch::Type::URL_WHAT_YOU_TYPED), |
| BuildMatch(AutocompleteMatch::Type::SEARCH_SUGGEST), |
| BuildMatch(AutocompleteMatch::Type::URL_WHAT_YOU_TYPED)}); |
| OmniboxLog log = BuildOmniboxLog(result, /*selected_index=*/1); |
| log.ukm_source_id = ukm::NoURLSourceId(); |
| RecordLogAndVerifyClientSummarizedResultType(log, /*expected_uma_sample=*/1, |
| /*expected_ukm_value=*/1); |
| } |
| |
| TEST_F(OmniboxMetricsProviderTest, |
| ClientSummarizedResultTypeInvalidUkmSourceId) { |
| AutocompleteResult result; |
| result.AppendMatches( |
| {BuildMatch(AutocompleteMatch::Type::URL_WHAT_YOU_TYPED)}); |
| OmniboxLog log = BuildOmniboxLog(result, /*selected_index=*/0); |
| RecordLogAndVerifyClientSummarizedResultType(log, /*expected_uma_sample=*/0, |
| /*expected_ukm_value=*/0); |
| } |
| |
| // TODO(b/261895038): This test is flaky on android. Currently scoring signals |
| // logging is only enabled on desktop, so disable for mobile. |
| #if !(BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID)) |
| TEST_F(OmniboxMetricsProviderTest, LogScoringSignals) { |
| // Enable feature flag to log scoring signals. |
| OmniboxFieldTrial::ScopedMLConfigForTesting scoped_ml_config; |
| scoped_ml_config.GetMLConfig().log_url_scoring_signals = true; |
| |
| // Populate a set of scoring signals with some test values. This will be used |
| // to ensure the scoring signals are being propagated correctly. |
| ScoringSignals expected_scoring_signals; |
| expected_scoring_signals.set_first_bookmark_title_match_position(3); |
| expected_scoring_signals.set_allowed_to_be_default_match(true); |
| expected_scoring_signals.set_length_of_url(20); |
| |
| // Create matches and populate the scoring signals. Signals should only be |
| // logged for non-search suggestions. |
| ACMatches matches = { |
| BuildMatch(AutocompleteMatchType::Type::BOOKMARK_TITLE), |
| BuildMatch(AutocompleteMatchType::Type::SEARCH_WHAT_YOU_TYPED)}; |
| for (auto& match : matches) { |
| match.scoring_signals = expected_scoring_signals; |
| } |
| AutocompleteResult result; |
| result.AppendMatches(matches); |
| |
| // Create the log and call simulate logging. |
| OmniboxLog log = BuildOmniboxLog(result, /*selected_index=*/1); |
| RecordLogAndVerifyScoringSignals(log, *matches[0].scoring_signals); |
| |
| // Now, "turn on" incognito mode, scoring signals should not be logged. |
| log.is_incognito = true; |
| RecordLogAndVerifyScoringSignals(log, *matches[0].scoring_signals); |
| } |
| #endif // !(BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID)) |