blob: b2efa5f60b84bf38ac43ccf6e7c56ee13b0ba481 [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/user_education/user_education_configuration_provider.h"
#include <algorithm>
#include "base/feature_list.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "components/feature_engagement/public/configuration.h"
#include "components/user_education/common/feature_promo/feature_promo_registry.h"
#include "components/user_education/common/feature_promo/feature_promo_specification.h"
#include "components/user_education/common/user_education_features.h"
namespace {
std::string FeatureNameToEventName(const base::Feature& feature) {
constexpr char kIPHPrefix[] = "IPH_";
std::string_view name = feature.name;
auto remainder = base::RemovePrefix(name, kIPHPrefix);
return std::string(remainder.value_or(name));
}
// Returns whether a comparator is bounded from above.
bool IsAdditionalConfigBounded(const feature_engagement::EventConfig& config) {
const auto& comparator = config.comparator;
return (comparator.value > 0 &&
comparator.type == feature_engagement::LESS_THAN) ||
comparator.type == feature_engagement::LESS_THAN_OR_EQUAL ||
(comparator.value == 0 &&
comparator.type == feature_engagement::EQUAL);
}
} // namespace
// Implemented in chrome/browser/ui/views/user_education.
extern void MaybeRegisterChromeFeaturePromos(
user_education::FeaturePromoRegistry& registry);
UserEducationConfigurationProvider::UserEducationConfigurationProvider() {
MaybeRegisterChromeFeaturePromos(registry_);
}
UserEducationConfigurationProvider::UserEducationConfigurationProvider(
user_education::FeaturePromoRegistry registry_for_testing)
: registry_(std::move(registry_for_testing)) {}
UserEducationConfigurationProvider::~UserEducationConfigurationProvider() =
default;
bool UserEducationConfigurationProvider::MaybeProvideFeatureConfiguration(
const base::Feature& feature,
feature_engagement::FeatureConfig& config,
const feature_engagement::FeatureVector& known_features,
const feature_engagement::GroupVector& known_groups) const {
// Features not controlled by FeaturePromoController are ignored.
if (!registry_.IsFeatureRegistered(feature)) {
return false;
}
const auto* const promo_spec = registry_.GetParamsForFeature(feature);
// These are baseline session rate values.
config.session_rate.type = feature_engagement::ANY;
config.session_rate.value = 0;
config.session_rate_impact.type =
feature_engagement::SessionRateImpact::Type::NONE;
config.session_rate_impact.affected_features.reset();
switch (promo_spec->promo_type()) {
case user_education::FeaturePromoSpecification::PromoType::kSnooze:
case user_education::FeaturePromoSpecification::PromoType::kCustomAction:
case user_education::FeaturePromoSpecification::PromoType::kTutorial:
case user_education::FeaturePromoSpecification::PromoType::kCustomUi:
// Heavyweight promos prevent future low-priority heavyweight promos.
config.session_rate_impact.type =
feature_engagement::SessionRateImpact::Type::ALL;
break;
case user_education::FeaturePromoSpecification::PromoType::kToast:
case user_education::FeaturePromoSpecification::PromoType::kLegacy:
case user_education::FeaturePromoSpecification::PromoType::kRotating:
// Toasts and rotating promos can always show and do not impact other IPH.
break;
case user_education::FeaturePromoSpecification::PromoType::kUnspecified:
// Should never get here.
NOTREACHED();
}
// All IPH block all other IPH.
config.blocked_by.type = feature_engagement::BlockedBy::Type::ALL;
config.blocked_by.affected_features.reset();
config.blocking.type = feature_engagement::Blocking::Type::ALL;
// Set up a default trigger if one is not present.
if (config.trigger.name.empty()) {
config.trigger.name = GetDefaultTriggerName(feature);
}
config.trigger.comparator.type = feature_engagement::ANY;
config.trigger.comparator.value = 0;
config.trigger.storage = feature_engagement::kMaxStoragePeriod;
config.trigger.window = feature_engagement::kMaxStoragePeriod;
auto& additional = promo_spec->additional_conditions();
// Set up a default "used" event if one is not present.
if (config.used.name.empty()) {
config.used.name = GetDefaultUsedName(feature);
}
// The allowed number of uses is only overridden if it's not set.
if (config.used.comparator.value == 0 &&
additional.used_limit().has_value()) {
config.used.comparator.type = feature_engagement::LESS_THAN_OR_EQUAL;
config.used.comparator.value = additional.used_limit().value();
} else if (config.used.comparator.type == feature_engagement::ANY) {
// However, since the default comparator is (ANY, 0) which is invalid for a
// "used" event, change it to a reasonable default.
config.used.comparator.type = feature_engagement::EQUAL;
config.used.comparator.value = 0;
}
// The default window and storage are zero, which are not valid. If these have
// not been set, set them to reasonable values.
if (config.used.window == 0) {
config.used.window = feature_engagement::kMaxStoragePeriod;
}
if (config.used.storage < config.used.window) {
config.used.storage = config.used.window;
}
// In V2, since trigger config is overwritten, also remove additional
// references in the existing event configs.
std::erase_if(config.event_configs, [&trigger_name = config.trigger.name](
const auto& event_config) {
return event_config.name == trigger_name;
});
// Set up additional constraints, if specified and not overridden in the
// existing config.
std::vector<feature_engagement::EventConfig> new_event_configs;
for (const auto& condition : additional.additional_conditions()) {
const std::string name = condition.event_name;
// While the "used" condition can participate in additional constraints, the
// trigger should not.
CHECK_NE(config.trigger.name, name);
// Determine if there's already a config for this event.
if (std::any_of(config.event_configs.begin(), config.event_configs.end(),
[&name](const auto& event_config) {
return event_config.name == name;
})) {
continue;
}
// Add the additional event configuration.
feature_engagement::ComparatorType comparator;
switch (condition.constraint) {
using Constraint = user_education::FeaturePromoSpecification::
AdditionalConditions::Constraint;
case Constraint::kAtLeast:
comparator = feature_engagement::GREATER_THAN_OR_EQUAL;
break;
case Constraint::kAtMost:
comparator = feature_engagement::LESS_THAN_OR_EQUAL;
break;
case Constraint::kExactly:
comparator = feature_engagement::EQUAL;
break;
}
const size_t window =
condition.in_days.value_or(feature_engagement::kMaxStoragePeriod);
new_event_configs.emplace_back(
name, feature_engagement::Comparator{comparator, condition.count},
window, window);
}
std::copy(new_event_configs.begin(), new_event_configs.end(),
std::inserter(config.event_configs, config.event_configs.begin()));
// Set up some reasonable availability values.
if (config.availability != feature_engagement::Comparator()) {
// There is already configuration specified for availability other than the
// default. Make sure the availability is a lower bound.
if (config.availability.type != feature_engagement::GREATER_THAN) {
config.availability.type = feature_engagement::GREATER_THAN_OR_EQUAL;
}
} else if (additional.initial_delay_days().has_value()) {
// Explicit availability was set in User Ed config, so use that.
config.availability.type = feature_engagement::GREATER_THAN_OR_EQUAL;
config.availability.value = additional.initial_delay_days().value();
} else if (std::any_of(config.event_configs.begin(),
config.event_configs.end(),
&IsAdditionalConfigBounded)) {
// If any of the additional event configs have put a maximum cap on a
// particular event, then default to a minimum availability period to give
// enough time to determine if the events in question happened.
config.availability.type = feature_engagement::GREATER_THAN_OR_EQUAL;
config.availability.value = 7;
} else {
// This should already be the default - the promo will be available as soon
// as the feature is enabled.
config.availability.type = feature_engagement::ANY;
config.availability.value = 0;
}
// By this point the configuration should be valid.
config.valid = true;
return true;
}
const char*
UserEducationConfigurationProvider::GetConfigurationSourceDescription() const {
return "Browser User Education";
}
// static
std::string UserEducationConfigurationProvider::GetDefaultTriggerName(
const base::Feature& feature) {
return FeatureNameToEventName(feature) + "_trigger";
}
std::string UserEducationConfigurationProvider::GetDefaultUsedName(
const base::Feature& feature) {
return FeatureNameToEventName(feature) + "_used";
}