blob: 7bcce0fafc8be28d8ea70b114bf322d2e1ed45d7 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/metrics/feature_discovery_duration_reporter_impl.h"
#include "ash/public/cpp/feature_discovery_metric_util.h"
#include "ash/public/cpp/tablet_mode.h"
#include "ash/shell.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_functions.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
namespace ash {
namespace {
// Parameters used by the time duration metrics.
constexpr base::TimeDelta kTimeMetricsMin = base::Seconds(1);
constexpr base::TimeDelta kTimeMetricsMax = base::Days(7);
constexpr int kTimeMetricsBucketCount = 100;
// A dictionary that maps the features observed by
// `FeatureDiscoveryDurationReporter` to observation data (which is also a
// dictionary. See observation data dictionary keys for more details). A
// key-value mapping is added to the dictionary when the observation on a
// feature starts. The entries of the dictionary are never deleted after
// addition. It helps to avoid duplicate recordings on the same feature.
// NOTE: since it is a pref service key, do not change its value.
constexpr char kObservedFeatures[] = "FeatureDiscoveryReporterObservedFeatures";
// Observation data dictionary keys --------------------------------------------
// The key to the cumulated time duration since the onbservation starts. This
// key and its paired value get cleared when the observation finishes.
// NOTE: since it is a pref service key, do not change its value.
constexpr char kCumulatedDuration[] = "cumulative_duration";
// The key to the boolean value that indicates whether the observation finishes.
// NOTE: since it is a pref service key, do not change its value.
constexpr char kIsObservationFinished[] = "is_observation_finished";
// The key to the boolean value that is true if the observation starts in
// tablet. This key should only be used when the metrics data collected from a
// tracked feature should be split by tablet mode.
// NOTE: since it is a pref service key, do not change its value.
constexpr char kActivatedInTablet[] = "activated_in_tablet";
// Helper functions ------------------------------------------------------------
void ReportFeatureDiscoveryDuration(const char* histogram,
const base::TimeDelta& duration) {
base::UmaHistogramCustomTimes(histogram, duration, kTimeMetricsMin,
kTimeMetricsMax, kTimeMetricsBucketCount);
}
// Returns a trackable feature's info.
const feature_discovery::TrackableFeatureInfo& FindMappedFeatureInfo(
feature_discovery::TrackableFeature feature) {
auto* iter =
base::ranges::find(feature_discovery::kTrackableFeatureArray, feature,
&feature_discovery::TrackableFeatureInfo::feature);
DCHECK_NE(feature_discovery::kTrackableFeatureArray.cend(), iter);
return *iter;
}
// Returns a trackable feature's name.
const char* FindMappedName(feature_discovery::TrackableFeature feature) {
return FindMappedFeatureInfo(feature).name;
}
// Calculates the histogram for metric reporting. `feature` specifies a
// trackable feature. `in_tablet` is true if the observation on `feature` is
// activated in tablet.
// NOTE: if the metric reporting for `feature` is not separated by tablet mode,
// `in_tablet` is null.
const char* CalculateHistogram(feature_discovery::TrackableFeature feature,
absl::optional<bool> in_tablet) {
const feature_discovery::TrackableFeatureInfo& info =
FindMappedFeatureInfo(feature);
if (!info.split_by_tablet_mode)
return info.histogram;
DCHECK(in_tablet);
return *in_tablet ? info.histogram_tablet : info.histogram_clamshell;
}
} // namespace
FeatureDiscoveryDurationReporterImpl::FeatureDiscoveryDurationReporterImpl(
SessionController* session_controller) {
session_controller_observation_.Observe(session_controller);
}
FeatureDiscoveryDurationReporterImpl::~FeatureDiscoveryDurationReporterImpl() {
// Handle the case when a user signs out all accounts. Store the states of
// the ongoing observations through the pref service.
SetActive(false);
}
// static
void FeatureDiscoveryDurationReporterImpl::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(kObservedFeatures);
}
void FeatureDiscoveryDurationReporterImpl::MaybeActivateObservation(
feature_discovery::TrackableFeature feature) {
if (!is_active())
return;
const base::Value::Dict& observed_features =
active_pref_service_->GetDict(kObservedFeatures);
// If `feature` is already under observation, return early.
// TODO(https://crbug.com/1311344): implement the option that allows the
// observation start time gets reset by the subsequent observation
// activation callings.
const feature_discovery::TrackableFeatureInfo& info =
FindMappedFeatureInfo(feature);
const char* feature_name = info.name;
if (observed_features.Find(feature_name))
return;
// Initialize the pref data for the new observation.
base::Value::Dict observed_feature_data;
observed_feature_data.Set(kCumulatedDuration,
base::TimeDeltaToValue(base::TimeDelta()));
observed_feature_data.Set(kIsObservationFinished, false);
if (info.split_by_tablet_mode) {
// Record the current tablet mode if `feature`'s discovery duration data
// should be separated by tablet mode.
observed_feature_data.Set(kActivatedInTablet,
TabletMode::Get()->IsInTabletMode());
}
ScopedDictPrefUpdate update(active_pref_service_, kObservedFeatures);
update->Set(feature_name, std::move(observed_feature_data));
// Record observation start time.
DCHECK(active_time_recordings_.find(feature) ==
active_time_recordings_.cend());
active_time_recordings_.emplace(feature, base::TimeTicks::Now());
}
void FeatureDiscoveryDurationReporterImpl::MaybeFinishObservation(
feature_discovery::TrackableFeature feature) {
if (!is_active())
return;
// If the observation on the given metric has not started yet, return early.
auto iter = active_time_recordings_.find(feature);
if (iter == active_time_recordings_.end())
return;
const base::Value::Dict& observed_features =
active_pref_service_->GetDict(kObservedFeatures);
const char* const feature_name = FindMappedName(feature);
const base::Value::Dict* feature_pref_data =
observed_features.Find(feature_name)->GetIfDict();
DCHECK(feature_pref_data);
const absl::optional<base::TimeDelta> accumulated_duration =
base::ValueToTimeDelta(feature_pref_data->Find(kCumulatedDuration));
DCHECK(accumulated_duration);
bool skip_report = false;
// Get the boolean that indicates under which mode (clamshell or tablet) the
// observation is activated. If the metric data should not be separated, the
// value is null.
absl::optional<bool> activated_in_tablet;
if (FindMappedFeatureInfo(feature).split_by_tablet_mode) {
activated_in_tablet = feature_pref_data->FindBool(kActivatedInTablet);
DCHECK(activated_in_tablet);
// It is abnormal to miss `activated_in_tablet`. Handle this case for
// safety. Skip metric reporting if `activated_in_tablet` is missing when
// the metric data should be split by tablet mode. One reason leading to
// this case is that a feature switches from non-split to tablet-mode-split
// due to later code changes.
if (!activated_in_tablet) {
LOG(ERROR) << "Cannot find the tablet mode state under which the feature "
"observation starts for "
<< FindMappedName(feature);
skip_report = true;
}
}
// Report metric data if there is no errors.
if (!skip_report) {
ReportFeatureDiscoveryDuration(
CalculateHistogram(feature, activated_in_tablet),
*accumulated_duration + base::TimeTicks::Now() - iter->second);
}
// Update the observed feature pref data by:
// 1. Clearing the cumulated duration
// 2. Marking that the observation finishes
// 3. Erasing the saved tablet state if any
ScopedDictPrefUpdate update(active_pref_service_, kObservedFeatures);
base::Value::Dict* mutable_feature_pref_data = update->FindDict(feature_name);
mutable_feature_pref_data->Remove(kCumulatedDuration);
mutable_feature_pref_data->Set(kIsObservationFinished, true);
mutable_feature_pref_data->Remove(kActivatedInTablet);
active_time_recordings_.erase(iter);
}
void FeatureDiscoveryDurationReporterImpl::AddObserver(
ReporterObserver* observer) {
observers_.AddObserver(observer);
}
void FeatureDiscoveryDurationReporterImpl::RemoveObserver(
ReporterObserver* observer) {
observers_.RemoveObserver(observer);
}
void FeatureDiscoveryDurationReporterImpl::SetActive(bool active) {
// Return early if:
// 1. the activity state does not change; or
// 2. `active_pref_service_` is not set.
if (active == is_active() || !active_pref_service_)
return;
if (!active) {
Deactivate();
return;
}
Activate();
}
void FeatureDiscoveryDurationReporterImpl::Activate() {
// Disable the reporter for secondary accounts so that the feature discovery
// duration is only reported on primary accounts.
if (!Shell::Get()->session_controller()->IsUserPrimary())
return;
// Verify data members before activation.
DCHECK(active_time_recordings_.empty());
DCHECK(!is_active_);
DCHECK(active_pref_service_);
is_active_ = true;
const base::Value::Dict& observed_features =
active_pref_service_->GetDict(kObservedFeatures);
const base::Value::Dict& immutable_observed_features_dict = observed_features;
// Iterate trackable features and resume unfinished observations.
for (const auto& feature_info : feature_discovery::kTrackableFeatureArray) {
// Skip the features that are not under observation.
const base::Value* feature_data =
immutable_observed_features_dict.Find(feature_info.name);
if (!feature_data)
continue;
// Skip the finished observations.
absl::optional<bool> is_finished =
feature_data->GetDict().FindBool(kIsObservationFinished);
DCHECK(is_finished);
if (*is_finished)
continue;
active_time_recordings_.emplace(feature_info.feature,
base::TimeTicks::Now());
}
for (ReporterObserver& observer : observers_)
observer.OnReporterActivated();
}
void FeatureDiscoveryDurationReporterImpl::Deactivate() {
if (!active_time_recordings_.empty()) {
ScopedDictPrefUpdate update(active_pref_service_, kObservedFeatures);
base::Value::Dict& mutable_observed_features_dict = update.Get();
// Store the accumulated time duration as pref data.
for (const auto& name_timestamp_pair : active_time_recordings_) {
// Fetch cumulated duration from pref service.
const char* feature_name = FindMappedName(name_timestamp_pair.first);
base::Value* feature_data =
mutable_observed_features_dict.Find(feature_name);
DCHECK(feature_data);
base::Value::Dict& mutable_data_dict = feature_data->GetDict();
const base::Value* cumulated_duration_value =
mutable_data_dict.Find(kCumulatedDuration);
DCHECK(cumulated_duration_value);
absl::optional<base::TimeDelta> cumulated_duration =
base::ValueToTimeDelta(cumulated_duration_value);
DCHECK(cumulated_duration);
// Add the observation duration under the current active session. Then
// store the total duration.
mutable_data_dict.Set(
kCumulatedDuration,
base::TimeDeltaToValue(*cumulated_duration + base::TimeTicks::Now() -
name_timestamp_pair.second));
}
active_time_recordings_.clear();
}
is_active_ = false;
}
void FeatureDiscoveryDurationReporterImpl::OnSessionStateChanged(
session_manager::SessionState state) {
SetActive(state == session_manager::SessionState::ACTIVE);
}
void FeatureDiscoveryDurationReporterImpl::OnActiveUserPrefServiceChanged(
PrefService* pref_service) {
// Halt the observations for the old active account if any.
if (is_active())
SetActive(false);
active_pref_service_ = pref_service;
SetActive(Shell::Get()->session_controller()->GetSessionState() ==
session_manager::SessionState::ACTIVE);
}
} // namespace ash