// Copyright 2017 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 "components/variations/synthetic_trial_registry.h" #include #include "base/metrics/histogram_functions.h" #include "base/stl_util.h" #include "base/strings/string_number_conversions.h" #include "components/variations/hashing.h" #include "components/variations/variations_associated_data.h" namespace variations { namespace internal { const base::Feature kExternalExperimentAllowlist{ "ExternalExperimentAllowlist", base::FEATURE_ENABLED_BY_DEFAULT}; } // namespace internal SyntheticTrialRegistry::SyntheticTrialRegistry( bool enable_external_experiment_allowlist) : enable_external_experiment_allowlist_( enable_external_experiment_allowlist && base::FeatureList::IsEnabled( internal::kExternalExperimentAllowlist)) {} SyntheticTrialRegistry::SyntheticTrialRegistry() : enable_external_experiment_allowlist_(base::FeatureList::IsEnabled( internal::kExternalExperimentAllowlist)) {} SyntheticTrialRegistry::~SyntheticTrialRegistry() = default; void SyntheticTrialRegistry::AddSyntheticTrialObserver( SyntheticTrialObserver* observer) { synthetic_trial_observer_list_.AddObserver(observer); if (!synthetic_trial_groups_.empty()) observer->OnSyntheticTrialsChanged(synthetic_trial_groups_); } void SyntheticTrialRegistry::RemoveSyntheticTrialObserver( SyntheticTrialObserver* observer) { synthetic_trial_observer_list_.RemoveObserver(observer); } void SyntheticTrialRegistry::RegisterExternalExperiments( const std::string& fallback_study_name, const std::vector& experiment_ids, SyntheticTrialRegistry::OverrideMode mode) { DCHECK(!fallback_study_name.empty()); base::FieldTrialParams params; if (enable_external_experiment_allowlist_ && !GetFieldTrialParamsByFeature(internal::kExternalExperimentAllowlist, ¶ms)) { return; } // When overriding previous external experiments, remove them now. if (mode == kOverrideExistingIds) { auto is_external = [](const SyntheticTrialGroup& group) { return group.is_external; }; base::EraseIf(synthetic_trial_groups_, is_external); } const base::TimeTicks start_time = base::TimeTicks::Now(); int trials_added = 0; for (int experiment_id : experiment_ids) { const std::string experiment_id_str = base::NumberToString(experiment_id); const base::StringPiece study_name = GetStudyNameForExpId(fallback_study_name, params, experiment_id_str); if (study_name.empty()) continue; const uint32_t trial_hash = HashName(study_name); // If existing ids shouldn't be overridden, skip entries whose study names // are already registered. if (mode == kDoNotOverrideExistingIds) { auto matches_trial = [trial_hash](const SyntheticTrialGroup& group) { return group.id.name == trial_hash; }; const auto& groups = synthetic_trial_groups_; if (std::any_of(groups.begin(), groups.end(), matches_trial)) { continue; } } const uint32_t group_hash = HashName(experiment_id_str); // Since external experiments are not based on Chrome's low entropy source, // they are only sent to Google web properties for signed-in users to make // sure they couldn't be used to identify a user that's not signed-in. AssociateGoogleVariationIDForceHashes( GOOGLE_WEB_PROPERTIES_SIGNED_IN, {trial_hash, group_hash}, static_cast(experiment_id)); SyntheticTrialGroup entry(trial_hash, group_hash); entry.start_time = start_time; entry.is_external = true; synthetic_trial_groups_.push_back(entry); trials_added++; } base::UmaHistogramCounts100("UMA.ExternalExperiment.GroupCount", trials_added); if (trials_added > 0) NotifySyntheticTrialObservers(); } void SyntheticTrialRegistry::RegisterSyntheticFieldTrial( const SyntheticTrialGroup& trial) { for (auto& entry : synthetic_trial_groups_) { if (entry.id.name == trial.id.name) { if (entry.id.group != trial.id.group) { entry.id.group = trial.id.group; entry.start_time = base::TimeTicks::Now(); NotifySyntheticTrialObservers(); } return; } } SyntheticTrialGroup trial_group = trial; trial_group.start_time = base::TimeTicks::Now(); synthetic_trial_groups_.push_back(trial_group); NotifySyntheticTrialObservers(); } base::StringPiece SyntheticTrialRegistry::GetStudyNameForExpId( const std::string& fallback_study_name, const base::FieldTrialParams& params, const std::string& experiment_id) { if (!enable_external_experiment_allowlist_) return fallback_study_name; const auto it = params.find(experiment_id); if (it == params.end()) return base::StringPiece(); // To support additional parameters being passed, besides the study name, // truncate the study name at the first ',' character. // For example, for an entry like {"1234": "StudyName,FOO"}, we only want the // "StudyName" part. This allows adding support for additional things like FOO // in the future without backwards compatibility problems. const size_t comma_pos = it->second.find(','); const size_t truncate_pos = (comma_pos == std::string::npos ? it->second.length() : comma_pos); return base::StringPiece(it->second.data(), truncate_pos); } void SyntheticTrialRegistry::NotifySyntheticTrialObservers() { for (SyntheticTrialObserver& observer : synthetic_trial_observer_list_) { observer.OnSyntheticTrialsChanged(synthetic_trial_groups_); } } void SyntheticTrialRegistry::GetSyntheticFieldTrialsOlderThan( base::TimeTicks time, std::vector* synthetic_trials) const { DCHECK(synthetic_trials); synthetic_trials->clear(); for (const auto& entry : synthetic_trial_groups_) { if (entry.start_time <= time) synthetic_trials->push_back(entry.id); } } } // namespace variations