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