blob: 42951fe8b1ae8b79be0d465d76ccbb285e63b748 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/contextual_cueing/contextual_cueing_service.h"
#include <cmath>
#include "base/metrics/histogram_functions.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/contextual_cueing/contextual_cueing_enums.h"
#include "chrome/browser/contextual_cueing/contextual_cueing_features.h"
#include "chrome/browser/contextual_cueing/contextual_cueing_page_data.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/predictors/loading_predictor.h"
#include "chrome/browser/ui/tabs/glic_nudge_controller.h"
#include "chrome/common/buildflags.h"
#include "components/optimization_guide/core/optimization_guide_switches.h"
#include "components/optimization_guide/proto/hints.pb.h"
#include "components/prefs/pref_service.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_service.h"
#include "content/public/browser/web_contents.h"
#include "net/base/network_anonymization_key.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "url/gurl.h"
#if BUILDFLAG(ENABLE_GLIC)
#include "chrome/browser/glic/glic_pref_names.h"
#endif
namespace contextual_cueing {
namespace {
void LogNudgeInteractionHistogram(NudgeInteraction interaction) {
base::UmaHistogramEnumeration("ContextualCueing.NudgeInteraction",
interaction);
}
void LogNudgeInteractionUKM(ukm::SourceId source_id,
NudgeInteraction interaction,
base::TimeTicks document_available_time,
base::TimeTicks nudge_shown_time) {
auto* ukm_recorder = ukm::UkmRecorder::Get();
ukm::builders::ContextualCueing_NudgeInteraction(source_id)
.SetNudgeInteraction(static_cast<int64_t>(interaction))
.SetNudgeShownDuration(ukm::GetExponentialBucketMinForUserTiming(
(base::TimeTicks::Now() - nudge_shown_time).InMilliseconds()))
.SetNudgeLatencyAfterPageLoad(
(nudge_shown_time - document_available_time).InMilliseconds())
.Record(ukm_recorder->Get());
}
#if BUILDFLAG(ENABLE_GLIC)
bool IsGlicTabContextEnabled(PrefService* pref_service) {
return pref_service->GetBoolean(glic::prefs::kGlicTabContextEnabled);
}
#endif
} // namespace
ContextualCueingService::ContextualCueingService(
page_content_annotations::PageContentExtractionService*
page_content_extraction_service,
OptimizationGuideKeyedService* optimization_guide_keyed_service,
predictors::LoadingPredictor* loading_predictor,
PrefService* pref_service,
TemplateURLService* template_url_service)
: recent_nudge_tracker_(kNudgeCapCount.Get(), kNudgeCapTime.Get()),
recent_visited_origins_(kVisitedDomainsLimit.Get()),
page_content_extraction_service_(page_content_extraction_service),
optimization_guide_keyed_service_(optimization_guide_keyed_service),
loading_predictor_(loading_predictor),
pref_service_(pref_service),
template_url_service_(template_url_service),
mes_url_(optimization_guide::switches::GetModelExecutionServiceURL()) {
CHECK(base::FeatureList::IsEnabled(contextual_cueing::kContextualCueing) ||
base::FeatureList::IsEnabled(
contextual_cueing::kGlicZeroStateSuggestions));
if (optimization_guide_keyed_service_ &&
base::FeatureList::IsEnabled(
contextual_cueing::kGlicZeroStateSuggestions)) {
optimization_guide_keyed_service_->RegisterOptimizationTypes(
{optimization_guide::proto::GLIC_ZERO_STATE_SUGGESTIONS});
}
if (kEnablePageContentExtraction.Get() && page_content_extraction_service_) {
page_content_extraction_service_->AddObserver(this);
}
}
ContextualCueingService::~ContextualCueingService() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (kEnablePageContentExtraction.Get() && page_content_extraction_service_) {
page_content_extraction_service_->RemoveObserver(this);
}
}
void ContextualCueingService::ReportPageLoad() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (remaining_quiet_loads_) {
remaining_quiet_loads_--;
}
}
void ContextualCueingService::CueingNudgeShown(const GURL& url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
recent_nudge_tracker_.CueingNudgeShown();
shown_backoff_end_time_ =
base::TimeTicks::Now() + kMinTimeBetweenNudges.Get();
if (kMinPageCountBetweenNudges.Get()) {
// Let the cue logic be performed the next page after quiet count pages.
remaining_quiet_loads_ = kMinPageCountBetweenNudges.Get() + 1;
}
auto origin = url::Origin::Create(url);
auto iter = recent_visited_origins_.Get(origin);
if (iter == recent_visited_origins_.end()) {
iter = recent_visited_origins_.Put(
origin, NudgeCapTracker(kNudgeCapCountPerDomain.Get(),
kNudgeCapTimePerDomain.Get()));
}
iter->second.CueingNudgeShown();
}
void ContextualCueingService::CueingNudgeDismissed() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::TimeDelta backoff_duration =
kBackoffTime.Get() * pow(kBackoffMultiplierBase.Get(), dismiss_count_);
dismiss_backoff_end_time_ = base::TimeTicks::Now() + backoff_duration;
++dismiss_count_;
}
void ContextualCueingService::CueingNudgeClicked() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
dismiss_count_ = 0;
}
NudgeDecision ContextualCueingService::CanShowNudge(const GURL& url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (remaining_quiet_loads_ > 0) {
return NudgeDecision::kNotEnoughPageLoadsSinceLastNudge;
}
if (shown_backoff_end_time_ &&
base::TimeTicks::Now() < shown_backoff_end_time_) {
return NudgeDecision::kNotEnoughTimeSinceLastNudgeShown;
}
if (IsNudgeBlockedByBackoffRule()) {
return NudgeDecision::kNotEnoughTimeSinceLastNudgeDismissed;
}
if (!recent_nudge_tracker_.CanShowNudge()) {
return NudgeDecision::kTooManyNudgesShownToTheUser;
}
auto iter = recent_visited_origins_.Peek(url::Origin::Create(url));
if (iter != recent_visited_origins_.end() && !iter->second.CanShowNudge()) {
return NudgeDecision::kTooManyNudgesShownToTheUserForDomain;
}
return NudgeDecision::kSuccess;
}
bool ContextualCueingService::IsNudgeBlockedByBackoffRule() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return dismiss_backoff_end_time_ &&
(base::TimeTicks::Now() < dismiss_backoff_end_time_);
}
bool ContextualCueingService::IsPageTypeEligibleForContextualSuggestions(
GURL url) const {
// Non-HTTP/HTTPS pages are not eligible.
if (!url.SchemeIsHTTPOrHTTPS()) {
return false;
}
// Search results pages are not eligible.
if (template_url_service_ &&
template_url_service_->ExtractSearchMetadata(url)) {
return false;
}
return true;
}
void ContextualCueingService::OnNudgeActivity(
content::WebContents* web_contents,
base::TimeTicks document_available_time,
tabs::GlicNudgeActivity activity) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::optional<base::TimeTicks> nudge_time =
recent_nudge_tracker_.GetMostRecentNudgeTime();
const GURL& url = web_contents->GetLastCommittedURL();
NudgeInteraction interaction;
bool log_ukm = false;
switch (activity) {
case tabs::GlicNudgeActivity::kNudgeShown:
interaction = NudgeInteraction::kShown;
CueingNudgeShown(url);
break;
case tabs::GlicNudgeActivity::kNudgeClicked:
CueingNudgeClicked();
interaction = NudgeInteraction::kClicked;
log_ukm = true;
break;
case tabs::GlicNudgeActivity::kNudgeDismissed:
interaction = NudgeInteraction::kDismissed;
CueingNudgeDismissed();
log_ukm = true;
break;
case tabs::GlicNudgeActivity::kNudgeNotShownWebContents:
interaction = NudgeInteraction::kNudgeNotShownWebContents;
break;
case tabs::GlicNudgeActivity::kNudgeNotShownWindowCallToActionUI:
interaction = NudgeInteraction::kNudgeNotShownWindowCallToActionUI;
break;
case tabs::GlicNudgeActivity::kNudgeIgnoredActiveTabChanged:
interaction = NudgeInteraction::kIgnoredTabChange;
log_ukm = true;
break;
case tabs::GlicNudgeActivity::kNudgeIgnoredNavigation:
interaction = NudgeInteraction::kIgnoredNavigation;
log_ukm = true;
break;
}
LogNudgeInteractionHistogram(interaction);
// As this function is called multiple times per nudge only some of the
// activities result in a UKM call.
if (log_ukm) {
CHECK(nudge_time);
LogNudgeInteractionUKM(
web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId(), interaction,
document_available_time, *nudge_time);
}
}
void ContextualCueingService::PrepareToFetchContextualGlicZeroStateSuggestions(
content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!base::FeatureList::IsEnabled(kGlicZeroStateSuggestions)) {
return;
}
#if BUILDFLAG(ENABLE_GLIC)
if (!IsPageTypeEligibleForContextualSuggestions(
web_contents->GetLastCommittedURL())) {
return;
}
if (!IsGlicTabContextEnabled(pref_service_)) {
return;
}
// This call preflights grabbing the page content.
ZeroStateSuggestionsPageData::CreateForPage(web_contents->GetPrimaryPage());
if (loading_predictor_) {
net::NetworkAnonymizationKey anonymization_key =
net::NetworkAnonymizationKey::CreateSameSite(
net::SchemefulSite(mes_url_));
loading_predictor_->PreconnectURLIfAllowed(
mes_url_, /*allow_credentials=*/true, anonymization_key);
}
#endif
}
void ContextualCueingService::GetContextualGlicZeroStateSuggestions(
content::WebContents* web_contents,
bool is_fre,
GlicSuggestionsCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!base::FeatureList::IsEnabled(kGlicZeroStateSuggestions)) {
std::move(callback).Run(std::nullopt);
return;
}
if (!IsPageTypeEligibleForContextualSuggestions(
web_contents->GetLastCommittedURL())) {
std::move(callback).Run(std::nullopt);
return;
}
#if BUILDFLAG(ENABLE_GLIC)
if (!IsGlicTabContextEnabled(pref_service_)) {
std::move(callback).Run(std::nullopt);
return;
}
// Remote suggestions generation.
ZeroStateSuggestionsPageData* page_data =
ZeroStateSuggestionsPageData::GetOrCreateForPage(
web_contents->GetPrimaryPage());
page_data->FetchSuggestions(is_fre, std::move(callback));
#else
std::move(callback).Run(std::nullopt);
#endif
}
void ContextualCueingService::OnSuggestionsReceived(
content::WebContents* web_contents,
GlicSuggestionsCallback callback,
std::optional<std::vector<std::string>> suggestions) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::move(callback).Run(suggestions);
}
void ContextualCueingService::OnPageContentExtracted(
content::Page& page,
const optimization_guide::proto::AnnotatedPageContent& page_content) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto* cueing_page_data = ContextualCueingPageData::GetForPage(page);
if (!cueing_page_data) {
return;
}
cueing_page_data->OnPageContentExtracted(page_content);
}
} // namespace contextual_cueing