feature_discovery_duration_reporter_impl.cc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. // Copyright 2022 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. #include "ash/metrics/feature_discovery_duration_reporter_impl.h"
  5. #include "ash/public/cpp/feature_discovery_metric_util.h"
  6. #include "ash/public/cpp/tablet_mode.h"
  7. #include "ash/shell.h"
  8. #include "base/json/values_util.h"
  9. #include "base/metrics/histogram_functions.h"
  10. #include "components/prefs/pref_registry_simple.h"
  11. #include "components/prefs/pref_service.h"
  12. #include "components/prefs/scoped_user_pref_update.h"
  13. namespace ash {
  14. namespace {
  15. // Parameters used by the time duration metrics.
  16. constexpr base::TimeDelta kTimeMetricsMin = base::Seconds(1);
  17. constexpr base::TimeDelta kTimeMetricsMax = base::Days(7);
  18. constexpr int kTimeMetricsBucketCount = 100;
  19. // A dictionary that maps the features observed by
  20. // `FeatureDiscoveryDurationReporter` to observation data (which is also a
  21. // dictionary. See observation data dictionary keys for more details). A
  22. // key-value mapping is added to the dictionary when the observation on a
  23. // feature starts. The entries of the dictionary are never deleted after
  24. // addition. It helps to avoid duplicate recordings on the same feature.
  25. // NOTE: since it is a pref service key, do not change its value.
  26. constexpr char kObservedFeatures[] = "FeatureDiscoveryReporterObservedFeatures";
  27. // Observation data dictionary keys --------------------------------------------
  28. // The key to the cumulated time duration since the onbservation starts. This
  29. // key and its paired value get cleared when the observation finishes.
  30. // NOTE: since it is a pref service key, do not change its value.
  31. constexpr char kCumulatedDuration[] = "cumulative_duration";
  32. // The key to the boolean value that indicates whether the observation finishes.
  33. // NOTE: since it is a pref service key, do not change its value.
  34. constexpr char kIsObservationFinished[] = "is_observation_finished";
  35. // The key to the boolean value that is true if the observation starts in
  36. // tablet. This key should only be used when the metrics data collected from a
  37. // tracked feature should be split by tablet mode.
  38. // NOTE: since it is a pref service key, do not change its value.
  39. constexpr char kActivatedInTablet[] = "activated_in_tablet";
  40. // Helper functions ------------------------------------------------------------
  41. void ReportFeatureDiscoveryDuration(const char* histogram,
  42. const base::TimeDelta& duration) {
  43. base::UmaHistogramCustomTimes(histogram, duration, kTimeMetricsMin,
  44. kTimeMetricsMax, kTimeMetricsBucketCount);
  45. }
  46. // Returns a trackable feature's info.
  47. const feature_discovery::TrackableFeatureInfo& FindMappedFeatureInfo(
  48. feature_discovery::TrackableFeature feature) {
  49. auto* iter =
  50. base::ranges::find(feature_discovery::kTrackableFeatureArray, feature,
  51. &feature_discovery::TrackableFeatureInfo::feature);
  52. DCHECK_NE(feature_discovery::kTrackableFeatureArray.cend(), iter);
  53. return *iter;
  54. }
  55. // Returns a trackable feature's name.
  56. const char* FindMappedName(feature_discovery::TrackableFeature feature) {
  57. return FindMappedFeatureInfo(feature).name;
  58. }
  59. // Calculates the histogram for metric reporting. `feature` specifies a
  60. // trackable feature. `in_tablet` is true if the observation on `feature` is
  61. // activated in tablet.
  62. // NOTE: if the metric reporting for `feature` is not separated by tablet mode,
  63. // `in_tablet` is null.
  64. const char* CalculateHistogram(feature_discovery::TrackableFeature feature,
  65. absl::optional<bool> in_tablet) {
  66. const feature_discovery::TrackableFeatureInfo& info =
  67. FindMappedFeatureInfo(feature);
  68. if (!info.split_by_tablet_mode)
  69. return info.histogram;
  70. DCHECK(in_tablet);
  71. return *in_tablet ? info.histogram_tablet : info.histogram_clamshell;
  72. }
  73. } // namespace
  74. FeatureDiscoveryDurationReporterImpl::FeatureDiscoveryDurationReporterImpl(
  75. SessionController* session_controller) {
  76. session_controller_observation_.Observe(session_controller);
  77. }
  78. FeatureDiscoveryDurationReporterImpl::~FeatureDiscoveryDurationReporterImpl() {
  79. // Handle the case when a user signs out all accounts. Store the states of
  80. // the ongoing observations through the pref service.
  81. SetActive(false);
  82. }
  83. // static
  84. void FeatureDiscoveryDurationReporterImpl::RegisterProfilePrefs(
  85. PrefRegistrySimple* registry) {
  86. registry->RegisterDictionaryPref(kObservedFeatures);
  87. }
  88. void FeatureDiscoveryDurationReporterImpl::MaybeActivateObservation(
  89. feature_discovery::TrackableFeature feature) {
  90. if (!is_active())
  91. return;
  92. const base::Value::Dict& observed_features =
  93. active_pref_service_->GetValueDict(kObservedFeatures);
  94. // If `feature` is already under observation, return early.
  95. // TODO(https://crbug.com/1311344): implement the option that allows the
  96. // observation start time gets reset by the subsequent observation
  97. // activation callings.
  98. const feature_discovery::TrackableFeatureInfo& info =
  99. FindMappedFeatureInfo(feature);
  100. const char* feature_name = info.name;
  101. if (observed_features.Find(feature_name))
  102. return;
  103. // Initialize the pref data for the new observation.
  104. base::Value::Dict observed_feature_data;
  105. observed_feature_data.Set(kCumulatedDuration,
  106. base::TimeDeltaToValue(base::TimeDelta()));
  107. observed_feature_data.Set(kIsObservationFinished, false);
  108. if (info.split_by_tablet_mode) {
  109. // Record the current tablet mode if `feature`'s discovery duration data
  110. // should be separated by tablet mode.
  111. observed_feature_data.Set(kActivatedInTablet,
  112. TabletMode::Get()->IsInTabletMode());
  113. }
  114. DictionaryPrefUpdate update(active_pref_service_, kObservedFeatures);
  115. update->GetDict().Set(feature_name,
  116. base::Value(std::move(observed_feature_data)));
  117. // Record observation start time.
  118. DCHECK(active_time_recordings_.find(feature) ==
  119. active_time_recordings_.cend());
  120. active_time_recordings_.emplace(feature, base::TimeTicks::Now());
  121. }
  122. void FeatureDiscoveryDurationReporterImpl::MaybeFinishObservation(
  123. feature_discovery::TrackableFeature feature) {
  124. if (!is_active())
  125. return;
  126. // If the observation on the given metric has not started yet, return early.
  127. auto iter = active_time_recordings_.find(feature);
  128. if (iter == active_time_recordings_.end())
  129. return;
  130. const base::Value::Dict& observed_features =
  131. active_pref_service_->GetValueDict(kObservedFeatures);
  132. const char* const feature_name = FindMappedName(feature);
  133. const base::Value::Dict* feature_pref_data =
  134. observed_features.Find(feature_name)->GetIfDict();
  135. DCHECK(feature_pref_data);
  136. const absl::optional<base::TimeDelta> accumulated_duration =
  137. base::ValueToTimeDelta(feature_pref_data->Find(kCumulatedDuration));
  138. DCHECK(accumulated_duration);
  139. bool skip_report = false;
  140. // Get the boolean that indicates under which mode (clamshell or tablet) the
  141. // observation is activated. If the metric data should not be separated, the
  142. // value is null.
  143. absl::optional<bool> activated_in_tablet;
  144. if (FindMappedFeatureInfo(feature).split_by_tablet_mode) {
  145. activated_in_tablet = feature_pref_data->FindBool(kActivatedInTablet);
  146. DCHECK(activated_in_tablet);
  147. // It is abnormal to miss `activated_in_tablet`. Handle this case for
  148. // safety. Skip metric reporting if `activated_in_tablet` is missing when
  149. // the metric data should be split by tablet mode. One reason leading to
  150. // this case is that a feature switches from non-split to tablet-mode-split
  151. // due to later code changes.
  152. if (!activated_in_tablet) {
  153. LOG(ERROR) << "Cannot find the tablet mode state under which the feature "
  154. "observation starts for "
  155. << FindMappedName(feature);
  156. skip_report = true;
  157. }
  158. }
  159. // Report metric data if there is no errors.
  160. if (!skip_report) {
  161. ReportFeatureDiscoveryDuration(
  162. CalculateHistogram(feature, activated_in_tablet),
  163. *accumulated_duration + base::TimeTicks::Now() - iter->second);
  164. }
  165. // Update the observed feature pref data by:
  166. // 1. Clearing the cumulated duration
  167. // 2. Marking that the observation finishes
  168. // 3. Erasing the saved tablet state if any
  169. DictionaryPrefUpdate update(active_pref_service_, kObservedFeatures);
  170. base::Value::Dict* mutable_feature_pref_data =
  171. update->GetDict().FindDict(feature_name);
  172. mutable_feature_pref_data->Remove(kCumulatedDuration);
  173. mutable_feature_pref_data->Set(kIsObservationFinished, true);
  174. mutable_feature_pref_data->Remove(kActivatedInTablet);
  175. active_time_recordings_.erase(iter);
  176. }
  177. void FeatureDiscoveryDurationReporterImpl::AddObserver(
  178. ReporterObserver* observer) {
  179. observers_.AddObserver(observer);
  180. }
  181. void FeatureDiscoveryDurationReporterImpl::RemoveObserver(
  182. ReporterObserver* observer) {
  183. observers_.RemoveObserver(observer);
  184. }
  185. void FeatureDiscoveryDurationReporterImpl::SetActive(bool active) {
  186. // Return early if:
  187. // 1. the activity state does not change; or
  188. // 2. `active_pref_service_` is not set.
  189. if (active == is_active() || !active_pref_service_)
  190. return;
  191. if (!active) {
  192. Deactivate();
  193. return;
  194. }
  195. Activate();
  196. }
  197. void FeatureDiscoveryDurationReporterImpl::Activate() {
  198. // Disable the reporter for secondary accounts so that the feature discovery
  199. // duration is only reported on primary accounts.
  200. if (!Shell::Get()->session_controller()->IsUserPrimary())
  201. return;
  202. // Verify data members before activation.
  203. DCHECK(active_time_recordings_.empty());
  204. DCHECK(!is_active_);
  205. DCHECK(active_pref_service_);
  206. is_active_ = true;
  207. const base::Value::Dict& observed_features =
  208. active_pref_service_->GetValueDict(kObservedFeatures);
  209. const base::Value::Dict& immutable_observed_features_dict = observed_features;
  210. // Iterate trackable features and resume unfinished observations.
  211. for (const auto& feature_info : feature_discovery::kTrackableFeatureArray) {
  212. // Skip the features that are not under observation.
  213. const base::Value* feature_data =
  214. immutable_observed_features_dict.Find(feature_info.name);
  215. if (!feature_data)
  216. continue;
  217. // Skip the finished observations.
  218. absl::optional<bool> is_finished =
  219. feature_data->GetDict().FindBool(kIsObservationFinished);
  220. DCHECK(is_finished);
  221. if (*is_finished)
  222. continue;
  223. active_time_recordings_.emplace(feature_info.feature,
  224. base::TimeTicks::Now());
  225. }
  226. for (ReporterObserver& observer : observers_)
  227. observer.OnReporterActivated();
  228. }
  229. void FeatureDiscoveryDurationReporterImpl::Deactivate() {
  230. if (!active_time_recordings_.empty()) {
  231. DictionaryPrefUpdate update(active_pref_service_, kObservedFeatures);
  232. base::Value* observed_features = update.Get();
  233. DCHECK(observed_features);
  234. base::Value::Dict& mutable_observed_features_dict =
  235. observed_features->GetDict();
  236. // Store the accumulated time duration as pref data.
  237. for (const auto& name_timestamp_pair : active_time_recordings_) {
  238. // Fetch cumulated duration from pref service.
  239. const char* feature_name = FindMappedName(name_timestamp_pair.first);
  240. base::Value* feature_data =
  241. mutable_observed_features_dict.Find(feature_name);
  242. DCHECK(feature_data);
  243. base::Value::Dict& mutable_data_dict = feature_data->GetDict();
  244. const base::Value* cumulated_duration_value =
  245. mutable_data_dict.Find(kCumulatedDuration);
  246. DCHECK(cumulated_duration_value);
  247. absl::optional<base::TimeDelta> cumulated_duration =
  248. base::ValueToTimeDelta(cumulated_duration_value);
  249. DCHECK(cumulated_duration);
  250. // Add the observation duration under the current active session. Then
  251. // store the total duration.
  252. mutable_data_dict.Set(
  253. kCumulatedDuration,
  254. base::TimeDeltaToValue(*cumulated_duration + base::TimeTicks::Now() -
  255. name_timestamp_pair.second));
  256. }
  257. active_time_recordings_.clear();
  258. }
  259. is_active_ = false;
  260. }
  261. void FeatureDiscoveryDurationReporterImpl::OnSessionStateChanged(
  262. session_manager::SessionState state) {
  263. SetActive(state == session_manager::SessionState::ACTIVE);
  264. }
  265. void FeatureDiscoveryDurationReporterImpl::OnActiveUserPrefServiceChanged(
  266. PrefService* pref_service) {
  267. // Halt the observations for the old active account if any.
  268. if (is_active())
  269. SetActive(false);
  270. active_pref_service_ = pref_service;
  271. SetActive(Shell::Get()->session_controller()->GetSessionState() ==
  272. session_manager::SessionState::ACTIVE);
  273. }
  274. } // namespace ash