| // 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"; |
| } |