| // 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 "chrome/browser/ui/tabs/tab_metrics_logger_impl.h" |
| |
| #include <algorithm> |
| #include <string> |
| #include <vector> |
| |
| #include "base/time/time.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/custom_handlers/protocol_handler_registry.h" |
| #include "chrome/browser/custom_handlers/protocol_handler_registry_factory.h" |
| #include "chrome/browser/engagement/site_engagement_service.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/resource_coordinator/tab_manager.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/common/custom_handlers/protocol_handler.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/page_importance_signals.h" |
| #include "net/base/mime_util.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_recorder.h" |
| #include "third_party/WebKit/public/platform/WebSuddenTerminationDisablerType.h" |
| #include "url/gurl.h" |
| |
| using metrics::TabMetricsEvent; |
| |
| namespace { |
| |
| // Order must match the metrics.TabMetricsEvent.Scheme enum. |
| const char* kWhitelistedSchemes[] = { |
| "", // Placeholder for PROTOCOL_HANDLER_SCHEME_OTHER. |
| "bitcoin", "geo", "im", "irc", "ircs", "magnet", "mailto", |
| "mms", "news", "nntp", "openpgp4fpr", "sip", "sms", "smsto", |
| "ssh", "tel", "urn", "webcal", "wtai", "xmpp", |
| }; |
| |
| // Returns the ContentType that matches |mime_type|. |
| TabMetricsEvent::ContentType GetContentTypeFromMimeType( |
| const std::string& mime_type) { |
| // Test for special cases before testing wildcard types. |
| if (mime_type.empty()) |
| return TabMetricsEvent::CONTENT_TYPE_UNKNOWN; |
| if (net::MatchesMimeType("text/html", mime_type)) |
| return TabMetricsEvent::CONTENT_TYPE_TEXT_HTML; |
| if (net::MatchesMimeType("application/*", mime_type)) |
| return TabMetricsEvent::CONTENT_TYPE_APPLICATION; |
| if (net::MatchesMimeType("audio/*", mime_type)) |
| return TabMetricsEvent::CONTENT_TYPE_AUDIO; |
| if (net::MatchesMimeType("image/*", mime_type)) |
| return TabMetricsEvent::CONTENT_TYPE_IMAGE; |
| if (net::MatchesMimeType("text/*", mime_type)) |
| return TabMetricsEvent::CONTENT_TYPE_TEXT; |
| if (net::MatchesMimeType("video/*", mime_type)) |
| return TabMetricsEvent::CONTENT_TYPE_VIDEO; |
| return TabMetricsEvent::CONTENT_TYPE_OTHER; |
| } |
| |
| // Returns the ProtocolHandlerScheme enumerator matching the string. |
| // The enumerator value is used in the UKM entry, since UKM entries can't |
| // store strings. |
| TabMetricsEvent::ProtocolHandlerScheme GetSchemeValueFromString( |
| const std::string& scheme) { |
| const char* const* const scheme_ptr = std::find( |
| std::begin(kWhitelistedSchemes), std::end(kWhitelistedSchemes), scheme); |
| if (scheme_ptr == std::end(kWhitelistedSchemes)) |
| return TabMetricsEvent::PROTOCOL_HANDLER_SCHEME_OTHER; |
| |
| size_t index = scheme_ptr - std::begin(kWhitelistedSchemes); |
| return static_cast<TabMetricsEvent::ProtocolHandlerScheme>(index); |
| } |
| |
| // Returns the site engagement score for the WebContents, rounded down to 10s |
| // to limit granularity. |
| int GetSiteEngagementScore(const content::WebContents* web_contents) { |
| SiteEngagementService* service = SiteEngagementService::Get( |
| Profile::FromBrowserContext(web_contents->GetBrowserContext())); |
| DCHECK(service); |
| |
| // Scores range from 0 to 100. Round down to a multiple of 10 to conform to |
| // privacy guidelines. |
| double raw_score = service->GetScore(web_contents->GetVisibleURL()); |
| int rounded_score = static_cast<int>(raw_score / 10) * 10; |
| DCHECK_LE(0, rounded_score); |
| DCHECK_GE(100, rounded_score); |
| return rounded_score; |
| } |
| |
| // Adds a DefaultProtocolHandler metric with the handler's scheme to |entry| if |
| // the protocol handler is a default protocol handler. |
| void PopulateSchemeForHandler(ProtocolHandlerRegistry* registry, |
| const ProtocolHandler& handler, |
| ukm::builders::TabManager_TabMetrics* entry) { |
| if (registry->IsDefault(handler)) { |
| // Append a DefaultProtocolHandler metric whose value is the scheme. |
| // Note that multiple DefaultProtocolHandler metrics may be added, one for |
| // each scheme the entry's origin handles by default. |
| entry->SetDefaultProtocolHandler( |
| GetSchemeValueFromString(handler.protocol())); |
| } |
| } |
| |
| // Populates the protocol handler fields based on the WebContents' origin. |
| // We match the origin instead of the full page URL because: |
| // - a handler relevant for one page is probably relevant for the whole site |
| // - a handler maps to a template string, so matching on a full URL is hard |
| // - even if this page was opened from a protocol handler, a redirect may have |
| // changed the URL anyway. |
| void PopulateProtocolHandlers(content::WebContents* web_contents, |
| ukm::builders::TabManager_TabMetrics* entry) { |
| ProtocolHandlerRegistry* registry = |
| ProtocolHandlerRegistryFactory::GetForBrowserContext( |
| web_contents->GetBrowserContext()); |
| // May be null in tests. |
| if (!registry) |
| return; |
| |
| const GURL origin = web_contents->GetLastCommittedURL().GetOrigin(); |
| if (origin.is_empty()) |
| return; |
| |
| // Fetch all schemes that have been registered (accepted or denied). |
| std::vector<std::string> registered_schemes; |
| registry->GetRegisteredProtocols(®istered_schemes); |
| |
| // Protocol handlers are stored by scheme, not URL. For each scheme, find the |
| // URLs of the handlers registered for it. |
| for (const std::string& scheme : registered_schemes) { |
| for (const ProtocolHandler& handler : registry->GetHandlersFor(scheme)) { |
| if (handler.url().GetOrigin() == origin) |
| PopulateSchemeForHandler(registry, handler, entry); |
| } |
| } |
| } |
| |
| // Populates navigation-related metrics. |
| void PopulatePageTransitionMetrics(ukm::builders::TabManager_TabMetrics* entry, |
| ui::PageTransition page_transition) { |
| // We only report the following core types. |
| // Note: Redirects unrelated to clicking a link still get the "link" type. |
| if (ui::PageTransitionCoreTypeIs(page_transition, ui::PAGE_TRANSITION_LINK) || |
| ui::PageTransitionCoreTypeIs(page_transition, |
| ui::PAGE_TRANSITION_AUTO_BOOKMARK) || |
| ui::PageTransitionCoreTypeIs(page_transition, |
| ui::PAGE_TRANSITION_FORM_SUBMIT) || |
| ui::PageTransitionCoreTypeIs(page_transition, |
| ui::PAGE_TRANSITION_RELOAD)) { |
| entry->SetPageTransitionCoreType( |
| ui::PageTransitionStripQualifier(page_transition)); |
| } |
| |
| entry->SetPageTransitionFromAddressBar( |
| (page_transition & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR) != 0); |
| entry->SetPageTransitionIsRedirect( |
| ui::PageTransitionIsRedirect(page_transition)); |
| } |
| |
| } // namespace |
| |
| TabMetricsLoggerImpl::TabMetricsLoggerImpl() = default; |
| TabMetricsLoggerImpl::~TabMetricsLoggerImpl() = default; |
| |
| void TabMetricsLoggerImpl::LogBackgroundTab(ukm::SourceId ukm_source_id, |
| const TabMetrics& tab_metrics) { |
| if (!ukm_source_id) |
| return; |
| |
| ukm::UkmRecorder* ukm_recorder = ukm::UkmRecorder::Get(); |
| if (!ukm_recorder) |
| return; |
| |
| content::WebContents* web_contents = tab_metrics.web_contents; |
| Browser* browser = chrome::FindBrowserWithWebContents(web_contents); |
| if (!browser) |
| return; |
| |
| // UKM recording is disabled in OTR. |
| if (web_contents->GetBrowserContext()->IsOffTheRecord()) |
| return; |
| |
| const TabStripModel* tab_strip_model = browser->tab_strip_model(); |
| int index = tab_strip_model->GetIndexOfWebContents(web_contents); |
| DCHECK_NE(index, TabStripModel::kNoTab); |
| |
| ukm::builders::TabManager_TabMetrics entry(ukm_source_id); |
| |
| // The browser window logs its own usage UKMs with its session ID. |
| entry.SetWindowId(browser->session_id().id()); |
| |
| entry.SetKeyEventCount(tab_metrics.page_metrics.key_event_count) |
| .SetMouseEventCount(tab_metrics.page_metrics.mouse_event_count) |
| .SetTouchEventCount(tab_metrics.page_metrics.touch_event_count); |
| |
| PopulateProtocolHandlers(web_contents, &entry); |
| |
| if (SiteEngagementService::IsEnabled()) |
| entry.SetSiteEngagementScore(GetSiteEngagementScore(web_contents)); |
| |
| TabMetricsEvent::ContentType content_type = |
| GetContentTypeFromMimeType(web_contents->GetContentsMimeType()); |
| entry.SetContentType(static_cast<int>(content_type)); |
| // TODO(michaelpg): Add PluginType field if mime type matches "application/*" |
| // using PluginUMAReporter. |
| |
| // This checks if the tab was audible within the past two seconds, same as the |
| // audio indicator in the tab strip. |
| entry.SetWasRecentlyAudible(web_contents->WasRecentlyAudible()); |
| |
| resource_coordinator::TabManager* tab_manager = |
| g_browser_process->GetTabManager(); |
| DCHECK(tab_manager); |
| |
| PopulatePageTransitionMetrics(&entry, tab_metrics.page_transition); |
| entry |
| .SetHasBeforeUnloadHandler( |
| web_contents->GetMainFrame()->GetSuddenTerminationDisablerState( |
| blink::kBeforeUnloadHandler)) |
| .SetHasFormEntry( |
| web_contents->GetPageImportanceSignals().had_form_interaction) |
| // TODO(michaelpg): This dependency should be reversed during the |
| // resource_coordinator refactor: crbug.com/775644. |
| .SetIsExtensionProtected(!tab_manager->IsTabAutoDiscardable(web_contents)) |
| .SetIsPinned(tab_strip_model->IsTabPinned(index)) |
| .SetNavigationEntryCount(web_contents->GetController().GetEntryCount()) |
| .SetSequenceId(++sequence_id_) |
| .Record(ukm_recorder); |
| } |
| |
| // static |
| TabMetricsEvent::ProtocolHandlerScheme |
| TabMetricsLoggerImpl::GetSchemeValueFromString(const std::string& scheme) { |
| // Exposes GetSchemeValueFromString for testing. |
| return ::GetSchemeValueFromString(scheme); |
| } |