chromium/chrome/browser/ash/hats/hats_finch_helper.cc

// Copyright 2016 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/ash/hats/hats_finch_helper.h"

#include "base/metrics/field_trial_params.h"
#include "base/rand_util.h"
#include "base/strings/string_util.h"
#include "chrome/browser/ash/hats/hats_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"

namespace ash {

// These values should match the param key values in the finch config file.
// static
const char HatsFinchHelper::kEnabledForGooglersParam[] = "enabled_for_googlers";
// static
const char HatsFinchHelper::kCustomClientDataParam[] = "custom_client_data";
// static
const char HatsFinchHelper::kProbabilityParam[] = "prob";
// static
const char HatsFinchHelper::kResetAllParam[] = "reset_all";
// static
const char HatsFinchHelper::kResetSurveyCycleParam[] = "reset_survey_cycle";
// static
const char HatsFinchHelper::kSurveyCycleLengthParam[] = "survey_cycle_length";
// static
const char HatsFinchHelper::kSurveyStartDateMsParam[] = "survey_start_date_ms";
// static
const char HatsFinchHelper::kTriggerIdParam[] = "trigger_id";

const char HatsFinchHelper::kHistogramNameParam[] = "histogram_name";

std::string HatsFinchHelper::GetTriggerID(const HatsConfig& hats_config) {
  DCHECK(base::FeatureList::IsEnabled(hats_config.feature));
  return base::GetFieldTrialParamValueByFeature(hats_config.feature,
                                                kTriggerIdParam);
}

// To enable UMA collection for a specific survey, the Finch configuration
// file for this survey should be updated to include a `histogram_name`
// parameter along side the `trigger_id` parameter. Without this
// `histogram_name` parameter specified, no survey-specific UMA data will be
// collected.
std::string HatsFinchHelper::GetHistogramName(const HatsConfig& hats_config) {
  DCHECK(base::FeatureList::IsEnabled(hats_config.feature));
  // Fetch the histogram name from the feature parameters, if it is assigned.
  // An empty string will be returned if the parameter is not set in Finch.
  // This value should be a valid histogram that has been registered in the
  // histograms.xml file, otherwise it will not be ingested by UMA.
  std::string histogram_name = base::GetFieldTrialParamValueByFeature(
      hats_config.feature, kHistogramNameParam);

  // Valid histogram names for HaTS/UMA integration must start with the
  // prefix "ChromeOS.HaTS.". This corresponds to the histogram definition
  // in the histograms.xml file.
  if (!base::StartsWith(histogram_name, "ChromeOS.HaTS.")) {
    LOG(ERROR) << "Invalid HaTS histogram name: " << histogram_name;
    return std::string();
  }

  return histogram_name;
}

std::string HatsFinchHelper::GetCustomClientDataAsString(
    const HatsConfig& hats_config) {
  DCHECK(base::FeatureList::IsEnabled(hats_config.feature));
  return base::GetFieldTrialParamValueByFeature(hats_config.feature,
                                                kCustomClientDataParam);
}

bool HatsFinchHelper::IsEnabledForGooglers(const HatsConfig& hats_config) {
  DCHECK(base::FeatureList::IsEnabled(hats_config.feature));
  return base::GetFieldTrialParamByFeatureAsBool(
      hats_config.feature, kEnabledForGooglersParam, false);
}

HatsFinchHelper::HatsFinchHelper(Profile* profile,
                                 const HatsConfig& hats_config)
    : profile_(profile), hats_config_(hats_config) {
  LoadFinchParamValues(hats_config);

  // Reset prefs related to survey cycle if the finch seed has the reset param
  // set. Do no futher op until a new finch seed with the reset flags unset is
  // received.
  // Warning: |reset_hats_| applies to all surveys.
  if (reset_survey_cycle_ || reset_hats_) {
    profile_->GetPrefs()->ClearPref(hats_config.cycle_end_timestamp_pref_name);
    profile_->GetPrefs()->ClearPref(hats_config.is_selected_pref_name);
    if (reset_hats_)
      profile_->GetPrefs()->ClearPref(prefs::kHatsLastInteractionTimestamp);
    return;
  }

  CheckForDeviceSelection();
}

HatsFinchHelper::~HatsFinchHelper() = default;

void HatsFinchHelper::LoadFinchParamValues(const HatsConfig& hats_config) {
  if (!base::FeatureList::IsEnabled(hats_config.feature))
    return;

  probability_of_pick_ = base::GetFieldTrialParamByFeatureAsDouble(
      hats_config.feature, kProbabilityParam, -1.0);

  if (probability_of_pick_ < 0.0 || probability_of_pick_ > 1.0) {
    LOG(ERROR) << "Invalid value for probability: " << probability_of_pick_;
    probability_of_pick_ = 0;
  }

  survey_cycle_length_ = base::GetFieldTrialParamByFeatureAsInt(
      hats_config.feature, kSurveyCycleLengthParam, 0);

  if (survey_cycle_length_ <= 0) {
    LOG(ERROR) << "Invalid value for survey cycle length: "
               << survey_cycle_length_;
    survey_cycle_length_ = INT_MAX;
  }

  double first_survey_start_date_ms = base::GetFieldTrialParamByFeatureAsDouble(
      hats_config.feature, kSurveyStartDateMsParam, -1.0);
  if (first_survey_start_date_ms < 0) {
    LOG(ERROR) << "Invalid timestamp for survey start date: "
               << first_survey_start_date_ms;
    // Set a random date in the distant future so that the survey never starts
    // until a new finch seed is received with the correct start date.
    first_survey_start_date_ms =
        2 * base::Time::Now().InMillisecondsFSinceUnixEpoch();
  }
  first_survey_start_date_ =
      base::Time().FromMillisecondsSinceUnixEpoch(first_survey_start_date_ms);

  trigger_id_ = GetTriggerID(hats_config);

  reset_survey_cycle_ = base::GetFieldTrialParamByFeatureAsBool(
      hats_config.feature, kResetSurveyCycleParam, false);

  reset_hats_ = base::GetFieldTrialParamByFeatureAsBool(hats_config.feature,
                                                        kResetAllParam, false);

  // Set every property to no op values if this is a reset finch seed.
  if (reset_survey_cycle_ || reset_hats_) {
    probability_of_pick_ = 0;
    survey_cycle_length_ = INT_MAX;
    first_survey_start_date_ = base::Time().FromMillisecondsSinceUnixEpoch(
        2 * base::Time::Now().InMillisecondsFSinceUnixEpoch());
  }
}

bool HatsFinchHelper::HasPreviousCycleEnded() {
  int64_t serialized_timestamp = profile_->GetPrefs()->GetInt64(
      hats_config_->cycle_end_timestamp_pref_name);
  base::Time recent_survey_cycle_end_time =
      base::Time::FromInternalValue(serialized_timestamp);
  return recent_survey_cycle_end_time < base::Time::Now();
}

base::Time HatsFinchHelper::ComputeNextEndDate() {
  base::Time start_date = first_survey_start_date_;
  base::TimeDelta delta = base::Days(survey_cycle_length_);
  do {
    start_date += delta;
  } while (start_date < base::Time::Now());
  return start_date;
}

void HatsFinchHelper::CheckForDeviceSelection() {
  device_is_selected_for_cycle_ = false;

  // The dice is rolled only once per survey cycle. If it has already been done
  // for the current cycle, then return the stored value of the result.
  if (!HasPreviousCycleEnded()) {
    device_is_selected_for_cycle_ =
        profile_->GetPrefs()->GetBoolean(hats_config_->is_selected_pref_name);
    return;
  }

  // If the start date for the survey is in the future, do nothing.
  if (first_survey_start_date_ > base::Time::Now())
    return;

  // Start a new survey cycle and compute its end date.
  base::Time survey_cycle_end_date = ComputeNextEndDate();

  PrefService* pref_service = profile_->GetPrefs();
  pref_service->SetInt64(hats_config_->cycle_end_timestamp_pref_name,
                         survey_cycle_end_date.ToInternalValue());

  double rand_double = base::RandDouble();
  bool is_selected = false;
  if (rand_double < probability_of_pick_)
    is_selected = true;

  // Check if the trigger id is a valid string. Trigger IDs are a hash strings
  // of around 26 characters.
  is_selected = is_selected && (trigger_id_.length() > 15);

  pref_service->SetBoolean(hats_config_->is_selected_pref_name, is_selected);
  device_is_selected_for_cycle_ = is_selected;
}

}  // namespace ash