| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/blocked_content/popup_tracker.h" |
| |
| #include <algorithm> |
| |
| #include "base/check.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/time/default_tick_clock.h" |
| #include "components/blocked_content/popup_opener_tab_helper.h" |
| #include "components/ukm/content/source_url_recorder.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 blocked_content { |
| namespace { |
| |
| int CappedUserInteractions(int user_interactions, int max_interactions) { |
| return std::min(user_interactions, max_interactions); |
| } |
| |
| } // namespace |
| |
| PopupTracker* PopupTracker::CreateForWebContents( |
| content::WebContents* contents, |
| content::WebContents* opener, |
| WindowOpenDisposition disposition) { |
| DCHECK(contents); |
| DCHECK(opener); |
| auto* tracker = FromWebContents(contents); |
| if (!tracker) { |
| tracker = new PopupTracker(contents, opener, disposition); |
| contents->SetUserData(UserDataKey(), base::WrapUnique(tracker)); |
| } |
| return tracker; |
| } |
| |
| PopupTracker::~PopupTracker() = default; |
| |
| PopupTracker::PopupTracker(content::WebContents* contents, |
| content::WebContents* opener, |
| WindowOpenDisposition disposition) |
| : content::WebContentsObserver(contents), |
| visibility_tracker_( |
| base::DefaultTickClock::GetInstance(), |
| contents->GetVisibility() != content::Visibility::HIDDEN), |
| opener_source_id_(ukm::GetSourceIdForWebContentsDocument(opener)), |
| window_open_disposition_(disposition) { |
| if (auto* popup_opener = PopupOpenerTabHelper::FromWebContents(opener)) |
| popup_opener->OnOpenedPopup(this); |
| |
| auto* observation_manager = |
| subresource_filter::SubresourceFilterObserverManager::FromWebContents( |
| contents); |
| if (observation_manager) { |
| scoped_observation_.Observe(observation_manager); |
| } |
| } |
| |
| void PopupTracker::WebContentsDestroyed() { |
| base::TimeDelta total_foreground_duration = |
| visibility_tracker_.GetForegroundDuration(); |
| if (first_load_visible_time_start_) { |
| base::TimeDelta first_load_visible_time = |
| first_load_visible_time_ |
| ? *first_load_visible_time_ |
| : total_foreground_duration - *first_load_visible_time_start_; |
| UMA_HISTOGRAM_LONG_TIMES( |
| "ContentSettings.Popups.FirstDocumentEngagementTime2", |
| first_load_visible_time); |
| } |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "ContentSettings.Popups.EngagementTime", total_foreground_duration, |
| base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromHours(6), 50); |
| if (web_contents()->GetClosedByUserGesture()) { |
| UMA_HISTOGRAM_CUSTOM_TIMES( |
| "ContentSettings.Popups.EngagementTime.GestureClose", |
| total_foreground_duration, base::TimeDelta::FromMilliseconds(1), |
| base::TimeDelta::FromHours(6), 50); |
| } |
| |
| if (opener_source_id_ != ukm::kInvalidSourceId) { |
| const int kMaxInteractions = 100; |
| const int kMaxSubcatagoryInteractions = 50; |
| ukm::builders::Popup_Closed(opener_source_id_) |
| .SetEngagementTime(ukm::GetExponentialBucketMinForUserTiming( |
| total_foreground_duration.InMilliseconds())) |
| .SetUserInitiatedClose(web_contents()->GetClosedByUserGesture()) |
| .SetTrusted(is_trusted_) |
| .SetSafeBrowsingStatus(static_cast<int>(safe_browsing_status_)) |
| .SetWindowOpenDisposition(static_cast<int>(window_open_disposition_)) |
| .SetNumInteractions( |
| CappedUserInteractions(num_interactions_, kMaxInteractions)) |
| .SetNumActivationInteractions(CappedUserInteractions( |
| num_activation_events_, kMaxSubcatagoryInteractions)) |
| .SetNumGestureScrollBeginInteractions(CappedUserInteractions( |
| num_gesture_scroll_begin_events_, kMaxSubcatagoryInteractions)) |
| .SetRedirectCount(num_redirects_) |
| .Record(ukm::UkmRecorder::Get()); |
| } |
| } |
| |
| void PopupTracker::DidFinishNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (!navigation_handle->HasCommitted() || |
| navigation_handle->IsSameDocument()) { |
| return; |
| } |
| |
| if (navigation_handle->IsInMainFrame() && !first_navigation_committed_) { |
| first_navigation_committed_ = true; |
| // The last page in the redirect chain is the current page, the number of |
| // redirects is one less than the size of the chain. |
| num_redirects_ = navigation_handle->GetRedirectChain().size() - 1; |
| } |
| |
| if (!first_load_visible_time_start_) { |
| first_load_visible_time_start_ = |
| visibility_tracker_.GetForegroundDuration(); |
| } else if (!first_load_visible_time_) { |
| first_load_visible_time_ = visibility_tracker_.GetForegroundDuration() - |
| *first_load_visible_time_start_; |
| } |
| } |
| |
| void PopupTracker::OnVisibilityChanged(content::Visibility visibility) { |
| // TODO(csharrison): Consider handling OCCLUDED tabs the same way as HIDDEN |
| // tabs. |
| if (visibility == content::Visibility::HIDDEN) |
| visibility_tracker_.OnHidden(); |
| else |
| visibility_tracker_.OnShown(); |
| } |
| |
| void PopupTracker::DidGetUserInteraction(const blink::WebInputEvent& event) { |
| // TODO(csharrison): It would be nice if ctrl-W could be filtered out here, |
| // but the initial ctrl key press is registered as a kRawKeyDown. |
| num_interactions_++; |
| |
| if (event.GetType() == blink::WebInputEvent::Type::kGestureScrollBegin) { |
| num_gesture_scroll_begin_events_++; |
| } else { |
| num_activation_events_++; |
| } |
| } |
| |
| // This method will always be called before the DidFinishNavigation associated |
| // with this handle. |
| // The exception is a navigation restoring a page from back-forward cache -- |
| // in that case don't issue any requests, therefore we don't get any |
| // safe browsing callbacks. See the comment above for the mitigation. |
| void PopupTracker::OnSafeBrowsingChecksComplete( |
| content::NavigationHandle* navigation_handle, |
| const subresource_filter::SubresourceFilterSafeBrowsingClient::CheckResult& |
| result) { |
| DCHECK(navigation_handle->IsInMainFrame()); |
| safe_browsing_status_ = PopupSafeBrowsingStatus::kSafe; |
| if (result.threat_type == |
| safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING || |
| result.threat_type == safe_browsing::SBThreatType:: |
| SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING || |
| result.threat_type == |
| safe_browsing::SBThreatType::SB_THREAT_TYPE_SUBRESOURCE_FILTER) { |
| safe_browsing_status_ = PopupSafeBrowsingStatus::kUnsafe; |
| } |
| } |
| |
| void PopupTracker::OnSubresourceFilterGoingAway() { |
| DCHECK(scoped_observation_.IsObserving()); |
| scoped_observation_.Reset(); |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(PopupTracker) |
| |
| } // namespace blocked_content |