// Copyright 2022 The Chromium Authors. All rights reserved. // 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 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_->GetValueDict(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()); } DictionaryPrefUpdate update(active_pref_service_, kObservedFeatures); update->GetDict().Set(feature_name, base::Value(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_->GetValueDict(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 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 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 DictionaryPrefUpdate update(active_pref_service_, kObservedFeatures); base::Value::Dict* mutable_feature_pref_data = update->GetDict().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_->GetValueDict(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 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()) { DictionaryPrefUpdate update(active_pref_service_, kObservedFeatures); base::Value* observed_features = update.Get(); DCHECK(observed_features); base::Value::Dict& mutable_observed_features_dict = observed_features->GetDict(); // 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 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