| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // 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/logging.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/configuration.h" |
| #include "components/feature_engagement/internal/stats.h" |
| #include "components/feature_engagement/public/feature_list.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 kSessionRateImpactTypeAll[] = "all"; |
| const char kSessionRateImpactTypeNone[] = "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 kAvailabilityKey[] = "availability"; |
| const char kTrackingOnlyKey[] = "tracking_only"; |
| const char kIgnoredKeyPrefix[] = "x_"; |
| |
| 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::LowerCaseEqualsASCII(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::LowerCaseEqualsASCII(key, kEventConfigDataNameKey)) { |
| if (has_name) { |
| *event_config = EventConfig(); |
| return false; |
| } |
| has_name = true; |
| |
| event_config->name = value.as_string(); |
| } else if (base::LowerCaseEqualsASCII(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::LowerCaseEqualsASCII(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::LowerCaseEqualsASCII(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.as_string()) |
| return true; |
| } |
| return false; |
| } |
| |
| bool ParseSessionRateImpact(const base::StringPiece& definition, |
| SessionRateImpact* session_rate_impact, |
| const base::Feature* this_feature, |
| const FeatureVector& all_features) { |
| base::StringPiece trimmed_def = |
| base::TrimWhitespaceASCII(definition, base::TRIM_ALL); |
| |
| if (trimmed_def.length() == 0) |
| return false; |
| |
| if (base::LowerCaseEqualsASCII(trimmed_def, kSessionRateImpactTypeAll)) { |
| session_rate_impact->type = SessionRateImpact::Type::ALL; |
| return true; |
| } |
| |
| if (base::LowerCaseEqualsASCII(trimmed_def, kSessionRateImpactTypeNone)) { |
| 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::LowerCaseEqualsASCII(feature_name, kSessionRateImpactTypeAll) || |
| base::LowerCaseEqualsASCII(feature_name, kSessionRateImpactTypeNone)) { |
| 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)) { |
| 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(feature_name.as_string()); |
| } |
| |
| 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 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::LowerCaseEqualsASCII(trimmed_def, kTrackingOnlyTrue)) { |
| *tracking_only = true; |
| return true; |
| } |
| |
| return base::LowerCaseEqualsASCII(trimmed_def, kTrackingOnlyFalse); |
| } |
| } // namespace |
| |
| ChromeVariationsConfiguration::ChromeVariationsConfiguration() = default; |
| |
| ChromeVariationsConfiguration::~ChromeVariationsConfiguration() = default; |
| |
| void ChromeVariationsConfiguration::ParseFeatureConfigs( |
| const FeatureVector& features) { |
| for (auto* feature : features) { |
| ParseFeatureConfig(feature, features); |
| } |
| } |
| |
| void ChromeVariationsConfiguration::ParseFeatureConfig( |
| const base::Feature* feature, |
| const FeatureVector& all_features) { |
| DCHECK(feature); |
| DCHECK(configs_.find(feature->name) == configs_.end()); |
| |
| DVLOG(3) << "Parsing feature config for " << feature->name; |
| |
| // Initially all new configurations are considered invalid. |
| FeatureConfig& config = configs_[feature->name]; |
| config.valid = false; |
| uint32_t parse_errors = 0; |
| |
| std::map<std::string, std::string> params; |
| bool result = base::GetFieldTrialParamsByFeature(*feature, ¶ms); |
| if (!result) { |
| 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 for " << feature->name; |
| return; |
| } |
| |
| for (const auto& it : params) { |
| const std::string& key = it.first; |
| if (key == kEventConfigUsedKey) { |
| EventConfig event_config; |
| if (!ParseEventConfig(params[key], &event_config)) { |
| ++parse_errors; |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_USED_EVENT_PARSE); |
| continue; |
| } |
| config.used = event_config; |
| } else if (key == kEventConfigTriggerKey) { |
| EventConfig event_config; |
| if (!ParseEventConfig(params[key], &event_config)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_TRIGGER_EVENT_PARSE); |
| ++parse_errors; |
| continue; |
| } |
| config.trigger = event_config; |
| } else if (key == kSessionRateKey) { |
| Comparator comparator; |
| if (!ParseComparator(params[key], &comparator)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_SESSION_RATE_PARSE); |
| ++parse_errors; |
| continue; |
| } |
| config.session_rate = comparator; |
| } else if (key == kSessionRateImpactKey) { |
| SessionRateImpact impact; |
| if (!ParseSessionRateImpact(params[key], &impact, feature, |
| all_features)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_SESSION_RATE_IMPACT_PARSE); |
| ++parse_errors; |
| continue; |
| } |
| config.session_rate_impact = impact; |
| } else if (key == kTrackingOnlyKey) { |
| bool tracking_only; |
| if (!ParseTrackingOnly(params[key], &tracking_only)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_TRACKING_ONLY_PARSE); |
| ++parse_errors; |
| continue; |
| } |
| config.tracking_only = tracking_only; |
| } else if (key == kAvailabilityKey) { |
| Comparator comparator; |
| if (!ParseComparator(params[key], &comparator)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_AVAILABILITY_PARSE); |
| ++parse_errors; |
| continue; |
| } |
| config.availability = comparator; |
| } else if (base::StartsWith(key, kEventConfigKeyPrefix, |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| EventConfig event_config; |
| if (!ParseEventConfig(params[key], &event_config)) { |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_OTHER_EVENT_PARSE); |
| ++parse_errors; |
| continue; |
| } |
| config.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 << ": " << key; |
| } else { |
| DVLOG(1) << "Unknown key found when parsing config for feature " |
| << feature->name << ": " << key; |
| stats::RecordConfigParsingEvent( |
| stats::ConfigParsingEvent::FAILURE_UNKNOWN_KEY); |
| } |
| } |
| |
| // 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; |
| |
| if (config.valid) { |
| stats::RecordConfigParsingEvent(stats::ConfigParsingEvent::SUCCESS); |
| DVLOG(2) << "Config for " << feature->name << " is valid."; |
| DVLOG(3) << "Config for " << feature->name << " = " << config; |
| } else { |
| stats::RecordConfigParsingEvent(stats::ConfigParsingEvent::FAILURE); |
| DVLOG(2) << "Config for " << feature->name << " is invalid."; |
| } |
| |
| // 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); |
| } |
| } |
| |
| 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; |
| } |
| |
| } // namespace feature_engagement |