// Copyright 2022 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/metrics/chrome_browser_sampling_trials.h"

#include <utility>

#include "base/feature_list.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_params.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/metrics/chrome_metrics_services_manager_client.h"
#include "chrome/common/channel_info.h"
#include "components/ukm/ukm_recorder_impl.h"
#include "components/version_info/channel.h"

namespace metrics {
namespace {

// Note that the trial name must be kept in sync with the server config
// controlling sampling. If they don't match, then clients will be shuffled into
// different groups when the server config takes over from the fallback trial.
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_ANDROID)
constexpr char kSamplingTrialName[] = "MetricsAndCrashSampling";
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_ANDROID)
#if BUILDFLAG(IS_ANDROID)
constexpr char kPostFREFixSamplingTrialName[] =
    "PostFREFixMetricsAndCrashSampling";
#endif  // BUILDFLAG(IS_ANDROID)
constexpr char kUkmSamplingTrialName[] = "UkmSamplingRate";

#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_ANDROID)
// Appends a group to the sampling controlling |trial|. The group will be
// associated with a variation param for reporting sampling |rate| in per mille.
void AppendSamplingTrialGroup(const std::string& group_name,
                              int rate,
                              bool disable_crashes,
                              base::FieldTrial* trial) {
  std::map<std::string, std::string> params = {
      {metrics::internal::kRateParamName, base::NumberToString(rate)}};

  if (disable_crashes) {
    params.insert({"disable_crashes", "true"});
  }

  base::AssociateFieldTrialParams(trial->trial_name(), group_name,
                                  std::move(params));
  trial->AppendGroup(group_name, rate);
}

// Unconditionally attempts to create a field trial to control client side
// metrics/crash sampling to use as a fallback when one hasn't been
// provided. This is expected to occur on first-run on platforms that don't
// have first-run variations support, or when no valid seed is available. This
// should only be called when there is no existing field trial controlling the
// sampling feature. |feature_name| is the name of the feature that determines
// sampling.
// Rates:
//   |sampled_in_rate_per_mille| is the sampling rate per mille.
//   |reporting_full_rate_per_mille| is the rate for clients who are sampled in,
//   in the special ReportingFull group.
//   All other clients are in the OutOfSampling group.
void CreateFallbackSamplingTrial(
    const base::FieldTrial::EntropyProvider& entropy_provider,
    const std::string& trial_name,
    const std::string& feature_name,
    const int sampled_in_rate_per_mille,
    const int reporting_full_rate_per_mille,
    const bool starts_active,
    base::FeatureList* feature_list) {
  scoped_refptr<base::FieldTrial> trial(
      base::FieldTrialList::FactoryGetFieldTrial(
          trial_name, /*total_probability=*/1000, "Default", entropy_provider));

  // Like the trial name, the order that these groups are added to the trial
  // must be kept in sync with the order that they appear in the server config.
  // The desired order is: OutOfReportingSample, ReportingFull,
  // InReportingSample.

  const char kSampledOutGroup[] = "OutOfReportingSample";
  const int sampled_out_rate_per_mille =
      1000 - sampled_in_rate_per_mille - reporting_full_rate_per_mille;
  AppendSamplingTrialGroup(kSampledOutGroup, sampled_out_rate_per_mille,
                           /*disable_crashes=*/false, trial.get());

  // This group uploads to UMA but does not upload crashes.
  const char kReportingFullGroup[] = "ReportingFull";
  AppendSamplingTrialGroup(kReportingFullGroup, reporting_full_rate_per_mille,
                           /*disable_crashes=*/true, trial.get());

  const char kInSampleGroup[] = "InReportingSample";
  AppendSamplingTrialGroup(kInSampleGroup, sampled_in_rate_per_mille,
                           /*disable_crashes=*/false, trial.get());

  // Set up the feature. This must be done after all groups are added since
  // GetGroupNameWithoutActivation() will finalize the group choice.
  const std::string& group_name = trial->GetGroupNameWithoutActivation();

  // Note that this will set both ReportingFull and InReportingSample to enable
  // the feature.
  feature_list->RegisterFieldTrialOverride(
      feature_name,
      group_name == kSampledOutGroup
          ? base::FeatureList::OVERRIDE_DISABLE_FEATURE
          : base::FeatureList::OVERRIDE_ENABLE_FEATURE,
      trial.get());

  if (starts_active) {
    trial->Activate();
  }
}
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_ANDROID)

// Unconditionally attempts to create a field trial to control client side
// UKM sampling to use as a fallback when one hasn't been provided. This is
// expected to occur on first-run on platforms that don't have first-run
// variations support. This should only be called when there is no existing
// field trial controlling the sampling feature.
void CreateFallbackUkmSamplingTrial(
    const base::FieldTrial::EntropyProvider& entropy_provider,
    bool is_stable_channel,
    base::FeatureList* feature_list) {
  static const char kSampledGroup_Stable[] = "Sampled_NoSeed_Stable";
  static const char kSampledGroup_Other[] = "Sampled_NoSeed_Other";
  const char* sampled_group = kSampledGroup_Other;
  int default_sampling = 1;  // Sampling is 1-in-N; this is N.

  // Nothing is sampled out except for "stable" which omits almost everything
  // in this configuration. This is done so that clients that fail to receive
  // a configuration from the server do not bias aggregated results because
  // of a relatively large number of records from them.
  if (is_stable_channel) {
    sampled_group = kSampledGroup_Stable;
    default_sampling = 1000000;
  }

  scoped_refptr<base::FieldTrial> trial(
      base::FieldTrialList::FactoryGetFieldTrial(
          kUkmSamplingTrialName, /*total_probability=*/100, sampled_group,
          entropy_provider));

  // Everybody (100%) should have a sampling configuration.
  std::map<std::string, std::string> params = {
      {"_default_sampling", base::NumberToString(default_sampling)}};
  base::AssociateFieldTrialParams(trial->trial_name(), sampled_group,
                                  std::move(params));
  trial->AppendGroup(sampled_group, 100);

  // Setup the feature.
  feature_list->RegisterFieldTrialOverride(
      ukm::kUkmSamplingRateFeature.name,
      base::FeatureList::OVERRIDE_ENABLE_FEATURE, trial.get());
}

}  // namespace

void CreateFallbackSamplingTrialsIfNeeded(
    const base::FieldTrial::EntropyProvider& entropy_provider,
    base::FeatureList* feature_list) {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_ANDROID)

  const bool is_stable = chrome::GetChannel() == version_info::Channel::STABLE;

  if (!base::FieldTrialList::TrialExists(kSamplingTrialName)) {
#if BUILDFLAG(IS_WIN)

    // On all channels except stable, we sample out at a minimal rate to ensure
    // the code paths are exercised in the wild before hitting stable.
    const int kPreStableSampledInRatePerMille = 990;    // 99%
    const int kPreStableReportingFullRatePerMille = 5;  // 0.5%
    // This leaves 0.5% for OutOfReportingSample.

    const int kStableSampledInRatePerMille = 100;      // 10%
    const int kStableReportingFullRatePerMille = 900;  // 90%

#endif  // BUILDFLAG(IS_WIN)

#if BUILDFLAG(IS_ANDROID)

    // On all channels except stable, we sample out at a minimal rate to ensure
    // the code paths are exercised in the wild before hitting stable.
    const int kPreStableSampledInRatePerMille = 995;    // 99.5%
    const int kPreStableReportingFullRatePerMille = 0;  // 0%
    // This leaves 0.5% for OutOfReportingSample.

    // We use 5.3% for this set of users to work around an old bug
    // (crbug.com/40218371). This should be ~10% in practice.
    const int kStableSampledInRatePerMille = 53;     // 5.3%
    const int kStableReportingFullRatePerMille = 0;  // 0%

#endif  // BUILDFLAG(IS_ANDROID)

    const int kSamplingInRatePerMille = is_stable
                                            ? kStableSampledInRatePerMille
                                            : kPreStableSampledInRatePerMille;

    const int kReportingFullRatePerMille =
        is_stable ? kStableReportingFullRatePerMille
                  : kPreStableReportingFullRatePerMille;

    // Note that the trial has to be activated immediately. Otherwise, it would
    // be possible for this session to crash before its feature was queried, and
    // the independent log produced would not contain the sampling trial.
    CreateFallbackSamplingTrial(
        entropy_provider, kSamplingTrialName,
        metrics::internal::kMetricsReportingFeature.name,
        kSamplingInRatePerMille, kReportingFullRatePerMille,
        /*starts_active=*/true, feature_list);
  }

#if BUILDFLAG(IS_ANDROID)
  if (!base::FieldTrialList::TrialExists(kPostFREFixSamplingTrialName)) {
    // On all channels except stable, we sample out at a minimal rate to ensure
    // the code paths are exercised in the wild before hitting stable.
    const int kPreStableSampledInRatePerMille = 995;  // 99.5%

    // This is meant to be 10%, and this population, unlike the set of users
    // under the kSamplingTrialName trial should correctly be 10% in practice.
    const int kStableSampledInRatePerMille = 100;  // 10%

    const int kReportingFullRatePerMille = 0;

    // Note that as per the serverside config, this trial does not start active
    // (so that it is possible to determine from the serverside whether the
    // client used the old or new trial to determine sampling). So if Chrome
    // crashes before its feature is queried, the independent log produced will
    // not contain this trial, even if the client normally uses this trial to
    // determine sampling.
    CreateFallbackSamplingTrial(
        entropy_provider, kPostFREFixSamplingTrialName,
        metrics::internal::kPostFREFixMetricsReportingFeature.name,
        is_stable ? kStableSampledInRatePerMille
                  : kPreStableSampledInRatePerMille,
        kReportingFullRatePerMille,
        /*starts_active=*/false, feature_list);
  }
#endif  // BUILDFLAG(IS_ANDROID)

#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_ANDROID)
}

void CreateFallbackUkmSamplingTrialIfNeeded(
    const base::FieldTrial::EntropyProvider& entropy_provider,
    base::FeatureList* feature_list) {
  if (!base::FieldTrialList::TrialExists(kUkmSamplingTrialName)) {
    const bool is_stable =
        chrome::GetChannel() == version_info::Channel::STABLE;
    CreateFallbackUkmSamplingTrial(entropy_provider, is_stable, feature_list);
  }
}

}  // namespace metrics
