305 lines
12 KiB
C++
305 lines
12 KiB
C++
|
|
// Copyright 2019 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/metrics/demographics/user_demographics.h"
|
|||
|
|
|
|||
|
|
#include <utility>
|
|||
|
|
|
|||
|
|
#include "base/check.h"
|
|||
|
|
#include "base/rand_util.h"
|
|||
|
|
#include "base/values.h"
|
|||
|
|
#include "build/build_config.h"
|
|||
|
|
#include "components/pref_registry/pref_registry_syncable.h"
|
|||
|
|
#include "components/prefs/pref_service.h"
|
|||
|
|
#include "third_party/abseil-cpp/absl/types/optional.h"
|
|||
|
|
|
|||
|
|
namespace metrics {
|
|||
|
|
|
|||
|
|
// Root dictionary pref to store the user's birth year and gender that are
|
|||
|
|
// provided by the sync server. This is a read-only syncable priority pref on
|
|||
|
|
// all platforms except ChromeOS Ash, where it is a syncable OS-level priority
|
|||
|
|
// pref.
|
|||
|
|
#if !BUILDFLAG(IS_CHROMEOS_ASH)
|
|||
|
|
const char kSyncDemographicsPrefName[] = "sync.demographics";
|
|||
|
|
constexpr auto kSyncDemographicsPrefFlags =
|
|||
|
|
user_prefs::PrefRegistrySyncable::SYNCABLE_PRIORITY_PREF;
|
|||
|
|
#else
|
|||
|
|
const char kSyncOsDemographicsPrefName[] = "sync.os_demographics";
|
|||
|
|
constexpr auto kSyncOsDemographicsPrefFlags =
|
|||
|
|
user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PRIORITY_PREF;
|
|||
|
|
// TODO(crbug/1367338): Make this non-syncable (on Ash only) after full rollout
|
|||
|
|
// of the syncable os priority pref; then delete it locally from Ash devices.
|
|||
|
|
const char kSyncDemographicsPrefName[] = "sync.demographics";
|
|||
|
|
constexpr auto kSyncDemographicsPrefFlags =
|
|||
|
|
user_prefs::PrefRegistrySyncable::SYNCABLE_PRIORITY_PREF;
|
|||
|
|
#endif
|
|||
|
|
|
|||
|
|
// Stores a "secret" offset that is used to randomize the birth year for metrics
|
|||
|
|
// reporting. This value should not be logged to UMA directly; instead, it
|
|||
|
|
// should be summed with the kSyncDemographicsBirthYear. This value is generated
|
|||
|
|
// locally on the client the first time a user begins to merge birth year data
|
|||
|
|
// into their UMA reports.
|
|||
|
|
const char kUserDemographicsBirthYearOffsetPrefName[] =
|
|||
|
|
"demographics_birth_year_offset";
|
|||
|
|
constexpr auto kUserDemographicsBirthYearOffsetPrefFlags =
|
|||
|
|
PrefRegistry::NO_REGISTRATION_FLAGS;
|
|||
|
|
// TODO(crbug/1367338): Delete after 2023/09
|
|||
|
|
const char kDeprecatedDemographicsBirthYearOffsetPrefName[] =
|
|||
|
|
"sync.demographics_birth_year_offset";
|
|||
|
|
constexpr auto kDeprecatedDemographicsBirthYearOffsetPrefFlags =
|
|||
|
|
PrefRegistry::NO_REGISTRATION_FLAGS;
|
|||
|
|
|
|||
|
|
// This pref value is subordinate to the kSyncDemographics dictionary pref and
|
|||
|
|
// is synced to the client. It stores the self-reported birth year of the
|
|||
|
|
// syncing user. as provided by the sync server. This value should not be logged
|
|||
|
|
// to UMA directly; instead, it should be summed with the
|
|||
|
|
// kSyncDemographicsBirthYearNoiseOffset.
|
|||
|
|
const char kSyncDemographicsBirthYearPath[] = "birth_year";
|
|||
|
|
|
|||
|
|
// This pref value is subordinate to the kSyncDemographics dictionary pref and
|
|||
|
|
// is synced to the client. It stores the self-reported gender of the syncing
|
|||
|
|
// user, as provided by the sync server. The gender is encoded using the Gender
|
|||
|
|
// enum defined in UserDemographicsProto
|
|||
|
|
// (see third_party/metrics_proto/user_demographics.proto).
|
|||
|
|
const char kSyncDemographicsGenderPath[] = "gender";
|
|||
|
|
|
|||
|
|
namespace {
|
|||
|
|
|
|||
|
|
const base::Value::Dict& GetDemographicsDict(PrefService* profile_prefs) {
|
|||
|
|
#if BUILDFLAG(IS_CHROMEOS_ASH)
|
|||
|
|
// TODO(crbug/1367338): On Ash only, clear sync demographics pref once
|
|||
|
|
// os-level syncable pref is fully rolled out and Ash drops support for
|
|||
|
|
// non-os-level syncable prefs.
|
|||
|
|
if (profile_prefs->HasPrefPath(kSyncOsDemographicsPrefName)) {
|
|||
|
|
return profile_prefs->GetDict(kSyncOsDemographicsPrefName);
|
|||
|
|
}
|
|||
|
|
#endif
|
|||
|
|
return profile_prefs->GetDict(kSyncDemographicsPrefName);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void MigrateBirthYearOffset(PrefService* to_local_state,
|
|||
|
|
PrefService* from_profile_prefs) {
|
|||
|
|
const int profile_offset = from_profile_prefs->GetInteger(
|
|||
|
|
kDeprecatedDemographicsBirthYearOffsetPrefName);
|
|||
|
|
if (profile_offset == kUserDemographicsBirthYearNoiseOffsetDefaultValue)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
// TODO(crbug/1367338): clear/remove deprecated pref after 2023/09
|
|||
|
|
|
|||
|
|
const int local_offset =
|
|||
|
|
to_local_state->GetInteger(kUserDemographicsBirthYearOffsetPrefName);
|
|||
|
|
if (local_offset == kUserDemographicsBirthYearNoiseOffsetDefaultValue) {
|
|||
|
|
to_local_state->SetInteger(kUserDemographicsBirthYearOffsetPrefName,
|
|||
|
|
profile_offset);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Returns the noise offset for the birth year. If not found in |local_state|,
|
|||
|
|
// the offset will be randomly generated within the offset range and cached in
|
|||
|
|
// |local_state|.
|
|||
|
|
int GetBirthYearOffset(PrefService* local_state) {
|
|||
|
|
int offset =
|
|||
|
|
local_state->GetInteger(kUserDemographicsBirthYearOffsetPrefName);
|
|||
|
|
if (offset == kUserDemographicsBirthYearNoiseOffsetDefaultValue) {
|
|||
|
|
// Generate a new random offset when not already cached.
|
|||
|
|
offset = base::RandInt(-kUserDemographicsBirthYearNoiseOffsetRange,
|
|||
|
|
kUserDemographicsBirthYearNoiseOffsetRange);
|
|||
|
|
local_state->SetInteger(kUserDemographicsBirthYearOffsetPrefName, offset);
|
|||
|
|
}
|
|||
|
|
return offset;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Determines whether the synced user has provided a birth year to Google which
|
|||
|
|
// is eligible, once aggregated and anonymized, to measure usage of Chrome
|
|||
|
|
// features by age groups. See doc of DemographicMetricsProvider in
|
|||
|
|
// demographic_metrics_provider.h for more details.
|
|||
|
|
bool HasEligibleBirthYear(base::Time now, int user_birth_year, int offset) {
|
|||
|
|
// Compute user age.
|
|||
|
|
base::Time::Exploded exploded_now_time;
|
|||
|
|
now.LocalExplode(&exploded_now_time);
|
|||
|
|
int user_age = exploded_now_time.year - (user_birth_year + offset);
|
|||
|
|
|
|||
|
|
// Verify if the synced user's age has a population size in the age
|
|||
|
|
// distribution of the society that is big enough to not raise the entropy of
|
|||
|
|
// the demographics too much. At a certain point, as the age increase, the
|
|||
|
|
// size of the population starts declining sharply as you can see in this
|
|||
|
|
// approximate representation of the age distribution:
|
|||
|
|
// | ________ max age
|
|||
|
|
// |______/ \_________ |
|
|||
|
|
// | |\
|
|||
|
|
// | | \
|
|||
|
|
// +--------------------------|---------
|
|||
|
|
// 0 10 20 30 40 50 60 70 80 90 100+
|
|||
|
|
if (user_age > kUserDemographicsMaxAgeInYears)
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
// Verify if the synced user is old enough. Use > rather than >= because we
|
|||
|
|
// want to be sure that the user is at least |kUserDemographicsMinAgeInYears|
|
|||
|
|
// without disclosing their birth date, which requires to add an extra year
|
|||
|
|
// margin to the minimal age to be safe. For example, if we are in 2019-07-10
|
|||
|
|
// (now) and the user was born in 1999-08-10, the user is not yet 20 years old
|
|||
|
|
// (minimal age) but we cannot know that because we only have access to the
|
|||
|
|
// year of the dates (2019 and 1999 respectively). If we make sure that the
|
|||
|
|
// minimal age (computed at year granularity) is at least 21, we are 100% sure
|
|||
|
|
// that the user will be at least 20 years old when providing the user’s birth
|
|||
|
|
// year and gender.
|
|||
|
|
return user_age > kUserDemographicsMinAgeInYears;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Gets the synced user's birth year from synced prefs, see doc of
|
|||
|
|
// DemographicMetricsProvider in demographic_metrics_provider.h for more
|
|||
|
|
// details.
|
|||
|
|
absl::optional<int> GetUserBirthYear(const base::Value::Dict& demographics) {
|
|||
|
|
return demographics.FindInt(kSyncDemographicsBirthYearPath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Gets the synced user's gender from synced prefs, see doc of
|
|||
|
|
// DemographicMetricsProvider in demographic_metrics_provider.h for more
|
|||
|
|
// details.
|
|||
|
|
absl::optional<UserDemographicsProto_Gender> GetUserGender(
|
|||
|
|
const base::Value::Dict& demographics) {
|
|||
|
|
const absl::optional<int> gender_int =
|
|||
|
|
demographics.FindInt(kSyncDemographicsGenderPath);
|
|||
|
|
|
|||
|
|
// Verify that the gender is unset.
|
|||
|
|
if (!gender_int)
|
|||
|
|
return absl::nullopt;
|
|||
|
|
|
|||
|
|
// Verify that the gender number is a valid UserDemographicsProto_Gender
|
|||
|
|
// encoding.
|
|||
|
|
if (!UserDemographicsProto_Gender_IsValid(*gender_int))
|
|||
|
|
return absl::nullopt;
|
|||
|
|
|
|||
|
|
const auto gender = UserDemographicsProto_Gender(*gender_int);
|
|||
|
|
|
|||
|
|
// Verify that the gender is in a large enough population set to preserve
|
|||
|
|
// anonymity.
|
|||
|
|
if (gender != UserDemographicsProto::GENDER_FEMALE &&
|
|||
|
|
gender != UserDemographicsProto::GENDER_MALE) {
|
|||
|
|
return absl::nullopt;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return gender;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} // namespace
|
|||
|
|
|
|||
|
|
// static
|
|||
|
|
UserDemographicsResult UserDemographicsResult::ForValue(
|
|||
|
|
UserDemographics value) {
|
|||
|
|
return UserDemographicsResult(std::move(value),
|
|||
|
|
UserDemographicsStatus::kSuccess);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// static
|
|||
|
|
UserDemographicsResult UserDemographicsResult::ForStatus(
|
|||
|
|
UserDemographicsStatus status) {
|
|||
|
|
DCHECK(status != UserDemographicsStatus::kSuccess);
|
|||
|
|
return UserDemographicsResult(UserDemographics(), status);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool UserDemographicsResult::IsSuccess() const {
|
|||
|
|
return status_ == UserDemographicsStatus::kSuccess;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
UserDemographicsStatus UserDemographicsResult::status() const {
|
|||
|
|
return status_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const UserDemographics& UserDemographicsResult::value() const {
|
|||
|
|
return value_;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
UserDemographicsResult::UserDemographicsResult(UserDemographics value,
|
|||
|
|
UserDemographicsStatus status)
|
|||
|
|
: value_(std::move(value)), status_(status) {}
|
|||
|
|
|
|||
|
|
void RegisterDemographicsLocalStatePrefs(PrefRegistrySimple* registry) {
|
|||
|
|
registry->RegisterIntegerPref(
|
|||
|
|
kUserDemographicsBirthYearOffsetPrefName,
|
|||
|
|
kUserDemographicsBirthYearNoiseOffsetDefaultValue,
|
|||
|
|
kUserDemographicsBirthYearOffsetPrefFlags);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void RegisterDemographicsProfilePrefs(PrefRegistrySimple* registry) {
|
|||
|
|
#if BUILDFLAG(IS_CHROMEOS_ASH)
|
|||
|
|
registry->RegisterDictionaryPref(kSyncOsDemographicsPrefName,
|
|||
|
|
kSyncOsDemographicsPrefFlags);
|
|||
|
|
#endif
|
|||
|
|
registry->RegisterDictionaryPref(kSyncDemographicsPrefName,
|
|||
|
|
kSyncDemographicsPrefFlags);
|
|||
|
|
registry->RegisterIntegerPref(
|
|||
|
|
kDeprecatedDemographicsBirthYearOffsetPrefName,
|
|||
|
|
kUserDemographicsBirthYearNoiseOffsetDefaultValue,
|
|||
|
|
kDeprecatedDemographicsBirthYearOffsetPrefFlags);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void ClearDemographicsPrefs(PrefService* profile_prefs) {
|
|||
|
|
// Clear the dict holding the user's birth year and gender.
|
|||
|
|
//
|
|||
|
|
// Note: We never clear kUserDemographicsBirthYearOffset from local state.
|
|||
|
|
// The device should continue to use the *same* noise value as long as the
|
|||
|
|
// device's UMA client id remains the same. If the noise value were allowed
|
|||
|
|
// to change for a given user + client id, then the min/max noisy birth year
|
|||
|
|
// values could both be reported, revealing the true value in the middle.
|
|||
|
|
profile_prefs->ClearPref(kSyncDemographicsPrefName);
|
|||
|
|
#if BUILDFLAG(IS_CHROMEOS_ASH)
|
|||
|
|
profile_prefs->ClearPref(kSyncOsDemographicsPrefName);
|
|||
|
|
#endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
UserDemographicsResult GetUserNoisedBirthYearAndGenderFromPrefs(
|
|||
|
|
base::Time now,
|
|||
|
|
PrefService* local_state,
|
|||
|
|
PrefService* profile_prefs) {
|
|||
|
|
// Verify that the now time is available. There are situations where the now
|
|||
|
|
// time cannot be provided.
|
|||
|
|
if (now.is_null()) {
|
|||
|
|
return UserDemographicsResult::ForStatus(
|
|||
|
|
UserDemographicsStatus::kCannotGetTime);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Get the synced user’s noised birth year and gender from synced profile
|
|||
|
|
// prefs. Only one error status code should be used to represent the case
|
|||
|
|
// where demographics are ineligible, see doc of UserDemographicsStatus in
|
|||
|
|
// user_demographics.h for more details.
|
|||
|
|
|
|||
|
|
// Get the pref that contains the user's birth year and gender.
|
|||
|
|
const base::Value::Dict& demographics = GetDemographicsDict(profile_prefs);
|
|||
|
|
|
|||
|
|
// Get the user's birth year.
|
|||
|
|
absl::optional<int> birth_year = GetUserBirthYear(demographics);
|
|||
|
|
if (!birth_year.has_value()) {
|
|||
|
|
return UserDemographicsResult::ForStatus(
|
|||
|
|
UserDemographicsStatus::kIneligibleDemographicsData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Get the user's gender.
|
|||
|
|
absl::optional<UserDemographicsProto_Gender> gender =
|
|||
|
|
GetUserGender(demographics);
|
|||
|
|
if (!gender.has_value()) {
|
|||
|
|
return UserDemographicsResult::ForStatus(
|
|||
|
|
UserDemographicsStatus::kIneligibleDemographicsData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Get the offset from local_state/profile_prefs and do one last check that
|
|||
|
|
// the birth year is eligible.
|
|||
|
|
// TODO(crbug/1367338): remove profile_prefs after 2023/09
|
|||
|
|
MigrateBirthYearOffset(local_state, profile_prefs);
|
|||
|
|
int offset = GetBirthYearOffset(local_state);
|
|||
|
|
if (!HasEligibleBirthYear(now, *birth_year, offset)) {
|
|||
|
|
return UserDemographicsResult::ForStatus(
|
|||
|
|
UserDemographicsStatus::kIneligibleDemographicsData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Set gender and noised birth year in demographics.
|
|||
|
|
UserDemographics user_demographics;
|
|||
|
|
user_demographics.gender = *gender;
|
|||
|
|
user_demographics.birth_year = *birth_year + offset;
|
|||
|
|
|
|||
|
|
return UserDemographicsResult::ForValue(std::move(user_demographics));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} // namespace metrics
|