1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/engagement/site_engagement_score.h"
6
7 #include <algorithm>
8 #include <cmath>
9 #include <utility>
10
11 #include "base/no_destructor.h"
12 #include "base/strings/string_number_conversions.h"
13 #include "base/time/clock.h"
14 #include "base/time/time.h"
15 #include "base/values.h"
16 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
17 #include "chrome/browser/engagement/site_engagement_metrics.h"
18 #include "components/content_settings/core/browser/host_content_settings_map.h"
19 #include "components/content_settings/core/common/content_settings.h"
20 #include "components/content_settings/core/common/content_settings_types.h"
21 #include "components/variations/variations_associated_data.h"
22
23 namespace {
24
25 // Delta within which to consider scores equal.
26 const double kScoreDelta = 0.001;
27
28 // Delta within which to consider internal time values equal. Internal time
29 // values are in microseconds, so this delta comes out at one second.
30 const double kTimeDelta = 1000000;
31
32 // Number of days after the last launch of an origin from an installed shortcut
33 // for which WEB_APP_INSTALLED_POINTS will be added to the engagement score.
34 const int kMaxDaysSinceShortcutLaunch = 10;
35
DoublesConsideredDifferent(double value1,double value2,double delta)36 bool DoublesConsideredDifferent(double value1, double value2, double delta) {
37 double abs_difference = fabs(value1 - value2);
38 return abs_difference > delta;
39 }
40
GetSiteEngagementScoreDictForSettings(const HostContentSettingsMap * settings,const GURL & origin_url)41 std::unique_ptr<base::DictionaryValue> GetSiteEngagementScoreDictForSettings(
42 const HostContentSettingsMap* settings,
43 const GURL& origin_url) {
44 if (!settings)
45 return std::make_unique<base::DictionaryValue>();
46
47 std::unique_ptr<base::DictionaryValue> value =
48 base::DictionaryValue::From(settings->GetWebsiteSetting(
49 origin_url, origin_url, ContentSettingsType::SITE_ENGAGEMENT, NULL));
50
51 if (value.get())
52 return value;
53
54 return std::make_unique<base::DictionaryValue>();
55 }
56
57 } // namespace
58
59 const double SiteEngagementScore::kMaxPoints = 100;
60
61 const char SiteEngagementScore::kRawScoreKey[] = "rawScore";
62 const char SiteEngagementScore::kPointsAddedTodayKey[] = "pointsAddedToday";
63 const char SiteEngagementScore::kLastEngagementTimeKey[] = "lastEngagementTime";
64 const char SiteEngagementScore::kLastShortcutLaunchTimeKey[] =
65 "lastShortcutLaunchTime";
66
67 // static
GetParamValues()68 SiteEngagementScore::ParamValues& SiteEngagementScore::GetParamValues() {
69 static base::NoDestructor<ParamValues> param_values([]() {
70 SiteEngagementScore::ParamValues param_values;
71 param_values[MAX_POINTS_PER_DAY] = {"max_points_per_day", 15};
72 param_values[DECAY_PERIOD_IN_HOURS] = {"decay_period_in_hours", 2};
73 param_values[DECAY_POINTS] = {"decay_points", 0};
74 param_values[DECAY_PROPORTION] = {"decay_proportion", 0.984};
75 param_values[SCORE_CLEANUP_THRESHOLD] = {"score_cleanup_threshold", 0.5};
76 param_values[NAVIGATION_POINTS] = {"navigation_points", 1.5};
77 param_values[USER_INPUT_POINTS] = {"user_input_points", 0.6};
78 param_values[VISIBLE_MEDIA_POINTS] = {"visible_media_playing_points", 0.06};
79 param_values[HIDDEN_MEDIA_POINTS] = {"hidden_media_playing_points", 0.01};
80 param_values[WEB_APP_INSTALLED_POINTS] = {"web_app_installed_points", 5};
81 param_values[FIRST_DAILY_ENGAGEMENT] = {"first_daily_engagement_points",
82 1.5};
83 param_values[BOOTSTRAP_POINTS] = {"bootstrap_points", 24};
84 param_values[MEDIUM_ENGAGEMENT_BOUNDARY] = {"medium_engagement_boundary",
85 15};
86 param_values[HIGH_ENGAGEMENT_BOUNDARY] = {"high_engagement_boundary", 50};
87 param_values[MAX_DECAYS_PER_SCORE] = {"max_decays_per_score", 4};
88 param_values[LAST_ENGAGEMENT_GRACE_PERIOD_IN_HOURS] = {
89 "last_engagement_grace_period_in_hours", 1};
90 param_values[NOTIFICATION_INTERACTION_POINTS] = {
91 "notification_interaction_points", 1};
92 return param_values;
93 }());
94 return *param_values;
95 }
96
GetMaxPointsPerDay()97 double SiteEngagementScore::GetMaxPointsPerDay() {
98 return GetParamValues()[MAX_POINTS_PER_DAY].second;
99 }
100
GetDecayPeriodInHours()101 double SiteEngagementScore::GetDecayPeriodInHours() {
102 return GetParamValues()[DECAY_PERIOD_IN_HOURS].second;
103 }
104
GetDecayPoints()105 double SiteEngagementScore::GetDecayPoints() {
106 return GetParamValues()[DECAY_POINTS].second;
107 }
108
GetDecayProportion()109 double SiteEngagementScore::GetDecayProportion() {
110 return GetParamValues()[DECAY_PROPORTION].second;
111 }
112
GetScoreCleanupThreshold()113 double SiteEngagementScore::GetScoreCleanupThreshold() {
114 return GetParamValues()[SCORE_CLEANUP_THRESHOLD].second;
115 }
116
GetNavigationPoints()117 double SiteEngagementScore::GetNavigationPoints() {
118 return GetParamValues()[NAVIGATION_POINTS].second;
119 }
120
GetUserInputPoints()121 double SiteEngagementScore::GetUserInputPoints() {
122 return GetParamValues()[USER_INPUT_POINTS].second;
123 }
124
GetVisibleMediaPoints()125 double SiteEngagementScore::GetVisibleMediaPoints() {
126 return GetParamValues()[VISIBLE_MEDIA_POINTS].second;
127 }
128
GetHiddenMediaPoints()129 double SiteEngagementScore::GetHiddenMediaPoints() {
130 return GetParamValues()[HIDDEN_MEDIA_POINTS].second;
131 }
132
GetWebAppInstalledPoints()133 double SiteEngagementScore::GetWebAppInstalledPoints() {
134 return GetParamValues()[WEB_APP_INSTALLED_POINTS].second;
135 }
136
GetFirstDailyEngagementPoints()137 double SiteEngagementScore::GetFirstDailyEngagementPoints() {
138 return GetParamValues()[FIRST_DAILY_ENGAGEMENT].second;
139 }
140
GetBootstrapPoints()141 double SiteEngagementScore::GetBootstrapPoints() {
142 return GetParamValues()[BOOTSTRAP_POINTS].second;
143 }
144
GetMediumEngagementBoundary()145 double SiteEngagementScore::GetMediumEngagementBoundary() {
146 return GetParamValues()[MEDIUM_ENGAGEMENT_BOUNDARY].second;
147 }
148
GetHighEngagementBoundary()149 double SiteEngagementScore::GetHighEngagementBoundary() {
150 return GetParamValues()[HIGH_ENGAGEMENT_BOUNDARY].second;
151 }
152
GetMaxDecaysPerScore()153 double SiteEngagementScore::GetMaxDecaysPerScore() {
154 return GetParamValues()[MAX_DECAYS_PER_SCORE].second;
155 }
156
GetLastEngagementGracePeriodInHours()157 double SiteEngagementScore::GetLastEngagementGracePeriodInHours() {
158 return GetParamValues()[LAST_ENGAGEMENT_GRACE_PERIOD_IN_HOURS].second;
159 }
160
GetNotificationInteractionPoints()161 double SiteEngagementScore::GetNotificationInteractionPoints() {
162 return GetParamValues()[NOTIFICATION_INTERACTION_POINTS].second;
163 }
164
SetParamValuesForTesting()165 void SiteEngagementScore::SetParamValuesForTesting() {
166 GetParamValues()[MAX_POINTS_PER_DAY].second = 5;
167 GetParamValues()[DECAY_PERIOD_IN_HOURS].second = 7 * 24;
168 GetParamValues()[DECAY_POINTS].second = 5;
169 GetParamValues()[NAVIGATION_POINTS].second = 0.5;
170 GetParamValues()[USER_INPUT_POINTS].second = 0.05;
171 GetParamValues()[VISIBLE_MEDIA_POINTS].second = 0.02;
172 GetParamValues()[HIDDEN_MEDIA_POINTS].second = 0.01;
173 GetParamValues()[WEB_APP_INSTALLED_POINTS].second = 5;
174 GetParamValues()[BOOTSTRAP_POINTS].second = 8;
175 GetParamValues()[MEDIUM_ENGAGEMENT_BOUNDARY].second = 5;
176 GetParamValues()[HIGH_ENGAGEMENT_BOUNDARY].second = 50;
177 GetParamValues()[MAX_DECAYS_PER_SCORE].second = 1;
178 GetParamValues()[LAST_ENGAGEMENT_GRACE_PERIOD_IN_HOURS].second = 72;
179 GetParamValues()[NOTIFICATION_INTERACTION_POINTS].second = 1;
180
181 // This is set to values that avoid interference with tests and are set when
182 // testing these features.
183 GetParamValues()[FIRST_DAILY_ENGAGEMENT].second = 0;
184 GetParamValues()[DECAY_PROPORTION].second = 1;
185 GetParamValues()[SCORE_CLEANUP_THRESHOLD].second = 0;
186 }
187 // static
UpdateFromVariations(const char * param_name)188 void SiteEngagementScore::UpdateFromVariations(const char* param_name) {
189 double param_vals[MAX_VARIATION];
190
191 for (int i = 0; i < MAX_VARIATION; ++i) {
192 std::string param_string = variations::GetVariationParamValue(
193 param_name, GetParamValues()[i].first);
194
195 // Bail out if we didn't get a param string for the key, or if we couldn't
196 // convert the param string to a double, or if we get a negative value.
197 if (param_string.empty() ||
198 !base::StringToDouble(param_string, ¶m_vals[i]) ||
199 param_vals[i] < 0) {
200 return;
201 }
202 }
203
204 // Once we're sure everything is valid, assign the variation to the param
205 // values array.
206 for (int i = 0; i < MAX_VARIATION; ++i)
207 SiteEngagementScore::GetParamValues()[i].second = param_vals[i];
208 }
209
SiteEngagementScore(base::Clock * clock,const GURL & origin,HostContentSettingsMap * settings)210 SiteEngagementScore::SiteEngagementScore(base::Clock* clock,
211 const GURL& origin,
212 HostContentSettingsMap* settings)
213 : SiteEngagementScore(
214 clock,
215 origin,
216 GetSiteEngagementScoreDictForSettings(settings, origin)) {
217 settings_map_ = settings;
218 }
219
220 SiteEngagementScore::SiteEngagementScore(SiteEngagementScore&& other) = default;
221
~SiteEngagementScore()222 SiteEngagementScore::~SiteEngagementScore() {}
223
224 SiteEngagementScore& SiteEngagementScore::operator=(
225 SiteEngagementScore&& other) = default;
226
AddPoints(double points)227 void SiteEngagementScore::AddPoints(double points) {
228 DCHECK_NE(0, points);
229
230 // As the score is about to be updated, commit any decay that has happened
231 // since the last update.
232 raw_score_ = DecayedScore();
233
234 base::Time now = clock_->Now();
235 if (!last_engagement_time_.is_null() &&
236 now.LocalMidnight() != last_engagement_time_.LocalMidnight()) {
237 points_added_today_ = 0;
238 }
239
240 if (points_added_today_ == 0) {
241 // Award bonus engagement for the first engagement of the day for a site.
242 points += GetFirstDailyEngagementPoints();
243 SiteEngagementMetrics::RecordEngagement(
244 SiteEngagementService::ENGAGEMENT_FIRST_DAILY_ENGAGEMENT);
245 }
246
247 double to_add = std::min(kMaxPoints - raw_score_,
248 GetMaxPointsPerDay() - points_added_today_);
249 to_add = std::min(to_add, points);
250
251 points_added_today_ += to_add;
252 raw_score_ += to_add;
253
254 last_engagement_time_ = now;
255 }
256
GetTotalScore() const257 double SiteEngagementScore::GetTotalScore() const {
258 return std::min(DecayedScore() + BonusIfShortcutLaunched(), kMaxPoints);
259 }
260
GetDetails() const261 mojom::SiteEngagementDetails SiteEngagementScore::GetDetails() const {
262 mojom::SiteEngagementDetails engagement;
263 engagement.origin = origin_;
264 engagement.base_score = DecayedScore();
265 engagement.installed_bonus = BonusIfShortcutLaunched();
266 engagement.total_score = GetTotalScore();
267 return engagement;
268 }
269
Commit()270 void SiteEngagementScore::Commit() {
271 DCHECK(settings_map_);
272 if (!UpdateScoreDict(score_dict_.get()))
273 return;
274
275 settings_map_->SetWebsiteSettingDefaultScope(
276 origin_, GURL(), ContentSettingsType::SITE_ENGAGEMENT,
277 std::move(score_dict_));
278 }
279
GetEngagementLevel() const280 blink::mojom::EngagementLevel SiteEngagementScore::GetEngagementLevel() const {
281 DCHECK_LT(GetMediumEngagementBoundary(), GetHighEngagementBoundary());
282
283 double score = GetTotalScore();
284 if (score == 0)
285 return blink::mojom::EngagementLevel::NONE;
286
287 if (score < 1)
288 return blink::mojom::EngagementLevel::MINIMAL;
289
290 if (score < GetMediumEngagementBoundary())
291 return blink::mojom::EngagementLevel::LOW;
292
293 if (score < GetHighEngagementBoundary())
294 return blink::mojom::EngagementLevel::MEDIUM;
295
296 if (score < SiteEngagementScore::kMaxPoints)
297 return blink::mojom::EngagementLevel::HIGH;
298
299 return blink::mojom::EngagementLevel::MAX;
300 }
301
MaxPointsPerDayAdded() const302 bool SiteEngagementScore::MaxPointsPerDayAdded() const {
303 if (!last_engagement_time_.is_null() &&
304 clock_->Now().LocalMidnight() != last_engagement_time_.LocalMidnight()) {
305 return false;
306 }
307
308 return points_added_today_ == GetMaxPointsPerDay();
309 }
310
Reset(double points,const base::Time last_engagement_time)311 void SiteEngagementScore::Reset(double points,
312 const base::Time last_engagement_time) {
313 raw_score_ = points;
314 points_added_today_ = 0;
315
316 // This must be set in order to prevent the score from decaying when read.
317 last_engagement_time_ = last_engagement_time;
318 }
319
UpdateScoreDict(base::DictionaryValue * score_dict)320 bool SiteEngagementScore::UpdateScoreDict(base::DictionaryValue* score_dict) {
321 double raw_score_orig = 0;
322 double points_added_today_orig = 0;
323 double last_engagement_time_internal_orig = 0;
324 double last_shortcut_launch_time_internal_orig = 0;
325
326 score_dict->GetDouble(kRawScoreKey, &raw_score_orig);
327 score_dict->GetDouble(kPointsAddedTodayKey, &points_added_today_orig);
328 score_dict->GetDouble(kLastEngagementTimeKey,
329 &last_engagement_time_internal_orig);
330 score_dict->GetDouble(kLastShortcutLaunchTimeKey,
331 &last_shortcut_launch_time_internal_orig);
332 bool changed =
333 DoublesConsideredDifferent(raw_score_orig, raw_score_, kScoreDelta) ||
334 DoublesConsideredDifferent(points_added_today_orig, points_added_today_,
335 kScoreDelta) ||
336 DoublesConsideredDifferent(last_engagement_time_internal_orig,
337 last_engagement_time_.ToInternalValue(),
338 kTimeDelta) ||
339 DoublesConsideredDifferent(last_shortcut_launch_time_internal_orig,
340 last_shortcut_launch_time_.ToInternalValue(),
341 kTimeDelta);
342
343 if (!changed)
344 return false;
345
346 score_dict->SetDouble(kRawScoreKey, raw_score_);
347 score_dict->SetDouble(kPointsAddedTodayKey, points_added_today_);
348 score_dict->SetDouble(kLastEngagementTimeKey,
349 last_engagement_time_.ToInternalValue());
350 score_dict->SetDouble(kLastShortcutLaunchTimeKey,
351 last_shortcut_launch_time_.ToInternalValue());
352
353 return true;
354 }
355
SiteEngagementScore(base::Clock * clock,const GURL & origin,std::unique_ptr<base::DictionaryValue> score_dict)356 SiteEngagementScore::SiteEngagementScore(
357 base::Clock* clock,
358 const GURL& origin,
359 std::unique_ptr<base::DictionaryValue> score_dict)
360 : clock_(clock),
361 raw_score_(0),
362 points_added_today_(0),
363 last_engagement_time_(),
364 last_shortcut_launch_time_(),
365 score_dict_(score_dict.release()),
366 origin_(origin),
367 settings_map_(nullptr) {
368 if (!score_dict_)
369 return;
370
371 score_dict_->GetDouble(kRawScoreKey, &raw_score_);
372 score_dict_->GetDouble(kPointsAddedTodayKey, &points_added_today_);
373
374 double internal_time;
375 if (score_dict_->GetDouble(kLastEngagementTimeKey, &internal_time))
376 last_engagement_time_ = base::Time::FromInternalValue(internal_time);
377 if (score_dict_->GetDouble(kLastShortcutLaunchTimeKey, &internal_time))
378 last_shortcut_launch_time_ = base::Time::FromInternalValue(internal_time);
379 }
380
DecayedScore() const381 double SiteEngagementScore::DecayedScore() const {
382 // Note that users can change their clock, so from this system's perspective
383 // time can go backwards. If that does happen and the system detects that the
384 // current day is earlier than the last engagement, no decay (or growth) is
385 // applied.
386 int hours_since_engagement =
387 (clock_->Now() - last_engagement_time_).InHours();
388 if (hours_since_engagement < 0)
389 return raw_score_;
390
391 int periods = hours_since_engagement / GetDecayPeriodInHours();
392 return std::max(0.0, raw_score_ * pow(GetDecayProportion(), periods) -
393 periods * GetDecayPoints());
394 }
395
BonusIfShortcutLaunched() const396 double SiteEngagementScore::BonusIfShortcutLaunched() const {
397 int days_since_shortcut_launch =
398 (clock_->Now() - last_shortcut_launch_time_).InDays();
399 if (days_since_shortcut_launch <= kMaxDaysSinceShortcutLaunch)
400 return GetWebAppInstalledPoints();
401 return 0;
402 }
403