Commit 05e6d5e5 authored by Chris Lu's avatar Chris Lu Committed by Commit Bot

[ios] Create UKM pageload duration TabHelper

This TabHelper will observe WebStateObserver and record page sessions
while its WebState is the active WebState (i.e. it is visible on the
screen). When a new page is navigated to, the app backgrounds, or if
the WebState is destroyed, the recording will stop and a UKM will be
logged for the page session duration.A new IOS.PageNavigation UKM will
be logged for every navigation completion.

Bug: 1006357
Change-Id: I9c7184a1cc3d8f9ff1a39f34f3363a388738e82a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2068244
Commit-Queue: Chris Lu <thegreenfrog@chromium.org>
Reviewed-by: default avatarRobert Kaplow <rkaplow@chromium.org>
Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Reviewed-by: default avatarAnnie Sullivan <sullivan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#747328}
parent eb17bfd1
......@@ -37,6 +37,8 @@ source_set("metrics") {
"ios_profile_session_durations_service_factory.mm",
"mobile_session_shutdown_metrics_provider.h",
"mobile_session_shutdown_metrics_provider.mm",
"pageload_foreground_duration_tab_helper.h",
"pageload_foreground_duration_tab_helper.mm",
"previous_session_info.h",
"previous_session_info.mm",
"previous_session_info_private.h",
......@@ -95,6 +97,7 @@ source_set("unit_tests") {
"ios_chrome_metrics_service_client_unittest.mm",
"ios_chrome_stability_metrics_provider_unittest.mm",
"mobile_session_shutdown_metrics_provider_unittest.mm",
"pageload_foreground_duration_tab_helper_unittest.mm",
"previous_session_info_unittest.mm",
]
deps = [
......@@ -109,6 +112,7 @@ source_set("unit_tests") {
"//components/prefs",
"//components/prefs:test_support",
"//components/ukm",
"//components/ukm:test_support",
"//components/version_info",
"//ios/chrome/browser",
"//ios/chrome/browser/browser_state:test_support",
......
// Copyright 2020 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.
#ifndef IOS_CHROME_BROWSER_METRICS_PAGELOAD_FOREGROUND_DURATION_TAB_HELPER_H_
#define IOS_CHROME_BROWSER_METRICS_PAGELOAD_FOREGROUND_DURATION_TAB_HELPER_H_
#include <memory>
#include <unordered_map>
#include "base/scoped_observer.h"
#include "base/time/time.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer.h"
#import "ios/web/public/web_state_user_data.h"
// Tracks the time spent on pages visible on the screen and logs them as UKMs.
class PageloadForegroundDurationTabHelper
: public web::WebStateUserData<PageloadForegroundDurationTabHelper>,
web::WebStateObserver {
public:
~PageloadForegroundDurationTabHelper() override;
private:
WEB_STATE_USER_DATA_KEY_DECL();
friend class web::WebStateUserData<PageloadForegroundDurationTabHelper>;
explicit PageloadForegroundDurationTabHelper(web::WebState* web_state);
// web::WebStateObserver override.
void WasShown(web::WebState* web_state) override;
void WasHidden(web::WebState* web_state) override;
void DidStartNavigation(web::WebState* web_state,
web::NavigationContext* navigation_context) override;
void DidFinishNavigation(web::WebState* web_state,
web::NavigationContext* navigation_context) override;
void RenderProcessGone(web::WebState* web_state) override;
void WebStateDestroyed(web::WebState* web_state) override;
// Indicates to this tab helper that the app has entered a foreground state.
void UpdateForAppWillForeground();
// Indicates to this tab helper that the app has entered a background state.
void UpdateForAppDidBackground();
// End recording and log a UKM if necessary.
void RecordUkmIfInForeground();
// Whether recording is happening for time spent on the current page.
bool currently_recording_ = false;
// Last time when recording started.
base::TimeTicks last_time_shown_;
// WebState reference.
web::WebState* web_state_ = nullptr;
// Scoped observer that facilitates observing the WebState.
ScopedObserver<web::WebState, WebStateObserver> scoped_observer_;
// Holds references to background NSNotification callback observer.
id foreground_notification_observer_;
// Holds references to foreground NSNotification callback observer.
id background_notification_observer_;
};
#endif // IOS_CHROME_BROWSER_METRICS_PAGELOAD_FOREGROUND_DURATION_TAB_HELPER_H_
// Copyright 2020 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.
#import "ios/chrome/browser/metrics/pageload_foreground_duration_tab_helper.h"
#import <UIKit/UIKit.h>
#include "components/ukm/ios/ukm_url_recorder.h"
#import "ios/web/public/navigation/navigation_context.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
WEB_STATE_USER_DATA_KEY_IMPL(PageloadForegroundDurationTabHelper)
PageloadForegroundDurationTabHelper::PageloadForegroundDurationTabHelper(
web::WebState* web_state)
: web_state_(web_state), scoped_observer_(this) {
DCHECK(web_state);
scoped_observer_.Add(web_state);
background_notification_observer_ = [[NSNotificationCenter defaultCenter]
addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:nil
usingBlock:^(NSNotification* notification) {
this->UpdateForAppDidBackground();
}];
foreground_notification_observer_ = [[NSNotificationCenter defaultCenter]
addObserverForName:UIApplicationWillEnterForegroundNotification
object:nil
queue:nil
usingBlock:^(NSNotification* notification) {
this->UpdateForAppWillForeground();
}];
}
PageloadForegroundDurationTabHelper::~PageloadForegroundDurationTabHelper() {
NSNotificationCenter* default_center = [NSNotificationCenter defaultCenter];
[default_center removeObserver:foreground_notification_observer_];
[default_center removeObserver:background_notification_observer_];
}
void PageloadForegroundDurationTabHelper::UpdateForAppWillForeground() {
// Return early if not currently active WebState.
if (!web_state_->IsVisible())
return;
last_time_shown_ = base::TimeTicks::Now();
currently_recording_ = true;
}
void PageloadForegroundDurationTabHelper::UpdateForAppDidBackground() {
// Return early if not currently active WebState.
if (!web_state_->IsVisible())
return;
RecordUkmIfInForeground();
}
void PageloadForegroundDurationTabHelper::WasShown(web::WebState* web_state) {
if (!currently_recording_) {
last_time_shown_ = base::TimeTicks::Now();
currently_recording_ = true;
}
}
void PageloadForegroundDurationTabHelper::WasHidden(web::WebState* web_state) {
RecordUkmIfInForeground();
}
void PageloadForegroundDurationTabHelper::DidStartNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
DCHECK_EQ(web_state_, web_state);
// Do not start recording if the WebState is not visible. This is important to
// not record for pre-rendering in the omnibox.
// Do not log as end of recording for the current page session if the
// navigation is same-document.
if (!web_state_->IsVisible() || navigation_context->IsSameDocument())
return;
if (currently_recording_)
RecordUkmIfInForeground();
currently_recording_ = true;
last_time_shown_ = base::TimeTicks::Now();
}
void PageloadForegroundDurationTabHelper::DidFinishNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
DCHECK_EQ(web_state_, web_state);
if (!web_state_->IsVisible() || navigation_context->IsSameDocument()) {
// Do not start recording if the WebState is not visible. This is important
// to not record for pre-rendering in the omnibox. Do not log successful
// navigation if it is same-document.
return;
}
int has_committed = navigation_context->HasCommitted() ? 1 : 0;
ukm::SourceId source_id = ukm::GetSourceIdForWebStateDocument(web_state_);
if (source_id != ukm::kInvalidSourceId) {
ukm::builders::PageLoad(source_id)
.SetDidCommit(has_committed)
.Record(ukm::UkmRecorder::Get());
}
}
void PageloadForegroundDurationTabHelper::RenderProcessGone(
web::WebState* web_state) {
DCHECK_EQ(web_state_, web_state);
if (!web_state_->IsVisible())
return;
RecordUkmIfInForeground();
}
void PageloadForegroundDurationTabHelper::WebStateDestroyed(
web::WebState* web_state) {
DCHECK_EQ(web_state_, web_state);
RecordUkmIfInForeground();
scoped_observer_.Remove(web_state);
web_state_ = nullptr;
}
void PageloadForegroundDurationTabHelper::RecordUkmIfInForeground() {
if (!currently_recording_)
return;
currently_recording_ = false;
base::TimeDelta foreground_duration =
base::TimeTicks::Now() - last_time_shown_;
ukm::SourceId source_id = ukm::GetSourceIdForWebStateDocument(web_state_);
if (source_id != ukm::kInvalidSourceId) {
ukm::builders::PageForegroundSession(source_id)
.SetForegroundDuration(foreground_duration.InMilliseconds())
.Record(ukm::UkmRecorder::Get());
}
}
// Copyright 2020 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.
#import "ios/chrome/browser/metrics/pageload_foreground_duration_tab_helper.h"
#include "components/ukm/ios/ukm_url_recorder.h"
#include "components/ukm/test_ukm_recorder.h"
#import "ios/web/public/test/fakes/fake_navigation_context.h"
#import "ios/web/public/test/fakes/test_web_state.h"
#include "ios/web/public/test/web_task_environment.h"
#import "testing/gtest_mac.h"
#include "testing/platform_test.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
const char kPageNavigationUkmEvent[] = "PageLoad";
const char kPageNavigationUkmMetric[] = "DidCommit";
const char kPageForegroundSessionUkmSearchMatchesEvent[] =
"PageForegroundSession";
}
class PageloadForegroundDurationTabHelperTest : public PlatformTest {
protected:
PageloadForegroundDurationTabHelperTest() {
// This must be initialized first so that it is the first receiver of
// WebStateObserver calls to set the navigation id.
ukm::InitializeSourceUrlRecorderForWebState(&web_state_);
PageloadForegroundDurationTabHelper::CreateForWebState(&web_state_);
}
base::test::TaskEnvironment environment_;
web::TestWebState web_state_;
ukm::TestAutoSetUkmRecorder test_ukm_recorder_;
};
// Tests that a navigation UKM is logged as true after a successful navigation.
TEST_F(PageloadForegroundDurationTabHelperTest,
VerifyUKMLoggedAfterNavigation) {
test_ukm_recorder_.Purge();
web::FakeNavigationContext context_with_zero_nav_id;
web_state_.WasShown();
// No entry should be recorded for the interaction above.
const auto& navigation_entries =
test_ukm_recorder_.GetEntriesByName(kPageNavigationUkmEvent);
ASSERT_EQ(0u, navigation_entries.size());
web::FakeNavigationContext context;
context.SetIsSameDocument(false);
context.SetHasCommitted(true);
web_state_.OnNavigationStarted(&context);
// No navigation logging have occurred yet.
const auto& navigation_start_navigation_entries =
test_ukm_recorder_.GetEntriesByName(kPageNavigationUkmEvent);
ASSERT_EQ(0u, navigation_start_navigation_entries.size());
web_state_.OnNavigationFinished(&context);
// A successful navigation should be recorded
const auto& post_navigation_entry =
test_ukm_recorder_.GetEntriesByName(kPageNavigationUkmEvent);
EXPECT_EQ(1u, post_navigation_entry.size());
const ukm::mojom::UkmEntry* entry = post_navigation_entry[0];
ASSERT_TRUE(entry);
EXPECT_NE(ukm::kInvalidSourceId, entry->source_id);
test_ukm_recorder_.ExpectEntryMetric(entry, kPageNavigationUkmMetric, true);
}
// Tests that a navigation UKM is not logged if the navigation is within the
// same document.
TEST_F(PageloadForegroundDurationTabHelperTest,
VerifyUKMNotLoggedAfterSameDocumentNavigation) {
test_ukm_recorder_.Purge();
web::FakeNavigationContext context_with_zero_nav_id;
web_state_.WasShown();
// No entry should be recorded for the interaction above.
const auto& navigation_entries =
test_ukm_recorder_.GetEntriesByName(kPageNavigationUkmEvent);
ASSERT_EQ(0u, navigation_entries.size());
web::FakeNavigationContext context;
context.SetIsSameDocument(true);
context.SetHasCommitted(true);
web_state_.OnNavigationStarted(&context);
// No navigation logging have occurred yet.
const auto& navigation_start_navigation_entries =
test_ukm_recorder_.GetEntriesByName(kPageNavigationUkmEvent);
ASSERT_EQ(0u, navigation_start_navigation_entries.size());
web_state_.OnNavigationFinished(&context);
// A same-document navigation should result in no logging.
const auto& post_navigation_entry =
test_ukm_recorder_.GetEntriesByName(kPageNavigationUkmEvent);
EXPECT_EQ(0u, post_navigation_entry.size());
}
// Tests that a UKM is logged after a page that is being shown is hidden.
TEST_F(PageloadForegroundDurationTabHelperTest,
VerifyUKMLoggedAfterShowAndHide) {
web::FakeNavigationContext context;
context.SetIsSameDocument(false);
context.SetHasCommitted(true);
web_state_.OnNavigationStarted(&context);
// Mark WebState as visible without logging a page session.
web_state_.WasShown();
web_state_.OnNavigationFinished(&context);
// There should be no entries yet.
const auto& page_session_entries = test_ukm_recorder_.GetEntriesByName(
kPageForegroundSessionUkmSearchMatchesEvent);
ASSERT_EQ(0u, page_session_entries.size());
web_state_.WasHidden();
const auto& after_hidden_page_session_entries =
test_ukm_recorder_.GetEntriesByName(
kPageForegroundSessionUkmSearchMatchesEvent);
EXPECT_EQ(1u, after_hidden_page_session_entries.size());
}
// Tests that a UKM is logged after OnRenderProcessGone is called for a page
// that is being shown.
TEST_F(PageloadForegroundDurationTabHelperTest,
VerifyUKMLoggedAfterRenderProcessGone) {
web::FakeNavigationContext context;
context.SetIsSameDocument(false);
context.SetHasCommitted(true);
web_state_.OnNavigationStarted(&context);
// Mark WebState as visible without logging a page session.
web_state_.WasShown();
web_state_.OnNavigationFinished(&context);
// There should be no entries yet.
const auto& page_session_entries = test_ukm_recorder_.GetEntriesByName(
kPageForegroundSessionUkmSearchMatchesEvent);
ASSERT_EQ(0u, page_session_entries.size());
web_state_.OnRenderProcessGone();
const auto& after_render_gone_entries = test_ukm_recorder_.GetEntriesByName(
kPageForegroundSessionUkmSearchMatchesEvent);
EXPECT_EQ(1u, after_render_gone_entries.size());
}
// Tests that a UKM is logged after a
// UIApplicationDidEnterBackgroundNotification notification and after a
// successive UIApplicationWillEnterForegroundNotification notification that is
// followed by the Webstate being hidden.
TEST_F(PageloadForegroundDurationTabHelperTest,
VerifyUKMLoggedAfterBackgroundingAndForegrounding) {
web::FakeNavigationContext context;
context.SetIsSameDocument(false);
context.SetHasCommitted(true);
web_state_.OnNavigationStarted(&context);
// Mark WebState as visible without logging a page session.
web_state_.WasShown();
web_state_.OnNavigationFinished(&context);
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidEnterBackgroundNotification
object:nil];
const auto& after_background_entries = test_ukm_recorder_.GetEntriesByName(
kPageForegroundSessionUkmSearchMatchesEvent);
ASSERT_EQ(1u, after_background_entries.size());
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationWillEnterForegroundNotification
object:nil];
const auto& after_foreground_entries = test_ukm_recorder_.GetEntriesByName(
kPageForegroundSessionUkmSearchMatchesEvent);
ASSERT_EQ(1u, after_foreground_entries.size());
web_state_.WasHidden();
const auto& after_hidden_page_session_entries =
test_ukm_recorder_.GetEntriesByName(
kPageForegroundSessionUkmSearchMatchesEvent);
EXPECT_EQ(2u, after_hidden_page_session_entries.size());
}
// Tests that no UKMs are logged as long as the WebState is not visible.
TEST_F(PageloadForegroundDurationTabHelperTest,
VerifyNoUKMLoggedIfWebStateNotVisible) {
// Mark WebState as not visible.
web_state_.WasHidden();
web::FakeNavigationContext context;
context.SetIsSameDocument(false);
context.SetHasCommitted(true);
web_state_.OnNavigationStarted(&context);
web_state_.OnNavigationFinished(&context);
const auto& post_navigation_entries =
test_ukm_recorder_.GetEntriesByName(kPageNavigationUkmEvent);
EXPECT_EQ(0u, post_navigation_entries.size());
web_state_.OnNavigationStarted(&context);
web_state_.OnNavigationFinished(&context);
const auto& second_post_navigation_entries =
test_ukm_recorder_.GetEntriesByName(kPageNavigationUkmEvent);
EXPECT_EQ(0u, second_post_navigation_entries.size());
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidEnterBackgroundNotification
object:nil];
const auto& after_background_entries = test_ukm_recorder_.GetEntriesByName(
kPageForegroundSessionUkmSearchMatchesEvent);
ASSERT_EQ(0u, after_background_entries.size());
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationWillEnterForegroundNotification
object:nil];
const auto& after_foreground_entries = test_ukm_recorder_.GetEntriesByName(
kPageForegroundSessionUkmSearchMatchesEvent);
ASSERT_EQ(0u, after_foreground_entries.size());
web_state_.WasHidden();
const auto& after_hidden_page_session_entries =
test_ukm_recorder_.GetEntriesByName(
kPageForegroundSessionUkmSearchMatchesEvent);
EXPECT_EQ(0u, after_hidden_page_session_entries.size());
}
......@@ -34,6 +34,7 @@
#import "ios/chrome/browser/infobars/overlays/infobar_overlay_request_inserter.h"
#import "ios/chrome/browser/infobars/overlays/infobar_overlay_tab_helper.h"
#import "ios/chrome/browser/itunes_urls/itunes_urls_handler_tab_helper.h"
#import "ios/chrome/browser/metrics/pageload_foreground_duration_tab_helper.h"
#import "ios/chrome/browser/network_activity/network_activity_indicator_tab_helper.h"
#import "ios/chrome/browser/open_in/open_in_tab_helper.h"
#import "ios/chrome/browser/overscroll_actions/overscroll_actions_tab_helper.h"
......@@ -145,6 +146,8 @@ void AttachTabHelpers(web::WebState* web_state, bool for_prerender) {
ARQuickLookTabHelper::CreateForWebState(web_state);
PageloadForegroundDurationTabHelper::CreateForWebState(web_state);
// TODO(crbug.com/794115): pre-rendered WebState have lots of unnecessary
// tab helpers for historical reasons. For the moment, AttachTabHelpers
// allows to inhibit the creation of some of them. Once PreloadController
......
......@@ -6834,6 +6834,11 @@ be describing additional metrics about the same event.
is in the foreground. Measured in milliseconds.
</summary>
</metric>
<metric name="DidCommit">
<summary>
Set to 1 if the navigation successfully commited.
</summary>
</metric>
<metric name="DocumentTiming.NavigationToDOMContentLoadedEventFired">
<summary>
Measures the time in milliseconds from navigation timing's navigation
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment