blob: 30f10728d0558ea094f17d5dce8d9a4f0019ee2b [file] [log] [blame]
// Copyright 2024 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_helper.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.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/contextual_cueing/contextual_cueing_service.h"
#include "chrome/browser/contextual_cueing/contextual_cueing_service_factory.h"
#include "chrome/browser/contextual_cueing/zero_state_suggestions_page_data.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/tabs/glic_nudge_controller.h"
#include "chrome/browser/ui/user_education/browser_user_education_interface.h"
#include "components/history/core/browser/features.h"
#include "components/optimization_guide/core/hints/hints_processing_util.h"
#include "components/optimization_guide/core/hints/optimization_guide_decider.h"
#include "components/optimization_guide/core/hints/optimization_metadata.h"
#include "components/optimization_guide/core/model_execution/model_execution_features_controller.h"
#include "components/optimization_guide/proto/contextual_cueing_metadata.pb.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "net/http/http_response_headers.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
#include "url/origin.h"
#if BUILDFLAG(ENABLE_GLIC)
#include "chrome/browser/glic/public/glic_enabling.h"
#include "chrome/browser/glic/public/glic_keyed_service.h"
#include "chrome/browser/glic/public/glic_keyed_service_factory.h"
#endif
namespace contextual_cueing {
class ScopedNudgeDecisionRecorder {
public:
ScopedNudgeDecisionRecorder(
optimization_guide::proto::OptimizationType optimization_type,
ukm::SourceId source_id)
: optimization_type_(optimization_type), source_id_(source_id) {}
~ScopedNudgeDecisionRecorder() {
base::UmaHistogramEnumeration(
"ContextualCueing.NudgeDecision." +
optimization_guide::GetStringNameForOptimizationType(
optimization_type_),
nudge_decision_);
auto* ukm_recorder = ukm::UkmRecorder::Get();
ukm::builders::ContextualCueing_NudgeDecision(source_id_)
.SetNudgeDecision(static_cast<int64_t>(nudge_decision_))
.SetOptimizationType(optimization_type_)
.Record(ukm_recorder->Get());
}
void set_nudge_decision(NudgeDecision nudge_decision) {
nudge_decision_ = nudge_decision;
}
NudgeDecision nudge_decision() const { return nudge_decision_; }
private:
optimization_guide::proto::OptimizationType optimization_type_;
ukm::SourceId source_id_;
NudgeDecision nudge_decision_ = NudgeDecision::kUnknown;
};
ContextualCueingHelper::ContextualCueingHelper(
content::WebContents* web_contents,
OptimizationGuideKeyedService* ogks,
ContextualCueingService* ccs)
: content::WebContentsObserver(web_contents),
content::WebContentsUserData<ContextualCueingHelper>(*web_contents),
optimization_guide_keyed_service_(ogks),
contextual_cueing_service_(ccs) {
if (base::FeatureList::IsEnabled(kContextualCueing)) {
// LINT.IfChange(OptType)
optimization_guide_keyed_service_->RegisterOptimizationTypes(
{optimization_guide::proto::GLIC_CONTEXTUAL_CUEING});
// LINT.ThenChange(//tools/metrics/histograms/metadata/contextual_cueing/histograms.xml:OptType)
}
}
ContextualCueingHelper::~ContextualCueingHelper() = default;
tabs::GlicNudgeController* ContextualCueingHelper::GetGlicNudgeController() {
if (!base::FeatureList::IsEnabled(kContextualCueing)) {
return nullptr;
}
Browser* browser = chrome::FindBrowserWithTab(web_contents());
if (!browser) {
return nullptr;
}
return browser->browser_window_features()->glic_nudge_controller();
}
void ContextualCueingHelper::PrimaryPageChanged(content::Page& page) {
has_first_contentful_paint_ = false;
}
void ContextualCueingHelper::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
// Ignore sub-frame and uncommitted navigations.
if (!navigation_handle->IsInPrimaryMainFrame()) {
return;
}
if (!navigation_handle->HasCommitted()) {
return;
}
last_same_doc_navigation_committed_ =
navigation_handle->IsSameDocument()
? std::make_optional(base::TimeTicks::Now())
: std::nullopt;
// Ignore reloads.
if (PageTransitionCoreTypeIs(navigation_handle->GetPageTransition(),
ui::PAGE_TRANSITION_RELOAD)) {
return;
}
// Reset FCP state.
has_first_contentful_paint_ = false;
// Clear zero state suggestions if needed.
if (IsZeroStateSuggestionsEnabled() && navigation_handle->IsSameDocument() &&
ZeroStateSuggestionsPageData::GetForPage(
web_contents()->GetPrimaryPage())) {
ZeroStateSuggestionsPageData::DeleteForPage(
web_contents()->GetPrimaryPage());
}
// Ignore fragment changes.
if (navigation_handle->GetPreviousPrimaryMainFrameURL().GetWithoutRef() ==
navigation_handle->GetURL().GetWithoutRef()) {
return;
}
if (!base::FeatureList::IsEnabled(kContextualCueing)) {
return;
}
// Make sure we always clear the nudge label anyway despite operating on
// pages.
auto* glic_nudge_controller = GetGlicNudgeController();
if (glic_nudge_controller) {
glic_nudge_controller->UpdateNudgeLabel(
web_contents(), std::string(),
tabs::GlicNudgeActivity::kNudgeIgnoredNavigation, base::DoNothing());
}
// Do not report page loads for these types of navigations.
if (navigation_handle->IsErrorPage() ||
!navigation_handle->ShouldUpdateHistory()) {
return;
}
// If `history::kVisitedLinksOn404` is enabled, then
// `navigation_handle->ShouldUpdateHistory()` will return true for reachable
// 404 pages. In that case, we need to ignore such pages.
if (base::FeatureList::IsEnabled(history::kVisitedLinksOn404)) {
const int status_code =
navigation_handle->GetResponseHeaders()
? navigation_handle->GetResponseHeaders()->response_code()
: 0;
if (status_code == 404) {
return;
}
}
// We have already initiated nudging sequence for the page. Do not report page
// load.
if (ContextualCueingPageData::GetForPage(web_contents()->GetPrimaryPage())) {
return;
}
contextual_cueing_service_->ReportPageLoad();
}
void ContextualCueingHelper::PrimaryMainDocumentElementAvailable() {
if (!base::FeatureList::IsEnabled(kContextualCueing)) {
return;
}
// We have already initiated nudging sequence for the page. Do not see if we
// should nudge.
if (ContextualCueingPageData::GetForPage(web_contents()->GetPrimaryPage())) {
return;
}
auto* glic_nudge_controller = GetGlicNudgeController();
if (!glic_nudge_controller ||
!web_contents()->GetLastCommittedURL().SchemeIsHTTPOrHTTPS()) {
return;
}
// Determine if server data indicates a nudge should be shown.
optimization_guide_keyed_service_->CanApplyOptimization(
web_contents()->GetLastCommittedURL(),
optimization_guide::proto::GLIC_CONTEXTUAL_CUEING,
base::BindOnce(&ContextualCueingHelper::OnOptimizationGuideCueingMetadata,
weak_ptr_factory_.GetWeakPtr(), base::TimeTicks::Now()));
}
void ContextualCueingHelper::OnFirstContentfulPaintInPrimaryMainFrame() {
if (!IsZeroStateSuggestionsEnabled()) {
return;
}
has_first_contentful_paint_ = true;
ZeroStateSuggestionsPageData* page_data =
ZeroStateSuggestionsPageData::GetForPage(
web_contents()->GetPrimaryPage());
if (page_data) {
page_data->InitiatePageContentExtraction();
}
}
void ContextualCueingHelper::DocumentOnLoadCompletedInPrimaryMainFrame() {
if (!IsZeroStateSuggestionsEnabled()) {
return;
}
ZeroStateSuggestionsPageData* page_data =
ZeroStateSuggestionsPageData::GetForPage(
web_contents()->GetPrimaryPage());
if (page_data) {
page_data->InitiatePageContentExtraction();
}
}
void ContextualCueingHelper::OnOptimizationGuideCueingMetadata(
base::TimeTicks document_available_time,
optimization_guide::OptimizationGuideDecision decision,
const optimization_guide::OptimizationMetadata& metadata) {
std::unique_ptr<ScopedNudgeDecisionRecorder> recorder =
std::make_unique<ScopedNudgeDecisionRecorder>(
optimization_guide::proto::GLIC_CONTEXTUAL_CUEING,
web_contents()->GetPrimaryMainFrame()->GetPageUkmSourceId());
if (decision != optimization_guide::OptimizationGuideDecision::kTrue ||
metadata.empty()) {
recorder->set_nudge_decision(NudgeDecision::kServerDataUnavailable);
return;
}
auto parsed = metadata.ParsedMetadata<
optimization_guide::proto::GlicContextualCueingMetadata>();
if (!parsed) {
recorder->set_nudge_decision(NudgeDecision::kServerDataMalformed);
return;
}
ContextualCueingPageData::CreateForPage(
web_contents()->GetPrimaryPage(), std::move(*parsed),
base::BindOnce(&ContextualCueingHelper::OnCueingDecision,
weak_ptr_factory_.GetWeakPtr(), std::move(recorder),
document_available_time));
}
bool ContextualCueingHelper::IsBrowserBlockingNudges(
ScopedNudgeDecisionRecorder* recorder) {
// Determine if the Browser is available for nudging.
if (!web_contents()) {
return false;
}
auto* tab_interface = tabs::TabInterface::GetFromContents(web_contents());
if (!tab_interface) {
return false;
}
auto* browser_window_interface = tab_interface->GetBrowserWindowInterface();
if (!browser_window_interface) {
return false;
}
auto* user_education_interface =
BrowserUserEducationInterface::From(browser_window_interface);
if (!user_education_interface) {
return false;
}
if (user_education_interface->IsFeaturePromoActive(
feature_engagement::kIPHGlicPromoFeature)) {
recorder->set_nudge_decision(NudgeDecision::kNudgeNotShownIPH);
return true;
}
#if BUILDFLAG(ENABLE_GLIC)
Profile* profile =
Profile::FromBrowserContext(web_contents()->GetBrowserContext());
if (!glic::GlicEnabling::IsEnabledForProfile(profile)) {
return true;
}
auto* glic_service =
glic::GlicKeyedServiceFactory::GetGlicKeyedService(profile);
if (glic_service->IsWindowShowing()) {
recorder->set_nudge_decision(NudgeDecision::kNudgeNotShownWindowShowing);
return true;
}
#endif // BUILDFLAG(ENABLE_GLIC)
return false;
}
void ContextualCueingHelper::OnCueingDecision(
std::unique_ptr<ScopedNudgeDecisionRecorder> decision_recorder,
base::TimeTicks document_available_time,
base::expected<std::string, NudgeDecision> decision_result) {
CHECK_EQ(NudgeDecision::kUnknown, decision_recorder->nudge_decision());
if (ContextualCueingPageData::GetForPage(web_contents()->GetPrimaryPage())) {
ContextualCueingPageData::DeleteForPage(web_contents()->GetPrimaryPage());
}
if (!decision_result.has_value()) {
decision_recorder->set_nudge_decision(decision_result.error());
return;
}
std::string cue_label = decision_result.value();
if (IsBrowserBlockingNudges(decision_recorder.get())) {
return;
}
const GURL& url = web_contents()->GetLastCommittedURL();
auto can_show_decision = contextual_cueing_service_->CanShowNudge(url);
decision_recorder->set_nudge_decision(can_show_decision);
if (can_show_decision != NudgeDecision::kSuccess) {
return;
}
GetGlicNudgeController()->UpdateNudgeLabel(
web_contents(), cue_label, /*activity=*/std::nullopt,
base::BindRepeating(&ContextualCueingService::OnNudgeActivity,
contextual_cueing_service_->GetWeakPtr(),
web_contents(), document_available_time));
}
// static
void ContextualCueingHelper::MaybeCreateForWebContents(
content::WebContents* web_contents) {
if (!base::FeatureList::IsEnabled(contextual_cueing::kContextualCueing) &&
!base::FeatureList::IsEnabled(
contextual_cueing::kGlicZeroStateSuggestions)) {
return;
}
#if BUILDFLAG(ENABLE_GLIC)
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
if (!glic::GlicEnabling::IsProfileEligible(profile)) {
return;
}
auto* optimization_guide_keyed_service =
OptimizationGuideKeyedServiceFactory::GetForProfile(profile);
if (!optimization_guide_keyed_service) {
return;
}
auto* contextual_cueing_service =
ContextualCueingServiceFactory::GetForProfile(profile);
if (!contextual_cueing_service) {
return;
}
ContextualCueingHelper::CreateForWebContents(web_contents,
optimization_guide_keyed_service,
contextual_cueing_service);
#endif // BUILDFLAG(ENABLE_GLIC)
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(ContextualCueingHelper);
} // namespace contextual_cueing