| // 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 "components/saved_tab_groups/internal/stats.h" |
| |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/time/time.h" |
| #include "components/saved_tab_groups/internal/saved_tab_group_model.h" |
| #include "components/saved_tab_groups/public/saved_tab_group.h" |
| #include "components/saved_tab_groups/public/saved_tab_group_tab.h" |
| #include "components/saved_tab_groups/public/types.h" |
| #include "components/tab_groups/tab_group_visual_data.h" |
| |
| namespace { |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| // LINT.IfChange(TabGroupLoadedEmptiness) |
| enum class TabGroupLoadedEmptiness { |
| kNotEmpty = 0, |
| kAlreadyEmpty = 1, |
| kBecameEmptyAfterRemovingDuplicates = 2, |
| kMaxValue = kBecameEmptyAfterRemovingDuplicates, |
| }; |
| // LINT.ThenChange(/tools/metrics/histograms/metadata/tab/enums.xml:TabGroupLoadedEmptiness) |
| } // namespace |
| |
| namespace tab_groups { |
| namespace stats { |
| constexpr base::TimeDelta kModifiedThreshold = base::Days(30); |
| |
| // Only used for desktop code that uses SavedTabGroupKeyedService. Soon to be |
| // deprecated. |
| void RecordSavedTabGroupMetrics(SavedTabGroupModel* model) { |
| base::UmaHistogramCounts10000("TabGroups.SavedTabGroupCount", model->Count()); |
| |
| const base::Time current_time = base::Time::Now(); |
| |
| int pinned_group_count = 0; |
| int active_group_count = 0; |
| int open_group_count = 0; |
| |
| for (const SavedTabGroup& group : model->saved_tab_groups()) { |
| base::UmaHistogramCounts10000("TabGroups.SavedTabGroupTabCount", |
| group.saved_tabs().size()); |
| |
| const base::TimeDelta duration_saved = current_time - group.creation_time(); |
| if (!duration_saved.is_negative()) { |
| base::UmaHistogramCounts1M("TabGroups.SavedTabGroupAge", |
| duration_saved.InMinutes()); |
| } |
| |
| const base::TimeDelta duration_since_group_modification = |
| current_time - group.update_time(); |
| if (!duration_since_group_modification.is_negative()) { |
| base::UmaHistogramCounts1M("TabGroups.SavedTabGroupTimeSinceModification", |
| duration_since_group_modification.InMinutes()); |
| |
| if (duration_since_group_modification <= kModifiedThreshold) { |
| ++active_group_count; |
| } |
| } |
| |
| for (const SavedTabGroupTab& tab : group.saved_tabs()) { |
| const base::TimeDelta duration_since_tab_modification = |
| current_time - tab.update_time(); |
| if (duration_since_tab_modification.is_negative()) { |
| continue; |
| } |
| |
| base::UmaHistogramCounts1M( |
| "TabGroups.SavedTabGroupTabTimeSinceModification", |
| duration_since_tab_modification.InMinutes()); |
| } |
| |
| if (group.is_pinned()) { |
| ++pinned_group_count; |
| } |
| |
| if (group.local_group_id().has_value()) { |
| ++open_group_count; |
| } |
| } |
| |
| base::UmaHistogramCounts10000("TabGroups.SavedTabGroupPinnedCount", |
| pinned_group_count); |
| base::UmaHistogramCounts10000("TabGroups.SavedTabGroupUnpinnedCount", |
| model->Count() - pinned_group_count); |
| base::UmaHistogramCounts10000("TabGroups.SavedTabGroupActiveCount", |
| active_group_count); |
| base::UmaHistogramCounts10000("TabGroups.SavedTabGroupOpenCount", |
| open_group_count); |
| } |
| |
| void RecordTabCountMismatchOnConnect(size_t tabs_in_saved_group, |
| size_t tabs_in_group) { |
| if (tabs_in_group > tabs_in_saved_group) { |
| base::UmaHistogramCounts100( |
| "TabGroups.SavedTabGroups.TabCountDifference.Positive", |
| tabs_in_group - tabs_in_saved_group); |
| } else if (tabs_in_group < tabs_in_saved_group) { |
| base::UmaHistogramCounts100( |
| "TabGroups.SavedTabGroups.TabCountDifference.Negative", |
| tabs_in_saved_group - tabs_in_group); |
| } |
| } |
| |
| void RecordMigrationResult(MigrationResult migration_result) { |
| base::UmaHistogramEnumeration( |
| "TabGroups.SavedTabGroupSyncBridge.MigrationResult", migration_result, |
| MigrationResult::kCount); |
| } |
| |
| void RecordSpecificsParseFailureCount(int parse_failure_count, |
| int total_entries) { |
| int parse_failure_percentage = 0; |
| if (total_entries != 0) { |
| parse_failure_percentage = base::ClampRound( |
| 100.f * (static_cast<float>(parse_failure_count) / total_entries)); |
| } |
| |
| base::UmaHistogramPercentage( |
| "TabGroups.SpecificsToDataMigration.ParseFailurePercentage", |
| parse_failure_percentage); |
| } |
| |
| void RecordParsedSavedTabGroupDataCount(int parsed_entries_count, |
| int total_entries) { |
| int parse_failure_percentage = 0; |
| if (total_entries != 0) { |
| parse_failure_percentage = base::ClampRound( |
| 100.f * (static_cast<float>(total_entries - parsed_entries_count) / |
| total_entries)); |
| } |
| |
| base::UmaHistogramPercentage( |
| "TabGroups.ParseSavedTabGroupDataEntries.ParseFailurePercentage", |
| parse_failure_percentage); |
| } |
| |
| void RecordTabGroupVisualsMetrics( |
| const tab_groups::TabGroupVisualData* visual_data) { |
| if (!visual_data) { |
| return; |
| } |
| |
| base::UmaHistogramCounts100("TabGroups.Sync.TabGroupTitleLength", |
| visual_data->title().length()); |
| } |
| |
| void RecordSharedTabGroupDataLoadFromDiskResult( |
| SharedTabGroupDataLoadFromDiskResult result) { |
| base::UmaHistogramEnumeration( |
| "TabGroups.Shared.LoadFromDiskResult", result); |
| } |
| |
| void RecordEmptyGroupsMetricsOnLoad( |
| const std::vector<SavedTabGroup>& all_groups, |
| const std::vector<SavedTabGroupTab>& all_tabs, |
| const std::unordered_set<base::Uuid, base::UuidHash>& |
| groups_with_filtered_tabs) { |
| // Empty groups will sometimes happen because the order of sync messages are |
| // not guaranteed - and clients might quit while they are missing some out of |
| // order messages, so groups might be empty on load too. Record metrics so we |
| // can validate that this is rare as expected, and that we don't create empty |
| // groups in other ways. |
| std::unordered_set<base::Uuid, base::UuidHash> empty_groups; |
| for (const SavedTabGroup& group : all_groups) { |
| empty_groups.emplace(group.saved_guid()); |
| } |
| for (const SavedTabGroupTab& tab : all_tabs) { |
| empty_groups.erase(tab.saved_group_guid()); |
| } |
| |
| for (const SavedTabGroup& group : all_groups) { |
| TabGroupLoadedEmptiness metric_value = TabGroupLoadedEmptiness::kNotEmpty; |
| if (empty_groups.contains(group.saved_guid())) { |
| metric_value = |
| groups_with_filtered_tabs.contains(group.saved_guid()) |
| ? TabGroupLoadedEmptiness::kBecameEmptyAfterRemovingDuplicates |
| : TabGroupLoadedEmptiness::kAlreadyEmpty; |
| } |
| base::UmaHistogramEnumeration( |
| "TabGroups.SavedTabGroups.TabGroupLoadedEmptiness", metric_value); |
| } |
| } |
| |
| void RecordEmptyGroupsMetricsOnGroupAddedLocally(const SavedTabGroup& group, |
| bool model_is_loaded) { |
| // This method is used to load the model from disk. Avoid recording metrics |
| // during load so we don't double count with the previous session. |
| if (!model_is_loaded) { |
| return; |
| } |
| |
| base::UmaHistogramBoolean("TabGroups.Sync.AddedGroupIsEmptyLocally", |
| group.saved_tabs().empty()); |
| } |
| |
| void RecordEmptyGroupsMetricsOnGroupAddedFromSync(const SavedTabGroup& group, |
| bool model_is_loaded) { |
| // Avoid recording metrics during load so we don't double count with the |
| // previous session. This method is currently not called during load, but that |
| // could change. |
| if (!model_is_loaded) { |
| return; |
| } |
| |
| base::UmaHistogramBoolean("TabGroups.Sync.AddedGroupIsEmptyFromSync", |
| group.saved_tabs().empty()); |
| } |
| |
| void RecordEmptyGroupsMetricsOnTabAddedLocally(const SavedTabGroup& group, |
| const SavedTabGroupTab& tab, |
| bool model_is_loaded) { |
| // Avoid recording metrics during load so we don't double count with the |
| // previous session. This method is currently not called during load, but that |
| // could change. |
| if (!model_is_loaded) { |
| return; |
| } |
| |
| // The tab must not be already in the group; otherwise we can't tell if the |
| // group was empty before (as the tab may have been replaced). |
| CHECK(!group.ContainsTab(tab.saved_tab_guid())); |
| |
| base::UmaHistogramBoolean("TabGroups.Sync.TabAddedToEmptyGroupLocally", |
| group.saved_tabs().empty()); |
| } |
| |
| void RecordEmptyGroupsMetricsOnTabAddedFromSync(const SavedTabGroup& group, |
| const SavedTabGroupTab& tab, |
| bool model_is_loaded) { |
| // This method is used to load the model from disk. Avoid recording metrics |
| // during load so we don't double count with the previous session. |
| if (!model_is_loaded) { |
| return; |
| } |
| |
| // The tab must not be already in the group; otherwise we can't tell if the |
| // group was empty before (as the tab may have been replaced). |
| CHECK(!group.ContainsTab(tab.saved_tab_guid())); |
| |
| base::UmaHistogramBoolean("TabGroups.Sync.TabAddedToEmptyGroupFromSync", |
| group.saved_tabs().empty()); |
| } |
| |
| void RecordSharedGroupTitleSanitization( |
| bool use_url_as_title, |
| TitleSanitizationType title_sanitization_type) { |
| std::string histogram; |
| switch (title_sanitization_type) { |
| case TitleSanitizationType::kAddTab: |
| histogram = "TabGroups.Shared.UseUrlForTitle.AddTab"; |
| break; |
| case TitleSanitizationType::kNavigateTab: |
| histogram = "TabGroups.Shared.UseUrlForTitle.NavigateTab"; |
| break; |
| case TitleSanitizationType::kShareTabGroup: |
| histogram = "TabGroups.Shared.UseUrlForTitle.ShareTabGroup"; |
| break; |
| } |
| base::UmaHistogramBoolean(histogram, use_url_as_title); |
| } |
| } // namespace stats |
| } // namespace tab_groups |