Log UMA metrics with NTP tile data age/staleness

Most notably, we want to capture this metric for server-side suggestions
in order to detect regressions in upcoming UI changes (Chrome Home).

Bug: 763946
Cq-Include-Trybots: master.tryserver.chromium.linux:closure_compilation
Change-Id: I4cb7e6cea6026ad33182f70d7434ab59f7fcb0af
Reviewed-on: https://chromium-review.googlesource.com/708736
Reviewed-by: Alexei Svitkine <[email protected]>
Reviewed-by: Daniel Cheng <[email protected]>
Reviewed-by: Marc Treib <[email protected]>
Reviewed-by: Sylvain Defresne <[email protected]>
Reviewed-by: Peter Conn <[email protected]>
Commit-Queue: Mikel Astiz <[email protected]>
Cr-Commit-Position: refs/heads/master@{#508391}
diff --git a/components/ntp_tiles/metrics.cc b/components/ntp_tiles/metrics.cc
index 0723c3de..f292b97 100644
--- a/components/ntp_tiles/metrics.cc
+++ b/components/ntp_tiles/metrics.cc
@@ -6,7 +6,7 @@
 
 #include <string>
 
-#include "base/metrics/histogram.h"
+#include "base/metrics/histogram_functions.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/metrics/sparse_histogram.h"
 #include "base/strings/stringprintf.h"
@@ -37,17 +37,10 @@
 const char kTileTypeSuffixThumbnail[] = "Thumbnail";
 const char kTileTypeSuffixThumbnailFailed[] = "ThumbnailFailed";
 
-// Log an event for a given |histogram| at a given element |position|. This
-// routine exists because regular histogram macros are cached thus can't be used
-// if the name of the histogram will change at a given call site.
-void LogHistogramEvent(const std::string& histogram,
-                       int position,
-                       int num_sites) {
-  base::HistogramBase* counter = base::LinearHistogram::FactoryGet(
-      histogram, 1, num_sites, num_sites + 1,
-      base::Histogram::kUmaTargetedHistogramFlag);
-  if (counter)
-    counter->Add(position);
+void LogUmaHistogramAge(const std::string& name, const base::TimeDelta& value) {
+  // Log the value in number of seconds.
+  base::UmaHistogramCustomCounts(name, value.InSeconds(), 5,
+                                 base::TimeDelta::FromDays(14).InSeconds(), 20);
 }
 
 std::string GetSourceHistogramName(TileSource source) {
@@ -100,19 +93,28 @@
                             impression.index, kMaxNumTiles);
 
   std::string source_name = GetSourceHistogramName(impression.source);
-  std::string impression_histogram = base::StringPrintf(
-      "NewTabPage.SuggestionsImpression.%s", source_name.c_str());
-  LogHistogramEvent(impression_histogram, impression.index, kMaxNumTiles);
+  base::UmaHistogramExactLinear(
+      base::StringPrintf("NewTabPage.SuggestionsImpression.%s",
+                         source_name.c_str()),
+      impression.index, kMaxNumTiles);
+
+  if (!impression.data_generation_time.is_null()) {
+    const base::TimeDelta age =
+        base::Time::Now() - impression.data_generation_time;
+    LogUmaHistogramAge("NewTabPage.SuggestionsImpressionAge", age);
+    LogUmaHistogramAge(
+        base::StringPrintf("NewTabPage.SuggestionsImpressionAge.%s",
+                           source_name.c_str()),
+        age);
+  }
 
   UMA_HISTOGRAM_ENUMERATION("NewTabPage.TileTitle",
                             static_cast<int>(impression.title_source),
                             kLastTitleSource + 1);
-  std::string title_source_histogram =
+  base::UmaHistogramExactLinear(
       base::StringPrintf("NewTabPage.TileTitle.%s",
-                         GetSourceHistogramName(impression.source).c_str());
-  LogHistogramEvent(title_source_histogram,
-                    static_cast<int>(impression.title_source),
-                    kLastTitleSource + 1);
+                         GetSourceHistogramName(impression.source).c_str()),
+      static_cast<int>(impression.title_source), kLastTitleSource + 1);
 
   if (impression.visual_type > LAST_RECORDED_TILE_TYPE) {
     return;
@@ -121,10 +123,9 @@
   UMA_HISTOGRAM_ENUMERATION("NewTabPage.TileType", impression.visual_type,
                             LAST_RECORDED_TILE_TYPE + 1);
 
-  std::string tile_type_histogram =
-      base::StringPrintf("NewTabPage.TileType.%s", source_name.c_str());
-  LogHistogramEvent(tile_type_histogram, impression.visual_type,
-                    LAST_RECORDED_TILE_TYPE + 1);
+  base::UmaHistogramExactLinear(
+      base::StringPrintf("NewTabPage.TileType.%s", source_name.c_str()),
+      impression.visual_type, LAST_RECORDED_TILE_TYPE + 1);
 
   const char* tile_type_suffix = GetTileTypeSuffix(impression.visual_type);
   if (tile_type_suffix) {
@@ -136,10 +137,10 @@
           impression.url_for_rappor);
     }
 
-    std::string icon_impression_histogram = base::StringPrintf(
-        "NewTabPage.SuggestionsImpression.%s", tile_type_suffix);
-    LogHistogramEvent(icon_impression_histogram, impression.index,
-                      kMaxNumTiles);
+    base::UmaHistogramExactLinear(
+        base::StringPrintf("NewTabPage.SuggestionsImpression.%s",
+                           tile_type_suffix),
+        impression.index, kMaxNumTiles);
   }
 }
 
@@ -147,37 +148,44 @@
   UMA_HISTOGRAM_ENUMERATION("NewTabPage.MostVisited", impression.index,
                             kMaxNumTiles);
 
-  std::string histogram =
-      base::StringPrintf("NewTabPage.MostVisited.%s",
-                         GetSourceHistogramName(impression.source).c_str());
-  LogHistogramEvent(histogram, impression.index, kMaxNumTiles);
+  std::string source_name = GetSourceHistogramName(impression.source);
+  base::UmaHistogramExactLinear(
+      base::StringPrintf("NewTabPage.MostVisited.%s", source_name.c_str()),
+      impression.index, kMaxNumTiles);
+
+  if (!impression.data_generation_time.is_null()) {
+    const base::TimeDelta age =
+        base::Time::Now() - impression.data_generation_time;
+    LogUmaHistogramAge("NewTabPage.MostVisitedAge", age);
+    LogUmaHistogramAge(
+        base::StringPrintf("NewTabPage.MostVisitedAge.%s", source_name.c_str()),
+        age);
+  }
 
   const char* tile_type_suffix = GetTileTypeSuffix(impression.visual_type);
   if (tile_type_suffix) {
-    std::string tile_type_histogram =
-        base::StringPrintf("NewTabPage.MostVisited.%s", tile_type_suffix);
-    LogHistogramEvent(tile_type_histogram, impression.index, kMaxNumTiles);
+    base::UmaHistogramExactLinear(
+        base::StringPrintf("NewTabPage.MostVisited.%s", tile_type_suffix),
+        impression.index, kMaxNumTiles);
   }
 
   UMA_HISTOGRAM_ENUMERATION("NewTabPage.TileTitleClicked",
                             static_cast<int>(impression.title_source),
                             kLastTitleSource + 1);
-  std::string name_histogram =
+  base::UmaHistogramExactLinear(
       base::StringPrintf("NewTabPage.TileTitleClicked.%s",
-                         GetSourceHistogramName(impression.source).c_str());
-  LogHistogramEvent(name_histogram, static_cast<int>(impression.title_source),
-                    kLastTitleSource + 1);
+                         GetSourceHistogramName(impression.source).c_str()),
+      static_cast<int>(impression.title_source), kLastTitleSource + 1);
 
   if (impression.visual_type <= LAST_RECORDED_TILE_TYPE) {
     UMA_HISTOGRAM_ENUMERATION("NewTabPage.TileTypeClicked",
                               impression.visual_type,
                               LAST_RECORDED_TILE_TYPE + 1);
 
-    std::string type_histogram =
+    base::UmaHistogramExactLinear(
         base::StringPrintf("NewTabPage.TileTypeClicked.%s",
-                           GetSourceHistogramName(impression.source).c_str());
-    LogHistogramEvent(type_histogram, impression.visual_type,
-                      LAST_RECORDED_TILE_TYPE + 1);
+                           GetSourceHistogramName(impression.source).c_str()),
+        impression.visual_type, LAST_RECORDED_TILE_TYPE + 1);
   }
 }
 
diff --git a/components/ntp_tiles/metrics_unittest.cc b/components/ntp_tiles/metrics_unittest.cc
index cafd8e84..6fc5cb6 100644
--- a/components/ntp_tiles/metrics_unittest.cc
+++ b/components/ntp_tiles/metrics_unittest.cc
@@ -31,15 +31,14 @@
 using testing::ElementsAre;
 using testing::IsEmpty;
 
+MATCHER_P3(IsBucketBetween, lower_bound, upper_bound, count, "") {
+  return arg.min >= lower_bound && arg.min <= upper_bound && arg.count == count;
+}
+
 // Builder for instances of NTPTileImpression that uses sensible defaults.
 class Builder {
  public:
-  Builder()
-      : impression_(/*index=*/0,
-                    TileSource::TOP_SITES,
-                    TileTitleSource::UNKNOWN,
-                    TileVisualType::UNKNOWN_TILE_TYPE,
-                    GURL()) {}
+  Builder() {}
 
   Builder& WithIndex(int index) {
     impression_.index = index;
@@ -57,6 +56,10 @@
     impression_.visual_type = visual_type;
     return *this;
   }
+  Builder& WithDataGenerationTime(base::Time data_generation_time) {
+    impression_.data_generation_time = data_generation_time;
+    return *this;
+  }
   Builder& WithUrl(const GURL& url) {
     impression_.url_for_rappor = url;
     return *this;
@@ -291,6 +294,31 @@
                           base::Bucket(kInferredTitleSource, /*count=*/1)));
 }
 
+TEST(RecordTileImpressionTest, ShouldRecordAge) {
+  const base::TimeDelta kSuggestionAge = base::TimeDelta::FromMinutes(1);
+  const base::TimeDelta kBucketTolerance = base::TimeDelta::FromSeconds(20);
+  base::HistogramTester histogram_tester;
+  RecordTileImpression(
+      Builder()
+          .WithSource(TileSource::SUGGESTIONS_SERVICE)
+          .WithDataGenerationTime(base::Time::Now() - kSuggestionAge)
+          .Build(),
+      /*rappor_service=*/nullptr);
+
+  EXPECT_THAT(
+      histogram_tester.GetAllSamples("NewTabPage.SuggestionsImpressionAge"),
+      ElementsAre(
+          IsBucketBetween((kSuggestionAge - kBucketTolerance).InSeconds(),
+                          (kSuggestionAge + kBucketTolerance).InSeconds(),
+                          /*count=*/1)));
+  EXPECT_THAT(histogram_tester.GetAllSamples(
+                  "NewTabPage.SuggestionsImpressionAge.server"),
+              ElementsAre(IsBucketBetween(
+                  (kSuggestionAge - kBucketTolerance).InSeconds(),
+                  (kSuggestionAge + kBucketTolerance).InSeconds(),
+                  /*count=*/1)));
+}
+
 TEST(RecordTileClickTest, ShouldRecordUmaForIcon) {
   base::HistogramTester histogram_tester;
   RecordTileClick(Builder()
@@ -505,6 +533,29 @@
                           base::Bucket(kTitleTagTitleSource, /*count=*/2)));
 }
 
+TEST(RecordTileClickTest, ShouldRecordClickAge) {
+  const base::TimeDelta kSuggestionAge = base::TimeDelta::FromMinutes(1);
+  const base::TimeDelta kBucketTolerance = base::TimeDelta::FromSeconds(20);
+  base::HistogramTester histogram_tester;
+  RecordTileClick(
+      Builder()
+          .WithSource(TileSource::SUGGESTIONS_SERVICE)
+          .WithDataGenerationTime(base::Time::Now() - kSuggestionAge)
+          .Build());
+
+  EXPECT_THAT(histogram_tester.GetAllSamples("NewTabPage.MostVisitedAge"),
+              ElementsAre(IsBucketBetween(
+                  (kSuggestionAge - kBucketTolerance).InSeconds(),
+                  (kSuggestionAge + kBucketTolerance).InSeconds(),
+                  /*count=*/1)));
+  EXPECT_THAT(
+      histogram_tester.GetAllSamples("NewTabPage.MostVisitedAge.server"),
+      ElementsAre(
+          IsBucketBetween((kSuggestionAge - kBucketTolerance).InSeconds(),
+                          (kSuggestionAge + kBucketTolerance).InSeconds(),
+                          /*count=*/1)));
+}
+
 }  // namespace
 }  // namespace metrics
 }  // namespace ntp_tiles
diff --git a/components/ntp_tiles/most_visited_sites.cc b/components/ntp_tiles/most_visited_sites.cc
index a6fd4753..8a564fc 100644
--- a/components/ntp_tiles/most_visited_sites.cc
+++ b/components/ntp_tiles/most_visited_sites.cc
@@ -294,7 +294,8 @@
     // MostVisitedURL.title is either the title or the URL which is treated
     // exactly as the title. Differentiating here is not worth the overhead.
     tile.title_source = TileTitleSource::TITLE_TAG;
-
+    // TODO(crbug.com/773278): Populate |data_generation_time| here in order to
+    // log UMA metrics of age.
     tiles.push_back(std::move(tile));
   }
 
@@ -331,6 +332,10 @@
   if (num_sites_ < num_tiles)
     num_tiles = num_sites_;
 
+  const base::Time profile_timestamp =
+      base::Time::UnixEpoch() +
+      base::TimeDelta::FromMicroseconds(suggestions_profile.timestamp());
+
   NTPTilesVector tiles;
   for (size_t i = 0; i < num_tiles; ++i) {
     const ChromeSuggestion& suggestion_pb = suggestions_profile.suggestions(i);
@@ -347,6 +352,8 @@
     tile.whitelist_icon_path = GetWhitelistLargeIconPath(url);
     tile.thumbnail_url = GURL(suggestion_pb.thumbnail());
     tile.favicon_url = GURL(suggestion_pb.favicon_url());
+    tile.data_generation_time = profile_timestamp;
+
     if (AreNtpMostLikelyFaviconsFromServerEnabled()) {
       icon_cacher_->StartFetchMostLikely(
           url, base::Bind(&MostVisitedSites::OnIconMadeAvailable,
diff --git a/components/ntp_tiles/ntp_tile.h b/components/ntp_tiles/ntp_tile.h
index 4dc012c1..ea6d863 100644
--- a/components/ntp_tiles/ntp_tile.h
+++ b/components/ntp_tiles/ntp_tile.h
@@ -10,6 +10,7 @@
 #include "base/files/file_path.h"
 #include "base/macros.h"
 #include "base/strings/string16.h"
+#include "base/time/time.h"
 #include "components/ntp_tiles/tile_source.h"
 #include "components/ntp_tiles/tile_title_source.h"
 #include "url/gurl.h"
@@ -34,6 +35,10 @@
   // This won't be empty, but might 404 etc.
   GURL favicon_url;
 
+  // Timestamp representing when the tile was originally generated (produced by
+  // a ranking algorithm).
+  base::Time data_generation_time;
+
   NTPTile();
   NTPTile(const NTPTile&);
   ~NTPTile();
diff --git a/components/ntp_tiles/ntp_tile_impression.cc b/components/ntp_tiles/ntp_tile_impression.cc
index 20194940..325bb21 100644
--- a/components/ntp_tiles/ntp_tile_impression.cc
+++ b/components/ntp_tiles/ntp_tile_impression.cc
@@ -11,17 +11,20 @@
                         /*source=*/TileSource::TOP_SITES,
                         /*title_source=*/TileTitleSource::UNKNOWN,
                         /*visual_type=*/TileVisualType::UNKNOWN_TILE_TYPE,
+                        /*data_generation_time=*/base::Time(),
                         /*url_for_rappor=*/GURL()) {}
 
 NTPTileImpression::NTPTileImpression(int index,
                                      TileSource source,
                                      TileTitleSource title_source,
                                      TileVisualType visual_type,
+                                     base::Time data_generation_time,
                                      const GURL& url_for_rappor)
     : index(index),
       source(source),
       title_source(title_source),
       visual_type(visual_type),
+      data_generation_time(data_generation_time),
       url_for_rappor(url_for_rappor) {}
 
 NTPTileImpression::~NTPTileImpression() {}
diff --git a/components/ntp_tiles/ntp_tile_impression.h b/components/ntp_tiles/ntp_tile_impression.h
index 2384ccc8..586151b 100644
--- a/components/ntp_tiles/ntp_tile_impression.h
+++ b/components/ntp_tiles/ntp_tile_impression.h
@@ -5,6 +5,7 @@
 #ifndef COMPONENTS_NTP_TILES_NTP_TILE_IMPRESSION_H_
 #define COMPONENTS_NTP_TILES_NTP_TILE_IMPRESSION_H_
 
+#include "base/time/time.h"
 #include "components/ntp_tiles/tile_source.h"
 #include "components/ntp_tiles/tile_title_source.h"
 #include "components/ntp_tiles/tile_visual_type.h"
@@ -19,6 +20,7 @@
                     TileSource source,
                     TileTitleSource title_source,
                     TileVisualType visual_type,
+                    base::Time data_generation_time,
                     const GURL& url_for_rappor);
   ~NTPTileImpression();
 
@@ -27,6 +29,10 @@
   TileSource source;
   TileTitleSource title_source;
   TileVisualType visual_type;
+  // The timestamp representing when the tile data (e.g. URL) was generated
+  // originally, regardless of the impression timestamp or the time when it
+  // was fetched (for server-side suggestions).
+  base::Time data_generation_time;
   // URL the tile points to, used to report Rappor metrics only (might be empty
   // and is hence ignored, e.g. on desktop).
   GURL url_for_rappor;
diff --git a/components/suggestions/blacklist_store.cc b/components/suggestions/blacklist_store.cc
index 915fc28..a7d08b1a 100644
--- a/components/suggestions/blacklist_store.cc
+++ b/components/suggestions/blacklist_store.cc
@@ -197,6 +197,7 @@
 
   // Populate the filtered suggestions.
   SuggestionsProfile filtered_profile;
+  filtered_profile.set_timestamp(profile->timestamp());
   for (int i = 0; i < profile->suggestions_size(); ++i) {
     if (blacklist_set.find(profile->suggestions(i).url()) ==
         blacklist_set.end()) {
diff --git a/components/suggestions/blacklist_store_unittest.cc b/components/suggestions/blacklist_store_unittest.cc
index 14caf0f6..bb10e7d 100644
--- a/components/suggestions/blacklist_store_unittest.cc
+++ b/components/suggestions/blacklist_store_unittest.cc
@@ -32,6 +32,7 @@
     ChromeSuggestion* suggestion = suggestions.add_suggestions();
     suggestion->set_url(*it);
   }
+  suggestions.set_timestamp(123);
   return suggestions;
 }
 
diff --git a/components/suggestions/suggestions_store.cc b/components/suggestions/suggestions_store.cc
index 09a1eca..002dedd 100644
--- a/components/suggestions/suggestions_store.cc
+++ b/components/suggestions/suggestions_store.cc
@@ -76,6 +76,7 @@
   SuggestionsProfile filtered_suggestions;
   int64_t now_usec =
       (this->clock_->Now() - base::Time::UnixEpoch()).ToInternalValue();
+  filtered_suggestions.set_timestamp(suggestions->timestamp());
 
   for (int i = 0; i < suggestions->suggestions_size(); ++i) {
     ChromeSuggestion* suggestion = suggestions->mutable_suggestions(i);
diff --git a/components/suggestions/suggestions_store_unittest.cc b/components/suggestions/suggestions_store_unittest.cc
index 0a991d05..b39ad74 100644
--- a/components/suggestions/suggestions_store_unittest.cc
+++ b/components/suggestions/suggestions_store_unittest.cc
@@ -37,6 +37,7 @@
 
 SuggestionsProfile CreateTestSuggestions() {
   SuggestionsProfile suggestions;
+  suggestions.set_timestamp(123);
   ChromeSuggestion* suggestion = suggestions.add_suggestions();
   suggestion->set_url(kTestTitle);
   suggestion->set_title(kTestUrl);