// 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/ntp/shared/metrics/feed_metrics_recorder.h"
#import "base/apple/foundation_util.h"
#import "base/debug/dump_without_crashing.h"
#import "base/json/values_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 "components/feed/core/common/pref_names.h"
#import "components/feed/core/v2/public/ios/notice_card_tracker.h"
#import "components/feed/core/v2/public/ios/prefs.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/metrics/model/constants.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_state.h"
#import "ios/chrome/browser/ntp/ui_bundled/feed_control_delegate.h"
#import "ios/chrome/browser/ntp/shared/metrics/feed_metrics_constants.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_follow_delegate.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_metrics_delegate.h"
#import "ios/chrome/browser/shared/public/features/features.h"
namespace {
// The number of days for the Activity Buckets calculations.
constexpr base::TimeDelta kRangeForActivityBuckets = base::Days(28);
} // namespace
using feed::FeedEngagementType;
using feed::FeedUserActionType;
@interface FeedMetricsRecorder ()
// 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/40871863) 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;
// YES if the NTP is visible.
@property(nonatomic, assign) BOOL isNTPVisible;
// The ChromeBrowserState PrefService.
@property(nonatomic, assign) PrefService* prefService;
@end
@implementation FeedMetricsRecorder
- (instancetype)initWithPrefService:(PrefService*)prefService {
DCHECK(prefService);
self = [super init];
if (self) {
_prefService = prefService;
}
return self;
}
#pragma mark - Public
+ (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.NTPState.selectedFeed == FeedTypeDiscover &&
!self.scrolledReportedDiscover) {
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedEngagementTypeHistogram,
FeedEngagementType::kFeedScrolled);
self.scrolledReportedDiscover = YES;
}
// Log scrolled into Following feed.
if (self.NTPState.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;
if (visible) {
// Sets `feedBecameVisibleTime` before any time based check is ran to
// prevent negative values from non-initialized variables.
self.feedBecameVisibleTime = base::Time::Now();
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kOpenedFeedSurface
asInteraction:NO];
base::Time lastInteractionTimeForGoodVisitsDate =
self.prefService->GetTime(kLastInteractionTimeForGoodVisits);
if (lastInteractionTimeForGoodVisitsDate != base::Time()) {
self.lastInteractionTimeForGoodVisits =
lastInteractionTimeForGoodVisitsDate;
}
base::Time lastInteractionTimeForDiscoverGoodVisitsDate =
self.prefService->GetTime(kLastInteractionTimeForDiscoverGoodVisits);
if (lastInteractionTimeForDiscoverGoodVisitsDate != base::Time()) {
self.lastInteractionTimeForDiscoverGoodVisits =
lastInteractionTimeForDiscoverGoodVisitsDate;
}
base::Time lastInteractionTimeForFollowingGoodVisitsDate =
self.prefService->GetTime(kLastInteractionTimeForFollowingGoodVisits);
if (lastInteractionTimeForFollowingGoodVisitsDate != base::Time()) {
self.lastInteractionTimeForFollowingGoodVisits =
lastInteractionTimeForFollowingGoodVisitsDate;
}
// Total time spent in feed metrics.
self.timeSpentInFeed = base::Seconds(
self.prefService->GetDouble(kTimeSpentInFeedAggregateKey));
[self computeActivityBuckets];
[self recordTimeSpentInFeedIfDayIsDone];
self.previousTimeInFeedForGoodVisitSession =
self.prefService->GetDouble(kLongFeedVisitTimeAggregateKey);
self.discoverPreviousTimeInFeedGV =
self.prefService->GetDouble(kLongDiscoverFeedVisitTimeAggregateKey);
self.followingPreviousTimeInFeedGV =
self.prefService->GetDouble(kLongFollowingFeedVisitTimeAggregateKey);
// TODO(crbug.com/40075889) This scenario can happen (this is very rare)
// because key kLongFeedVisitTimeAggregateKey was moved out of
// NSUserDefaults later than kLongDiscoverFeedVisitTimeAggregateKey and
// kLongFollowingFeedVisitTimeAggregateKey. Clean this code in the future.
if (self.previousTimeInFeedForGoodVisitSession <
self.discoverPreviousTimeInFeedGV ||
self.previousTimeInFeedForGoodVisitSession <
self.followingPreviousTimeInFeedGV) {
self.previousTimeInFeedForGoodVisitSession =
std::max(self.discoverPreviousTimeInFeedGV,
self.followingPreviousTimeInFeedGV);
}
if (self.previousTimeInFeedForGoodVisitSession < 0 ||
self.discoverPreviousTimeInFeedGV < 0 ||
self.followingPreviousTimeInFeedGV < 0) {
base::debug::DumpWithoutCrashing();
}
// Checks if there is a timestamp in PrefService for when a user clicked
// on an article in order to be able to trigger a non-short click
// interaction.
base::Time articleVisitStart =
self.prefService->GetTime(kArticleVisitTimestampKey);
if (articleVisitStart != base::Time()) {
// Report Good Visit if user came back to the NTP after spending
// kNonShortClickSeconds in a feed article.
if (base::Time::Now() - articleVisitStart >
base::Seconds(kNonShortClickSeconds)) {
// Trigger a GV for a specific feed.
FeedType lastUsedFeedType =
self.prefService->GetInteger(kLastUsedFeedForGoodVisitsKey) == 1
? FeedTypeFollowing
: FeedTypeDiscover;
[self recordEngagedGoodVisits:lastUsedFeedType allFeedsOnly:NO];
}
// Clear PrefService for new session.
self.prefService->ClearPref(kArticleVisitTimestampKey);
}
} else {
// Once the NTP becomes hidden, check for Good Visit which updates
// `self.previousTimeInFeedForGoodVisitSession` and then we save it in
// PrefService.
// Also calculate total aggregate for the time in feed aggregate metric.
// When the user opens the browser directly to a website while they
// originally were on the NTP. Set `feedBecameVisibleTime` to now if it has
// never been set before.
base::Time now = base::Time::Now();
if (self.feedBecameVisibleTime.is_null()) {
self.feedBecameVisibleTime = now;
}
self.timeSpentInFeed = now - self.feedBecameVisibleTime;
[self checkEngagementGoodVisitWithInteraction:NO];
self.prefService->SetDouble(kTimeSpentInFeedAggregateKey,
self.timeSpentInFeed.InSecondsF());
self.prefService->SetDouble(kLongFeedVisitTimeAggregateKey,
self.previousTimeInFeedForGoodVisitSession);
self.prefService->SetDouble(kLongDiscoverFeedVisitTimeAggregateKey,
self.discoverPreviousTimeInFeedGV);
self.prefService->SetDouble(kLongFollowingFeedVisitTimeAggregateKey,
self.followingPreviousTimeInFeedGV);
}
}
- (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)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 handleURLOpened];
}
- (void)recordOpenURLInNewTab {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedOpenInNewTab
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionOpenNewTab));
[self handleURLOpened];
}
- (void)recordOpenURLInIncognitoTab {
[self recordDiscoverFeedUserActionHistogram:FeedUserActionType::
kTappedOpenInNewIncognitoTab
asInteraction:YES];
base::RecordAction(
base::UserMetricsAction(kDiscoverFeedUserActionOpenIncognitoTab));
[self handleURLOpened];
}
- (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.NTPState.selectedFeed) {
case FeedTypeDiscover:
UMA_HISTOGRAM_EXACT_LINEAR(kDiscoverFeedCardShownAtIndex, index,
kMaxCardsInFeed);
break;
case FeedTypeFollowing:
UMA_HISTOGRAM_EXACT_LINEAR(kFollowingFeedCardShownAtIndex, index,
kMaxCardsInFeed);
}
}
- (void)recordCardTappedAtIndex:(NSUInteger)index {
switch (self.NTPState.selectedFeed) {
case FeedTypeDiscover:
UMA_HISTOGRAM_EXACT_LINEAR(kDiscoverFeedURLOpened, 0, 1);
break;
case FeedTypeFollowing:
UMA_HISTOGRAM_EXACT_LINEAR(kFollowingFeedURLOpened, 0, 1);
}
}
- (void)recordNoticeCardShown:(BOOL)shown {
base::UmaHistogramBoolean(kDiscoverFeedNoticeCardFulfilled, shown);
feed::prefs::SetLastFetchHadNoticeCard(*self.prefService, 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);
self.prefService->SetBoolean(feed::prefs::kLastFetchHadLoggingEnabled,
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/40260057): 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 {
if (!self.followDelegate) {
NOTREACHED(base::NotFatalUntil::M129);
return;
}
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:
// TODO(b/323593501): Report on-feed-engagement follow count.
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)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::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 {
const base::Time now = base::Time::Now();
// Check if the array is initialized.
base::Value::List lastReportedArray =
self.prefService->GetList(kActivityBucketLastReportedDateArrayKey)
.Clone();
// Adds a daily entry to the `lastReportedArray` array
// only once when the user engages.
if ((lastReportedArray.size() > 0 &&
(now - ValueToTime(lastReportedArray.back()).value()) >=
base::Days(1)) ||
lastReportedArray.size() == 0) {
lastReportedArray.Append(TimeToValue(now));
self.prefService->SetList(kActivityBucketLastReportedDateArrayKey,
std::move(lastReportedArray));
}
}
// Calculates the amount of dates the user has been active for the past 28 days.
- (void)computeActivityBuckets {
const base::Time now = base::Time::Now();
base::Time lastActivityBucket =
self.prefService->GetTime(kActivityBucketLastReportedDateKey);
// If the `lastActivityBucket` is not set, set it to now to
// prevent the first day from logging a metric.
if (lastActivityBucket == base::Time()) {
lastActivityBucket = now;
self.prefService->SetTime(kActivityBucketLastReportedDateKey,
lastActivityBucket);
}
// Nothing to do if the activity was reported recently.
if ((now - lastActivityBucket) < base::Days(1)) {
return;
}
// Calculate activity buckets.
// Check if the array is initialized.
const base::Value::List& lastReportedArray =
self.prefService->GetList(kActivityBucketLastReportedDateArrayKey);
base::Value::List newLastReportedArray;
// Do not save in newLastReportedArray dates > 28 days.
for (NSUInteger i = 0; i < lastReportedArray.size(); ++i) {
std::optional<base::Time> date = ValueToTime(lastReportedArray[i]);
if (!date.has_value()) {
continue;
}
if ((now - date.value()) <= kRangeForActivityBuckets) {
newLastReportedArray.Append(TimeToValue(date.value()));
}
}
FeedActivityBucket activityBucket = FeedActivityBucket::kNoActivity;
// Check how many items in array.
switch (newLastReportedArray.size()) {
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;
}
self.prefService->SetInteger(kActivityBucketKey,
static_cast<int>(activityBucket));
self.prefService->SetList(kActivityBucketLastReportedDateArrayKey,
std::move(newLastReportedArray));
// Activity Buckets Daily Run.
[self recordActivityBuckets:activityBucket];
self.prefService->SetTime(kActivityBucketLastReportedDateKey,
base::Time::Now());
}
// 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];
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];
}
}
// 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 {
// Certain actions can be dispatched by a background thread, such as showing a
// snackbar. We shouldn't access the PrefService in the background, so these
// are ignored.
if (![NSThread isMainThread]) {
return;
}
// 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.NTPState.selectedFeed == FeedTypeDiscover) {
self.lastInteractionTimeForDiscoverGoodVisits = now;
}
if (self.NTPState.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.NTPState.selectedFeed allFeedsOnly:NO];
return;
}
// 2. Good time in feed (`kGoodVisitTimeInFeedSeconds` with >= 1 scroll in an
// entire session).
if (([self timeSpentForCurrentGoodVisitSessionInFeed:self.NTPState
.selectedFeed] >
kGoodVisitTimeInFeedSeconds) &&
self.goodVisitScroll) {
[self recordEngagedGoodVisits:self.NTPState.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.NTPState.selectedFeed == FeedTypeDiscover) {
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedEngagementTypeHistogram,
FeedEngagementType::kFeedInteracted);
}
// Log interaction for Following feed.
if (self.NTPState.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.NTPState.selectedFeed == FeedTypeDiscover &&
!self.engagedSimpleReportedDiscover) {
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedEngagementTypeHistogram,
FeedEngagementType::kFeedEngagedSimple);
self.engagedSimpleReportedDiscover = YES;
}
// Log simple engagement for Following feed.
if (self.NTPState.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.NTPState.selectedFeed == FeedTypeDiscover &&
!self.engagedReportedDiscover) {
UMA_HISTOGRAM_ENUMERATION(kDiscoverFeedEngagementTypeHistogram,
FeedEngagementType::kFeedEngaged);
self.engagedReportedDiscover = YES;
}
// Log engagement for Following feed.
if (self.NTPState.selectedFeed == FeedTypeFollowing &&
!self.engagedReportedFollowing) {
UMA_HISTOGRAM_ENUMERATION(kFollowingFeedEngagementTypeHistogram,
FeedEngagementType::kFeedEngaged);
UMA_HISTOGRAM_ENUMERATION(kFollowingFeedSortTypeWhenEngaged,
[self convertFollowingFeedSortTypeForHistogram:
self.NTPState.followingFeedSortType]);
self.engagedReportedFollowing = YES;
// Log follow count when engaging with Following feed.
// TODO(crbug.com/40838123): `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/40838123): 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;
}
// 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;
if (additionalTimeInFeed.is_negative()) {
// TODO(crbug.com/340554892): Fix Good Visits metric.
// Temporary fix, but it should reduce the number of occurances.
self.feedBecameVisibleTime = now;
additionalTimeInFeed = now - self.feedBecameVisibleTime;
}
// Temporary fix to resolve negative values in prefs.
// TODO(crbug.com/329274886): Remove fix once crashes are down to zero.
if (self.previousTimeInFeedForGoodVisitSession < 0) {
self.previousTimeInFeedForGoodVisitSession = 0;
}
self.previousTimeInFeedForGoodVisitSession =
self.previousTimeInFeedForGoodVisitSession +
additionalTimeInFeed.InSecondsF();
if (self.previousTimeInFeedForGoodVisitSession < 0) {
base::debug::DumpWithoutCrashing();
}
// Calculate for specific feed.
switch (currentFeed) {
case FeedTypeFollowing:
self.followingPreviousTimeInFeedGV += additionalTimeInFeed.InSecondsF();
break;
case FeedTypeDiscover:
self.discoverPreviousTimeInFeedGV += additionalTimeInFeed.InSecondsF();
break;
}
DCHECK_LE(self.followingPreviousTimeInFeedGV,
self.previousTimeInFeedForGoodVisitSession);
DCHECK_LE(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.
self.prefService->ClearPref(kArticleVisitTimestampKey);
self.prefService->ClearPref(kLongFeedVisitTimeAggregateKey);
base::Time now = base::Time::Now();
self.lastInteractionTimeForGoodVisits = now;
self.prefService->SetTime(kLastInteractionTimeForGoodVisits, now);
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();
if (feedType == FeedTypeDiscover) {
self.prefService->ClearPref(kLongDiscoverFeedVisitTimeAggregateKey);
self.lastInteractionTimeForDiscoverGoodVisits = now;
self.prefService->SetTime(kLastInteractionTimeForDiscoverGoodVisits, now);
self.discoverPreviousTimeInFeedGV = 0;
self.goodVisitReportedDiscover = NO;
}
if (feedType == FeedTypeFollowing) {
self.prefService->ClearPref(kLongFollowingFeedVisitTimeAggregateKey);
self.lastInteractionTimeForFollowingGoodVisits = now;
self.prefService->SetTime(kLastInteractionTimeForFollowingGoodVisits, now);
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.
const base::Time lastInteractionReported =
self.prefService->GetTime(kLastDayTimeInFeedReportedKey);
DCHECK(self.timeSpentInFeed >= base::Seconds(0));
BOOL shouldResetData = NO;
if (lastInteractionReported != base::Time()) {
base::Time now = base::Time::Now();
base::TimeDelta sinceDayStart = (now - lastInteractionReported);
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) {
// Update the last report time in PrefService.
self.prefService->SetTime(kLastDayTimeInFeedReportedKey, base::Time::Now());
// Reset time spent in feed aggregate.
self.timeSpentInFeed = base::Seconds(0);
self.prefService->SetDouble(kTimeSpentInFeedAggregateKey,
self.timeSpentInFeed.InSecondsF());
}
}
// Records the `duration` it took to Discover feed to perform any
// network operation.
- (void)recordNetworkRequestDuration:(base::TimeDelta)duration {
UMA_HISTOGRAM_MEDIUM_TIMES(kDiscoverFeedNetworkDuration, duration);
}
// Called when a URL was opened regardless of the target surface (e.g. New Tab,
// Same Tab, Incognito Tab, etc.).
- (void)handleURLOpened {
// Save the time of the open so we can then calculate how long the user spent
// in that page.
self.prefService->SetTime(kArticleVisitTimestampKey, base::Time::Now());
self.prefService->SetInteger(kLastUsedFeedForGoodVisitsKey,
self.NTPState.selectedFeed);
[self.NTPMetricsDelegate feedArticleOpened];
}
#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