| // Copyright 2026 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/sync/service/device_statistics_tracker.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/base64.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/rand_util.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "build/build_config.h" |
| #include "components/signin/public/identity_manager/identity_manager.h" |
| #include "components/sync/base/data_type.h" |
| #include "components/sync/base/time.h" |
| #include "components/sync/engine/net/url_translator.h" |
| #include "components/sync/protocol/device_info_specifics.pb.h" |
| #include "components/sync/protocol/entity_specifics.pb.h" |
| #include "components/sync/protocol/sync_entity.pb.h" |
| #include "components/sync/protocol/sync_enums.pb.h" |
| #include "components/sync/service/device_statistics_request.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "third_party/abseil-cpp/absl/container/flat_hash_map.h" |
| |
| namespace syncer { |
| |
| namespace { |
| |
| // A device is considered active if it has been used within this amount of time. |
| constexpr base::TimeDelta kDeviceActivityTimeRange = base::Days(28); |
| |
| std::string GenerateCacheGUID() { |
| // Generate a GUID with 128 bits of randomness. |
| constexpr int kGuidBytes = 128 / 8; |
| return base::Base64Encode(base::RandBytesAsVector(kGuidBytes)); |
| } |
| |
| bool ShouldRecordOutcomeMetrics( |
| DeviceStatisticsTracker::RequestsCompletedSuccess success) { |
| using RequestsCompletedSuccess = |
| DeviceStatisticsTracker::RequestsCompletedSuccess; |
| switch (success) { |
| case RequestsCompletedSuccess::kAllSucceeded: |
| case RequestsCompletedSuccess::kPrimarySucceededButNonPrimaryFailed: |
| case RequestsCompletedSuccess::kPrimaryNAAndSomeNonPrimaryFailed: |
| return true; |
| case RequestsCompletedSuccess::kPrimaryFailedButNonPrimarySucceeded: |
| case RequestsCompletedSuccess::kAllFailed: |
| case RequestsCompletedSuccess::kPrimaryAccountChangedOrRemoved: |
| return false; |
| } |
| NOTREACHED(); |
| } |
| |
| std::optional<DeviceStatisticsTracker::Platform> PlatformFromProto( |
| sync_pb::SyncEnums::OsType os_type) { |
| switch (os_type) { |
| case sync_pb::SyncEnums::OsType::SyncEnums_OsType_OS_TYPE_WINDOWS: |
| return DeviceStatisticsTracker::Platform::kWindows; |
| case sync_pb::SyncEnums::OsType::SyncEnums_OsType_OS_TYPE_MAC: |
| return DeviceStatisticsTracker::Platform::kMac; |
| case sync_pb::SyncEnums::OsType::SyncEnums_OsType_OS_TYPE_LINUX: |
| return DeviceStatisticsTracker::Platform::kLinux; |
| case sync_pb::SyncEnums::OsType::SyncEnums_OsType_OS_TYPE_CHROME_OS_ASH: |
| return DeviceStatisticsTracker::Platform::kChromeOS; |
| case sync_pb::SyncEnums::OsType::SyncEnums_OsType_OS_TYPE_ANDROID: |
| return DeviceStatisticsTracker::Platform::kAndroid; |
| case sync_pb::SyncEnums::OsType::SyncEnums_OsType_OS_TYPE_IOS: |
| return DeviceStatisticsTracker::Platform::kIOS; |
| case sync_pb::SyncEnums::OsType::SyncEnums_OsType_OS_TYPE_CHROME_OS_LACROS: |
| case sync_pb::SyncEnums::OsType::SyncEnums_OsType_OS_TYPE_FUCHSIA: |
| case sync_pb::SyncEnums::OsType::SyncEnums_OsType_OS_TYPE_UNSPECIFIED: |
| // Unknown, deprecated, or not interesting. |
| return std::nullopt; |
| } |
| NOTREACHED(); |
| } |
| |
| std::optional<DeviceStatisticsTracker::Platform> GetLocalPlatform() { |
| #if BUILDFLAG(IS_WIN) |
| return DeviceStatisticsTracker::Platform::kWindows; |
| #elif BUILDFLAG(IS_MAC) |
| return DeviceStatisticsTracker::Platform::kMac; |
| #elif BUILDFLAG(IS_LINUX) |
| return DeviceStatisticsTracker::Platform::kLinux; |
| #elif BUILDFLAG(IS_CHROMEOS) |
| return DeviceStatisticsTracker::Platform::kChromeOS; |
| #elif BUILDFLAG(IS_ANDROID) |
| return DeviceStatisticsTracker::Platform::kAndroid; |
| #elif BUILDFLAG(IS_IOS) |
| return DeviceStatisticsTracker::Platform::kIOS; |
| #else |
| return std::nullopt; |
| #endif |
| } |
| |
| bool IsOptedInToHistory(const sync_pb::DeviceInfoSpecifics device_info) { |
| // Check whether the device interested in history invalidations. Note that |
| // it's better to check for `DataType::HISTORY_DELETE_DIRECTIVES` rather than |
| // `DataType::HISTORY`, since Android devices are generally not subscribed |
| // to `DataType::HISTORY`. |
| for (int data_type_number : |
| device_info.invalidation_fields().interested_data_type_ids()) { |
| if (GetDataTypeFromSpecificsFieldNumber(data_type_number) == |
| DataType::HISTORY_DELETE_DIRECTIVES) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| } // namespace |
| |
| DeviceStatisticsTracker::DeviceStatisticsTracker( |
| signin::IdentityManager* identity_manager, |
| const GURL& sync_server_url, |
| RequestFactory request_factory, |
| std::vector<std::string> current_device_cache_guids) |
| : identity_manager_(identity_manager), |
| sync_server_url_(sync_server_url), |
| request_factory_(std::move(request_factory)), |
| current_device_cache_guids_(std::move(current_device_cache_guids)), |
| primary_account_(identity_manager_->GetPrimaryAccountInfo( |
| signin::ConsentLevel::kSignin)) { |
| CHECK(identity_manager_); |
| CHECK(request_factory_); |
| } |
| |
| void DeviceStatisticsTracker::Start(base::OnceClosure callback) { |
| CHECK(!callback_); |
| CHECK(callback); |
| CHECK(identity_manager_->AreRefreshTokensLoaded()); |
| |
| callback_ = std::move(callback); |
| |
| const std::vector<CoreAccountInfo> accounts = |
| identity_manager_->GetAccountsWithRefreshTokens(); |
| |
| if (!primary_account_.IsEmpty() && |
| !std::ranges::contains(accounts, primary_account_)) { |
| // The primary account must have been removed between the constructor and |
| // now, or something's wrong with the IdentityManager. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(callback_)); |
| return; |
| } |
| |
| if (primary_account_.IsEmpty()) { |
| // Record the PrimaryAccountMulti[Device|Platform]Readiness metrics for the |
| // signed-out case. |
| RecordPrimaryAccountMultiDeviceReadiness( |
| /*other_devices=*/0, /*other_devices_with_history_opt_in=*/0); |
| RecordPrimaryAccountMultiPlatformReadiness( |
| /*other_platforms=*/0, /*other_platforms_with_history_opt_in=*/0); |
| } |
| |
| // If there are no accounts at all, there's not much to do. |
| if (accounts.empty()) { |
| RecordOverallDevicesOutcome(); |
| RecordOverallPlatformsOutcome(); |
| |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, std::move(callback_)); |
| return; |
| } |
| |
| GURL::Replacements path_replacement; |
| std::string path = sync_server_url_.GetPath() + "/command/"; |
| path_replacement.SetPathStr(path); |
| GURL base_url = sync_server_url_.ReplaceComponents(path_replacement); |
| |
| for (const CoreAccountInfo& account : accounts) { |
| GURL request_url = |
| syncer::AppendSyncQueryString(base_url, GenerateCacheGUID()); |
| requests_[account.gaia] = request_factory_.Run(account, request_url); |
| } |
| |
| for (const auto& [gaia, request] : requests_) { |
| // Note: Unretained() is safe because `this` owns the request, and the |
| // request will not run its callback after being deleted itself. |
| request->Start( |
| base::BindOnce(&DeviceStatisticsTracker::RequestDoneForGaiaId, |
| base::Unretained(this), gaia)); |
| } |
| |
| base::UmaHistogramCounts100("Sync.DeviceStatistics.RequestsStartedCount", |
| requests_.size()); |
| } |
| |
| DeviceStatisticsTracker::~DeviceStatisticsTracker() = default; |
| |
| void DeviceStatisticsTracker::RequestDoneForGaiaId(const GaiaId& gaia) { |
| CHECK(requests_.contains(gaia)); |
| std::unique_ptr<DeviceStatisticsRequest> request = std::move(requests_[gaia]); |
| CHECK(request); |
| requests_.erase(gaia); |
| |
| if (request->GetState() == DeviceStatisticsRequest::State::kComplete) { |
| other_devices_by_gaia_[gaia] = |
| DeduplicateEntities(request->GetResults(), current_device_cache_guids_); |
| |
| // If this request was for the primary account, figure out whether the user |
| // has opted in to history on the current device, and on any device matching |
| // the current platform. |
| if (gaia == primary_account_.gaia) { |
| for (const sync_pb::SyncEntity& entity : request->GetResults()) { |
| const sync_pb::DeviceInfoSpecifics& device_info = |
| entity.specifics().device_info(); |
| if (IsOptedInToHistory(device_info)) { |
| // Note: `current_device_cache_guids_` contains all known cache GUIDs |
| // for the current device, including some that may belong to other |
| // accounts. But since `device_info` belongs to the primary account, |
| // and cache GUIDs should be unique (and in particular, never shared |
| // across different accounts), that's okay. |
| if (current_device_cache_guids_.contains(device_info.cache_guid())) { |
| primary_account_history_opt_in_ = true; |
| } |
| if (PlatformFromProto(device_info.os_type()) == GetLocalPlatform()) { |
| local_platform_history_opt_in_ = true; |
| } |
| } |
| } |
| } |
| } else { |
| other_devices_by_gaia_[gaia] = base::unexpected(kRequestFailed); |
| } |
| |
| if (requests_.empty()) { |
| AllRequestsDone(); |
| } |
| } |
| |
| void DeviceStatisticsTracker::AllRequestsDone() { |
| CHECK(requests_.empty()); |
| CHECK(callback_); |
| |
| RequestsCompletedSuccess success = GetOverallSuccess(); |
| base::UmaHistogramEnumeration( |
| "Sync.DeviceStatistics.RequestsCompletedSuccess", success); |
| |
| if (ShouldRecordOutcomeMetrics(success)) { |
| RecordOverallDevicesOutcome(); |
| RecordOverallPlatformsOutcome(); |
| |
| for (const auto& [gaia, other_devices] : other_devices_by_gaia_) { |
| if (!other_devices.has_value()) { |
| // The statistics request for this account failed, so nothing to record. |
| continue; |
| } |
| |
| const bool is_primary = (gaia == primary_account_.gaia); |
| const std::string_view infix = |
| is_primary ? "PrimaryAccount" : "NonPrimaryAccount"; |
| |
| base::UmaHistogramCounts100( |
| absl::StrFormat( |
| "Sync.DeviceStatistics.Outcome.%s.NumberOfAdditionalClients2", |
| infix), |
| other_devices->size()); |
| |
| const size_t other_devices_with_history_opt_in = std::ranges::count_if( |
| *other_devices, |
| [](const DeviceData& device) { return device.history_opt_in; }); |
| base::UmaHistogramCounts100( |
| absl::StrFormat("Sync.DeviceStatistics.Outcome.%s." |
| "NumberOfAdditionalClientsWithHistoryOptIn2", |
| infix), |
| other_devices_with_history_opt_in); |
| |
| base::flat_map<Platform, bool> history_opt_in_by_platform; |
| for (const DeviceData& device : *other_devices) { |
| if (device.platform != GetLocalPlatform()) { |
| history_opt_in_by_platform[device.platform] |= device.history_opt_in; |
| } |
| } |
| size_t other_platforms = history_opt_in_by_platform.size(); |
| size_t other_platforms_with_history_opt_in = |
| std::ranges::count_if(history_opt_in_by_platform, |
| [](const auto& pair) { return pair.second; }); |
| |
| base::UmaHistogramCounts100( |
| absl::StrFormat( |
| "Sync.DeviceStatistics.Outcome.%s.NumberOfAdditionalPlatforms2", |
| infix), |
| other_platforms); |
| base::UmaHistogramCounts100( |
| absl::StrFormat("Sync.DeviceStatistics.Outcome.%s." |
| "NumberOfAdditionalPlatformsWithHistoryOptIn2", |
| infix), |
| other_platforms_with_history_opt_in); |
| |
| if (is_primary) { |
| RecordPrimaryAccountMultiDeviceReadiness( |
| other_devices->size(), other_devices_with_history_opt_in); |
| |
| base::UmaHistogramEnumeration( |
| "Sync.DeviceStatistics.Outcome.PrimaryAccount.HistoryOptIn2", |
| GetHistoryOptInDevicesSummary(other_devices->size(), |
| other_devices_with_history_opt_in)); |
| |
| RecordPrimaryAccountMultiPlatformReadiness( |
| other_platforms, other_platforms_with_history_opt_in); |
| |
| base::UmaHistogramEnumeration( |
| "Sync.DeviceStatistics.Outcome.PrimaryAccount." |
| "HistoryOptInMultiPlatform2", |
| GetHistoryOptInPlatformsSummary( |
| other_platforms, other_platforms_with_history_opt_in)); |
| } |
| |
| for (DeviceData device : *other_devices) { |
| base::UmaHistogramEnumeration( |
| absl::StrFormat( |
| "Sync.DeviceStatistics.Outcome.%s.PlatformOfAdditionalClient2", |
| infix), |
| device.platform); |
| } |
| } |
| } |
| |
| std::move(callback_).Run(); |
| // NOTE: `this` may be destroyed now; don't do anything else! |
| } |
| |
| void DeviceStatisticsTracker::RecordOverallDevicesOutcome() const { |
| base::UmaHistogramEnumeration("Sync.DeviceStatistics.Outcome.Overall2", |
| GetOverallDevicesOutcome()); |
| } |
| |
| void DeviceStatisticsTracker::RecordOverallPlatformsOutcome() const { |
| base::UmaHistogramEnumeration( |
| "Sync.DeviceStatistics.Outcome.OverallMultiPlatform2", |
| GetOverallPlatformsOutcome()); |
| } |
| |
| void DeviceStatisticsTracker::RecordPrimaryAccountMultiDeviceReadiness( |
| size_t other_devices, |
| size_t other_devices_with_history_opt_in) const { |
| base::UmaHistogramEnumeration( |
| "Sync.DeviceStatistics.Outcome.PrimaryAccount.MultiDeviceReadiness2", |
| GetPrimaryAccountMultiDeviceReadiness(other_devices, |
| other_devices_with_history_opt_in)); |
| } |
| |
| void DeviceStatisticsTracker::RecordPrimaryAccountMultiPlatformReadiness( |
| size_t other_platforms, |
| size_t other_platforms_with_history_opt_in) const { |
| base::UmaHistogramEnumeration( |
| "Sync.DeviceStatistics.Outcome.PrimaryAccount.MultiPlatformReadiness2", |
| GetPrimaryAccountMultiPlatformReadiness( |
| other_platforms, other_platforms_with_history_opt_in)); |
| } |
| |
| DeviceStatisticsTracker::RequestsCompletedSuccess |
| DeviceStatisticsTracker::GetOverallSuccess() const { |
| if (primary_account_.gaia != |
| identity_manager_->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin) |
| .gaia) { |
| return RequestsCompletedSuccess::kPrimaryAccountChangedOrRemoved; |
| } |
| |
| size_t requests_succeeded = 0; |
| size_t requests_failed = 0; |
| bool primary_failed = false; |
| for (const auto& [gaia, other_devices] : other_devices_by_gaia_) { |
| if (other_devices.has_value()) { |
| ++requests_succeeded; |
| } else { |
| ++requests_failed; |
| if (gaia == primary_account_.gaia) { |
| primary_failed = true; |
| } |
| } |
| } |
| |
| // TODO(crbug.com/465716865): Consider treating some types of errors |
| // specially, e.g disabled-by-admin. |
| |
| if (requests_succeeded == other_devices_by_gaia_.size()) { |
| return RequestsCompletedSuccess::kAllSucceeded; |
| } else if (requests_failed == other_devices_by_gaia_.size()) { |
| return RequestsCompletedSuccess::kAllFailed; |
| } else if (primary_account_.IsEmpty()) { |
| return RequestsCompletedSuccess::kPrimaryNAAndSomeNonPrimaryFailed; |
| } else if (primary_failed) { |
| return RequestsCompletedSuccess::kPrimaryFailedButNonPrimarySucceeded; |
| } else { |
| return RequestsCompletedSuccess::kPrimarySucceededButNonPrimaryFailed; |
| } |
| } |
| |
| DeviceStatisticsTracker::AccountsHaveOtherDevicesSummary |
| DeviceStatisticsTracker::GetOverallDevicesOutcome() const { |
| if (other_devices_by_gaia_.empty()) { |
| return AccountsHaveOtherDevicesSummary::kNoAccounts; |
| } |
| |
| bool primary_account_has_other_devices = false; |
| bool non_primary_account_has_other_devices = false; |
| for (const auto& [gaia, other_devices] : other_devices_by_gaia_) { |
| if (other_devices.has_value() && !other_devices->empty()) { |
| if (gaia == primary_account_.gaia) { |
| primary_account_has_other_devices = true; |
| } else { |
| non_primary_account_has_other_devices = true; |
| } |
| } |
| } |
| |
| if (!primary_account_.IsEmpty()) { |
| CHECK(other_devices_by_gaia_.contains(primary_account_.gaia)); |
| bool has_non_primary_account = other_devices_by_gaia_.size() > 1; |
| if (has_non_primary_account) { |
| if (non_primary_account_has_other_devices) { |
| return primary_account_has_other_devices |
| ? AccountsHaveOtherDevicesSummary::kPrimaryYesNonPrimaryYes |
| : AccountsHaveOtherDevicesSummary::kPrimaryNoNonPrimaryYes; |
| } else { |
| return primary_account_has_other_devices |
| ? AccountsHaveOtherDevicesSummary::kPrimaryYesNonPrimaryNo |
| : AccountsHaveOtherDevicesSummary::kPrimaryNoNonPrimaryNo; |
| } |
| } else { |
| return primary_account_has_other_devices |
| ? AccountsHaveOtherDevicesSummary::kPrimaryYesNonPrimaryNA |
| : AccountsHaveOtherDevicesSummary::kPrimaryNoNonPrimaryNA; |
| } |
| } else { |
| if (non_primary_account_has_other_devices) { |
| return AccountsHaveOtherDevicesSummary::kPrimaryNANonPrimaryYes; |
| } else { |
| return AccountsHaveOtherDevicesSummary::kPrimaryNANonPrimaryNo; |
| } |
| } |
| } |
| |
| DeviceStatisticsTracker::AccountsHaveOtherPlatformsSummary |
| DeviceStatisticsTracker::GetOverallPlatformsOutcome() const { |
| if (other_devices_by_gaia_.empty()) { |
| return AccountsHaveOtherPlatformsSummary::kNoAccounts; |
| } |
| |
| bool primary_account_has_other_platforms = false; |
| bool non_primary_account_has_other_platforms = false; |
| std::optional<Platform> local_platform = GetLocalPlatform(); |
| |
| for (const auto& [gaia, other_devices] : other_devices_by_gaia_) { |
| if (other_devices.has_value() && !other_devices->empty()) { |
| bool has_other_platform = false; |
| for (const DeviceData& device : *other_devices) { |
| if (!local_platform || device.platform != *local_platform) { |
| has_other_platform = true; |
| break; |
| } |
| } |
| |
| if (has_other_platform) { |
| if (gaia == primary_account_.gaia) { |
| primary_account_has_other_platforms = true; |
| } else { |
| non_primary_account_has_other_platforms = true; |
| } |
| } |
| } |
| } |
| |
| if (!primary_account_.IsEmpty()) { |
| CHECK(other_devices_by_gaia_.contains(primary_account_.gaia)); |
| bool has_non_primary_account = other_devices_by_gaia_.size() > 1; |
| if (has_non_primary_account) { |
| if (non_primary_account_has_other_platforms) { |
| return primary_account_has_other_platforms |
| ? AccountsHaveOtherPlatformsSummary::kPrimaryYesNonPrimaryYes |
| : AccountsHaveOtherPlatformsSummary::kPrimaryNoNonPrimaryYes; |
| } else { |
| return primary_account_has_other_platforms |
| ? AccountsHaveOtherPlatformsSummary::kPrimaryYesNonPrimaryNo |
| : AccountsHaveOtherPlatformsSummary::kPrimaryNoNonPrimaryNo; |
| } |
| } else { |
| return primary_account_has_other_platforms |
| ? AccountsHaveOtherPlatformsSummary::kPrimaryYesNonPrimaryNA |
| : AccountsHaveOtherPlatformsSummary::kPrimaryNoNonPrimaryNA; |
| } |
| } else { |
| if (non_primary_account_has_other_platforms) { |
| return AccountsHaveOtherPlatformsSummary::kPrimaryNANonPrimaryYes; |
| } else { |
| return AccountsHaveOtherPlatformsSummary::kPrimaryNANonPrimaryNo; |
| } |
| } |
| } |
| |
| DeviceStatisticsTracker::MultiDeviceReadiness |
| DeviceStatisticsTracker::GetPrimaryAccountMultiDeviceReadiness( |
| size_t other_devices, |
| size_t other_devices_with_history_opt_in) const { |
| if (primary_account_.IsEmpty()) { |
| return MultiDeviceReadiness::kSignedOut; |
| } |
| if (other_devices == 0) { |
| return MultiDeviceReadiness::kSingleDevice; |
| } |
| if (!primary_account_history_opt_in_ || |
| other_devices_with_history_opt_in == 0) { |
| return MultiDeviceReadiness::kMultiDeviceWithoutHistory; |
| } |
| return MultiDeviceReadiness::kMultiDeviceWithHistory; |
| } |
| |
| DeviceStatisticsTracker::MultiPlatformReadiness |
| DeviceStatisticsTracker::GetPrimaryAccountMultiPlatformReadiness( |
| size_t other_platforms, |
| size_t other_platforms_with_history_opt_in) const { |
| if (primary_account_.IsEmpty()) { |
| return MultiPlatformReadiness::kSignedOut; |
| } |
| if (other_platforms == 0) { |
| return MultiPlatformReadiness::kSinglePlatform; |
| } |
| if (!primary_account_history_opt_in_ || |
| other_platforms_with_history_opt_in == 0) { |
| return MultiPlatformReadiness::kMultiPlatformWithoutHistory; |
| } |
| return MultiPlatformReadiness::kMultiPlatformWithHistory; |
| } |
| |
| DeviceStatisticsTracker::HistoryOptInDevicesSummary |
| DeviceStatisticsTracker::GetHistoryOptInDevicesSummary( |
| size_t other_devices, |
| size_t other_devices_with_history_opt_in) const { |
| if (other_devices == 0) { |
| if (primary_account_history_opt_in_) { |
| return HistoryOptInDevicesSummary::kThisDeviceYesOtherDevicesNA; |
| } |
| return HistoryOptInDevicesSummary::kThisDeviceNoOtherDevicesNA; |
| } |
| |
| if (other_devices_with_history_opt_in > 0) { |
| if (primary_account_history_opt_in_) { |
| return HistoryOptInDevicesSummary::kThisDeviceYesOtherDevicesYes; |
| } |
| return HistoryOptInDevicesSummary::kThisDeviceNoOtherDevicesYes; |
| } |
| |
| if (primary_account_history_opt_in_) { |
| return HistoryOptInDevicesSummary::kThisDeviceYesOtherDevicesNo; |
| } |
| return HistoryOptInDevicesSummary::kThisDeviceNoOtherDevicesNo; |
| } |
| |
| DeviceStatisticsTracker::HistoryOptInPlatformsSummary |
| DeviceStatisticsTracker::GetHistoryOptInPlatformsSummary( |
| size_t other_platforms, |
| size_t other_platforms_with_history_opt_in) const { |
| if (other_platforms == 0) { |
| if (local_platform_history_opt_in_) { |
| return HistoryOptInPlatformsSummary::kThisPlatformYesOtherPlatformsNA; |
| } |
| return HistoryOptInPlatformsSummary::kThisPlatformNoOtherPlatformsNA; |
| } |
| |
| if (other_platforms_with_history_opt_in > 0) { |
| if (local_platform_history_opt_in_) { |
| return HistoryOptInPlatformsSummary::kThisPlatformYesOtherPlatformsYes; |
| } |
| return HistoryOptInPlatformsSummary::kThisPlatformNoOtherPlatformsYes; |
| } |
| |
| if (local_platform_history_opt_in_) { |
| return HistoryOptInPlatformsSummary::kThisPlatformYesOtherPlatformsNo; |
| } |
| return HistoryOptInPlatformsSummary::kThisPlatformNoOtherPlatformsNo; |
| } |
| |
| // static |
| std::vector<DeviceStatisticsTracker::DeviceData> |
| DeviceStatisticsTracker::DeduplicateEntities( |
| const std::vector<sync_pb::SyncEntity>& entities, |
| const base::flat_set<std::string>& current_device_cache_guids) { |
| absl::flat_hash_map<std::pair<sync_pb::SyncEnums::DeviceFormFactor, |
| sync_pb::SyncEnums::OsType>, |
| std::vector<sync_pb::SyncEntity>> |
| devices_by_type; |
| |
| const base::Time now = base::Time::Now(); |
| |
| // Group all the relevant DeviceInfos by OS+FormFactor. |
| for (const sync_pb::SyncEntity& entity : entities) { |
| const sync_pb::DeviceInfoSpecifics& device = |
| entity.specifics().device_info(); |
| // Only consider Chrome devices (not Google Play Services or iGSA). |
| if (!device.has_chrome_version_info() || |
| device.sync_user_agent().starts_with("iGSA")) { |
| continue; |
| } |
| |
| // Don't consider the current device. |
| if (current_device_cache_guids.contains(device.cache_guid())) { |
| continue; |
| } |
| |
| // Only consider recently-used devices. |
| base::Time last_updated_time = |
| ProtoTimeToTime(device.last_updated_timestamp()); |
| if (now - last_updated_time > kDeviceActivityTimeRange) { |
| continue; |
| } |
| |
| // This is a relevant device! |
| sync_pb::SyncEnums::DeviceFormFactor form_factor = |
| device.device_form_factor(); |
| sync_pb::SyncEnums::OsType os_type = device.os_type(); |
| devices_by_type[{form_factor, os_type}].push_back(entity); |
| } |
| |
| std::vector<DeviceData> deduped_devices; |
| |
| // Heuristically de-dupe entities based on their activity time ranges (similar |
| // in spirit to DeviceInfoSyncBridge::CountActiveDevicesByType()): If two |
| // entries have non-overlapping activity times (ctime/mtime), they most likely |
| // represent the same device, just with different cache GUIDs. |
| // As an approximation, to avoid an O(n^2) algorithm, just de-dupe all entries |
| // against the last-created one. |
| for (auto& [form_factor_and_os_type, type_entities] : devices_by_type) { |
| CHECK(!type_entities.empty()); |
| int64_t max_ctime = std::ranges::max(type_entities, /*comp=*/{}, |
| [](const sync_pb::SyncEntity& entity) { |
| return entity.ctime(); |
| }) |
| .ctime(); |
| for (const sync_pb::SyncEntity& entity : type_entities) { |
| if (entity.mtime() < max_ctime) { |
| // This entity was last used before the newest one was created. That |
| // means it's likely the same device, just with a different cache GUID. |
| continue; |
| } |
| |
| // Figure out the platform/OS of the device, and skip unknown or |
| // uninteresting ones. |
| std::optional<DeviceStatisticsTracker::Platform> platform = |
| PlatformFromProto(entity.specifics().device_info().os_type()); |
| if (!platform) { |
| continue; |
| } |
| |
| // Figure out whether the device has opted in to history. |
| bool history_opt_in = |
| IsOptedInToHistory(entity.specifics().device_info()); |
| |
| deduped_devices.emplace_back(*platform, history_opt_in); |
| } |
| } |
| |
| return deduped_devices; |
| } |
| |
| } // namespace syncer |