From 817dc1542d6d3ebca20b4eb69192bf32c53783d5 Mon Sep 17 00:00:00 2001 From: Guillem Perez Date: Mon, 5 Jun 2023 18:21:14 +0000 Subject: [PATCH] [iOS] New Feed Activity Buckets metric. Added a new metric to distribute users into feed activity buckets to be able to better understand how users use the feed. (cherry picked from commit 8eb76697aa47f81649b033f2827c886688e25b9f) Bug: 1446085 Change-Id: I8ab00931e09b0244297400f040e0bbdb8cb79917 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4507183 Auto-Submit: Guillem Perez Commit-Queue: Justin DeWitt Reviewed-by: Adam Arcaro Reviewed-by: Justin DeWitt Cr-Original-Commit-Position: refs/heads/main@{#1148786} Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4575801 Commit-Queue: Guillem Perez Cr-Commit-Position: refs/branch-heads/5790@{#360} Cr-Branched-From: 1d71a337b1f6e707a13ae074dca1e2c34905eb9f-refs/heads/main@{#1148114} --- .../ui/ntp/metrics/feed_metrics_constants.h | 27 ++++ .../ui/ntp/metrics/feed_metrics_constants.mm | 8 ++ .../ui/ntp/metrics/feed_metrics_recorder.mm | 119 ++++++++++++++++++ tools/metrics/histograms/enums.xml | 15 +++ .../metadata/content/histograms.xml | 18 +++ 5 files changed, 187 insertions(+) diff --git a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.h b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.h index 43d1b599498f7..69ca250a3b8b6 100644 --- a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.h +++ b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.h @@ -31,6 +31,9 @@ extern const int kMinutesBetweenSessions; // The max amount of cards in the Discover Feed. extern const int kMaxCardsInFeed; +// The number of days for the Activity Buckets calculations. +extern const int kRangeForActivityBucketsInDays; + // Stores the time when the user visits an article on the feed. extern NSString* const kArticleVisitTimestampKey; // Stores the time elapsed on the feed when the user leaves. @@ -47,6 +50,12 @@ extern NSString* const kLastInteractionTimeForFollowingGoodVisits; extern NSString* const kLastDayTimeInFeedReportedKey; // Stores the time spent on the feed for a day. extern NSString* const kTimeSpentInFeedAggregateKey; +// Stores the last time the activity bucket was reported. +extern NSString* const kActivityBucketLastReportedDateKey; +// Stores the last 28 days of activity bucket reported days. +extern NSString* const kActivityBucketLastReportedDateArrayKey; +// Stores the latest activity bucket the user was on. +extern NSString* const kActivityBucketKey; #pragma mark - Enums @@ -208,6 +217,21 @@ enum class FeedSortType { kMaxValue = kSortedByLatest, }; +// TODO(crbug.com/1447234): Clean up the kError enum. +// The values for the Feed Activity Buckets metric. +enum class FeedActivityBucket { + // No activity bucket for users active 0/28 days. + kNoActivity = 0, + // Low activity bucket for users active 1-7/28 days. + kLowActivity = 1, + // Medium activity bucket for users active 8-15/28 days. + kMediumActivity = 2, + // High activity bucket for users active 16+/28 days. + kHighActivity = 3, + // Highest enumerator. Recommended by Histogram metrics best practices. + kMaxValue = kHighActivity, +}; + #pragma mark - Histograms // Histogram name for the Time Spent in Feed. @@ -223,6 +247,9 @@ extern const char kDiscoverFeedEngagementTypeHistogram[]; extern const char kFollowingFeedEngagementTypeHistogram[]; extern const char kAllFeedsEngagementTypeHistogram[]; +// Histogram name for the feed activity bucket metric. +extern const char kAllFeedsActivityBucketsHistogram[]; + // Histogram name for a Discover feed card shown at index. extern const char kDiscoverFeedCardShownAtIndex[]; diff --git a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.mm b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.mm index 2eb9d73c1bb88..e4ecd9ee10890 100644 --- a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.mm +++ b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.mm @@ -13,6 +13,7 @@ const int kNonShortClickSeconds = 10; const int kMinutesBetweenSessions = 5; const int kMaxCardsInFeed = 50; +const int kRangeForActivityBucketsInDays = 28; NSString* const kArticleVisitTimestampKey = @"ShortClickInteractionTimestamp"; NSString* const kLongFeedVisitTimeAggregateKey = @@ -30,6 +31,11 @@ @"LastInteractionTimeForGoodVisitsFollowing"; NSString* const kLastDayTimeInFeedReportedKey = @"LastDayTimeInFeedReported"; NSString* const kTimeSpentInFeedAggregateKey = @"TimeSpentInFeedAggregate"; +NSString* const kActivityBucketLastReportedDateKey = + @"ActivityBucketLastReportedDate"; +NSString* const kActivityBucketLastReportedDateArrayKey = + @"ActivityBucketLastReportedDateArray"; +NSString* const kActivityBucketKey = @"FeedActivityBucket"; #pragma mark - Histograms @@ -49,6 +55,8 @@ "NewTabPage.ContentSuggestions.Shown"; const char kFollowingFeedCardShownAtIndex[] = "ContentSuggestions.Feed.WebFeed.Shown"; +const char kAllFeedsActivityBucketsHistogram[] = + "ContentSuggestions.Feed.AllFeeds.Activity"; const char kDiscoverFeedNoticeCardFulfilled[] = "ContentSuggestions.Feed.NoticeCardFulfilled2"; const char kDiscoverFeedArticlesFetchNetworkDurationSuccess[] = diff --git a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.mm b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.mm index 2baf10d817971..fd3eb4c575de3 100644 --- a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.mm +++ b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.mm @@ -47,6 +47,10 @@ @interface FeedMetricsRecorder () @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`. @@ -194,6 +198,7 @@ - (void)recordNTPDidChangeVisibility:(BOOL)visible { // Total time spent in feed metrics. self.timeSpentInFeed = base::Seconds([defaults doubleForKey:kTimeSpentInFeedAggregateKey]); + [self computeActivityBuckets]; [self recordTimeSpentInFeedIfDayIsDone]; self.previousTimeInFeedForGoodVisitSession = @@ -887,6 +892,117 @@ - (void)recordDiscoverFeedUserActionHistogram:(FeedUserActionType)actionType } } +// 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* 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::mac::ObjCCast( + [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* 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); @@ -1055,6 +1171,9 @@ - (void)recordEngaged { NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; [defaults setBool:YES forKey:kEngagedWithFeedKey]; + // Log engagement for Activity Buckets. + [self logDailyActivity]; + UMA_HISTOGRAM_ENUMERATION(kAllFeedsEngagementTypeHistogram, FeedEngagementType::kFeedEngaged); } diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml index 5ac715af6675e..8b1d61799b8d2 100644 --- a/tools/metrics/histograms/enums.xml +++ b/tools/metrics/histograms/enums.xml @@ -42897,6 +42897,21 @@ Called by update_permissions_policy_enum.py.--> + + + The user has no activity in the last 28 days. + + + The user has 1-7 days of activity in the last 28 days. + + + The user has 8-15 days of activity in the last 28 days. + + + The user has 16+ days of activity in the last 28 days. + + + Removed as of 05/2021. Replaced by FeedVideoPlayEvent. diff --git a/tools/metrics/histograms/metadata/content/histograms.xml b/tools/metrics/histograms/metadata/content/histograms.xml index 984bf37adaf1a..1c7dd2cb5f238 100644 --- a/tools/metrics/histograms/metadata/content/histograms.xml +++ b/tools/metrics/histograms/metadata/content/histograms.xml @@ -1994,6 +1994,24 @@ chromium-metrics-reviews@google.com. + + guiperez@google.com + feed@chromium.org + + Tracks user activity buckets with {FeedType}. Each bucket composed of the + user's activity level. Logs the for the first time 1 day after the metric is + active (only on first time use), then logs an activity bucket after at least + 24hrs from the last log have elapsed. When the "Engaged" metric is + triggered we log one more day of activity during the past 28 days. + + + + + + + + adamta@google.com