blob: 92feb7a284e20682ed3c10802bd4a8cb1c057921 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/tpcd/experiment/experiment_manager_impl.h"
#include <optional>
#include <string>
#include <utility>
#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/metrics/field_trial_params.h"
#include "base/no_destructor.h"
#include "base/sequence_checker.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/metrics/chrome_metrics_service_accessor.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/tpcd/experiment/tpcd_experiment_features.h"
#include "components/metrics/metrics_pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/privacy_sandbox/tpcd_pref_names.h"
#include "components/privacy_sandbox/tpcd_utils.h"
#include "components/variations/synthetic_trials.h"
#include "content/public/common/content_features.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "chromeos/ash/components/browser_context_helper/browser_context_types.h"
#endif // BUILDFLAG(IS_CHROMEOS)
namespace tpcd::experiment {
namespace {
const base::FeatureParam<std::string> kSyntheticTrialGroupOverride{
&features::kCookieDeprecationFacilitatedTesting,
"synthetic_trial_group_override", ""};
bool NeedsOnboardingForExperiment() {
if (!kDisable3PCookies.Get() && !kEnableSilentOnboarding.Get()) {
return false;
}
return kNeedOnboardingForSyntheticTrial.Get();
}
} // namespace
// static
ExperimentManagerImpl* ExperimentManagerImpl::GetForProfile(Profile* profile) {
if (!base::FeatureList::IsEnabled(
features::kCookieDeprecationFacilitatedTesting)) {
return nullptr;
}
#if BUILDFLAG(IS_CHROMEOS)
// Ash internal profile should not be accounted for the experiment
// eligibility, and therefore should not create the experiment manager.
if (!ash::IsUserBrowserContext(profile)) {
return nullptr;
}
#endif // BUILDFLAG(IS_CHROMEOS)
if (!features::kCookieDeprecationFacilitatedTestingEnableOTRProfiles.Get() &&
(profile->IsOffTheRecord() || profile->IsGuestSession())) {
return nullptr;
}
return GetInstance();
}
// static
ExperimentManagerImpl* ExperimentManagerImpl::GetInstance() {
static base::NoDestructor<ExperimentManagerImpl> instance;
return instance.get();
}
ExperimentManagerImpl::ExperimentManagerImpl() {
CHECK(base::FeatureList::IsEnabled(
features::kCookieDeprecationFacilitatedTesting));
PrefService* local_state = g_browser_process->local_state();
CHECK(local_state);
const int currentVersion = kVersion.Get();
if (local_state->GetInteger(prefs::kTPCDExperimentClientStateVersion) !=
currentVersion) {
local_state->SetInteger(prefs::kTPCDExperimentClientStateVersion,
currentVersion);
local_state->ClearPref(prefs::kTPCDExperimentClientState);
did_version_change_ = true;
}
// If client eligibility is already known, do not recompute it.
if (IsClientEligible().has_value()) {
// The user must be re-registered to the synthetic trial on restart.
MaybeUpdateSyntheticTrialRegistration();
return;
}
// `ExperimentManager` is a singleton that lives forever, therefore it's safe
// to use `base::Unretained()`.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ExperimentManagerImpl::CaptureEligibilityInLocalStatePref,
base::Unretained(this)),
kDecisionDelayTime.Get());
}
ExperimentManagerImpl::~ExperimentManagerImpl() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void ExperimentManagerImpl::SetClientEligibility(
bool is_eligible,
EligibilityDecisionCallback on_eligibility_decision_callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (std::optional<bool> client_is_eligible = IsClientEligible()) {
// If client eligibility is already known, just run callback.
client_is_eligible_ = *client_is_eligible;
std::move(on_eligibility_decision_callback).Run(client_is_eligible_);
return;
}
// Wait to run callback when decision is made in
// `CaptureEligibilityInLocalStatePref`
client_is_eligible_ = client_is_eligible_ && is_eligible;
callbacks_.push_back(std::move(on_eligibility_decision_callback));
}
void ExperimentManagerImpl::CaptureEligibilityInLocalStatePref() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Update kTPCDExperimentClientState in the local state prefs.
g_browser_process->local_state()->SetInteger(
prefs::kTPCDExperimentClientState,
client_is_eligible_
? static_cast<int>(utils::ExperimentState::kEligible)
: static_cast<int>(utils::ExperimentState::kIneligible));
// Register or unregister for the synthetic trial based on the new
// eligibility local state pref.
MaybeUpdateSyntheticTrialRegistration();
// Run the EligibilityDecisionCallback for every profile that marked its
// eligibility.
for (auto& callback : callbacks_) {
std::move(callback).Run(client_is_eligible_);
}
callbacks_.clear();
}
void ExperimentManagerImpl::MaybeUpdateSyntheticTrialRegistration() {
if (!CanRegisterSyntheticTrial()) {
return;
}
std::optional<bool> is_client_eligible = IsClientEligible();
CHECK(is_client_eligible.has_value());
std::string eligible_group_name =
!kSyntheticTrialGroupOverride.Get().empty()
? kSyntheticTrialGroupOverride.Get()
: features::kCookieDeprecationLabel.Get();
std::string group_name = *is_client_eligible
? eligible_group_name
: kSyntheticTrialInvalidGroupName;
ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial(
kSyntheticTrialName, group_name,
variations::SyntheticTrialAnnotationMode::kCurrentLog);
}
std::optional<bool> ExperimentManagerImpl::IsClientEligible() const {
if (kForceEligibleForTesting.Get()) {
return true;
}
switch (g_browser_process->local_state()->GetInteger(
prefs::kTPCDExperimentClientState)) {
case static_cast<int>(utils::ExperimentState::kEligible):
case static_cast<int>(utils::ExperimentState::kOnboarded):
return true;
case static_cast<int>(utils::ExperimentState::kIneligible):
return false;
case static_cast<int>(utils::ExperimentState::kUnknownEligibility):
return std::nullopt;
default:
// invalid
return false;
}
}
bool ExperimentManagerImpl::DidVersionChange() const {
return did_version_change_;
}
void ExperimentManagerImpl::NotifyProfileTrackingProtectionOnboarded() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!NeedsOnboardingForExperiment()) {
return;
}
switch (g_browser_process->local_state()->GetInteger(
prefs::kTPCDExperimentClientState)) {
case static_cast<int>(utils::ExperimentState::kEligible):
break;
case static_cast<int>(utils::ExperimentState::kIneligible):
case static_cast<int>(utils::ExperimentState::kOnboarded):
return;
case static_cast<int>(utils::ExperimentState::kUnknownEligibility):
if (kForceEligibleForTesting.Get()) {
break;
} else {
return;
}
default:
// invalid
return;
}
g_browser_process->local_state()->SetInteger(
prefs::kTPCDExperimentClientState,
static_cast<int>(utils::ExperimentState::kOnboarded));
MaybeUpdateSyntheticTrialRegistration();
}
bool ExperimentManagerImpl::CanRegisterSyntheticTrial() const {
switch (g_browser_process->local_state()->GetInteger(
prefs::kTPCDExperimentClientState)) {
case static_cast<int>(utils::ExperimentState::kEligible):
return !NeedsOnboardingForExperiment();
case static_cast<int>(utils::ExperimentState::kIneligible):
case static_cast<int>(utils::ExperimentState::kOnboarded):
return true;
case static_cast<int>(utils::ExperimentState::kUnknownEligibility):
if (kForceEligibleForTesting.Get()) {
return !NeedsOnboardingForExperiment();
} else {
return false;
}
default:
// invalid
return true;
}
}
} // namespace tpcd::experiment