| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/feature_engagement/internal/chrome_variations_configuration.h" |
| |
| #include <map> |
| #include <memory> |
| #include <string> |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/feature_list.h" |
| #include "base/logging.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/raw_ref.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "components/feature_engagement/internal/stats.h" |
| #include "components/feature_engagement/public/configuration.h" |
| #include "components/feature_engagement/public/feature_configurations.h" |
| #include "components/feature_engagement/public/feature_list.h" |
| #include "components/feature_engagement/public/group_configurations.h" |
| |
| namespace { |
| |
| const char kComparatorTypeAny[] = "any"; |
| const char kComparatorTypeLessThan[] = "<"; |
| const char kComparatorTypeGreaterThan[] = ">"; |
| const char kComparatorTypeLessThanOrEqual[] = "<="; |
| const char kComparatorTypeGreaterThanOrEqual[] = ">="; |
| const char kComparatorTypeEqual[] = "=="; |
| const char kComparatorTypeNotEqual[] = "!="; |
| |
| const char kImpactedFeaturesTypeAll[] = "all"; |
| const char kImpactedFeaturesTypeNone[] = "none"; |
| |
| const char kEventConfigUsedKey[] = "event_used"; |
| const char kEventConfigTriggerKey[] = "event_trigger"; |
| const char kEventConfigKeyPrefix[] = "event_"; |
| const char kSessionRateKey[] = "session_rate"; |
| const char kSessionRateImpactKey[] = "session_rate_impact"; |
| const char kBlockingKey[] = "blocking"; |
| const char kBlockedByKey[] = "blocked_by"; |
| const char kAvailabilityKey[] = "availability"; |
| const char kTrackingOnlyKey[] = "tracking_only"; |
| const char kGroupsKey[] = "groups"; |
| const char kIgnoredKeyPrefix[] = "x_"; |
| |
| const char kSnoozeParams[] = "snooze_params"; |
| const char kSnoozeParamsMaxLimit[] = "max_limit"; |
| const char kSnoozeParamsInterval[] = "snooze_interval"; |
| |
| const char kEventConfigDataNameKey[] = "name"; |
| const char kEventConfigDataComparatorKey[] = "comparator"; |
| const char kEventConfigDataWindowKey[] = "window"; |
| const char kEventConfigDataStorageKey[] = "storage"; |
| |
| const char kTrackingOnlyTrue[] = "true"; |
| const char kTrackingOnlyFalse[] = "false"; |
| } // namespace |
| |
| namespace feature_engagement { |
| |
| namespace { |
| |
| bool ParseComparatorSubstring(const base::StringPiece& definition, |
| Comparator* comparator, |
| ComparatorType type, |
| uint32_t type_len) { |
| base::StringPiece number_string = |
| base::TrimWhitespaceASCII(definition.substr(type_len), base::TRIM_ALL); |
| uint32_t value; |
| if (!base::StringToUint(number_string, &value)) |
| return false; |
| |
| comparator->type = type; |
| comparator->value = value; |
| return true; |
| } |
| |
| bool ParseComparator(const base::StringPiece& definition, |
| Comparator* comparator) { |
| if (base::EqualsCaseInsensitiveASCII(definition, kComparatorTypeAny)) { |
| comparator->type = ANY; |
| comparator->value = 0; |
| return true; |
| } |
| |
| if (base::StartsWith(definition, kComparatorTypeLessThanOrEqual, |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| return ParseComparatorSubstring(definition, comparator, LESS_THAN_OR_EQUAL, |
| 2); |
| } |
| |
| if (base::StartsWith(definition, kComparatorTypeGreaterThanOrEqual, |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| return ParseComparatorSubstring(definition, comparator, |
| GREATER_THAN_OR_EQUAL, 2); |
| } |
| |
| if (base::StartsWith(definition, kComparatorTypeEqual, |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| return ParseComparatorSubstring(definition, comparator, EQUAL, 2); |
| } |
| |
| if (base::StartsWith(definition, kComparatorTypeNotEqual, |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| return ParseComparatorSubstring(definition, comparator, NOT_EQUAL, 2); |
| } |
| |
| if (base::StartsWith(definition, kComparatorTypeLessThan, |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| return ParseComparatorSubstring(definition, comparator, LESS_THAN, 1); |
| } |
| |
| if (base::StartsWith(definition, kComparatorTypeGreaterThan, |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| return ParseComparatorSubstring(definition, comparator, GREATER_THAN, 1); |
| } |
| |
| return false; |
| } |
| |
| bool ParseEventConfig(const base::StringPiece& definition, |
| EventConfig* event_config) { |
| // Support definitions with at least 4 tokens. |
| auto tokens = base::SplitStringPiece(definition, ";", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_ALL); |
| if (tokens.size() < 4) { |
| *event_config = EventConfig(); |
| return false; |
| } |
| |
| // Parse tokens in any order. |
| bool has_name = false; |
| bool has_comparator = false; |
| bool has_window = false; |
| bool has_storage = false; |
| for (const auto& token : tokens) { |
| auto pair = base::SplitStringPiece(token, ":", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_ALL); |
| if (pair.size() != 2) { |
| *event_config = EventConfig(); |
| return false; |
| } |
| |
| const base::StringPiece& key = pair[0]; |
| const base::StringPiece& value = pair[1]; |
| // TODO(nyquist): Ensure that key matches regex /^[a-zA-Z0-9-_]+$/. |
| |
| if (base::EqualsCaseInsensitiveASCII(key, kEventConfigDataNameKey)) { |
| if (has_name) { |
| *event_config = EventConfig(); |
| return false; |
| } |
| has_name = true; |
| |
| event_config->name = std::string(value); |
| } else if (base::EqualsCaseInsensitiveASCII( |
| key, kEventConfigDataComparatorKey)) { |
| if (has_comparator) { |
| *event_config = EventConfig(); |
| return false; |
| } |
| has_comparator = true; |
| |
| Comparator comparator; |
| if (!ParseComparator(value, &comparator)) { |
| *event_config = EventConfig(); |
| return false; |
| } |
| |
| event_config->comparator = comparator; |
| } else if (base::EqualsCaseInsensitiveASCII(key, |
| kEventConfigDataWindowKey)) { |
| if (has_window) { |
| *event_config = EventConfig(); |
| return false; |
| } |
| has_window = true; |
| |
| uint32_t parsed_value; |
| if (!base::StringToUint(value, &parsed_value)) { |
| *event_config = EventConfig(); |
| return false; |
| } |
| |
| event_config->window = parsed_value; |
| } else if (base::EqualsCaseInsensitiveASCII(key, |
| kEventConfigDataStorageKey)) { |
| if (has_storage) { |
| *event_config = EventConfig(); |
| return false; |
| } |
| has_storage = true; |
| |
| uint32_t parsed_value; |
| if (!base::StringToUint(value, &parsed_value)) { |
| *event_config = EventConfig(); |
| return false; |
| } |
| |
| event_config->storage = parsed_value; |
| } |
| } |
| |
| return has_name && has_comparator && has_window && has_storage; |
| } |
| |
| bool IsKnownFeature(const base::StringPiece& feature_name, |
| const FeatureVector& features) { |
| for (const auto* feature : features) { |
| if (feature->name == feature_name) |
| return true; |
| } |
| return false; |
| } |
| |
| bool IsKnownGroup(const base::StringPiece& group_name, |
| const GroupVector& groups) { |
| for (const auto* group : groups) { |
| if (group->name == group_name) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool ParseSessionRateImpact(const base::StringPiece& definition, |
| SessionRateImpact* session_rate_impact, |
| const base::Feature* this_feature, |
| const FeatureVector& all_features, |
| const GroupVector& all_groups) { |
| base::StringPiece trimmed_def = |
| base::TrimWhitespaceASCII(definition, base::TRIM_ALL); |
| |
| if (trimmed_def.length() == 0) |
| return false; |
| |
| if (base::EqualsCaseInsensitiveASCII(trimmed_def, kImpactedFeaturesTypeAll)) { |
| session_rate_impact->type = SessionRateImpact::Type::ALL; |
| return true; |
| } |
| |
| if (base::EqualsCaseInsensitiveASCII(trimmed_def, |
| kImpactedFeaturesTypeNone)) { |
| session_rate_impact->type = SessionRateImpact::Type::NONE; |
| return true; |
| } |
| |
| auto parsed_feature_names = base::SplitStringPiece( |
| trimmed_def, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); |
| if (parsed_feature_names.empty()) |
| return false; |
| |
| std::vector<std::string> affected_features; |
| for (const auto& feature_name : parsed_feature_names) { |
| if (feature_name.length() == 0) { |
| DVLOG(1) << "Empty feature name when parsing session_rate_impact " |
| << "for feature " << this_feature->name; |
| continue; |
| } |
| if (base::EqualsCaseInsensitiveASCII(feature_name, |
| kImpactedFeaturesTypeAll) || |
| base::EqualsCaseInsensitiveASCII(feature_name, |
| kImpactedFeaturesTypeNone)) { |
| DVLOG(1) << "Illegal feature name when parsing session_rate_impact " |
| << "for feature " << this_feature->name << ": " << feature_name; |
| return false; |
| } |
| if (!IsKnownFeature(feature_name, all_features) && |
| !IsKnownGroup(feature_name, all_groups)) { |
| DVLOG(1) << "Unknown feature name found when parsing session_rate_impact " |
| << "for feature " << this_feature->name << ": " << feature_name; |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent:: |
| FAILURE_SESSION_RATE_IMPACT_UNKNOWN_FEATURE); |
| continue; |
| } |
| affected_features.push_back(std::string(feature_name)); |
| } |
| |
| if (affected_features.empty()) |
| return false; |
| |
| session_rate_impact->type = SessionRateImpact::Type::EXPLICIT; |
| session_rate_impact->affected_features = std::move(affected_features); |
| return true; |
| } |
| |
| bool ParseBlockedBy(const base::StringPiece& definition, |
| BlockedBy* blocked_by, |
| const base::Feature* this_feature, |
| const FeatureVector& all_features, |
| const GroupVector& all_groups) { |
| base::StringPiece trimmed_def = |
| base::TrimWhitespaceASCII(definition, base::TRIM_ALL); |
| |
| if (trimmed_def.length() == 0) |
| return false; |
| |
| if (base::EqualsCaseInsensitiveASCII(trimmed_def, kImpactedFeaturesTypeAll)) { |
| blocked_by->type = BlockedBy::Type::ALL; |
| return true; |
| } |
| |
| if (base::EqualsCaseInsensitiveASCII(trimmed_def, |
| kImpactedFeaturesTypeNone)) { |
| blocked_by->type = BlockedBy::Type::NONE; |
| return true; |
| } |
| |
| auto parsed_feature_names = base::SplitStringPiece( |
| trimmed_def, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); |
| if (parsed_feature_names.empty()) |
| return false; |
| |
| std::vector<std::string> affected_features; |
| for (const auto& feature_name : parsed_feature_names) { |
| if (feature_name.length() == 0) { |
| DVLOG(1) << "Empty feature name when parsing blocked_by " |
| << "for feature " << this_feature->name; |
| continue; |
| } |
| if (base::EqualsCaseInsensitiveASCII(feature_name, |
| kImpactedFeaturesTypeAll) || |
| base::EqualsCaseInsensitiveASCII(feature_name, |
| kImpactedFeaturesTypeNone)) { |
| DVLOG(1) << "Illegal feature name when parsing blocked_by " |
| << "for feature " << this_feature->name << ": " << feature_name; |
| return false; |
| } |
| if (!IsKnownFeature(feature_name, all_features) && |
| !IsKnownGroup(feature_name, all_groups)) { |
| DVLOG(1) << "Unknown feature name found when parsing blocked_by " |
| << "for feature " << this_feature->name << ": " << feature_name; |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_BLOCKED_BY_UNKNOWN_FEATURE); |
| continue; |
| } |
| affected_features.emplace_back(std::string(feature_name)); |
| } |
| |
| if (affected_features.empty()) |
| return false; |
| |
| blocked_by->type = BlockedBy::Type::EXPLICIT; |
| blocked_by->affected_features = std::move(affected_features); |
| return true; |
| } |
| |
| bool ParseBlocking(const base::StringPiece& definition, Blocking* blocking) { |
| base::StringPiece trimmed_def = |
| base::TrimWhitespaceASCII(definition, base::TRIM_ALL); |
| |
| if (trimmed_def.length() == 0) |
| return false; |
| |
| if (base::EqualsCaseInsensitiveASCII(trimmed_def, kImpactedFeaturesTypeAll)) { |
| blocking->type = Blocking::Type::ALL; |
| return true; |
| } |
| |
| if (base::EqualsCaseInsensitiveASCII(trimmed_def, |
| kImpactedFeaturesTypeNone)) { |
| blocking->type = Blocking::Type::NONE; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool ParseSnoozeParams(const base::StringPiece& definition, |
| SnoozeParams* snooze_params) { |
| auto tokens = base::SplitStringPiece(definition, ",", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_ALL); |
| if (tokens.size() != 2) |
| return false; |
| |
| bool has_max_limit = false; |
| bool has_snooze_interval = false; |
| for (const auto& token : tokens) { |
| auto pair = base::SplitStringPiece(token, ":", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_ALL); |
| |
| if (pair.size() != 2) |
| return false; |
| |
| const base::StringPiece& key = pair[0]; |
| const base::StringPiece& value = pair[1]; |
| if (base::EqualsCaseInsensitiveASCII(key, kSnoozeParamsMaxLimit)) { |
| uint32_t parsed_value; |
| if (!base::StringToUint(value, &parsed_value)) { |
| snooze_params->snooze_interval = 0u; |
| return false; |
| } |
| snooze_params->max_limit = parsed_value; |
| has_max_limit = true; |
| } else if (base::EqualsCaseInsensitiveASCII(key, kSnoozeParamsInterval)) { |
| uint32_t parsed_value; |
| if (!base::StringToUint(value, &parsed_value)) { |
| snooze_params->max_limit = 0u; |
| return false; |
| } |
| snooze_params->snooze_interval = parsed_value; |
| has_snooze_interval = true; |
| } |
| } |
| |
| return has_max_limit && has_snooze_interval; |
| } |
| |
| bool ParseTrackingOnly(const base::StringPiece& definition, |
| bool* tracking_only) { |
| // Since |tracking_only| is a primitive, ensure it set. |
| *tracking_only = false; |
| |
| base::StringPiece trimmed_def = |
| base::TrimWhitespaceASCII(definition, base::TRIM_ALL); |
| |
| if (base::EqualsCaseInsensitiveASCII(trimmed_def, kTrackingOnlyTrue)) { |
| *tracking_only = true; |
| return true; |
| } |
| |
| return base::EqualsCaseInsensitiveASCII(trimmed_def, kTrackingOnlyFalse); |
| } |
| |
| bool ParseGroups(const base::StringPiece& definition, |
| std::vector<std::string>* groups, |
| const base::Feature* this_feature, |
| const GroupVector& all_groups) { |
| base::StringPiece trimmed_def = |
| base::TrimWhitespaceASCII(definition, base::TRIM_ALL); |
| |
| if (trimmed_def.length() == 0) { |
| return false; |
| } |
| |
| auto parsed_group_names = base::SplitStringPiece( |
| trimmed_def, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); |
| if (parsed_group_names.empty()) { |
| return false; |
| } |
| |
| for (const auto& group_name : parsed_group_names) { |
| if (group_name.length() == 0) { |
| DVLOG(1) << "Empty group name when parsing groups " |
| << "for feature " << this_feature->name; |
| continue; |
| } |
| if (!IsKnownGroup(group_name, all_groups)) { |
| DVLOG(1) << "Unknown group name found when parsing groups " |
| << "for feature " << this_feature->name << ": " << group_name; |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_GROUPS_UNKNOWN_GROUP); |
| continue; |
| } |
| groups->emplace_back(std::string(group_name)); |
| } |
| |
| if (groups->empty()) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Takes a list of |original_names| and expands any groups in the list into the |
| // features that make up that group, using |group_mapping| and |all_groups|. |
| std::vector<std::string> FlattenGroupsAndFeatures( |
| std::vector<std::string> original_names, |
| std::map<std::string, std::vector<std::string>> group_mapping, |
| const GroupVector& all_groups) { |
| // Use set to make sure feature names don't occur twice. |
| std::set<std::string> flattened_feature_names; |
| for (auto name : original_names) { |
| if (IsKnownGroup(name, all_groups)) { |
| auto it = group_mapping.find(name); |
| if (it == group_mapping.end()) { |
| continue; |
| } |
| // Group is known and can be replaced by the features in it. |
| for (auto feature_name : it->second) { |
| flattened_feature_names.insert(feature_name); |
| } |
| } else { |
| // Otherwise, the name is a feature name already. |
| flattened_feature_names.insert(name); |
| } |
| } |
| |
| std::vector<std::string> result(flattened_feature_names.begin(), |
| flattened_feature_names.end()); |
| return result; |
| } |
| |
| // Holds all the possible fields that can be parsed. The parsing code will fill |
| // the provided items with parsed data. If any field is null, then it won't be |
| // parsed. |
| struct ConfigParseOutput { |
| const raw_ref<uint32_t> parse_errors; |
| raw_ptr<Comparator> session_rate = nullptr; |
| raw_ptr<SessionRateImpact> session_rate_impact = nullptr; |
| raw_ptr<Blocking> blocking = nullptr; |
| raw_ptr<BlockedBy> blocked_by = nullptr; |
| raw_ptr<EventConfig> trigger = nullptr; |
| raw_ptr<EventConfig> used = nullptr; |
| raw_ptr<std::set<EventConfig>> event_configs = nullptr; |
| raw_ptr<bool> tracking_only = nullptr; |
| raw_ptr<Comparator> availability = nullptr; |
| raw_ptr<SnoozeParams> snooze_params = nullptr; |
| raw_ptr<std::vector<std::string>> groups = nullptr; |
| |
| explicit ConfigParseOutput(uint32_t& parse_errors) |
| : parse_errors(parse_errors) {} |
| }; |
| |
| void ParseConfigFields(const base::Feature* feature, |
| const FeatureVector& all_features, |
| const GroupVector& all_groups, |
| std::map<std::string, std::string> params, |
| ConfigParseOutput& output) { |
| for (const auto& it : params) { |
| std::string param_name = it.first; |
| std::string param_value = params[param_name]; |
| std::string key = param_name; |
| // The param name might have a prefix containing the feature name with |
| // a trailing underscore, e.g. IPH_FooFeature_session_rate. Strip out |
| // the feature prefix for further comparison. |
| if (base::StartsWith(key, feature->name, base::CompareCase::SENSITIVE)) |
| key = param_name.substr(strlen(feature->name) + 1); |
| |
| if (key == kEventConfigUsedKey && output.used) { |
| EventConfig event_config; |
| if (!ParseEventConfig(param_value, &event_config)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_USED_EVENT_PARSE); |
| ++*output.parse_errors; |
| continue; |
| } |
| *output.used = event_config; |
| } else if (key == kEventConfigTriggerKey && output.trigger) { |
| EventConfig event_config; |
| if (!ParseEventConfig(param_value, &event_config)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_TRIGGER_EVENT_PARSE); |
| ++*output.parse_errors; |
| continue; |
| } |
| *output.trigger = event_config; |
| } else if (key == kSessionRateKey && output.session_rate) { |
| Comparator comparator; |
| if (!ParseComparator(param_value, &comparator)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_SESSION_RATE_PARSE); |
| ++*output.parse_errors; |
| continue; |
| } |
| *output.session_rate = comparator; |
| } else if (key == kSessionRateImpactKey && output.session_rate_impact) { |
| SessionRateImpact parsed_session_rate_impact; |
| if (!ParseSessionRateImpact(param_value, &parsed_session_rate_impact, |
| feature, all_features, all_groups)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_SESSION_RATE_IMPACT_PARSE); |
| ++*output.parse_errors; |
| continue; |
| } |
| *output.session_rate_impact = parsed_session_rate_impact; |
| } else if (key == kBlockingKey && output.blocking) { |
| Blocking parsed_blocking; |
| if (!ParseBlocking(param_value, &parsed_blocking)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_BLOCKING_PARSE); |
| ++*output.parse_errors; |
| continue; |
| } |
| *output.blocking = parsed_blocking; |
| } else if (key == kBlockedByKey && output.blocked_by) { |
| BlockedBy parsed_blocked_by; |
| if (!ParseBlockedBy(param_value, &parsed_blocked_by, feature, |
| all_features, all_groups)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_BLOCKED_BY_PARSE); |
| ++*output.parse_errors; |
| continue; |
| } |
| *output.blocked_by = parsed_blocked_by; |
| } else if (key == kTrackingOnlyKey && output.tracking_only) { |
| bool parsed_tracking_only; |
| if (!ParseTrackingOnly(param_value, &parsed_tracking_only)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_TRACKING_ONLY_PARSE); |
| ++*output.parse_errors; |
| continue; |
| } |
| *output.tracking_only = parsed_tracking_only; |
| } else if (key == kAvailabilityKey && output.availability) { |
| Comparator comparator; |
| if (!ParseComparator(param_value, &comparator)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_AVAILABILITY_PARSE); |
| ++*output.parse_errors; |
| continue; |
| } |
| *output.availability = comparator; |
| } else if (key == kSnoozeParams && output.snooze_params) { |
| SnoozeParams parsed_snooze_params; |
| if (!ParseSnoozeParams(param_value, &parsed_snooze_params)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_SNOOZE_PARAMS_PARSE); |
| ++*output.parse_errors; |
| continue; |
| } |
| *output.snooze_params = parsed_snooze_params; |
| } else if (key == kGroupsKey && output.groups) { |
| if (!base::FeatureList::IsEnabled(kIPHGroups)) { |
| continue; |
| } |
| std::vector<std::string> groups; |
| if (!ParseGroups(param_value, &groups, feature, all_groups)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_GROUPS_PARSE); |
| ++*output.parse_errors; |
| continue; |
| } |
| *output.groups = groups; |
| } else if (base::StartsWith(key, kEventConfigKeyPrefix, |
| base::CompareCase::INSENSITIVE_ASCII) && |
| output.event_configs) { |
| EventConfig event_config; |
| if (!ParseEventConfig(param_value, &event_config)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_OTHER_EVENT_PARSE); |
| ++*output.parse_errors; |
| continue; |
| } |
| output.event_configs->insert(event_config); |
| } else if (base::StartsWith(key, kIgnoredKeyPrefix, |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| // Intentionally ignoring parameter using registered ignored prefix. |
| DVLOG(2) << "Ignoring unknown key when parsing config for feature " |
| << feature->name << ": " << param_name; |
| } else { |
| DVLOG(1) << "Unknown key found when parsing config for feature " |
| << feature->name << ": " << param_name; |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_UNKNOWN_KEY); |
| } |
| } |
| } |
| |
| template <typename T> |
| void RecordParseResult(std::string name, T config) { |
| if (config.valid) { |
| stats::RecordConfigParsingEvent(stats::ConfigParsingEvent::SUCCESS); |
| DVLOG(2) << "Config for " << name << " is valid."; |
| DVLOG(3) << "Config for " << name << " = " << config; |
| } else { |
| stats::RecordConfigParsingEvent(stats::ConfigParsingEvent::FAILURE); |
| DVLOG(2) << "Config for " << name << " is invalid."; |
| } |
| } |
| |
| } // namespace |
| |
| ChromeVariationsConfiguration::ChromeVariationsConfiguration() = default; |
| |
| ChromeVariationsConfiguration::~ChromeVariationsConfiguration() = default; |
| |
| void ChromeVariationsConfiguration::ParseConfigs(const FeatureVector& features, |
| const GroupVector& groups) { |
| for (auto* feature : features) { |
| ParseFeatureConfig(feature, features, groups); |
| } |
| if (!base::FeatureList::IsEnabled(kIPHGroups)) { |
| return; |
| } |
| |
| ExpandGroupNamesInFeatures(groups); |
| |
| for (auto* group : groups) { |
| ParseGroupConfig(group, features, groups); |
| } |
| } |
| |
| bool ChromeVariationsConfiguration::ShouldUseClientSideConfig( |
| const base::Feature* feature, |
| base::FieldTrialParams* params) { |
| // Client-side configuration is used under any of the following circumstances: |
| // - The UseClientConfigIPH feature flag is enabled |
| // - There are no field trial parameters set for the feature |
| // - The field trial configuration is empty |
| // |
| // Note that the "empty configuration = use client-side" is quite useful, as |
| // it means that json field trial configs, Finch configurations, and tests can |
| // simply enable a feature engagement feature, and it will default to using |
| // the client-side configuration; there is no need to duplicate a standard |
| // configuration in more than one place. |
| return base::FeatureList::IsEnabled(kUseClientConfigIPH) || |
| !base::GetFieldTrialParamsByFeature(*feature, params) || |
| params->empty(); |
| } |
| |
| void ChromeVariationsConfiguration::TryAddingClientSideConfig( |
| const base::Feature* feature, |
| bool is_group) { |
| // Try to read the client-side configuration. |
| bool added_client_config = (is_group) |
| ? MaybeAddClientSideGroupConfig(feature) |
| : MaybeAddClientSideFeatureConfig(feature); |
| if (added_client_config) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::SUCCESS_FROM_SOURCE); |
| DVLOG(3) << "Read checked in config for " << feature->name; |
| return; |
| } |
| |
| // No server-side nor client side configuration is available, but the |
| // feature was on the list of available features, so give it an invalid |
| // config. |
| if (is_group) { |
| GroupConfig& config = group_configs_[feature->name]; |
| config.valid = false; |
| } else { |
| FeatureConfig& config = configs_[feature->name]; |
| config.valid = false; |
| } |
| |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_NO_FIELD_TRIAL); |
| // Returns early. If no field trial, ConfigParsingEvent::FAILURE will not be |
| // recorded. |
| DVLOG(3) << "No field trial or checked in config for " << feature->name; |
| return; |
| } |
| |
| void ChromeVariationsConfiguration::ParseFeatureConfig( |
| const base::Feature* feature, |
| const FeatureVector& all_features, |
| const GroupVector& all_groups) { |
| DCHECK(feature); |
| DCHECK(configs_.find(feature->name) == configs_.end()); |
| |
| DVLOG(3) << "Parsing feature config for " << feature->name; |
| std::map<std::string, std::string> params; |
| if (ShouldUseClientSideConfig(feature, ¶ms)) { |
| TryAddingClientSideConfig(feature, /*is_group=*/false); |
| return; |
| } |
| |
| // Initially all new configurations are considered invalid. |
| FeatureConfig& config = configs_[feature->name]; |
| config.valid = false; |
| uint32_t parse_errors = 0; |
| |
| ConfigParseOutput output(parse_errors); |
| output.session_rate = &config.session_rate; |
| output.session_rate_impact = &config.session_rate_impact; |
| output.blocking = &config.blocking; |
| output.blocked_by = &config.blocked_by; |
| output.trigger = &config.trigger; |
| output.used = &config.used; |
| output.event_configs = &config.event_configs; |
| output.tracking_only = &config.tracking_only; |
| output.availability = &config.availability; |
| output.snooze_params = &config.snooze_params; |
| output.groups = &config.groups; |
| |
| ParseConfigFields(feature, all_features, all_groups, params, output); |
| |
| // The |used| and |trigger| members are required, so should not be the |
| // default values. |
| bool has_used_event = config.used != EventConfig(); |
| bool has_trigger_event = config.trigger != EventConfig(); |
| config.valid = has_used_event && has_trigger_event && parse_errors == 0; |
| |
| RecordParseResult(feature->name, config); |
| |
| // Notice parse errors for used and trigger events will also cause the |
| // following histograms being recorded. |
| if (!has_used_event) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_USED_EVENT_MISSING); |
| } |
| if (!has_trigger_event) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_TRIGGER_EVENT_MISSING); |
| } |
| } |
| |
| bool ChromeVariationsConfiguration::MaybeAddClientSideFeatureConfig( |
| const base::Feature* feature) { |
| if (!base::FeatureList::IsEnabled(*feature)) |
| return false; |
| |
| DCHECK(configs_.find(feature->name) == configs_.end()); |
| if (auto config = GetClientSideFeatureConfig(feature)) { |
| configs_[feature->name] = *config; |
| return true; |
| } |
| return false; |
| } |
| |
| void ChromeVariationsConfiguration::ParseGroupConfig( |
| const base::Feature* group, |
| const FeatureVector& all_features, |
| const GroupVector& all_groups) { |
| DCHECK(group); |
| DCHECK(group_configs_.find(group->name) == group_configs_.end()); |
| |
| DVLOG(3) << "Parsing group config for " << group->name; |
| |
| std::map<std::string, std::string> params; |
| if (ShouldUseClientSideConfig(group, ¶ms)) { |
| TryAddingClientSideConfig(group, /*is_group=*/true); |
| return; |
| } |
| |
| // Initially all new configurations are considered invalid. |
| GroupConfig& config = group_configs_[group->name]; |
| config.valid = false; |
| uint32_t parse_errors = 0; |
| |
| ConfigParseOutput output(parse_errors); |
| output.session_rate = &config.session_rate; |
| output.trigger = &config.trigger; |
| output.event_configs = &config.event_configs; |
| |
| ParseConfigFields(group, all_features, all_groups, params, output); |
| |
| // The |trigger| member is required, so should not be the |
| // default value. |
| bool has_trigger_event = config.trigger != EventConfig(); |
| config.valid = has_trigger_event && parse_errors == 0; |
| |
| RecordParseResult(group->name, config); |
| |
| // Notice parse errors for trigger event will also cause the |
| // following histogram to be recorded. |
| if (!has_trigger_event) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_TRIGGER_EVENT_MISSING); |
| } |
| } |
| |
| void ChromeVariationsConfiguration::ExpandGroupNamesInFeatures( |
| const GroupVector& all_groups) { |
| // Create mapping of groups to their constituent features. |
| std::map<std::string, std::vector<std::string>> group_to_feature_mapping; |
| for (const auto& [feature_name, feature] : configs_) { |
| for (auto group_name : feature.groups) { |
| group_to_feature_mapping[group_name].push_back(feature_name); |
| } |
| } |
| |
| // Flatten any group names in each feature's blocked by or session rate impact |
| // list into the constituent features. |
| for (auto& [feature_name, feature] : configs_) { |
| if (feature.blocked_by.type == BlockedBy::Type::EXPLICIT) { |
| auto original_blocked_by_items = |
| feature.blocked_by.affected_features.value(); |
| feature.blocked_by.affected_features = FlattenGroupsAndFeatures( |
| original_blocked_by_items, group_to_feature_mapping, all_groups); |
| } |
| if (feature.session_rate_impact.type == SessionRateImpact::Type::EXPLICIT) { |
| feature.session_rate_impact.affected_features = FlattenGroupsAndFeatures( |
| feature.session_rate_impact.affected_features.value(), |
| group_to_feature_mapping, all_groups); |
| } |
| } |
| } |
| |
| bool ChromeVariationsConfiguration::MaybeAddClientSideGroupConfig( |
| const base::Feature* group) { |
| if (!base::FeatureList::IsEnabled(*group)) { |
| return false; |
| } |
| |
| DCHECK(configs_.find(group->name) == configs_.end()); |
| if (auto config = GetClientSideGroupConfig(group)) { |
| group_configs_[group->name] = *config; |
| return true; |
| } |
| return false; |
| } |
| |
| const FeatureConfig& ChromeVariationsConfiguration::GetFeatureConfig( |
| const base::Feature& feature) const { |
| auto it = configs_.find(feature.name); |
| DCHECK(it != configs_.end()); |
| return it->second; |
| } |
| |
| const FeatureConfig& ChromeVariationsConfiguration::GetFeatureConfigByName( |
| const std::string& feature_name) const { |
| auto it = configs_.find(feature_name); |
| DCHECK(it != configs_.end()); |
| return it->second; |
| } |
| |
| const Configuration::ConfigMap& |
| ChromeVariationsConfiguration::GetRegisteredFeatureConfigs() const { |
| return configs_; |
| } |
| |
| const std::vector<std::string> |
| ChromeVariationsConfiguration::GetRegisteredFeatures() const { |
| std::vector<std::string> features; |
| for (const auto& element : configs_) |
| features.push_back(element.first); |
| return features; |
| } |
| |
| const GroupConfig& ChromeVariationsConfiguration::GetGroupConfig( |
| const base::Feature& group) const { |
| auto it = group_configs_.find(group.name); |
| DCHECK(it != group_configs_.end()); |
| return it->second; |
| } |
| |
| const GroupConfig& ChromeVariationsConfiguration::GetGroupConfigByName( |
| const std::string& group_name) const { |
| auto it = group_configs_.find(group_name); |
| DCHECK(it != group_configs_.end()); |
| return it->second; |
| } |
| |
| const Configuration::GroupConfigMap& |
| ChromeVariationsConfiguration::GetRegisteredGroupConfigs() const { |
| return group_configs_; |
| } |
| |
| const std::vector<std::string> |
| ChromeVariationsConfiguration::GetRegisteredGroups() const { |
| std::vector<std::string> groups; |
| for (const auto& element : group_configs_) |
| groups.push_back(element.first); |
| return groups; |
| } |
| |
| } // namespace feature_engagement |