blob: 087105e5b9d8174081f02bae13d0853903123697 [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/dips/dips_navigation_flow_detector.h"
#include "chrome/browser/dips/dips_service.h"
#include "content/public/browser/cookie_access_details.h"
#include "content/public/browser/web_contents.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"
namespace dips {
PageVisitInfo::PageVisitInfo() {
site = "";
source_id = ukm::kInvalidSourceId;
did_page_access_cookies = false;
did_page_access_storage = false;
did_page_receive_user_activation = false;
did_page_have_successful_waa = false;
was_navigation_to_page_user_initiated = std::nullopt;
was_navigation_to_page_renderer_initiated = std::nullopt;
did_site_have_prior_activation_record = std::nullopt;
}
PageVisitInfo::PageVisitInfo(PageVisitInfo&& other) = default;
} // namespace dips
DipsNavigationFlowDetector::DipsNavigationFlowDetector(
content::WebContents* web_contents,
DIPSService* dips_service)
: content::WebContentsObserver(web_contents),
content::WebContentsUserData<DipsNavigationFlowDetector>(*web_contents),
current_page_visit_info_(dips::PageVisitInfo()),
dips_service_(dips_service) {}
DipsNavigationFlowDetector::~DipsNavigationFlowDetector() = default;
/* static */
void DipsNavigationFlowDetector::MaybeCreateForWebContents(
content::WebContents* web_contents) {
DIPSService* dips_service =
DIPSService::Get(web_contents->GetBrowserContext());
if (!dips_service) {
return;
}
DipsNavigationFlowDetector::CreateForWebContents(web_contents, dips_service);
}
void DipsNavigationFlowDetector::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
bool primary_page_changed = navigation_handle->IsInPrimaryMainFrame() &&
!navigation_handle->IsSameDocument() &&
navigation_handle->HasCommitted();
if (!primary_page_changed) {
return;
}
content::RenderFrameHost* render_frame_host =
navigation_handle->GetWebContents()->GetPrimaryMainFrame();
GURL current_page_url = render_frame_host->GetLastCommittedURL();
if (current_page_url == url::kAboutBlankURL) {
return;
}
bool is_first_page_load_in_tab = current_page_visit_info_->site.empty();
if (!is_first_page_load_in_tab) {
if (previous_page_visit_info_) {
two_pages_ago_visit_info_.emplace(std::move(*previous_page_visit_info_));
}
if (current_page_visit_info_) {
previous_page_visit_info_.emplace(std::move(*current_page_visit_info_));
}
current_page_visit_info_.emplace(dips::PageVisitInfo());
}
CheckIfSiteHadPriorActivation(current_page_url);
current_page_visit_info_->site = GetSiteForDIPS(current_page_url);
current_page_visit_info_->source_id = render_frame_host->GetPageUkmSourceId();
current_page_visit_info_->was_navigation_to_page_renderer_initiated =
navigation_handle->IsRendererInitiated();
current_page_visit_info_->was_navigation_to_page_user_initiated =
!navigation_handle->IsRendererInitiated() ||
navigation_handle->HasUserGesture();
base::Time now = clock_->Now();
if (!is_first_page_load_in_tab) {
int64_t raw_visit_duration_ms =
(now - last_page_change_time_).InMilliseconds();
bucketized_previous_page_visit_duration_ =
ukm::GetExponentialBucketMinForUserTiming(raw_visit_duration_ms);
}
last_page_change_time_ = now;
MaybeEmitUkmForPreviousPage();
}
void DipsNavigationFlowDetector::CheckIfSiteHadPriorActivation(GURL url) {
dips_service_->DidSiteHaveInteractionSince(
url, base::Time::Min(),
base::BindOnce(&DipsNavigationFlowDetector::GotDipsInteraction,
weak_factory_.GetWeakPtr(),
current_page_visit_info_->site));
}
void DipsNavigationFlowDetector::GotDipsInteraction(
std::string site_read_state_for,
bool had_interaction) {
// If the site we got state for is not the current site, then the DIPS DB read
// didn't return until after the site was navigated away from. In that case,
// we've already emitted UKM (or decided not to emit) for that page, so
// discard the value.
if (site_read_state_for != current_page_visit_info_->site) {
return;
}
current_page_visit_info_->did_site_have_prior_activation_record =
had_interaction;
}
void DipsNavigationFlowDetector::MaybeEmitUkmForPreviousPage() {
if (!CanEmitUkmForPreviousPage()) {
return;
}
ukm::builders::DIPS_NavigationFlowNode builder(
previous_page_visit_info_->source_id);
builder
.SetWerePreviousAndNextSiteSame(two_pages_ago_visit_info_->site ==
current_page_visit_info_->site)
.SetDidHaveUserActivation(
previous_page_visit_info_->did_page_receive_user_activation)
.SetDidHaveSuccessfulWAA(
previous_page_visit_info_->did_page_have_successful_waa)
.SetWereEntryAndExitRendererInitiated(
*previous_page_visit_info_
->was_navigation_to_page_renderer_initiated &&
*current_page_visit_info_->was_navigation_to_page_renderer_initiated)
.SetWasEntryUserInitiated(
*previous_page_visit_info_->was_navigation_to_page_user_initiated)
.SetWasExitUserInitiated(
*current_page_visit_info_->was_navigation_to_page_user_initiated)
.SetVisitDurationMilliseconds(bucketized_previous_page_visit_duration_);
if (previous_page_visit_info_->did_site_have_prior_activation_record) {
builder.SetDidSiteHavePreviousUserActivation(
*previous_page_visit_info_->did_site_have_prior_activation_record);
}
builder.Record(ukm::UkmRecorder::Get());
}
void DipsNavigationFlowDetector::OnCookiesAccessed(
content::RenderFrameHost* render_frame_host,
const content::CookieAccessDetails& details) {
// Ignore notifications for prerenders, fenced frames, etc., and for blocked
// access attempts.
if (!IsInPrimaryPage(render_frame_host) || details.blocked_by_policy) {
return;
}
// Attribute accesses by iframes to the first-party page they're embedded in.
const std::optional<GURL> first_party_url =
GetFirstPartyURL(render_frame_host);
if (!first_party_url.has_value()) {
return;
}
const std::string first_party_site = GetSiteForDIPS(first_party_url.value());
// DIPS mitigations are only turned on when non-CHIPS 3PCs are blocked, so
// mirror that behavior by ignoring non-CHIPS 3PC accesses.
if (!HasCHIPS(details.cookie_access_result_list) &&
!IsSameSiteForDIPS(first_party_url.value(), details.url)) {
return;
}
// If the site we received the cookie access notification for is not the same
// as the current site, that means that site has since been navigated away
// from. In that case, we've already emitted UKM (or decided not to emit) for
// that page, so ignore the notification.
if (first_party_site != current_page_visit_info_->site) {
return;
}
current_page_visit_info_->did_page_access_cookies = true;
}
void DipsNavigationFlowDetector::OnCookiesAccessed(
content::NavigationHandle* navigation_handle,
const content::CookieAccessDetails& details) {
// Ignore notifications for prerenders, fenced frames, etc., and for blocked
// access attempts.
if (!IsInPrimaryPage(navigation_handle) || details.blocked_by_policy) {
return;
}
// Treat cookie accesses from iframe navigations as content-initiated.
if (IsInPrimaryPageIFrame(navigation_handle)) {
const std::optional<GURL> first_party_url =
GetFirstPartyURL(navigation_handle);
if (!first_party_url.has_value()) {
return;
}
// DIPS mitigations are only turned on when non-CHIPS 3PCs are blocked, so
// mirror that behavior by ignoring non-CHIPS 3PC accesses.
if (!HasCHIPS(details.cookie_access_result_list) &&
!IsSameSiteForDIPS(first_party_url.value(), details.url)) {
return;
}
current_page_visit_info_->did_page_access_cookies = true;
return;
}
// For accesses in main frame navigations, only count writes, as the browser
// sends cookies automatically and so sites have no control over whether they
// read cookies or not.
if (details.type == network::mojom::CookieAccessDetails_Type::kChange) {
current_page_visit_info_->did_page_access_cookies = true;
}
}
void DipsNavigationFlowDetector::NotifyStorageAccessed(
content::RenderFrameHost* render_frame_host,
blink::mojom::StorageTypeAccessed storage_type,
bool blocked) {
if (!render_frame_host->IsInPrimaryMainFrame() || blocked) {
return;
}
current_page_visit_info_->did_page_access_storage = true;
}
void DipsNavigationFlowDetector::FrameReceivedUserActivation(
content::RenderFrameHost* render_frame_host) {
current_page_visit_info_->did_page_receive_user_activation = true;
}
void DipsNavigationFlowDetector::WebAuthnAssertionRequestSucceeded(
content::RenderFrameHost* render_frame_host) {
if (!render_frame_host->IsInPrimaryMainFrame()) {
return;
}
current_page_visit_info_->did_page_have_successful_waa = true;
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(DipsNavigationFlowDetector);