1 // Copyright 2022 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/permissions/notifications_engagement_service.h"
7 #include "components/permissions/permissions_client.h"
10 namespace permissions {
14 // For each origin that has the |ContentSettingsType::NOTIFICATIONS|
15 // permission, we record the number of notifications that were displayed
16 // and interacted with. The data is stored in the website setting
17 // |NOTIFICATION_INTERACTIONS| keyed to the same origin. The internal
18 // structure of this metadata is a dictionary:
20 // {"1644163200": {"display_count": 3}, # Implied click_count = 0.
21 // "1644768000": {"display_count": 6, "click_count": 1}}
23 // Currently, entries will be recorded daily.
25 constexpr char kEngagementKey[] = "click_count";
26 constexpr char kDisplayedKey[] = "display_count";
28 // Entries in notifications engagement expire after they become this old.
29 constexpr base::TimeDelta kMaxAge = base::Days(30);
31 // Discards notification interactions stored in `engagement` for time
32 // periods older than |kMaxAge|.
33 void EraseStaleEntries(base::Value::Dict& engagement) {
34 const base::Time cutoff = base::Time::Now() - kMaxAge;
36 for (auto it = engagement.begin(); it != engagement.end();) {
37 const auto& [key, value] = *it;
39 absl::optional<base::Time> last_time =
40 NotificationsEngagementService::ParsePeriodBeginFromBucketLabel(key);
41 if (!last_time.has_value() || last_time.value() < cutoff) {
42 it = engagement.erase(it);
50 NotificationsEngagementService::NotificationsEngagementService(
51 content::BrowserContext* context,
52 PrefService* pref_service)
53 : pref_service_(pref_service) {
55 permissions::PermissionsClient::Get()->GetSettingsMap(context);
58 void NotificationsEngagementService::Shutdown() {
59 settings_map_ = nullptr;
62 void NotificationsEngagementService::RecordNotificationDisplayed(
64 IncrementCounts(url, 1 /*display_count_delta*/, 0 /*click_count_delta*/);
67 void NotificationsEngagementService::RecordNotificationDisplayed(
70 IncrementCounts(url, display_count, 0 /*click_count_delta*/);
73 void NotificationsEngagementService::RecordNotificationInteraction(
75 IncrementCounts(url, 0 /*display_count_delta*/, 1 /*click_count_delta*/);
78 void NotificationsEngagementService::IncrementCounts(const GURL& url,
79 int display_count_delta,
80 int click_count_delta) {
81 base::Value engagement_as_value = settings_map_->GetWebsiteSetting(
82 url, GURL(), ContentSettingsType::NOTIFICATION_INTERACTIONS);
84 base::Value::Dict engagement;
85 if (engagement_as_value.is_dict())
86 engagement = std::move(engagement_as_value).TakeDict();
88 std::string date = GetBucketLabel(base::Time::Now());
89 if (date == std::string())
92 EraseStaleEntries(engagement);
93 base::Value::Dict* bucket = engagement.FindDict(date);
95 bucket = &engagement.Set(date, base::Value::Dict())->GetDict();
97 if (display_count_delta) {
98 bucket->Set(kDisplayedKey, display_count_delta +
99 bucket->FindInt(kDisplayedKey).value_or(0));
101 if (click_count_delta) {
104 click_count_delta + bucket->FindInt(kEngagementKey).value_or(0));
107 // Set the website setting of this origin with the updated |engagement|.
108 settings_map_->SetWebsiteSettingDefaultScope(
109 url, GURL(), ContentSettingsType::NOTIFICATION_INTERACTIONS,
110 base::Value(std::move(engagement)));
114 std::string NotificationsEngagementService::GetBucketLabel(base::Time date) {
115 // For human-readability, return the UTC midnight on the same date as
117 base::Time local_date = date.LocalMidnight();
119 base::Time::Exploded local_date_exploded;
120 local_date.LocalExplode(&local_date_exploded);
121 // Intentionally converting a locally exploded time, to an UTC time, so that
122 // the Midnight in UTC is on the same date the date on local time.
123 base::Time last_date;
124 bool converted = base::Time::FromUTCExploded(local_date_exploded, &last_date);
127 return base::NumberToString(last_date.base::Time::ToTimeT());
129 return std::string();
133 absl::optional<base::Time>
134 NotificationsEngagementService::ParsePeriodBeginFromBucketLabel(
135 const std::string& label) {
136 int maybe_engagement_time;
137 base::Time local_period_begin;
139 // Store the time as local time.
140 if (base::StringToInt(label.c_str(), &maybe_engagement_time)) {
141 base::Time::Exploded date_exploded;
142 base::Time::FromTimeT(maybe_engagement_time).UTCExplode(&date_exploded);
143 if (base::Time::FromLocalExploded(date_exploded, &local_period_begin))
144 return local_period_begin;
147 return absl::nullopt;
150 } // namespace permissions