blob: 566ffead69e6a63a4414e4468e5879aec1b25261 [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 "content/browser/dips/dips_navigation_flow_detector.h"
#include "base/rand_util.h"
#include "content/browser/dips/dips_utils.h"
#include "content/public/browser/cookie_access_details.h"
#include "content/public/browser/navigation_handle.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 content {
namespace {
enum class QuantityBucket {
kZero = 0,
kOne,
kMultiple,
};
// Types that qualify a navigation for the
// DIPS.TrustIndicator.DirectNavigationV2 UKM event. Should only contain core
// page transition types (no qualifiers).
constexpr const std::array<ui::PageTransition, 2>&
kDirectNavigationPageTransitions{
ui::PAGE_TRANSITION_TYPED,
ui::PAGE_TRANSITION_AUTO_BOOKMARK,
};
bool IsPageTransitionDirectNavigation(ui::PageTransition page_transition) {
for (auto& direct_navigation_type : kDirectNavigationPageTransitions) {
if (ui::PageTransitionCoreTypeIs(page_transition, direct_navigation_type)) {
return true;
}
}
return false;
}
dips::DirectNavigationSource ToDirectNavigationSource(
ui::PageTransition page_transition) {
if (ui::PageTransitionCoreTypeIs(page_transition,
ui::PAGE_TRANSITION_TYPED)) {
return dips::DirectNavigationSource::kOmnibar;
}
if (ui::PageTransitionCoreTypeIs(page_transition,
ui::PAGE_TRANSITION_AUTO_BOOKMARK)) {
return dips::DirectNavigationSource::kBookmark;
}
return dips::DirectNavigationSource::kUnknown;
}
// Looks for a redirect to the current page that qualifies as a server-redirect
// exit from a suspected tracker flow (i.e., a single-hop server-side redirect)
// and returns it, if one exists. Returns nullptr otherwise.
const BtmRedirectInfo* GetEntrypointExitServerRedirect(
const BtmRedirectContext& redirect_context) {
base::span<const BtmRedirectInfoPtr> server_redirects =
redirect_context.GetServerRedirectsSinceLastPrimaryPageChange();
return server_redirects.size() == 1 ? server_redirects.front().get()
: nullptr;
}
const BtmRedirectInfo* GetFirstServerRedirect(
const BtmRedirectContext& redirect_context) {
base::span<const BtmRedirectInfoPtr> server_redirects =
redirect_context.GetServerRedirectsSinceLastPrimaryPageChange();
return server_redirects.empty() ? nullptr : server_redirects.front().get();
}
QuantityBucket GetCrossSiteRedirectQuantity(
const std::string& initial_site,
base::span<const BtmRedirectInfoPtr> server_redirects,
const std::string& final_site) {
const std::string* referring_site = &initial_site;
size_t num_cross_site_redirects = 0;
for (const auto& server_redirect : server_redirects) {
if (server_redirect->site != *referring_site) {
num_cross_site_redirects += 1;
if (num_cross_site_redirects >= 2) {
return QuantityBucket::kMultiple;
}
referring_site = &server_redirect->site;
}
}
if (final_site != *referring_site) {
num_cross_site_redirects += 1;
}
switch (num_cross_site_redirects) {
case 0:
return QuantityBucket::kZero;
case 1:
return QuantityBucket::kOne;
default:
return QuantityBucket::kMultiple;
}
}
void EmitSuspectedTrackerFlowUkm(ukm::SourceId referrer_source_id,
ukm::SourceId entrypoint_source_id,
int32_t flow_id,
BtmRedirectType exit_redirect_type) {
ukm::builders::DIPS_SuspectedTrackerFlowReferrerV2(referrer_source_id)
.SetFlowId(flow_id)
.Record(ukm::UkmRecorder::Get());
ukm::builders::DIPS_SuspectedTrackerFlowEntrypointV2(entrypoint_source_id)
.SetExitRedirectType(static_cast<int64_t>(exit_redirect_type))
.SetFlowId(flow_id)
.Record(ukm::UkmRecorder::Get());
}
void MaybeEmitDirectNavigationUkm(NavigationHandle* navigation_handle,
const BtmRedirectContext& redirect_context) {
if (!IsPageTransitionDirectNavigation(
navigation_handle->GetPageTransition())) {
return;
}
const BtmRedirectInfo* first_server_redirect =
GetFirstServerRedirect(redirect_context);
ukm::SourceId source_id = first_server_redirect
? first_server_redirect->url.source_id
: navigation_handle->GetNextPageUkmSourceId();
ukm::builders::DIPS_TrustIndicator_DirectNavigationV2(source_id)
.SetNavigationSource(static_cast<int64_t>(
ToDirectNavigationSource(navigation_handle->GetPageTransition())))
.Record(ukm::UkmRecorder::Get());
}
} // namespace
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;
}
PageVisitInfo::PageVisitInfo(PageVisitInfo&& other) = default;
PageVisitInfo& PageVisitInfo::operator=(PageVisitInfo&& other) = default;
bool PageVisitInfo::WasNavigationToPageClientRedirect() const {
return was_navigation_to_page_renderer_initiated.has_value() &&
*was_navigation_to_page_renderer_initiated &&
was_navigation_to_page_user_initiated.has_value() &&
!*was_navigation_to_page_user_initiated;
}
EntrypointInfo::EntrypointInfo(const BtmRedirectInfo& server_redirect_info,
const dips::PageVisitInfo& exit_page_info)
: site(server_redirect_info.site),
source_id(server_redirect_info.url.source_id),
had_triggering_storage_access(
server_redirect_info.access_type == BtmDataAccessType::kWrite ||
server_redirect_info.access_type == BtmDataAccessType::kReadWrite),
was_referral_client_redirect(
exit_page_info.WasNavigationToPageClientRedirect()) {}
EntrypointInfo::EntrypointInfo(
const dips::PageVisitInfo& client_redirector_info)
: site(client_redirector_info.site),
source_id(client_redirector_info.source_id),
had_triggering_storage_access(
client_redirector_info.did_page_access_storage ||
client_redirector_info.did_page_access_cookies),
was_referral_client_redirect(
client_redirector_info.WasNavigationToPageClientRedirect()) {}
InFlowSuccessorInteractionState::InFlowSuccessorInteractionState(
dips::EntrypointInfo flow_entrypoint)
: flow_entrypoint_(flow_entrypoint) {}
InFlowSuccessorInteractionState::~InFlowSuccessorInteractionState() = default;
void InFlowSuccessorInteractionState::
RecordTriggeringStorageAccessByEntrypoint() {
flow_entrypoint_.had_triggering_storage_access = true;
}
void InFlowSuccessorInteractionState::IncrementFlowIndex(size_t increment) {
flow_index_ += increment;
}
void InFlowSuccessorInteractionState::
RecordSuccessorInteractionAtCurrentFlowIndex() {
bool has_existing_record_for_current_index =
!successor_interaction_indices_.empty() &&
successor_interaction_indices_.back() == flow_index_;
if (!has_existing_record_for_current_index) {
successor_interaction_indices_.push_back(flow_index_);
}
}
bool InFlowSuccessorInteractionState::IsAtSuccessor() const {
return flow_index_ > 0;
}
} // namespace dips
BtmNavigationFlowDetector::BtmNavigationFlowDetector(WebContents* web_contents)
: WebContentsObserver(web_contents),
WebContentsUserData<BtmNavigationFlowDetector>(*web_contents),
current_page_visit_info_(dips::PageVisitInfo()) {
redirect_chain_observation_.Observe(
RedirectChainDetector::FromWebContents(web_contents));
}
BtmNavigationFlowDetector::~BtmNavigationFlowDetector() = default;
void BtmNavigationFlowDetector::OnNavigationCommitted(
NavigationHandle* navigation_handle) {
bool primary_page_changed = navigation_handle->IsInPrimaryMainFrame() &&
!navigation_handle->IsSameDocument() &&
navigation_handle->HasCommitted();
if (!primary_page_changed) {
return;
}
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();
two_pages_ago_visit_info_ = std::move(previous_page_visit_info_);
previous_page_visit_info_ = std::move(current_page_visit_info_);
current_page_visit_info_.emplace();
current_page_visit_info_->url = current_page_url;
current_page_visit_info_->site = GetSiteForBtm(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();
if (navigation_cookie_access_url_ == current_page_url) {
current_page_visit_info_->did_page_access_cookies = true;
}
navigation_cookie_access_url_ = std::nullopt;
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;
const BtmRedirectContext& redirect_context = GetRedirectContext();
bool did_start_new_flow = MaybeInitializeSuccessorInteractionTrackingState();
flow_status_ = FlowStatusAfterNavigation(did_start_new_flow);
if (flow_status_ == dips::FlowStatus::kOngoing && !did_start_new_flow) {
successor_interaction_tracking_state_->IncrementFlowIndex(
redirect_context.GetServerRedirectsSinceLastPrimaryPageChange().size() +
1);
}
if (flow_status_ == dips::FlowStatus::kEnded) {
MaybeEmitInFlowSuccessorInteraction();
}
if (flow_status_ != dips::FlowStatus::kOngoing) {
successor_interaction_tracking_state_.reset();
}
MaybeEmitDirectNavigationUkm(navigation_handle, redirect_context);
MaybeEmitNavFlowNodeUkmForPreviousPage();
int32_t flow_id = static_cast<int32_t>(base::RandUint64());
const BtmRedirectInfo* server_redirect_entrypoint_exit =
GetEntrypointExitServerRedirect(redirect_context);
if (server_redirect_entrypoint_exit != nullptr) {
MaybeEmitSuspectedTrackerFlowUkmForServerRedirectExit(
server_redirect_entrypoint_exit, flow_id);
} else {
MaybeEmitSuspectedTrackerFlowUkmForClientRedirectExit(flow_id);
MaybeEmitInFlowInteraction(flow_id);
}
}
void BtmNavigationFlowDetector::MaybeEmitNavFlowNodeUkmForPreviousPage() {
if (!CanEmitNavFlowNodeUkmForPreviousPage()) {
return;
}
ukm::builders::DIPS_NavigationFlowNode(previous_page_visit_info_->source_id)
.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_)
.Record(ukm::UkmRecorder::Get());
}
bool BtmNavigationFlowDetector::CanEmitNavFlowNodeUkmForPreviousPage() const {
bool page_is_in_series_of_three = two_pages_ago_visit_info_.has_value() &&
!two_pages_ago_visit_info_->site.empty() &&
previous_page_visit_info_.has_value() &&
!previous_page_visit_info_->site.empty() &&
current_page_visit_info_.has_value() &&
!current_page_visit_info_->site.empty();
if (!page_is_in_series_of_three) {
return false;
}
bool page_has_valid_source_id =
previous_page_visit_info_->source_id != ukm::kInvalidSourceId;
bool site_had_triggering_storage_access =
previous_page_visit_info_->did_page_access_cookies ||
previous_page_visit_info_->did_page_access_storage;
bool is_site_different_from_prior_page =
previous_page_visit_info_->site != two_pages_ago_visit_info_->site;
bool is_site_different_from_next_page =
previous_page_visit_info_->site != current_page_visit_info_->site;
return page_has_valid_source_id && site_had_triggering_storage_access &&
is_site_different_from_prior_page && is_site_different_from_next_page;
}
void BtmNavigationFlowDetector::
MaybeEmitSuspectedTrackerFlowUkmForServerRedirectExit(
const BtmRedirectInfo* exit_info,
int32_t flow_id) {
if (!CanEmitSuspectedTrackerFlowUkmForServerRedirectExit(exit_info)) {
return;
}
EmitSuspectedTrackerFlowUkm(previous_page_visit_info_->source_id,
exit_info->url.source_id, flow_id,
BtmRedirectType::kServer);
}
bool BtmNavigationFlowDetector::
CanEmitSuspectedTrackerFlowUkmForServerRedirectExit(
const BtmRedirectInfo* exit_info) const {
if (!previous_page_visit_info_.has_value() || exit_info == nullptr ||
!current_page_visit_info_.has_value()) {
return false;
}
dips::EntrypointInfo entrypoint_info_for_server_redirect_exit(
*exit_info, *current_page_visit_info_);
return CanEmitSuspectedTrackerFlowUkm(
*previous_page_visit_info_, entrypoint_info_for_server_redirect_exit,
*current_page_visit_info_);
}
void BtmNavigationFlowDetector::
MaybeEmitSuspectedTrackerFlowUkmForClientRedirectExit(int32_t flow_id) {
if (!CanEmitSuspectedTrackerFlowUkmForClientRedirectExit()) {
return;
}
EmitSuspectedTrackerFlowUkm(two_pages_ago_visit_info_->source_id,
previous_page_visit_info_->source_id, flow_id,
BtmRedirectType::kClient);
}
bool BtmNavigationFlowDetector::
CanEmitSuspectedTrackerFlowUkmForClientRedirectExit() const {
bool page_is_in_series_of_three = two_pages_ago_visit_info_.has_value() &&
previous_page_visit_info_.has_value() &&
current_page_visit_info_.has_value();
if (!page_is_in_series_of_three) {
return false;
}
std::optional<bool> is_exit_client_redirect =
current_page_visit_info_->WasNavigationToPageClientRedirect();
if (!is_exit_client_redirect.has_value() ||
!is_exit_client_redirect.value()) {
return false;
}
dips::EntrypointInfo entrypoint_info(previous_page_visit_info_.value());
return CanEmitSuspectedTrackerFlowUkm(two_pages_ago_visit_info_.value(),
entrypoint_info,
current_page_visit_info_.value());
}
bool BtmNavigationFlowDetector::CanEmitSuspectedTrackerFlowUkm(
const dips::PageVisitInfo& referrer_page_info,
const dips::EntrypointInfo& entrypoint_info,
const dips::PageVisitInfo& exit_page_info) const {
bool referrer_has_valid_source_id =
referrer_page_info.source_id != ukm::kInvalidSourceId;
bool entrypoint_has_valid_source_id =
entrypoint_info.source_id != ukm::kInvalidSourceId;
bool is_entrypoint_site_different_from_referrer =
entrypoint_info.site != referrer_page_info.site;
bool is_entrypoint_site_different_from_exit_page =
entrypoint_info.site != exit_page_info.site;
return referrer_has_valid_source_id && entrypoint_has_valid_source_id &&
is_entrypoint_site_different_from_referrer &&
is_entrypoint_site_different_from_exit_page &&
entrypoint_info.had_triggering_storage_access &&
entrypoint_info.was_referral_client_redirect;
}
void BtmNavigationFlowDetector::MaybeEmitInFlowInteraction(int32_t flow_id) {
if (!CanEmitSuspectedTrackerFlowUkmForClientRedirectExit() ||
!previous_page_visit_info_->did_page_receive_user_activation) {
return;
}
ukm::builders::DIPS_TrustIndicator_InFlowInteractionV2(
previous_page_visit_info_->source_id)
.SetFlowId(flow_id)
.Record(ukm::UkmRecorder::Get());
}
void BtmNavigationFlowDetector::MaybeEmitInFlowSuccessorInteraction() {
if (!successor_interaction_tracking_state_.has_value() ||
successor_interaction_tracking_state_->successor_interaction_indices()
.empty()) {
return;
}
for (size_t index :
successor_interaction_tracking_state_->successor_interaction_indices()) {
ukm::builders::DIPS_TrustIndicator_InFlowSuccessorInteraction(
successor_interaction_tracking_state_->flow_entrypoint().source_id)
.SetSuccessorRedirectIndex(index)
.SetDidEntrypointAccessStorage(
successor_interaction_tracking_state_->flow_entrypoint()
.had_triggering_storage_access)
.Record(ukm::UkmRecorder::Get());
}
}
dips::FlowStatus BtmNavigationFlowDetector::FlowStatusAfterNavigation(
bool did_most_recent_navigation_start_new_flow) const {
if (!current_page_visit_info_->WasNavigationToPageClientRedirect()) {
return dips::FlowStatus::kInvalidated;
}
if (!successor_interaction_tracking_state_.has_value()) {
return dips::FlowStatus::kInvalidated;
}
const base::span<const BtmRedirectInfoPtr> server_redirects =
GetRedirectContext().GetServerRedirectsSinceLastPrimaryPageChange();
if (did_most_recent_navigation_start_new_flow) {
bool is_still_on_entrypoint = server_redirects.empty();
if (is_still_on_entrypoint) {
return dips::FlowStatus::kOngoing;
}
return successor_interaction_tracking_state_->flow_entrypoint().site ==
current_page_visit_info_->site
? dips::FlowStatus::kOngoing
: dips::FlowStatus::kEnded;
}
QuantityBucket cross_site_redirect_quantity_bucket =
GetCrossSiteRedirectQuantity(previous_page_visit_info_->site,
server_redirects,
current_page_visit_info_->site);
switch (cross_site_redirect_quantity_bucket) {
case QuantityBucket::kZero:
return dips::FlowStatus::kOngoing;
case QuantityBucket::kOne:
return dips::FlowStatus::kEnded;
case QuantityBucket::kMultiple:
return dips::FlowStatus::kInvalidated;
}
}
bool BtmNavigationFlowDetector::
MaybeInitializeSuccessorInteractionTrackingState() {
if (flow_status_ == dips::FlowStatus::kOngoing) {
return false;
}
if (!previous_page_visit_info_ || !current_page_visit_info_) {
return false;
}
if (!current_page_visit_info_->WasNavigationToPageClientRedirect()) {
return false;
}
// Look for an entrypoint, which must either be the current page or the first
// server redirect since the prior page.
base::span<const BtmRedirectInfoPtr> server_redirects =
GetRedirectContext().GetServerRedirectsSinceLastPrimaryPageChange();
bool can_entrypoint_be_current_page = server_redirects.empty();
if (can_entrypoint_be_current_page) {
if (current_page_visit_info_->site != previous_page_visit_info_->site) {
successor_interaction_tracking_state_.emplace(
dips::EntrypointInfo(*current_page_visit_info_));
return true;
}
return false;
}
const BtmRedirectInfo* possible_entrypoint = server_redirects.front().get();
if (possible_entrypoint->site == previous_page_visit_info_->site) {
return false;
}
bool had_cross_site_redirect_after_entrypoint =
GetCrossSiteRedirectQuantity(
previous_page_visit_info_->site, server_redirects,
current_page_visit_info_->site) == QuantityBucket::kMultiple;
if (had_cross_site_redirect_after_entrypoint) {
return false;
}
successor_interaction_tracking_state_.emplace(
dips::EntrypointInfo(*possible_entrypoint, *current_page_visit_info_));
successor_interaction_tracking_state_->IncrementFlowIndex(
server_redirects.size());
return true;
}
const BtmRedirectContext& BtmNavigationFlowDetector::GetRedirectContext()
const {
return redirect_chain_observation_.GetSource()->CommittedRedirectContext();
}
void BtmNavigationFlowDetector::OnCookiesAccessed(
RenderFrameHost* render_frame_host,
const CookieAccessDetails& details) {
// Ignore notifications for prerenders, fenced frames, etc., and for blocked
// access attempts.
if (!dips::IsOrWasInPrimaryPage(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;
}
// 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) &&
!IsSameSiteForBtm(first_party_url.value(), details.url)) {
return;
}
current_page_visit_info_->did_page_access_cookies = true;
if (flow_status_ == dips::FlowStatus::kOngoing &&
!successor_interaction_tracking_state_->IsAtSuccessor()) {
successor_interaction_tracking_state_
->RecordTriggeringStorageAccessByEntrypoint();
}
}
void BtmNavigationFlowDetector::OnCookiesAccessed(
NavigationHandle* navigation_handle,
const 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) &&
!IsSameSiteForBtm(first_party_url.value(), details.url)) {
return;
}
current_page_visit_info_->did_page_access_cookies = true;
if (flow_status_ == dips::FlowStatus::kOngoing &&
!successor_interaction_tracking_state_->IsAtSuccessor()) {
successor_interaction_tracking_state_
->RecordTriggeringStorageAccessByEntrypoint();
}
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 != CookieOperation::kChange) {
return;
}
if (details.url == current_page_visit_info_->url) {
current_page_visit_info_->did_page_access_cookies = true;
if (flow_status_ == dips::FlowStatus::kOngoing &&
!successor_interaction_tracking_state_->IsAtSuccessor()) {
successor_interaction_tracking_state_
->RecordTriggeringStorageAccessByEntrypoint();
}
} else {
// This notification might be for an in-progress navigation, so we should
// remember this notification until the next navigation finishes.
navigation_cookie_access_url_ = details.url;
}
}
void BtmNavigationFlowDetector::NotifyStorageAccessed(
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;
if (flow_status_ == dips::FlowStatus::kOngoing &&
!successor_interaction_tracking_state_->IsAtSuccessor()) {
successor_interaction_tracking_state_
->RecordTriggeringStorageAccessByEntrypoint();
}
}
void BtmNavigationFlowDetector::FrameReceivedUserActivation(
RenderFrameHost* render_frame_host) {
current_page_visit_info_->did_page_receive_user_activation = true;
if (successor_interaction_tracking_state_.has_value() &&
successor_interaction_tracking_state_->IsAtSuccessor()) {
successor_interaction_tracking_state_
->RecordSuccessorInteractionAtCurrentFlowIndex();
}
}
void BtmNavigationFlowDetector::WebAuthnAssertionRequestSucceeded(
RenderFrameHost* render_frame_host) {
if (!render_frame_host->IsInPrimaryMainFrame()) {
return;
}
current_page_visit_info_->did_page_have_successful_waa = true;
}
void BtmNavigationFlowDetector::WebContentsDestroyed() {
redirect_chain_observation_.Reset();
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(BtmNavigationFlowDetector);
} // namespace content