blob: 5b5dca3d7b22ff7be76f30d84a4436251e263f53 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/user_education/recent_session_policy.h"
#include <optional>
#include "base/dcheck_is_on.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chrome/browser/user_education/browser_user_education_storage_service.h"
#include "chrome/browser/user_education/user_education_service.h"
namespace {
constexpr int kMaxRecords = RecentSessionTracker::kMaxRecentSessionRecords;
// Gets midnight at the start of the next local day.
// Note: inaccurate if the time is *exactly* midnight, but this will happen so
// rarely that it's not worth worrying about.
base::Time GetEndOfDay(base::Time time) {
return (time + base::Days(1)).LocalMidnight();
}
// Counts the number of active days in `recent_sessions` going back `num_days`
// from `last_day`. Returns null if session data recording does not go back far
// enough to cover the whole span.
std::optional<int> CountActiveDays(const RecentSessionData& recent_sessions,
base::Time last_day,
int num_days) {
const base::Time end = GetEndOfDay(last_day);
const base::Time start = end - base::Days(num_days);
if (recent_sessions.enabled_time > start) {
return std::nullopt;
}
std::vector<bool> active_days(num_days, false);
for (const auto& start_time : recent_sessions.recent_session_start_times) {
if (start_time >= start && start_time < end) {
const size_t index = (start_time - start) / base::Days(1);
active_days[index] = true;
}
}
return std::count(active_days.begin(), active_days.end(), true);
}
std::optional<int> ValueOrNull(int value) {
return value ? std::make_optional(value) : std::nullopt;
}
} // namespace
bool RecentSessionPolicyImpl::Constraint::ShouldSkipRecording(
const RecentSessionData& recent_sessions) const {
return false;
}
bool RecentSessionPolicyImpl::DailyConstraint::ShouldSkipRecording(
const RecentSessionData& recent_sessions) const {
// Do not record if there are at least two recent sessions and the most recent
// session is on the same calendar day as the second-most-recent session; the
// session would have already been recorded on this day.
//
// It is critical that calendar day is used rather than just a 24-hour period,
// since if the test were simply less than 24 hours, there could be a sequence
// of, say, 16-hour separations between sessions and only the first one would
// be recorded, no matter how long the sequence lasted.
return recent_sessions.recent_session_start_times.size() > 1U &&
GetEndOfDay(recent_sessions.recent_session_start_times[0]) ==
GetEndOfDay(recent_sessions.recent_session_start_times[1]);
}
std::optional<int> RecentSessionPolicyImpl::SessionCountConstraint::GetCount(
const RecentSessionData& recent_sessions) const {
const base::Time start =
recent_sessions.recent_session_start_times.front() - base::Days(days_);
if (recent_sessions.enabled_time > start) {
return std::nullopt;
}
int count = 0;
for (const auto& start_time : recent_sessions.recent_session_start_times) {
if (start_time >= start) {
++count;
}
}
return count;
}
std::optional<int> RecentSessionPolicyImpl::ActiveDaysConstraint::GetCount(
const RecentSessionData& recent_sessions) const {
return CountActiveDays(recent_sessions,
recent_sessions.recent_session_start_times.front(),
days_);
}
std::optional<int> RecentSessionPolicyImpl::ActiveWeeksConstraint::GetCount(
const RecentSessionData& recent_sessions) const {
int count = 0;
base::Time counting_back_from =
recent_sessions.recent_session_start_times.front();
for (int week = 0; week < weeks_; ++week) {
const auto active_days =
CountActiveDays(recent_sessions, counting_back_from, 7);
if (!active_days) {
return active_days;
} else if (*active_days >= active_days_) {
++count;
}
counting_back_from -= base::Days(7);
}
return count;
}
RecentSessionPolicyImpl::ConstraintInfo::ConstraintInfo() = default;
RecentSessionPolicyImpl::ConstraintInfo::ConstraintInfo(
std::unique_ptr<Constraint> constraint_,
std::string histogram_name_,
std::optional<int> histogram_max_,
std::optional<int> low_usage_max_)
: constraint(std::move(constraint_)),
histogram_name(std::move(histogram_name_)),
histogram_max(histogram_max_),
low_usage_max(low_usage_max_) {}
RecentSessionPolicyImpl::ConstraintInfo::ConstraintInfo(
ConstraintInfo&&) noexcept = default;
RecentSessionPolicyImpl::ConstraintInfo&
RecentSessionPolicyImpl::ConstraintInfo::operator=(ConstraintInfo&&) noexcept =
default;
RecentSessionPolicyImpl::ConstraintInfo::~ConstraintInfo() = default;
RecentSessionPolicyImpl::RecentSessionPolicyImpl(ConstraintInfos constraints)
: constraints_(std::move(constraints)) {
CHECK(!constraints_.empty());
for (const auto& constraint : constraints_) {
CHECK(constraint.constraint);
}
}
RecentSessionPolicyImpl::~RecentSessionPolicyImpl() = default;
void RecentSessionPolicyImpl::RecordRecentUsageMetrics(
const RecentSessionData& recent_sessions) {
for (const auto& constraint : constraints_) {
if (!constraint.histogram_name.empty() &&
!constraint.constraint->ShouldSkipRecording(recent_sessions)) {
if (const auto result =
constraint.constraint->GetCount(recent_sessions)) {
base::UmaHistogramExactLinear(
constraint.histogram_name.c_str(), *result,
constraint.histogram_max.value_or(kMaxRecords));
}
}
}
}
bool RecentSessionPolicyImpl::ShouldEnableLowUsagePromoMode(
const RecentSessionData& recent_sessions) const {
for (const auto& constraint : constraints_) {
if (const auto limit = constraint.low_usage_max) {
const auto result = constraint.constraint->GetCount(recent_sessions);
if (!result || *result > *limit) {
return false;
}
}
}
return true;
}
// static
RecentSessionPolicyImpl::ConstraintInfos
RecentSessionPolicyImpl::GetDefaultConstraints() {
static constexpr int kShortTermDays = 7;
static constexpr int kLongTermWeeks = 4;
static constexpr int kLongTermDays = kLongTermWeeks * 7;
const int max_active_weeks = base::GetFieldTrialParamByFeatureAsInt(
kAllowRecentSessionTracking, "max_active_weeks", 0);
const int max_active_days = base::GetFieldTrialParamByFeatureAsInt(
kAllowRecentSessionTracking, "max_active_days", 0);
const int super_active_days = base::GetFieldTrialParamByFeatureAsInt(
kAllowRecentSessionTracking, "super_active_days", 4);
const int max_monthly_active_days = base::GetFieldTrialParamByFeatureAsInt(
kAllowRecentSessionTracking, "max_monthly_active_days", 2);
const int max_super_active_weeks = base::GetFieldTrialParamByFeatureAsInt(
kAllowRecentSessionTracking, "max_super_active_weeks", 0);
const int max_weekly_sessions = base::GetFieldTrialParamByFeatureAsInt(
kAllowRecentSessionTracking, "max_weekly_sessions", 0);
const int max_monthly_sessions = base::GetFieldTrialParamByFeatureAsInt(
kAllowRecentSessionTracking, "max_monthly_sessions", 0);
ConstraintInfos result;
result.emplace_back(std::make_unique<ActiveDaysConstraint>(kShortTermDays),
"UserEducation.Session.RecentActiveDays", kShortTermDays,
ValueOrNull(max_active_days));
result.emplace_back(std::make_unique<ActiveDaysConstraint>(kLongTermDays),
"UserEducation.Session.MonthlyActiveDays", kLongTermDays,
ValueOrNull(max_monthly_active_days));
result.emplace_back(
std::make_unique<ActiveWeeksConstraint>(kLongTermWeeks, 1),
"UserEducation.Session.RecentActiveWeeks", kLongTermWeeks,
ValueOrNull(max_active_weeks));
result.emplace_back(std::make_unique<ActiveWeeksConstraint>(
kLongTermWeeks, super_active_days),
"UserEducation.Session.RecentSuperActiveWeeks",
kLongTermWeeks, ValueOrNull(max_super_active_weeks));
result.emplace_back(std::make_unique<SessionCountConstraint>(kShortTermDays),
"UserEducation.Session.ShortTermCount",
kShortTermDays + 1, ValueOrNull(max_weekly_sessions));
result.emplace_back(std::make_unique<SessionCountConstraint>(kLongTermDays),
"UserEducation.Session.LongTermCount", kMaxRecords,
ValueOrNull(max_monthly_sessions));
return result;
}