blob: 211cf7bd4c322b27e90cd5f50fe68feb09aadd36 [file] [log] [blame]
// Copyright 2025 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/variations/sticky_activation_manager.h"
#include "base/debug/dump_without_crashing.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_list_including_low_anonymity.h"
#include "base/strings/string_split.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/variations/pref_names.h"
namespace variations {
namespace {
// Used as the group names for studies that we know have STICKY_AFTER_QUERY
// activation, but haven't been made active yet.
//
// Note: We intentionally use the same character as the separator for the pref,
// since a) that character is already reserved and can't appear naturally in
// these strings and b) to guarantee it's not something we'd load or save to the
// pref, as doing so would make it invalid.
const char kInactiveStickyTrialSentinel[] = "/";
// Parses the sticky studies pref value, which is expected to be of the format
// "Study1/Group1/Study2/Group2" and returns as a map from trial names to
// groups names.
StickyActivationManager::TrialNameToGroupNameMap ParsePref(
const std::string& pref_value) {
StickyActivationManager::TrialNameToGroupNameMap result;
// Note: Even though base::FieldTrial::ParseFieldTrialsString() provides more
// features than we need, by using it we benefit from the validation it does.
std::vector<base::FieldTrial::State> entries;
if (!base::FieldTrial::ParseFieldTrialsString(
pref_value, /*override_trials=*/false, entries)) {
// This is not a CHECK() since the pref value is external, but we still want
// to monitor the occurrence of invalid prefs in case there is a a code
// issue, so dump without crashing to signal the issue.
base::debug::DumpWithoutCrashing();
return result;
}
for (const auto& entry : entries) {
result[std::string(entry.trial_name)] = std::string(entry.group_name);
}
return result;
}
// Encodes `trials` as a string pref value of the format
// "Study1/Group1/Study2/Group2".
std::string EncodePref(
const StickyActivationManager::TrialNameToGroupNameMap& trials) {
std::string pref_value;
for (const auto& [key, value] : trials) {
if (value == kInactiveStickyTrialSentinel) {
continue;
}
if (!pref_value.empty()) {
base::StrAppend(&pref_value, {"/"});
}
base::StrAppend(&pref_value, {key, "/", value});
}
return pref_value;
}
} // namespace
StickyActivationManager::StickyActivationManager(PrefService* local_state,
bool sticky_activation_enabled)
: local_state_(local_state),
sticky_activation_enabled_(sticky_activation_enabled) {
if (local_state && sticky_activation_enabled_) {
loaded_sticky_trials_ =
ParsePref(local_state_->GetString(prefs::kVariationsStickyStudies));
}
}
StickyActivationManager::~StickyActivationManager() {
if (monitoring_started_ && sticky_activation_enabled_) {
base::FieldTrialListIncludingLowAnonymity::RemoveObserver(this);
}
}
// static
void StickyActivationManager::RegisterPrefs(PrefRegistrySimple& registry) {
registry.RegisterStringPref(prefs::kVariationsStickyStudies, "",
PrefRegistry::LOSSY_PREF);
}
void StickyActivationManager::StartMonitoring() {
CHECK(!monitoring_started_);
monitoring_started_ = true;
if (!sticky_activation_enabled_) {
return;
}
// Clear the loaded sticky trials, since these are no longer needed. The
// entries that were activated have been copied over to
// `active_sticky_trials_`.
loaded_sticky_trials_.clear();
base::FieldTrialListIncludingLowAnonymity::AddObserver(this);
UpdatePref();
}
bool StickyActivationManager::ShouldActivate(const std::string& trial_name,
const std::string& group_name) {
CHECK(!monitoring_started_);
if (!sticky_activation_enabled_) {
return false;
}
auto it = loaded_sticky_trials_.find(trial_name);
if (it != loaded_sticky_trials_.end() && it->second == group_name) {
active_sticky_trials_[trial_name] = group_name;
return true;
}
// Otherwise, we know this is a sticky trial and it's not active yet, so
// reserve a slot for it so we can tell it's a sticky trial when we observe
// its activation.
active_sticky_trials_[trial_name] = kInactiveStickyTrialSentinel;
return false;
}
void StickyActivationManager::OnFieldTrialGroupFinalized(
const base::FieldTrial& trial,
const std::string& group_name) {
CHECK(monitoring_started_);
CHECK(sticky_activation_enabled_);
// Check whether the trial is present in `active_sticky_trials_`, which is how
// we track which trials have the STICKY_AFTER_QUERY activation type.
auto it = active_sticky_trials_.find(trial.trial_name());
if (it != active_sticky_trials_.end()) {
// We don't expect to be notified of the same trial twice, so the entry for
// this trial should be the sentinel.
//
// Note: We DCHECK() instead of CHECK() here because this code being hit
// relies both on a client-side coding bug, but also a specific server-side
// payload that would exercise this (i.e. existence of STICKY_AFTER_QUERY
// studies). We don't want a case where the client-side bug is introduced
// but the server-side payload not exercising this to make it to Stable and
// then start crashing lots of users, so use a DCHECK.
DCHECK_EQ(it->second, kInactiveStickyTrialSentinel);
it->second = group_name;
UpdatePref();
}
}
void StickyActivationManager::UpdatePref() {
CHECK(monitoring_started_);
CHECK(sticky_activation_enabled_);
// TODO: crbug.com/435630455 - Instead of updating the pref each time,
// schedule an update so that we can batch multiple updates together.
if (!local_state_) {
return;
}
std::string pref_value = EncodePref(active_sticky_trials_);
local_state_->SetString(prefs::kVariationsStickyStudies, pref_value);
}
} // namespace variations