class Evaluator
Public Class Methods
new(store)
click to toggle source
# File lib/evaluator.rb, line 14 def initialize(store) @spec_store = store @initialized = true @ua_parser = UserAgentParser::Parser.new CountryLookup.initialize end
Public Instance Methods
check_gate(user, gate_name)
click to toggle source
# File lib/evaluator.rb, line 21 def check_gate(user, gate_name) return nil unless @initialized && @spec_store.has_gate?(gate_name) self.eval_spec(user, @spec_store.get_gate(gate_name)) end
get_config(user, config_name)
click to toggle source
# File lib/evaluator.rb, line 26 def get_config(user, config_name) return nil unless @initialized && @spec_store.has_config?(config_name) self.eval_spec(user, @spec_store.get_config(config_name)) end
Private Instance Methods
compute_user_hash(user_hash)
click to toggle source
# File lib/evaluator.rb, line 252 def compute_user_hash(user_hash) Digest::SHA256.digest(user_hash).unpack('Q>')[0] end
eval_condition(user, condition)
click to toggle source
# File lib/evaluator.rb, line 67 def eval_condition(user, condition) value = nil field = condition['field'] target = condition['targetValue'] type = condition['type'] operator = condition['operator'] additional_values = condition['additionalValues'] additional_values = Hash.new unless additional_values.is_a? Hash return $fetch_from_server unless type.is_a? String type = type.downcase case type when 'public' return true when 'fail_gate', 'pass_gate' other_gate_result = self.check_gate(user, target) return $fetch_from_server if other_gate_result == $fetch_from_server return type == 'pass_gate' ? other_gate_result.gate_value : !other_gate_result.gate_value when 'ip_based' value = get_value_from_user(user, field) || get_value_from_ip(user, field) return $fetch_from_server if value == $fetch_from_server when 'ua_based' value = get_value_from_user(user, field) || get_value_from_ua(user, field) return $fetch_from_server if value == $fetch_from_server when 'user_field' value = get_value_from_user(user, field) when 'environment_field' value = get_value_from_environment(user, field) when 'current_time' value = Time.now.to_f # epoch time in seconds when 'user_bucket' begin salt = additional_values['salt'] user_id = user.user_id || '' # there are only 1000 user buckets as opposed to 10k for gate pass % value = compute_user_hash("#{salt}.#{user_id}") % 1000 rescue return false end else return $fetch_from_server end return $fetch_from_server if value == $fetch_from_server || !operator.is_a?(String) operator = operator.downcase case operator # numerical comparison when 'gt' return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a > b }) when 'gte' return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a >= b }) when 'lt' return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a < b }) when 'lte' return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a <= b }) # version comparison when 'version_gt' return (Gem::Version.new(value) > Gem::Version.new(target) rescue false) when 'version_gte' return (Gem::Version.new(value) >= Gem::Version.new(target) rescue false) when 'version_lt' return (Gem::Version.new(value) < Gem::Version.new(target) rescue false) when 'version_lte' return (Gem::Version.new(value) <= Gem::Version.new(target) rescue false) when 'version_eq' return (Gem::Version.new(value) == Gem::Version.new(target) rescue false) when 'version_neq' return (Gem::Version.new(value) != Gem::Version.new(target) rescue false) # array operations when 'any' return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b }) when 'none' return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b }) when 'any_case_sensitive' return EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b }) when 'none_case_sensitive' return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b }) #string when 'str_starts_with_any' return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) }) when 'str_ends_with_any' return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) }) when 'str_contains_any' return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) }) when 'str_contains_none' return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) }) when 'str_matches' return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false) when 'eq' return value == target when 'neq' return value != target # dates when 'before' return EvaluationHelpers::compare_times(value, target, ->(a, b) { a < b }) when 'after' return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b }) when 'on' return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day }) else return $fetch_from_server end end
eval_pass_percent(user, rule, config_salt)
click to toggle source
# File lib/evaluator.rb, line 240 def eval_pass_percent(user, rule, config_salt) return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil? begin user_id = user.user_id || '' rule_salt = rule['salt'] || rule['id'] || '' hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{user_id}") return (hash % 10000) < (rule['passPercentage'].to_f * 100) rescue return false end end
eval_rule(user, rule)
click to toggle source
# File lib/evaluator.rb, line 57 def eval_rule(user, rule) i = 0 until i >= rule['conditions'].length do result = self.eval_condition(user, rule['conditions'][i]) return result unless result == true i += 1 end true end
eval_spec(user, config)
click to toggle source
# File lib/evaluator.rb, line 33 def eval_spec(user, config) if config['enabled'] i = 0 until i >= config['rules'].length do rule = config['rules'][i] result = self.eval_rule(user, rule) return $fetch_from_server if result == $fetch_from_server if result pass = self.eval_pass_percent(user, rule, config['salt']) return ConfigResult.new( config['name'], pass, pass ? rule['returnValue'] : config['defaultValue'], rule['id'], ) end i += 1 end end ConfigResult.new(config['name'], false, config['defaultValue'], 'default') end
get_value_from_environment(user, field)
click to toggle source
# File lib/evaluator.rb, line 201 def get_value_from_environment(user, field) return nil unless user.instance_of?(StatsigUser) && field.is_a?(String) field = field.downcase return nil unless user.statsig_environment.is_a? Hash user.statsig_environment.each do |key, value| return value if key.downcase == (field) end nil end
get_value_from_ip(user, field)
click to toggle source
# File lib/evaluator.rb, line 211 def get_value_from_ip(user, field) return nil unless user.is_a?(StatsigUser) && field.is_a?(String) && field.downcase == 'country' ip = get_value_from_user(user, 'ip') return nil unless ip.is_a?(String) CountryLookup.lookup_ip_string(ip) end
get_value_from_ua(user, field)
click to toggle source
# File lib/evaluator.rb, line 219 def get_value_from_ua(user, field) return nil unless user.is_a?(StatsigUser) && field.is_a?(String) ua = get_value_from_user(user, 'userAgent') return nil unless ua.is_a?(String) parsed = @ua_parser.parse ua os = parsed.os case field.downcase when 'os_name', 'osname' return os&.family when 'os_version', 'osversion' return os&.version unless os&.version.nil? when 'browser_name', 'browsername' return parsed.family when 'browser_version', 'browserversion' return parsed.version.to_s else nil end end
get_value_from_user(user, field)
click to toggle source
# File lib/evaluator.rb, line 177 def get_value_from_user(user, field) return nil unless user.instance_of?(StatsigUser) && field.is_a?(String) user_lookup_table = user&.value_lookup return nil unless user_lookup_table.is_a?(Hash) return user_lookup_table[field.downcase] if user_lookup_table.has_key?(field.downcase) && !user_lookup_table[field.downcase].nil? user_custom = user_lookup_table['custom'] if user_custom.is_a?(Hash) user_custom.each do |key, value| return value if key.downcase.casecmp?(field.downcase) && !value.nil? end end private_attributes = user_lookup_table['privateAttributes'] if private_attributes.is_a?(Hash) private_attributes.each do |key, value| return value if key.downcase.casecmp?(field.downcase) && !value.nil? end end nil end