blob: 40848d14d6b3e5618e6d1a6e66427fc3c1815d4e [file] [log] [blame]
// Copyright 2021 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/actions/history_clusters_action.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/escape.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "components/history_clusters/core/config.h"
#include "components/history_clusters/core/features.h"
#include "components/history_clusters/core/history_clusters_service.h"
#include "components/history_clusters/core/history_clusters_util.h"
#include "components/history_clusters/core/url_constants.h"
#include "components/omnibox/browser/actions/omnibox_action.h"
#include "components/omnibox/browser/actions/omnibox_action_concepts.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_result.h"
#include "components/optimization_guide/core/entity_metadata.h"
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_strings.h"
#include "net/base/url_util.h"
#if BUILDFLAG(IS_ANDROID)
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "components/omnibox/browser/actions/omnibox_pedal_jni_wrapper.h"
#include "url/android/gurl_android.h"
#endif
#if defined(SUPPORT_PEDALS_VECTOR_ICONS)
#include "components/omnibox/browser/vector_icons.h" // nogncheck
#endif
namespace history_clusters {
namespace {
// A template function for recording enum metrics for shown and used journey
// chips as well as their CTR metrics.
template <class EnumT>
void RecordShownUsedEnumAndCtrMetrics(const std::string& metric_name,
EnumT val,
const std::string& label,
bool executed) {
base::UmaHistogramEnumeration("Omnibox.ResumeJourneyShown." + metric_name,
val);
if (executed) {
base::UmaHistogramEnumeration(
"Omnibox.SuggestionUsed.ResumeJourney." + metric_name, val);
}
// Record the CTR metric.
std::string ctr_metric_name =
base::StringPrintf("Omnibox.SuggestionUsed.ResumeJourney.%s.%s.CTR",
metric_name.c_str(), label.c_str());
base::UmaHistogramBoolean(ctr_metric_name, executed);
}
// Multiplies a keyword score by 100, and converts it to int.
int TransformKeywordScoreForUma(float keyword_score) {
return static_cast<int>(keyword_score * 100);
}
} // namespace
int TopRelevance(std::vector<AutocompleteMatch>::const_iterator matches_begin,
std::vector<AutocompleteMatch>::const_iterator matches_end,
TopRelevanceFilter filter) {
if (matches_begin == matches_end)
return 0;
std::vector<int> relevances(matches_end - matches_begin);
base::ranges::transform(
matches_begin, matches_end, relevances.begin(), [&](const auto& match) {
return AutocompleteMatch::IsSearchType(match.type) ==
(filter == TopRelevanceFilter::FILTER_FOR_SEARCH_MATCHES)
? match.relevance
: 0;
});
return base::ranges::max(relevances);
}
bool IsNavigationIntent(int top_search_relevance,
int top_navigation_relevance,
int navigation_intent_score_threshold) {
return top_navigation_relevance > top_search_relevance &&
top_navigation_relevance > navigation_intent_score_threshold;
}
GURL GetFullJourneysUrlForQuery(const std::string& query) {
return net::AppendOrReplaceQueryParameter(GURL(kChromeUIHistoryClustersURL),
"q", query);
}
HistoryClustersAction::HistoryClustersAction(
const std::string& query,
const history::ClusterKeywordData& matched_keyword_data)
: OmniboxAction(
OmniboxAction::LabelStrings(
IDS_OMNIBOX_ACTION_HISTORY_CLUSTERS_SEARCH_HINT,
IDS_OMNIBOX_ACTION_HISTORY_CLUSTERS_SEARCH_SUGGESTION_CONTENTS,
IDS_ACC_OMNIBOX_ACTION_HISTORY_CLUSTERS_SEARCH_SUFFIX,
IDS_ACC_OMNIBOX_ACTION_HISTORY_CLUSTERS_SEARCH),
GetFullJourneysUrlForQuery(query)),
matched_keyword_data_(matched_keyword_data),
query_(query) {}
void HistoryClustersAction::RecordActionShown(size_t position,
bool executed) const {
base::UmaHistogramExactLinear(
"Omnibox.ResumeJourneyShown", position,
AutocompleteResult::kMaxAutocompletePositionValue);
if (executed) {
base::UmaHistogramExactLinear(
"Omnibox.SuggestionUsed.ResumeJourney", position,
AutocompleteResult::kMaxAutocompletePositionValue);
}
base::UmaHistogramBoolean("Omnibox.SuggestionUsed.ResumeJourneyCTR",
executed);
// Record cluster keyword score UMA metrics.
base::UmaHistogramCounts1000(
"Omnibox.ResumeJourneyShown.ClusterKeywordScore",
TransformKeywordScoreForUma(matched_keyword_data_.score));
if (executed) {
base::UmaHistogramCounts1000(
"Omnibox.SuggestionUsed.ResumeJourney.ClusterKeywordScore",
TransformKeywordScoreForUma(matched_keyword_data_.score));
}
// Record cluster keyword type UMA metrics.
RecordShownUsedEnumAndCtrMetrics<
history::ClusterKeywordData::ClusterKeywordType>(
"ClusterKeywordType", matched_keyword_data_.type,
matched_keyword_data_.GetKeywordTypeLabel(), executed);
// Record entity collection UMA metrics.
if (matched_keyword_data_.entity_collections.empty()) {
return;
}
const auto& collection_str = matched_keyword_data_.entity_collections.front();
const optimization_guide::PageEntityCollection collection =
optimization_guide::GetPageEntityCollectionForString(collection_str);
const auto collection_label =
optimization_guide::GetPageEntityCollectionLabel(collection_str);
RecordShownUsedEnumAndCtrMetrics<optimization_guide::PageEntityCollection>(
"PageEntityCollection", collection, collection_label, executed);
}
void HistoryClustersAction::Execute(ExecutionContext& context) const {
if (context.client_->OpenJourneys(query_)) {
// If the client opens Journeys in the Side Panel, we are done.
return;
}
// Otherwise call the superclass, which will open the WebUI URL.
OmniboxAction::Execute(context);
}
OmniboxActionId HistoryClustersAction::ActionId() const {
return OmniboxActionId::HISTORY_CLUSTERS;
}
#if defined(SUPPORT_PEDALS_VECTOR_ICONS)
const gfx::VectorIcon& HistoryClustersAction::GetVectorIcon() const {
return omnibox::kJourneysIcon;
}
#endif
#if BUILDFLAG(IS_ANDROID)
base::android::ScopedJavaLocalRef<jobject>
HistoryClustersAction::GetOrCreateJavaObject(JNIEnv* env) const {
if (!j_omnibox_action_) {
j_omnibox_action_.Reset(
BuildHistoryClustersAction(env, strings_.hint, query_));
}
return base::android::ScopedJavaLocalRef<jobject>(j_omnibox_action_);
}
#endif
HistoryClustersAction::~HistoryClustersAction() = default;
// Should be invoked after `AutocompleteResult::AttachPedalsToMatches()`.
void AttachHistoryClustersActions(
history_clusters::HistoryClustersService* service,
PrefService* prefs,
AutocompleteResult& result) {
#if BUILDFLAG(IS_IOS)
// Compile out this method for Mobile, which doesn't omnibox actions yet.
// This is to prevent binary size increase for no reason.
return;
#else
if (!IsJourneysEnabledInOmnibox(service, prefs))
return;
if (!GetConfig().omnibox_action)
return;
if (result.empty())
return;
// If there's any action in `result`, don't add a history cluster action to
// avoid over-crowding.
if (!GetConfig().omnibox_action_with_pedals &&
base::ranges::any_of(
result, [](const auto& match) { return !match.actions.empty(); })) {
return;
}
// If there's a reasonably clear navigation intent, don't distract the user
// with the actions chip.
if (!GetConfig().omnibox_action_on_navigation_intents &&
IsNavigationIntent(
TopRelevance(result.begin(), result.end(),
TopRelevanceFilter::FILTER_FOR_SEARCH_MATCHES),
TopRelevance(result.begin(), result.end(),
TopRelevanceFilter::FILTER_FOR_NON_SEARCH_MATCHES),
GetConfig().omnibox_action_navigation_intent_score_threshold)) {
return;
}
for (auto& match : result) {
// Skip incompatible matches (like entities) or ones with existing actions.
// TODO(manukh): We don't use `AutocompleteMatch::IsActionCompatible()`
// because we're not sure if we want to show on entities or not. Once we
// decide, either share `IsActionCompatible()` or inline it to its
// remaining callsite.
if (!match.actions.empty()) {
continue;
}
if (match.type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL) {
continue;
}
if (AutocompleteMatch::IsSearchType(match.type)) {
std::string query = base::UTF16ToUTF8(match.contents);
absl::optional<history::ClusterKeywordData> matched_keyword_data =
service->DoesQueryMatchAnyCluster(query);
if (matched_keyword_data) {
match.actions.push_back(base::MakeRefCounted<HistoryClustersAction>(
query, std::move(matched_keyword_data.value())));
}
}
// Only ever attach one action (to the highest match), to not overwhelm
// the user with multiple "Resume Journey" action buttons.
if (!match.actions.empty()) {
return;
}
}
#endif
}
} // namespace history_clusters