[go: nahoru, domu]

blob: f95acb01a2d0304b856ab2f8899653781e6e25e1 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.h"
#import "base/apple/foundation_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/histogram_macros.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/time/time.h"
#import "ios/chrome/browser/discover_feed/discover_feed_refresher.h"
#import "ios/chrome/browser/ntp/features.h"
#import "ios/chrome/browser/ui/ntp/feed_control_delegate.h"
#import "ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.h"
#import "ios/chrome/browser/ui/ntp/metrics/feed_session_recorder.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_follow_delegate.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_metrics_delegate.h"
using feed::FeedEngagementType;
using feed::FeedUserActionType;
@interface FeedMetricsRecorder ()
// Helper for recording session time metrics.
@property(nonatomic, strong) FeedSessionRecorder* sessionRecorder;
// Tracking property to avoid duplicate recordings of
// FeedEngagementType::kFeedEngagedSimple.
@property(nonatomic, assign) BOOL engagedSimpleReportedDiscover;
@property(nonatomic, assign) BOOL engagedSimpleReportedFollowing;
// Tracking property to avoid duplicate recordings of
// FeedEngagementType::kFeedEngaged.
@property(nonatomic, assign) BOOL engagedReportedDiscover;
@property(nonatomic, assign) BOOL engagedReportedFollowing;
// Tracking property to avoid duplicate recordings of
// FeedEngagementType::kFeedScrolled.
@property(nonatomic, assign) BOOL scrolledReportedDiscover;
@property(nonatomic, assign) BOOL scrolledReportedFollowing;
// Tracking property to avoid duplicate recordings of
// FeedEngagementType::kGoodVisit.
@property(nonatomic, assign) BOOL goodVisitReportedAllFeeds;
@property(nonatomic, assign) BOOL goodVisitReportedDiscover;
@property(nonatomic, assign) BOOL goodVisitReportedFollowing;
// Tracking property to avoid duplicate recordings of the Activity Buckets
// metric.
@property(nonatomic, assign) NSDate* activityBucketLastReportedDate;
// Tracks whether user has engaged with the latest refreshed content. The term
// "engaged" is defined by its usage in this file. For example, it may be
// similar to `engagedSimpleReportedDiscover`.
@property(nonatomic, assign, getter=hasEngagedWithLatestRefreshedContent)
BOOL engagedWithLatestRefreshedContent;
// Tracking property to record a scroll for Good Visits.
// TODO(crbug.com/1373650) separate the property below in two, one for each
// feed.
@property(nonatomic, assign) BOOL goodVisitScroll;
// The timestamp when the first metric is being recorded for this session.
@property(nonatomic, assign) base::Time sessionStartTime;
// The timestamp when the last interaction happens for Good Visits.
@property(nonatomic, assign) base::Time lastInteractionTimeForGoodVisits;
@property(nonatomic, assign)
base::Time lastInteractionTimeForDiscoverGoodVisits;
@property(nonatomic, assign)
base::Time lastInteractionTimeForFollowingGoodVisits;
// The timestamp when the feed becomes visible again for Good Visits. It
// is reset when a new Good Visit session starts
@property(nonatomic, assign) base::Time feedBecameVisibleTime;
// The time the user has spent in the feed during a Good Visit session.
// This property is preserved across NTP usages if they are part of the same
// Good Visit Session.
@property(nonatomic, assign)
NSTimeInterval previousTimeInFeedForGoodVisitSession;
@property(nonatomic, assign) NSTimeInterval discoverPreviousTimeInFeedGV;
@property(nonatomic, assign) NSTimeInterval followingPreviousTimeInFeedGV;
// The aggregate of time a user has spent in the feed for
// `ContentSuggestions.Feed.TimeSpentInFeed`
@property(nonatomic, assign) base::TimeDelta timeSpentInFeed;
// Timer to refresh the feed.
@property(nonatomic, strong) NSTimer* refreshTimer;
// YES if the NTP is visible.
@property(nonatomic, assign) BOOL isNTPVisible;
@end
@implementation FeedMetricsRecorder
#pragma mark - Properties
- (FeedSessionRecorder*)sessionRecorder {
if (!_sessionRecorder) {
_sessionRecorder = [[FeedSessionRecorder alloc] init];
}
return _sessionRecorder;
}
#pragma mark - Public
- (void)dealloc {
[self.refreshTimer invalidate];
self.refreshTimer = nil;
}
+ (void)recordFeedRefreshTrigger:(FeedRefreshTrigger)trigger {
base::UmaHistogramEnumeration(kDiscoverFeedRefreshTrigger, trigger);
}
- (void)recordFeedScrolled:(int)scrollDistance {
self.goodVisitScroll = YES;
[self checkEngagementGoodVisitWithInteraction:NO];
// If neither feed has been scrolled into, log "AllFeeds" scrolled.
if (!self.scrolledReportedDiscover && !self.scrolledReportedFollowing) {
UMA_HISTOGRAM_ENUMERATION(kAllFeedsEngagementTypeHistogram,
FeedEngagementType::kFeedScrolled);
}
// Log scrolled into Discover feed.
if ([self.feedControlDelegate selectedFeed] == FeedTypeDiscover &&
!self.scrolledReportedDiscover) {
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedEngagementTypeHistogram,
FeedEngagementType::kFeedScrolled);
self.scrolledReportedDiscover = YES;
}
// Log scrolled into Following feed.
if ([self.feedControlDelegate selectedFeed] == FeedTypeFollowing &&
!self.scrolledReportedFollowing) {
UMA_HISTOGRAM_ENUMERATION(kFollowingFeedEngagementTypeHistogram,
FeedEngagementType::kFeedScrolled);
self.scrolledReportedFollowing = YES;
}
[self recordEngagement:scrollDistance interacted:NO];
}
- (void)recordDeviceOrientationChanged:(UIDeviceOrientation)orientation {
if (orientation == UIDeviceOrientationPortrait) {
base::RecordAction(base::UserMetricsAction(
kDiscoverFeedHistogramDeviceOrientationChangedToPortrait));
} else if (orientation == UIDeviceOrientationLandscapeLeft ||
orientation == UIDeviceOrientationLandscapeRight) {
base::RecordAction(base::UserMetricsAction(
kDiscoverFeedHistogramDeviceOrientationChangedToLandscape));
}
}
- (void)recordFeedTypeChangedFromFeed:(FeedType)previousFeed {
// Recalculate time spent in previous surface.
[self timeSpentForCurrentGoodVisitSessionInFeed:previousFeed];
}
- (void)recordNTPDidChangeVisibility:(BOOL)visible {
self.isNTPVisible = visible;
// Invalidate the timer when the user returns to the feed since the feed
// should not be refreshed when the user is viewing it.
if (visible) {
[self.refreshTimer invalidate];
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kOpenedFeedSurface
asInteraction:NO];
}
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
if (visible) {
NSDate* lastInteractionTimeForGoodVisitsDate =
base::apple::ObjCCast<NSDate>(
[defaults objectForKey:kLastInteractionTimeForGoodVisits]);
if (lastInteractionTimeForGoodVisitsDate != nil) {
self.lastInteractionTimeForGoodVisits =
base::Time::FromNSDate(lastInteractionTimeForGoodVisitsDate);
}
NSDate* lastInteractionTimeForDiscoverGoodVisitsDate =
base::apple::ObjCCast<NSDate>(
[defaults objectForKey:kLastInteractionTimeForDiscoverGoodVisits]);
if (lastInteractionTimeForDiscoverGoodVisitsDate != nil) {
self.lastInteractionTimeForDiscoverGoodVisits =
base::Time::FromNSDate(lastInteractionTimeForDiscoverGoodVisitsDate);
}
NSDate* lastInteractionTimeForFollowingGoodVisitsDate =
base::apple::ObjCCast<NSDate>(
[defaults objectForKey:kLastInteractionTimeForFollowingGoodVisits]);
if (lastInteractionTimeForFollowingGoodVisitsDate != nil) {
self.lastInteractionTimeForFollowingGoodVisits =
base::Time::FromNSDate(lastInteractionTimeForFollowingGoodVisitsDate);
}
// Total time spent in feed metrics.
self.timeSpentInFeed =
base::Seconds([defaults doubleForKey:kTimeSpentInFeedAggregateKey]);
[self computeActivityBuckets];
[self recordTimeSpentInFeedIfDayIsDone];
self.previousTimeInFeedForGoodVisitSession =
[defaults doubleForKey:kLongFeedVisitTimeAggregateKey];
self.discoverPreviousTimeInFeedGV =
[defaults doubleForKey:kLongDiscoverFeedVisitTimeAggregateKey];
self.followingPreviousTimeInFeedGV =
[defaults doubleForKey:kLongFollowingFeedVisitTimeAggregateKey];
// Checks if there is a timestamp in defaults for when a user clicked
// on an article in order to be able to trigger a non-short click
// interaction.
NSDate* articleVisitStart = base::apple::ObjCCast<NSDate>(
[defaults objectForKey:kArticleVisitTimestampKey]);
self.feedBecameVisibleTime = base::Time::Now();
if (articleVisitStart) {
// Report Good Visit if user came back to the NTP after spending
// kNonShortClickSeconds in a feed article.
if (base::Time::FromNSDate(articleVisitStart) - base::Time::Now() >
base::Seconds(kNonShortClickSeconds)) {
// Trigger a GV for a specific feed.
[self
recordEngagedGoodVisits:
(FeedType)[defaults integerForKey:kLastUsedFeedForGoodVisitsKey]
allFeedsOnly:NO];
}
// Clear defaults for new session.
[defaults setObject:nil forKey:kArticleVisitTimestampKey];
}
} else {
// Once the NTP becomes hidden, check for Good Visit which updates
// `self.previousTimeInFeedForGoodVisitSession` and then we save it to
// defaults.
// Also calculate total aggregate for the time in feed aggregate metric.
self.timeSpentInFeed = base::Time::Now() - self.feedBecameVisibleTime;
[self checkEngagementGoodVisitWithInteraction:NO];
[defaults setDouble:self.timeSpentInFeed.InSecondsF()
forKey:kTimeSpentInFeedAggregateKey];
[defaults setDouble:self.previousTimeInFeedForGoodVisitSession
forKey:kLongFeedVisitTimeAggregateKey];
[defaults setDouble:self.discoverPreviousTimeInFeedGV
forKey:kLongDiscoverFeedVisitTimeAggregateKey];
[defaults setDouble:self.followingPreviousTimeInFeedGV
forKey:kLongFollowingFeedVisitTimeAggregateKey];
}
}
- (void)recordDiscoverFeedPreviewTapped {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedDiscoverFeedPreview
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionPreviewTapped));
}
- (void)recordHeaderMenuLearnMoreTapped {
[self
recordDiscoverFeedUserActionHistogram:FeedUserActionType::kTappedLearnMore
asInteraction:NO];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionLearnMoreTapped));
}
- (void)recordHeaderMenuManageTapped {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::kTappedManage
asInteraction:NO];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionManageTapped));
}
- (void)recordHeaderMenuManageActivityTapped {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedManageActivity
asInteraction:NO];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionManageActivityTapped));
}
- (void)recordHeaderMenuManageInterestsTapped {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedManageInterests
asInteraction:NO];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionManageInterestsTapped));
}
- (void)recordHeaderMenuManageHiddenTapped {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedManageHidden
asInteraction:NO];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionManageHiddenTapped));
}
- (void)recordHeaderMenuManageFollowingTapped {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedManageFollowing
asInteraction:NO];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionManageFollowingTapped));
}
- (void)recordDiscoverFeedVisibilityChanged:(BOOL)visible {
if (visible) {
[self
recordDiscoverFeedUserActionHistogram:FeedUserActionType::kTappedTurnOn
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(kDiscoverFeedUserActionTurnOn));
} else {
[self
recordDiscoverFeedUserActionHistogram:FeedUserActionType::kTappedTurnOff
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(kDiscoverFeedUserActionTurnOff));
}
}
- (void)recordOpenURLInSameTab {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::kTappedOnCard
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionOpenSameTab));
[self recordOpenURL];
}
- (void)recordOpenURLInNewTab {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedOpenInNewTab
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionOpenNewTab));
[self recordOpenURL];
}
- (void)recordOpenURLInIncognitoTab {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedOpenInNewIncognitoTab
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionOpenIncognitoTab));
[self recordOpenURL];
}
- (void)recordAddURLToReadLater {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kAddedToReadLater
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionReadLaterTapped));
}
- (void)recordTapSendFeedback {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedSendFeedback
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionSendFeedbackOpened));
}
- (void)recordOpenBackOfCardMenu {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kOpenedContextMenu
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionContextMenuOpened));
}
- (void)recordCloseBackOfCardMenu {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kClosedContextMenu
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionCloseContextMenu));
}
- (void)recordOpenNativeBackOfCardMenu {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kOpenedNativeActionSheet
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionNativeActionSheetOpened));
}
- (void)recordShowDialog {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::kOpenedDialog
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionReportContentOpened));
}
- (void)recordDismissDialog {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::kClosedDialog
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionReportContentClosed));
}
- (void)recordDismissCard {
[self
recordDiscoverFeedUserActionHistogram:FeedUserActionType::kEphemeralChange
asInteraction:YES];
base::RecordAction(base::UserMetricsAction(kDiscoverFeedUserActionHideStory));
}
- (void)recordUndoDismissCard {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kEphemeralChangeRejected
asInteraction:YES];
}
- (void)recordCommittDismissCard {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kEphemeralChangeCommited
asInteraction:YES];
}
- (void)recordShowSnackbar {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::kShowSnackbar
asInteraction:NO];
}
- (void)recordCommandID:(int)commandID {
base::UmaHistogramSparse(kDiscoverFeedUserActionCommandHistogram, commandID);
}
- (void)recordCardShownAtIndex:(NSUInteger)index {
switch ([self.feedControlDelegate selectedFeed]) {
case FeedTypeDiscover:
UMA_HISTOGRAM_EXACT_LINEAR(kDiscoverFeedCardShownAtIndex, index,
kMaxCardsInFeed);
break;
case FeedTypeFollowing:
UMA_HISTOGRAM_EXACT_LINEAR(kFollowingFeedCardShownAtIndex, index,
kMaxCardsInFeed);
}
}
- (void)recordCardTappedAtIndex:(NSUInteger)index {
// TODO(crbug.com/1174088): No-op since this function gets called multiple
// times for a tap. Log index when this is fixed.
}
- (void)recordNoticeCardShown:(BOOL)shown {
base::UmaHistogramBoolean(kDiscoverFeedNoticeCardFulfilled, shown);
}
- (void)recordFeedArticlesFetchDurationInSeconds:
(NSTimeInterval)durationInSeconds
success:(BOOL)success {
[self recordFeedArticlesFetchDuration:base::Seconds(durationInSeconds)
success:success];
}
- (void)recordFeedArticlesFetchDuration:(base::TimeDelta)duration
success:(BOOL)success {
if (success) {
UMA_HISTOGRAM_MEDIUM_TIMES(kDiscoverFeedArticlesFetchNetworkDurationSuccess,
duration);
} else {
UMA_HISTOGRAM_MEDIUM_TIMES(kDiscoverFeedArticlesFetchNetworkDurationFailure,
duration);
}
[self recordNetworkRequestDuration:duration];
}
- (void)recordFeedMoreArticlesFetchDurationInSeconds:
(NSTimeInterval)durationInSeconds
success:(BOOL)success {
[self recordFeedMoreArticlesFetchDuration:base::Seconds(durationInSeconds)
success:success];
}
- (void)recordFeedMoreArticlesFetchDuration:(base::TimeDelta)duration
success:(BOOL)success {
if (success) {
UMA_HISTOGRAM_MEDIUM_TIMES(
kDiscoverFeedMoreArticlesFetchNetworkDurationSuccess, duration);
} else {
UMA_HISTOGRAM_MEDIUM_TIMES(
kDiscoverFeedMoreArticlesFetchNetworkDurationFailure, duration);
}
[self recordNetworkRequestDuration:duration];
}
- (void)recordFeedUploadActionsDurationInSeconds:
(NSTimeInterval)durationInSeconds
success:(BOOL)success {
[self recordFeedUploadActionsDuration:base::Seconds(durationInSeconds)
success:success];
}
- (void)recordFeedUploadActionsDuration:(base::TimeDelta)duration
success:(BOOL)success {
if (success) {
UMA_HISTOGRAM_MEDIUM_TIMES(kDiscoverFeedUploadActionsNetworkDurationSuccess,
duration);
} else {
UMA_HISTOGRAM_MEDIUM_TIMES(kDiscoverFeedUploadActionsNetworkDurationFailure,
duration);
}
[self recordNetworkRequestDuration:duration];
}
- (void)recordNativeContextMenuVisibilityChanged:(BOOL)shown {
if (shown) {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kOpenedNativeContextMenu
asInteraction:YES];
base::RecordAction(base::UserMetricsAction(
kDiscoverFeedUserActionNativeContextMenuOpened));
} else {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kClosedNativeContextMenu
asInteraction:YES];
base::RecordAction(base::UserMetricsAction(
kDiscoverFeedUserActionNativeContextMenuClosed));
}
}
- (void)recordNativePulldownMenuVisibilityChanged:(BOOL)shown {
if (shown) {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kOpenedNativePulldownMenu
asInteraction:YES];
base::RecordAction(base::UserMetricsAction(
kDiscoverFeedUserActionNativePulldownMenuOpened));
} else {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kClosedNativePulldownMenu
asInteraction:YES];
base::RecordAction(base::UserMetricsAction(
kDiscoverFeedUserActionNativePulldownMenuClosed));
}
}
- (void)recordActivityLoggingEnabled:(BOOL)loggingEnabled {
base::UmaHistogramBoolean(kDiscoverFeedActivityLoggingEnabled,
loggingEnabled);
}
- (void)recordBrokenNTPHierarchy:(BrokenNTPHierarchyRelationship)relationship {
base::UmaHistogramEnumeration(kDiscoverFeedBrokenNTPHierarchy, relationship);
base::RecordAction(base::UserMetricsAction(kNTPViewHierarchyFixed));
}
- (void)recordFeedWillRefresh {
base::RecordAction(base::UserMetricsAction(kFeedWillRefresh));
// The feed will have new content so reset the engagement tracking variable.
// TODO(crbug.com/1423467): We need to know whether the feed was actually
// refreshed, and not just when it was triggered.
self.engagedWithLatestRefreshedContent = NO;
}
- (void)recordFeedSelected:(FeedType)feedType
fromPreviousFeedPosition:(NSUInteger)index {
DCHECK(self.followDelegate);
switch (feedType) {
case FeedTypeDiscover:
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kDiscoverFeedSelected
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(kDiscoverFeedSelected));
UMA_HISTOGRAM_EXACT_LINEAR(kFollowingIndexWhenSwitchingFeed, index,
kMaxCardsInFeed);
break;
case FeedTypeFollowing:
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kFollowingFeedSelected
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(kFollowingFeedSelected));
UMA_HISTOGRAM_EXACT_LINEAR(kDiscoverIndexWhenSwitchingFeed, index,
kMaxCardsInFeed);
NSUInteger followCount = [self.followDelegate followedPublisherCount];
if (followCount > 0 &&
[self.followDelegate doesFollowingFeedHaveContent]) {
[self recordFollowCount:followCount
forLogReason:FollowCountLogReasonContentShown];
} else {
[self recordFollowCount:followCount
forLogReason:FollowCountLogReasonNoContentShown];
}
break;
}
}
- (void)recordFollowCount:(NSUInteger)followCount
forLogReason:(FollowCountLogReason)logReason {
switch (logReason) {
case FollowCountLogReasonContentShown:
base::UmaHistogramSparse(kFollowCountFollowingContentShown, followCount);
break;
case FollowCountLogReasonNoContentShown:
base::UmaHistogramSparse(kFollowCountFollowingNoContentShown,
followCount);
break;
case FollowCountLogReasonAfterFollow:
base::UmaHistogramSparse(kFollowCountAfterFollow, followCount);
break;
case FollowCountLogReasonAfterUnfollow:
base::UmaHistogramSparse(kFollowCountAfterUnfollow, followCount);
break;
case FollowCountLogReasonEngaged:
base::UmaHistogramSparse(kFollowCountWhenEngaged, followCount);
break;
}
}
- (void)recordFeedSettingsOnStartForEnterprisePolicy:(BOOL)enterprisePolicy
feedVisible:(BOOL)feedVisible
signedIn:(BOOL)signedIn
waaEnabled:(BOOL)waaEnabled
spywEnabled:(BOOL)spywEnabled
lastRefreshTime:
(base::Time)lastRefreshTime {
UserSettingsOnStart settings =
[self userSettingsOnStartForEnterprisePolicy:enterprisePolicy
feedVisible:feedVisible
signedIn:signedIn
waaEnabled:waaEnabled
lastRefreshTime:lastRefreshTime];
base::UmaHistogramEnumeration(kFeedUserSettingsOnStart, settings);
}
- (void)recordFollowingFeedSortTypeSelected:(FollowingFeedSortType)sortType {
switch (sortType) {
case FollowingFeedSortTypeByPublisher:
UMA_HISTOGRAM_ENUMERATION(kFollowingFeedSortType,
FeedSortType::kGroupedByPublisher);
base::RecordAction(
base::UserMetricsAction(kFollowingFeedGroupByPublisher));
return;
case FollowingFeedSortTypeByLatest:
UMA_HISTOGRAM_ENUMERATION(kFollowingFeedSortType,
FeedSortType::kSortedByLatest);
base::RecordAction(base::UserMetricsAction(kFollowingFeedSortByLatest));
return;
case FollowingFeedSortTypeUnspecified:
UMA_HISTOGRAM_ENUMERATION(kFollowingFeedSortType,
FeedSortType::kUnspecifiedSortType);
return;
}
}
#pragma mark - Follow
- (void)recordFollowRequestedWithType:(FollowRequestType)followRequestType {
switch (followRequestType) {
case FollowRequestType::kFollowRequestFollow:
base::RecordAction(base::UserMetricsAction(kFollowRequested));
break;
case FollowRequestType::kFollowRequestUnfollow:
base::RecordAction(base::UserMetricsAction(kUnfollowRequested));
break;
}
}
- (void)recordFollowFromMenu {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedFollowButton
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(kFollowFromMenu));
}
- (void)recordUnfollowFromMenu {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedUnfollowButton
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(kUnfollowFromMenu));
}
- (void)recordFollowConfirmationShownWithType:
(FollowConfirmationType)followConfirmationType {
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedUserActionHistogram,
FeedUserActionType::kShowSnackbar);
switch (followConfirmationType) {
case FollowConfirmationType::kFollowSucceedSnackbarShown:
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedUserActionHistogram,
FeedUserActionType::kShowFollowSucceedSnackbar);
break;
case FollowConfirmationType::kFollowErrorSnackbarShown:
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedUserActionHistogram,
FeedUserActionType::kShowFollowFailedSnackbar);
break;
case FollowConfirmationType::kUnfollowSucceedSnackbarShown:
UMA_HISTOGRAM_ENUMERATION(
kDiscoverFeedUserActionHistogram,
FeedUserActionType::kShowUnfollowSucceedSnackbar);
break;
case FollowConfirmationType::kUnfollowErrorSnackbarShown:
UMA_HISTOGRAM_ENUMERATION(
kDiscoverFeedUserActionHistogram,
FeedUserActionType::kShowUnfollowFailedSnackbar);
break;
}
}
- (void)recordFollowSnackbarTappedWithAction:
(FollowSnackbarActionType)followSnackbarActionType {
switch (followSnackbarActionType) {
case FollowSnackbarActionType::kSnackbarActionGoToFeed:
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedGoToFeedOnSnackbar
asInteraction:NO];
base::RecordAction(
base::UserMetricsAction(kSnackbarGoToFeedButtonTapped));
break;
case FollowSnackbarActionType::kSnackbarActionUndo:
[self recordDiscoverFeedUserActionHistogram:
FeedUserActionType::kTappedRefollowAfterUnfollowOnSnackbar
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(kSnackbarUndoButtonTapped));
break;
case FollowSnackbarActionType::kSnackbarActionRetryFollow:
[self recordDiscoverFeedUserActionHistogram:
FeedUserActionType::kTappedFollowTryAgainOnSnackbar
asInteraction:NO];
base::RecordAction(
base::UserMetricsAction(kSnackbarRetryFollowButtonTapped));
break;
case FollowSnackbarActionType::kSnackbarActionRetryUnfollow:
[self recordDiscoverFeedUserActionHistogram:
FeedUserActionType::kTappedUnfollowTryAgainOnSnackbar
asInteraction:NO];
base::RecordAction(
base::UserMetricsAction(kSnackbarRetryUnfollowButtonTapped));
break;
}
}
- (void)recordManagementTappedUnfollow {
[self recordDiscoverFeedUserActionHistogram:
FeedUserActionType::kTappedUnfollowOnManagementSurface
asInteraction:NO];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionManagementTappedUnfollow));
}
- (void)recordManagementTappedRefollowAfterUnfollowOnSnackbar {
[self recordDiscoverFeedUserActionHistogram:
FeedUserActionType::kTappedRefollowAfterUnfollowOnSnackbar
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(
kDiscoverFeedUserActionManagementTappedRefollowAfterUnfollowOnSnackbar));
}
- (void)recordManagementTappedUnfollowTryAgainOnSnackbar {
[self recordDiscoverFeedUserActionHistogram:
FeedUserActionType::kTappedUnfollowTryAgainOnSnackbar
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(
kDiscoverFeedUserActionManagementTappedUnfollowTryAgainOnSnackbar));
}
- (void)recordFirstFollowShown {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kFirstFollowSheetShown
asInteraction:NO];
}
- (void)recordFirstFollowTappedGoToFeed {
[self recordDiscoverFeedUserActionHistogram:
FeedUserActionType::kFirstFollowSheetTappedGoToFeed
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(kFirstFollowGoToFeedButtonTapped));
}
- (void)recordFirstFollowTappedGotIt {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kFirstFollowSheetTappedGotIt
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(kFirstFollowGotItButtonTapped));
}
- (void)recordFollowRecommendationIPHShown {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kFollowRecommendationIPHShown
asInteraction:NO];
}
- (void)recordSignInPromoUIContinueTapped {
[self recordDiscoverFeedUserActionHistogram:
FeedUserActionType::kTappedFeedSignInPromoUIContinue
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(kFeedSignInPromoUIContinueTapped));
}
- (void)recordSignInPromoUICancelTapped {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedFeedSignInPromoUICancel
asInteraction:NO];
base::RecordAction(base::UserMetricsAction(kFeedSignInPromoUICancelTapped));
}
- (void)recordShowSignInOnlyUIWithUserId:(BOOL)hasUserId {
base::RecordAction(
hasUserId ? base::UserMetricsAction(kShowFeedSignInOnlyUIWithUserId)
: base::UserMetricsAction(kShowFeedSignInOnlyUIWithoutUserId));
}
- (void)recordShowSignInRelatedUIWithType:(feed::FeedSignInUI)type {
base::UmaHistogramEnumeration(kFeedSignInUI, type);
switch (type) {
case feed::FeedSignInUI::kShowSyncHalfSheet:
return base::RecordAction(
base::UserMetricsAction(kShowSyncHalfSheetFromFeed));
case feed::FeedSignInUI::kShowSignInOnlyFlow:
return base::RecordAction(
base::UserMetricsAction(kShowSignInOnlyFlowFromFeed));
case feed::FeedSignInUI::kShowSignInDisableToast:
return base::RecordAction(
base::UserMetricsAction(kShowSignInDisableToastFromFeed));
}
}
- (void)recordShowSyncnRelatedUIWithType:(feed::FeedSyncPromo)type {
base::UmaHistogramEnumeration(kFeedSyncPromo, type);
switch (type) {
case feed::FeedSyncPromo::kShowSyncFlow:
return base::RecordAction(base::UserMetricsAction(kShowSyncFlowFromFeed));
case feed::FeedSyncPromo::kShowDisableToast:
return base::RecordAction(
base::UserMetricsAction(kShowDisableToastFromFeed));
}
}
#pragma mark - Private
// Returns the UserSettingsOnStart value based on the user settings.
- (UserSettingsOnStart)
userSettingsOnStartForEnterprisePolicy:(BOOL)enterprisePolicy
feedVisible:(BOOL)feedVisible
signedIn:(BOOL)signedIn
waaEnabled:(BOOL)waaEnabled
lastRefreshTime:(base::Time)lastRefreshTime {
if (!enterprisePolicy) {
return UserSettingsOnStart::kFeedNotEnabledByPolicy;
}
if (!feedVisible) {
if (signedIn) {
return UserSettingsOnStart::kFeedNotVisibleSignedIn;
}
return UserSettingsOnStart::kFeedNotVisibleSignedOut;
}
if (!signedIn) {
return UserSettingsOnStart::kSignedOut;
}
const base::TimeDelta delta = base::Time::Now() - lastRefreshTime;
const BOOL hasRecentData =
delta >= base::TimeDelta() && delta <= kUserSettingsMaxAge;
if (!hasRecentData) {
return UserSettingsOnStart::kSignedInNoRecentData;
}
if (waaEnabled) {
return UserSettingsOnStart::kSignedInWaaOnDpOff;
} else {
return UserSettingsOnStart::kSignedInWaaOffDpOff;
}
}
// Records histogram metrics for Discover feed user actions. If `isInteraction`,
// also logs an interaction to the visible feed.
- (void)recordDiscoverFeedUserActionHistogram:(FeedUserActionType)actionType
asInteraction:(BOOL)isInteraction {
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedUserActionHistogram, actionType);
if (isInteraction) {
[self recordInteraction];
}
// Check if actionType warrants a Good Explicit Visit
// If actionType is any of the cases below, trigger a Good Explicit
// interaction by calling recordEngagementGoodVisit
switch (actionType) {
case FeedUserActionType::kAddedToReadLater:
case FeedUserActionType::kOpenedNativeContextMenu:
case FeedUserActionType::kTappedOpenInNewIncognitoTab:
[self checkEngagementGoodVisitWithInteraction:YES];
break;
default:
// Default will handle the remaining FeedUserActionTypes that
// do not trigger a Good Explicit interaction, but might trigger a good
// visit due to other checks e.g. Using the feed for
// `kGoodVisitTimeInFeedSeconds`.
[self checkEngagementGoodVisitWithInteraction:NO];
break;
}
}
// Logs engagement daily for the Activity Buckets Calculation.
- (void)logDailyActivity {
NSDate* now = [NSDate date];
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
// Check if the array is initialized.
NSMutableArray<NSDate*>* lastReportedArray = [[defaults
arrayForKey:kActivityBucketLastReportedDateArrayKey] mutableCopy];
if (!lastReportedArray) {
// Initialized before (could be empty).
lastReportedArray = [NSMutableArray new];
}
// Adds a daily entry to the `lastReportedArray` array
// only once when the user engages.
if ([now timeIntervalSinceDate:[lastReportedArray lastObject]] >=
(24 * 60 * 60) ||
lastReportedArray.count == 0) {
[lastReportedArray addObject:now];
[defaults setObject:lastReportedArray
forKey:kActivityBucketLastReportedDateArrayKey];
}
}
// Calculates the amount of dates the user has been active for the past 28 days.
- (void)computeActivityBuckets {
NSDate* now = [NSDate date];
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSDate* lastActivityBucketReported = base::apple::ObjCCast<NSDate>(
[defaults objectForKey:kActivityBucketLastReportedDateKey]);
// If the `lastActivityBucketReported` does not exist, set it to now to
// prevent the first day from logging a metric.
if (!lastActivityBucketReported) {
lastActivityBucketReported = now;
[defaults setObject:lastActivityBucketReported
forKey:kActivityBucketLastReportedDateKey];
}
// Check if the last time the activity was reported is more than 24 hrs ago,
// and return for performance.
if ([now timeIntervalSinceDate:lastActivityBucketReported] < (24 * 60 * 60)) {
return;
}
// Retrieve activity bucket from storage.
FeedActivityBucket activityBucket =
(FeedActivityBucket)[defaults integerForKey:kActivityBucketKey];
// Calculate activity buckets.
// Check if the array is initialized.
NSMutableArray<NSDate*>* lastReportedArray = [[defaults
arrayForKey:kActivityBucketLastReportedDateArrayKey] mutableCopy];
if (!lastReportedArray) {
// Initialized before (could be empty).
lastReportedArray = [NSMutableArray new];
}
// Check for dates > 28 days and remove older items.
NSMutableIndexSet* toDelete = [[NSMutableIndexSet alloc] init];
for (NSUInteger i = 0; i < lastReportedArray.count; i++) {
if ([now timeIntervalSinceDate:[lastReportedArray objectAtIndex:i]] /
(24 * 60 * 60) >
kRangeForActivityBucketsInDays) {
[toDelete addIndex:i];
} else {
break;
}
}
// The count should never be < 1 for `lastReportedArray` when toDelete > 0 to
// prevent a crash / out of bounds errors.
if (toDelete.count > 0) {
CHECK(lastReportedArray.count >= 1);
[lastReportedArray removeObjectsAtIndexes:toDelete];
}
[defaults setObject:lastReportedArray
forKey:kActivityBucketLastReportedDateArrayKey];
// Check how many items in array.
NSUInteger datesActive = lastReportedArray.count;
switch (datesActive) {
case 0:
activityBucket = FeedActivityBucket::kNoActivity;
break;
case 1 ... 7:
activityBucket = FeedActivityBucket::kLowActivity;
break;
case 8 ... 15:
activityBucket = FeedActivityBucket::kMediumActivity;
break;
case 16 ... 28:
activityBucket = FeedActivityBucket::kHighActivity;
break;
default:
// This should never be reached, as dates should never be > 28 days.
CHECK(NO);
break;
}
[defaults setInteger:(int)activityBucket forKey:kActivityBucketKey];
// Activity Buckets Daily Run.
[self recordActivityBuckets:activityBucket];
[defaults setObject:now forKey:kActivityBucketLastReportedDateKey];
}
// Records the engagement buckets.
- (void)recordActivityBuckets:(FeedActivityBucket)activityBucket {
UMA_HISTOGRAM_ENUMERATION(kAllFeedsActivityBucketsHistogram, activityBucket);
}
// Records Feed engagement.
- (void)recordEngagement:(int)scrollDistance interacted:(BOOL)interacted {
scrollDistance = abs(scrollDistance);
// Determine if this interaction is part of a new 'session'.
base::Time now = base::Time::Now();
base::TimeDelta visitTimeout = base::Minutes(kMinutesBetweenSessions);
if (now - self.sessionStartTime > visitTimeout) {
[self finalizeSession];
}
// Reset the last active time for session measurement.
self.sessionStartTime = now;
// Report the user as engaged-simple if they have scrolled any amount or
// interacted with the card, and it has not already been reported for this
// Chrome run.
if (scrollDistance > 0 || interacted) {
[self recordEngagedSimple];
if (GetFeedRefreshEngagementCriteriaType() ==
FeedRefreshEngagementCriteriaType::kSimpleEngagement) {
self.engagedWithLatestRefreshedContent = YES;
}
}
// Report the user as engaged if they have scrolled more than the threshold or
// interacted with the card, and it has not already been reported this
// Chrome run.
if (scrollDistance > kMinScrollThreshold || interacted) {
[self recordEngaged];
if (GetFeedRefreshEngagementCriteriaType() ==
FeedRefreshEngagementCriteriaType::kEngagement) {
self.engagedWithLatestRefreshedContent = YES;
}
}
[self.sessionRecorder recordUserInteractionOrScrolling];
// This must be called after setting `engagedWithLatestRefreshedContent`
// properly after scrolling or interactions.
if (IsFeedSessionCloseForegroundRefreshEnabled() &&
[self hasEngagedWithLatestRefreshedContent]) {
[self setOrExtendRefreshTimer];
}
}
// Checks if a Good Visit should be recorded. `interacted` is YES if it was
// triggered by an explicit interaction. (e.g. Opening a new Tab in Incognito.)
- (void)checkEngagementGoodVisitWithInteraction:(BOOL)interacted {
// Determine if this interaction is part of a new session.
base::Time now = base::Time::Now();
if ((now - self.lastInteractionTimeForGoodVisits) >
base::Minutes(kMinutesBetweenSessions)) {
[self resetGoodVisitSession];
} else {
// Check if Discover only session has expired.
if ((now - self.lastInteractionTimeForDiscoverGoodVisits) >
base::Minutes(kMinutesBetweenSessions)) {
[self resetGoodVisitSessionForFeed:FeedTypeDiscover];
}
// Check if Following only session has expired.
if ((now - self.lastInteractionTimeForFollowingGoodVisits) >
base::Minutes(kMinutesBetweenSessions)) {
[self resetGoodVisitSessionForFeed:FeedTypeFollowing];
}
}
self.lastInteractionTimeForGoodVisits = now;
if ([self.feedControlDelegate selectedFeed] == FeedTypeDiscover) {
self.lastInteractionTimeForDiscoverGoodVisits = now;
}
if ([self.feedControlDelegate selectedFeed] == FeedTypeFollowing) {
self.lastInteractionTimeForFollowingGoodVisits = now;
}
// If the session hasn't been reset and a GoodVisit has already been
// reported for all possible surfaces return early as an optimization.
if (self.goodVisitReportedDiscover && self.goodVisitReportedFollowing &&
self.goodVisitReportedAllFeeds) {
return;
}
// Report a Good Visit if any of the conditions below is YES and
// no Good Visit has been recorded for the past `kMinutesBetweenSessions`:
// 1. Good Explicit Interaction (add to reading list, long press, open in
// new incognito tab ...).
if (interacted) {
[self recordEngagedGoodVisits:[self.feedControlDelegate selectedFeed]
allFeedsOnly:NO];
return;
}
// 2. Good time in feed (`kGoodVisitTimeInFeedSeconds` with >= 1 scroll in an
// entire session).
if (([self timeSpentForCurrentGoodVisitSessionInFeed:[self.feedControlDelegate
selectedFeed]] >
kGoodVisitTimeInFeedSeconds) &&
self.goodVisitScroll) {
[self recordEngagedGoodVisits:[self.feedControlDelegate selectedFeed]
allFeedsOnly:YES];
// Check if Good Visit should be triggered for Discover feed.
if (self.discoverPreviousTimeInFeedGV > kGoodVisitTimeInFeedSeconds) {
[self recordEngagedGoodVisits:FeedTypeDiscover allFeedsOnly:NO];
}
// Check if Good Visit should be triggered for Following feed.
if (self.followingPreviousTimeInFeedGV > kGoodVisitTimeInFeedSeconds) {
[self recordEngagedGoodVisits:FeedTypeFollowing allFeedsOnly:NO];
}
return;
}
}
// Records any direct interaction with the Feed, this doesn't include scrolling.
- (void)recordInteraction {
[self recordEngagement:0 interacted:YES];
// Log interaction for all feeds
UMA_HISTOGRAM_ENUMERATION(kAllFeedsEngagementTypeHistogram,
FeedEngagementType::kFeedInteracted);
// Log interaction for Discover feed.
if ([self.feedControlDelegate selectedFeed] == FeedTypeDiscover) {
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedEngagementTypeHistogram,
FeedEngagementType::kFeedInteracted);
}
// Log interaction for Following feed.
if ([self.feedControlDelegate selectedFeed] == FeedTypeFollowing) {
UMA_HISTOGRAM_ENUMERATION(kFollowingFeedEngagementTypeHistogram,
FeedEngagementType::kFeedInteracted);
}
}
// Records simple engagement for the current `selectedFeed`.
- (void)recordEngagedSimple {
// If neither feed has been engaged with, log "AllFeeds" simple engagement.
if (!self.engagedSimpleReportedDiscover &&
!self.engagedSimpleReportedFollowing) {
UMA_HISTOGRAM_ENUMERATION(kAllFeedsEngagementTypeHistogram,
FeedEngagementType::kFeedEngagedSimple);
}
// Log simple engagment for Discover feed.
if ([self.feedControlDelegate selectedFeed] == FeedTypeDiscover &&
!self.engagedSimpleReportedDiscover) {
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedEngagementTypeHistogram,
FeedEngagementType::kFeedEngagedSimple);
self.engagedSimpleReportedDiscover = YES;
}
// Log simple engagement for Following feed.
if ([self.feedControlDelegate selectedFeed] == FeedTypeFollowing &&
!self.engagedSimpleReportedFollowing) {
UMA_HISTOGRAM_ENUMERATION(kFollowingFeedEngagementTypeHistogram,
FeedEngagementType::kFeedEngagedSimple);
self.engagedSimpleReportedFollowing = YES;
}
}
// Records engagement for the currently selected feed.
- (void)recordEngaged {
// If neither feed has been engaged with, log "AllFeeds" engagement.
if (!self.engagedReportedDiscover && !self.engagedReportedFollowing) {
// If the user has engaged with a feed, this is recorded as a user default.
// This can be used for things which require feed engagement as a condition,
// such as the top-of-feed signin promo.
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults setBool:YES forKey:kEngagedWithFeedKey];
// Log engagement for Activity Buckets.
[self logDailyActivity];
UMA_HISTOGRAM_ENUMERATION(kAllFeedsEngagementTypeHistogram,
FeedEngagementType::kFeedEngaged);
}
// Log engagment for Discover feed.
if ([self.feedControlDelegate selectedFeed] == FeedTypeDiscover &&
!self.engagedReportedDiscover) {
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedEngagementTypeHistogram,
FeedEngagementType::kFeedEngaged);
self.engagedReportedDiscover = YES;
}
// Log engagement for Following feed.
if ([self.feedControlDelegate selectedFeed] == FeedTypeFollowing &&
!self.engagedReportedFollowing) {
UMA_HISTOGRAM_ENUMERATION(kFollowingFeedEngagementTypeHistogram,
FeedEngagementType::kFeedEngaged);
UMA_HISTOGRAM_ENUMERATION(
kFollowingFeedSortTypeWhenEngaged,
[self convertFollowingFeedSortTypeForHistogram:
[self.feedControlDelegate followingFeedSortType]]);
self.engagedReportedFollowing = YES;
// Log follow count when engaging with Following feed.
// TODO(crbug.com/1322640): `followDelegate` is nil when navigating to an
// article, since NTPCoordinator is stopped first. When this is fixed,
// `recordFollowCount` should be called here.
}
// TODO(crbug.com/1322640): Separate user action for Following feed.
base::RecordAction(base::UserMetricsAction(kDiscoverFeedUserActionEngaged));
}
// Records Good Visits for both the Following and Discover feed.
// `allFeedsOnly` will be YES when no individual feed should report a Good
// Visit, but a Good Visit should be triggered for all Feeds.
- (void)recordEngagedGoodVisits:(FeedType)feedType
allFeedsOnly:(BOOL)allFeedsOnly {
// Check if the user has previously engaged with the feed in the same
// session.
// If neither feed has been engaged with, log "AllFeeds" engagement.
if (!self.goodVisitReportedAllFeeds) {
// Log for the all feeds aggregate.
UMA_HISTOGRAM_ENUMERATION(kAllFeedsEngagementTypeHistogram,
FeedEngagementType::kGoodVisit);
self.goodVisitReportedAllFeeds = YES;
}
if (allFeedsOnly) {
return;
}
// A Good Visit for AllFeeds should have been reported in order to report feed
// specific Good Visits.
DCHECK(self.goodVisitReportedAllFeeds);
// Log interaction for Discover feed.
if (feedType == FeedTypeDiscover && !self.goodVisitReportedDiscover) {
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedEngagementTypeHistogram,
FeedEngagementType::kGoodVisit);
self.goodVisitReportedDiscover = YES;
if (GetFeedRefreshEngagementCriteriaType() ==
FeedRefreshEngagementCriteriaType::kGoodVisit) {
self.engagedWithLatestRefreshedContent = YES;
}
}
// Log interaction for Following feed.
if (feedType == FeedTypeFollowing && !self.goodVisitReportedFollowing) {
UMA_HISTOGRAM_ENUMERATION(kFollowingFeedEngagementTypeHistogram,
FeedEngagementType::kGoodVisit);
self.goodVisitReportedFollowing = YES;
}
}
// Calculates the time the user has spent in the feed during a good
// visit session.
- (NSTimeInterval)timeSpentForCurrentGoodVisitSessionInFeed:
(FeedType)currentFeed {
// Add the time spent since last recording.
base::Time now = base::Time::Now();
base::TimeDelta additionalTimeInFeed = now - self.feedBecameVisibleTime;
self.previousTimeInFeedForGoodVisitSession =
self.previousTimeInFeedForGoodVisitSession +
additionalTimeInFeed.InSecondsF();
// Calculate for specific feed.
switch (currentFeed) {
case FeedTypeFollowing:
self.followingPreviousTimeInFeedGV += additionalTimeInFeed.InSecondsF();
break;
case FeedTypeDiscover:
self.discoverPreviousTimeInFeedGV += additionalTimeInFeed.InSecondsF();
break;
}
DCHECK(self.followingPreviousTimeInFeedGV <=
self.previousTimeInFeedForGoodVisitSession);
DCHECK(self.discoverPreviousTimeInFeedGV <=
self.previousTimeInFeedForGoodVisitSession);
return self.previousTimeInFeedForGoodVisitSession;
}
// Resets the session tracking values, this occurs if there's been
// `kMinutesBetweenSessions` minutes between sessions.
- (void)finalizeSession {
// If simple engagement hasn't been logged, then there's no session to
// finalize.
if (!self.engagedSimpleReportedDiscover &&
!self.engagedSimpleReportedFollowing) {
return;
}
self.engagedReportedDiscover = NO;
self.engagedReportedFollowing = NO;
self.engagedSimpleReportedDiscover = NO;
self.engagedSimpleReportedFollowing = NO;
self.scrolledReportedDiscover = NO;
self.scrolledReportedFollowing = NO;
}
// Resets the Good Visits session tracking values, this occurs if there's been
// kMinutesBetweenSessions minutes between sessions.
- (void)resetGoodVisitSession {
// Reset defaults for new session.
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:nil forKey:kArticleVisitTimestampKey];
[defaults setDouble:0 forKey:kLongFeedVisitTimeAggregateKey];
base::Time now = base::Time::Now();
self.lastInteractionTimeForGoodVisits = now;
[defaults setObject:now.ToNSDate() forKey:kLastInteractionTimeForGoodVisits];
self.feedBecameVisibleTime = now;
self.goodVisitScroll = NO;
self.goodVisitReportedAllFeeds = NO;
// Reset individual feeds.
[self resetGoodVisitSessionForFeed:FeedTypeFollowing];
[self resetGoodVisitSessionForFeed:FeedTypeDiscover];
}
// Resets a Good Visit session for an individual feed. Used to allow for
// sessions to expire only for specific feeds.
- (void)resetGoodVisitSessionForFeed:(FeedType)feedType {
base::Time now = base::Time::Now();
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
if (feedType == FeedTypeDiscover) {
[defaults setDouble:0 forKey:kLongDiscoverFeedVisitTimeAggregateKey];
self.lastInteractionTimeForDiscoverGoodVisits = now;
[defaults setObject:now.ToNSDate()
forKey:kLastInteractionTimeForDiscoverGoodVisits];
self.discoverPreviousTimeInFeedGV = 0;
self.goodVisitReportedDiscover = NO;
}
if (feedType == FeedTypeFollowing) {
[defaults setDouble:0 forKey:kLongFollowingFeedVisitTimeAggregateKey];
self.lastInteractionTimeForFollowingGoodVisits = now;
[defaults setObject:now.ToNSDate()
forKey:kLastInteractionTimeForFollowingGoodVisits];
self.followingPreviousTimeInFeedGV = 0;
self.goodVisitReportedFollowing = NO;
}
}
// Records the time a user has spent in the feed for a day when 24hrs have
// passed.
- (void)recordTimeSpentInFeedIfDayIsDone {
// The midnight time for the day in which the
// `ContentSuggestions.Feed.TimeSpentInFeed` was last recorded.
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSDate* lastInteractionReported = base::apple::ObjCCast<NSDate>(
[defaults objectForKey:kLastDayTimeInFeedReportedKey]);
base::Time lastInteractionReportedInTime;
if (lastInteractionReported != nil) {
lastInteractionReportedInTime =
base::Time::FromNSDate(lastInteractionReported);
}
DCHECK(self.timeSpentInFeed >= base::Seconds(0));
BOOL shouldResetData = NO;
if (lastInteractionReported) {
base::Time now = base::Time::Now();
base::TimeDelta sinceDayStart = (now - lastInteractionReportedInTime);
if (sinceDayStart >= base::Days(1)) {
// Check if the user has spent any time in the feed.
if (self.timeSpentInFeed > base::Seconds(0)) {
UMA_HISTOGRAM_LONG_TIMES(kTimeSpentInFeedHistogram,
self.timeSpentInFeed);
}
shouldResetData = YES;
}
} else {
shouldResetData = YES;
}
if (shouldResetData) {
lastInteractionReported =
[NSDate dateWithTimeIntervalSince1970:base::Time::Now().ToDoubleT()];
// Save to Defaults
[defaults setObject:lastInteractionReported
forKey:kLastDayTimeInFeedReportedKey];
// Reset time spent in feed aggregate.
self.timeSpentInFeed = base::Seconds(0);
[defaults setDouble:self.timeSpentInFeed.InSecondsF()
forKey:kTimeSpentInFeedAggregateKey];
}
}
// Records the `duration` it took to Discover feed to perform any
// network operation.
- (void)recordNetworkRequestDuration:(base::TimeDelta)duration {
UMA_HISTOGRAM_MEDIUM_TIMES(kDiscoverFeedNetworkDuration, duration);
}
// Records that a URL was opened regardless of the target surface (e.g. New Tab,
// Same Tab, Incognito Tab, etc.).
- (void)recordOpenURL {
// Save the time of the open so we can then calculate how long the user spent
// in that page.
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:[[NSDate alloc] init] forKey:kArticleVisitTimestampKey];
[defaults setInteger:[self.feedControlDelegate selectedFeed]
forKey:kLastUsedFeedForGoodVisitsKey];
[self.NTPMetricsDelegate feedArticleOpened];
switch ([self.feedControlDelegate selectedFeed]) {
case FeedTypeDiscover:
UMA_HISTOGRAM_EXACT_LINEAR(kDiscoverFeedURLOpened, 0, 1);
break;
case FeedTypeFollowing:
UMA_HISTOGRAM_EXACT_LINEAR(kFollowingFeedURLOpened, 0, 1);
}
}
// Sets or extends the refresh timer.
- (void)setOrExtendRefreshTimer {
[self.refreshTimer invalidate];
__weak FeedMetricsRecorder* weakSelf = self;
self.refreshTimer = [NSTimer
scheduledTimerWithTimeInterval:GetFeedRefreshTimerTimeoutInSeconds()
target:weakSelf
selector:@selector(refreshTimerEnded)
userInfo:nil
repeats:NO];
}
// Signals that the refresh timer ended.
- (void)refreshTimerEnded {
[self.refreshTimer invalidate];
self.refreshTimer = nil;
if (!self.isNTPVisible) {
// The feed refresher checks feed engagement criteria.
self.feedRefresher->RefreshFeed(
FeedRefreshTrigger::kForegroundFeedNotVisible);
}
}
#pragma mark - Converters
// Converts a FollowingFeedSortType NSEnum into a FeedSortType enum.
- (FeedSortType)convertFollowingFeedSortTypeForHistogram:
(FollowingFeedSortType)followingFeedSortType {
switch (followingFeedSortType) {
case FollowingFeedSortTypeUnspecified:
return FeedSortType::kUnspecifiedSortType;
case FollowingFeedSortTypeByPublisher:
return FeedSortType::kGroupedByPublisher;
case FollowingFeedSortTypeByLatest:
return FeedSortType::kSortedByLatest;
}
}
@end