statsig 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/config_result.rb +13 -0
- data/lib/dynamic_config.rb +16 -0
- data/lib/evaluation_helpers.rb +25 -0
- data/lib/evaluator.rb +212 -0
- data/lib/network.rb +72 -0
- data/lib/spec_store.rb +51 -0
- data/lib/statsig.rb +42 -0
- data/lib/statsig_driver.rb +146 -0
- data/lib/statsig_event.rb +21 -0
- data/lib/statsig_logger.rb +52 -0
- data/lib/statsig_user.rb +49 -0
- metadata +173 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: 60035280630edb2172623545efa323cc6b3ec0374b8ebc99ba7f28312a227b21
         | 
| 4 | 
            +
              data.tar.gz: af799a2649633d8cc364d6e69fed72f3a30ff96673212d348f20b5fa0ef83f30
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: f69cf4534af68927136ec9d9bf1900ea206e3c86c7bdcdd294e03c0109071449adc44db0d5026f98cae2ff281985022e19ddb6e9a8cc9a275c0f53513c292e0a
         | 
| 7 | 
            +
              data.tar.gz: 39269a606a0291a83c5f486535b7a075ccde3a82a50436598aed93732cd95b27c765220c56de6b50e1e515f7346ec107081dc71ea23be2ee83e33a3e88b745eb
         | 
| @@ -0,0 +1,13 @@ | |
| 1 | 
            +
            class ConfigResult
         | 
| 2 | 
            +
              attr_accessor :name
         | 
| 3 | 
            +
              attr_accessor :gate_value
         | 
| 4 | 
            +
              attr_accessor :json_value
         | 
| 5 | 
            +
              attr_accessor :rule_id
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              def initialize(name, gate_value = false, json_value = {}, rule_id = '')
         | 
| 8 | 
            +
                @name = name
         | 
| 9 | 
            +
                @gate_value = gate_value
         | 
| 10 | 
            +
                @json_value = json_value
         | 
| 11 | 
            +
                @rule_id = rule_id
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
            end
         | 
| @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            class DynamicConfig
         | 
| 2 | 
            +
              attr_accessor :name
         | 
| 3 | 
            +
              attr_accessor :value
         | 
| 4 | 
            +
              attr_accessor :rule_id
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              def initialize(name, value = {}, rule_id = '')
         | 
| 7 | 
            +
                @name = name
         | 
| 8 | 
            +
                @value = value
         | 
| 9 | 
            +
                @rule_id = rule_id
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              def get(index)
         | 
| 13 | 
            +
                return nil if @value.nil?
         | 
| 14 | 
            +
                value[index]
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
            end
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            module EvaluationHelpers
         | 
| 2 | 
            +
              def self.compare_numbers(a, b, func)
         | 
| 3 | 
            +
                return false unless self.is_numeric(a) && self.is_numeric(b)
         | 
| 4 | 
            +
                func.call(a.to_f, b.to_f) rescue false
         | 
| 5 | 
            +
              end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              # returns true if array contains value, ignoring case when comparing strings
         | 
| 8 | 
            +
              def self.array_contains(array, value)
         | 
| 9 | 
            +
                return false unless array.is_a?(Array) && !value.nil?
         | 
| 10 | 
            +
                return array.include?(value) unless value.is_a?(String)
         | 
| 11 | 
            +
                array.any?{ |s| s.is_a?(String) && s.casecmp(value) == 0 } rescue false
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              # returns true if array has any element that evaluates to true with value using func lambda, ignoring case
         | 
| 15 | 
            +
              def self.match_string_in_array(array, value, func)
         | 
| 16 | 
            +
                return false unless array.is_a?(Array) && value.is_a?(String)
         | 
| 17 | 
            +
                array.any?{ |s| s.is_a?(String) && func.call(value.downcase, s.downcase) } rescue false
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              private
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              def self.is_numeric(v)
         | 
| 23 | 
            +
                !(v.to_s =~ /\A[-+]?\d*\.?\d+\z/).nil?
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
    
        data/lib/evaluator.rb
    ADDED
    
    | @@ -0,0 +1,212 @@ | |
| 1 | 
            +
            require 'browser'
         | 
| 2 | 
            +
            require 'config_result'
         | 
| 3 | 
            +
            require 'digest'
         | 
| 4 | 
            +
            require 'evaluation_helpers'
         | 
| 5 | 
            +
            require 'spec_store'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            $fetch_from_server = :fetch_from_server
         | 
| 8 | 
            +
            $type_dynamic_config = 'dynamic_config'
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            class Evaluator
         | 
| 11 | 
            +
              include EvaluationHelpers
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              def initialize(store)
         | 
| 14 | 
            +
                @spec_store = store
         | 
| 15 | 
            +
                @initialized = true
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              def check_gate(user, gate_name)
         | 
| 19 | 
            +
                return nil unless @initialized && @spec_store.has_gate?(gate_name)
         | 
| 20 | 
            +
                self.eval_spec(user, @spec_store.get_gate(gate_name))
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              def get_config(user, config_name)
         | 
| 24 | 
            +
                return nil unless @initialized && @spec_store.has_config?(config_name)
         | 
| 25 | 
            +
                self.eval_spec(user, @spec_store.get_config(config_name))
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              private
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              def eval_spec(user, config)
         | 
| 31 | 
            +
                if config['enabled']
         | 
| 32 | 
            +
                  i = 0
         | 
| 33 | 
            +
                  until i >= config['rules'].length do
         | 
| 34 | 
            +
                    rule = config['rules'][i]
         | 
| 35 | 
            +
                    result = self.eval_rule(user, rule)
         | 
| 36 | 
            +
                    return $fetch_from_server if result == $fetch_from_server
         | 
| 37 | 
            +
                    if result
         | 
| 38 | 
            +
                      pass = self.eval_pass_percent(user, rule, config['salt'])
         | 
| 39 | 
            +
                      return ConfigResult.new(
         | 
| 40 | 
            +
                        config['name'],
         | 
| 41 | 
            +
                        pass,
         | 
| 42 | 
            +
                        pass ? rule['returnValue'] : config['defaultValue'],
         | 
| 43 | 
            +
                        rule['id'],
         | 
| 44 | 
            +
                      )
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    i += 1
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                ConfigResult.new(config['name'], false, config['defaultValue'], 'default')
         | 
| 52 | 
            +
              end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
              def eval_rule(user, rule)
         | 
| 55 | 
            +
                i = 0
         | 
| 56 | 
            +
                until i >= rule['conditions'].length do
         | 
| 57 | 
            +
                  result = self.eval_condition(user, rule['conditions'][i])
         | 
| 58 | 
            +
                  return result unless result == true
         | 
| 59 | 
            +
                  i += 1
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
                true
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              def eval_condition(user, condition)
         | 
| 65 | 
            +
                value = nil
         | 
| 66 | 
            +
                field = condition['field']
         | 
| 67 | 
            +
                target = condition['targetValue']
         | 
| 68 | 
            +
                type = condition['type']
         | 
| 69 | 
            +
                operator = condition['operator']
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                return $fetch_from_server unless type.is_a?(String)
         | 
| 72 | 
            +
                type = type.downcase
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                case type
         | 
| 75 | 
            +
                when 'public'
         | 
| 76 | 
            +
                  return true
         | 
| 77 | 
            +
                when 'fail_gate'
         | 
| 78 | 
            +
                when 'pass_gate'
         | 
| 79 | 
            +
                  other_gate_result = self.check_gate(user, target)
         | 
| 80 | 
            +
                  return $fetch_from_server if other_gate_result == $fetch_from_server
         | 
| 81 | 
            +
                  return type == 'pass_gate' ? other_gate_result[:gate_value] : !other_gate_result[:gate_value]
         | 
| 82 | 
            +
                when 'ip_based'
         | 
| 83 | 
            +
                  value = get_value_from_user(user, field) || get_value_from_ip(user['ip'], field)
         | 
| 84 | 
            +
                  return $fetch_from_server if value == $fetch_from_server
         | 
| 85 | 
            +
                when 'ua_based'
         | 
| 86 | 
            +
                  value = get_value_from_user(user, field) || get_value_from_ua(user['userAgent'], field)
         | 
| 87 | 
            +
                  return $fetch_from_server if value == $fetch_from_server
         | 
| 88 | 
            +
                when 'user_field'
         | 
| 89 | 
            +
                  value = get_value_from_user(user, field)
         | 
| 90 | 
            +
                when 'current_time'
         | 
| 91 | 
            +
                  value = Time.now.to_f # epoch time in seconds
         | 
| 92 | 
            +
                else
         | 
| 93 | 
            +
                  return $fetch_from_server
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                return $fetch_from_server if value == $fetch_from_server
         | 
| 97 | 
            +
                return false if value.nil?
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                return $fetch_from_server unless operator.is_a?(String)
         | 
| 100 | 
            +
                operator = operator.downcase
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                case operator
         | 
| 103 | 
            +
                  # numerical comparison
         | 
| 104 | 
            +
                when 'gt'
         | 
| 105 | 
            +
                  return compare_numbers(value, target, ->(a, b) { a > b })
         | 
| 106 | 
            +
                when 'gte'
         | 
| 107 | 
            +
                  return compare_numbers(value, target, ->(a, b) { a >= b })
         | 
| 108 | 
            +
                when 'lt'
         | 
| 109 | 
            +
                  return compare_numbers(value, target, ->(a, b) { a < b })
         | 
| 110 | 
            +
                when 'lte'
         | 
| 111 | 
            +
                  return compare_numbers(value, target, ->(a, b) { a <= b })
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                  # version comparison
         | 
| 114 | 
            +
                when 'version_gt'
         | 
| 115 | 
            +
                  return (Gem::Version.new(value) > Gem::Version.new(target) rescue false)
         | 
| 116 | 
            +
                when 'version_gte'
         | 
| 117 | 
            +
                  return (Gem::Version.new(value) >= Gem::Version.new(target) rescue false)
         | 
| 118 | 
            +
                when 'version_lt'
         | 
| 119 | 
            +
                  return (Gem::Version.new(value) < Gem::Version.new(target) rescue false)
         | 
| 120 | 
            +
                when 'version_lte'
         | 
| 121 | 
            +
                  return (Gem::Version.new(value) <= Gem::Version.new(target) rescue false)
         | 
| 122 | 
            +
                when 'version_eq'
         | 
| 123 | 
            +
                  return (Gem::Version.new(value) == Gem::Version.new(target) rescue false)
         | 
| 124 | 
            +
                when 'version_neq'
         | 
| 125 | 
            +
                  return (Gem::Version.new(value) != Gem::Version.new(target) rescue false)
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                  # array operations
         | 
| 128 | 
            +
                when 'any'
         | 
| 129 | 
            +
                  return array_contains(target, value)
         | 
| 130 | 
            +
                when 'none'
         | 
| 131 | 
            +
                  return !array_contains(target, value)
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                  #string
         | 
| 134 | 
            +
                when 'str_starts_with_any'
         | 
| 135 | 
            +
                  return match_string_in_array(target, value, ->(a, b) { a.start_with?(b) })
         | 
| 136 | 
            +
                when 'str_ends_with_any'
         | 
| 137 | 
            +
                  return match_string_in_array(target, value, ->(a, b) { a.end_with?(b) })
         | 
| 138 | 
            +
                when 'str_contains_any'
         | 
| 139 | 
            +
                  return match_string_in_array(target, value, ->(a, b) { a.include?(b) })
         | 
| 140 | 
            +
                when 'str_matches'
         | 
| 141 | 
            +
                  return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
         | 
| 142 | 
            +
                when 'eq'
         | 
| 143 | 
            +
                  return value == target
         | 
| 144 | 
            +
                when 'neq'
         | 
| 145 | 
            +
                  return value != target
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                  # dates
         | 
| 148 | 
            +
                when 'before'
         | 
| 149 | 
            +
                  # TODO - planned future conditions
         | 
| 150 | 
            +
                when 'after'
         | 
| 151 | 
            +
                  # TODO - planned future conditions
         | 
| 152 | 
            +
                when 'on'
         | 
| 153 | 
            +
                  # TODO - planned future conditions
         | 
| 154 | 
            +
                else
         | 
| 155 | 
            +
                  return $fetch_from_server
         | 
| 156 | 
            +
                end
         | 
| 157 | 
            +
              end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
              def get_value_from_user(user, field)
         | 
| 160 | 
            +
                return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                user_lookup_table = user&.value_lookup
         | 
| 163 | 
            +
                return nil unless user_lookup_table.is_a?(Hash)
         | 
| 164 | 
            +
                return user_lookup_table[field.downcase] if user_lookup_table.has_key?(field.downcase)
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                user_custom = user_lookup_table['custom']
         | 
| 167 | 
            +
                return nil unless user_custom.is_a?(Hash)
         | 
| 168 | 
            +
                user_custom.each do |key, value|
         | 
| 169 | 
            +
                  return value if key.downcase.casecmp(field.downcase)
         | 
| 170 | 
            +
                end
         | 
| 171 | 
            +
              end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
              def get_value_from_ip(ip, field)
         | 
| 174 | 
            +
                return nil unless ip.is_a?(String) && field.is_a?(String)
         | 
| 175 | 
            +
                # TODO
         | 
| 176 | 
            +
                $fetch_from_server
         | 
| 177 | 
            +
              end
         | 
| 178 | 
            +
             | 
| 179 | 
            +
              def get_value_from_ua(ua, field)
         | 
| 180 | 
            +
                return nil unless ua.is_a?(String) && field.is_a?(String)
         | 
| 181 | 
            +
                b = Browser.new(ua)
         | 
| 182 | 
            +
                case field.downcase
         | 
| 183 | 
            +
                when 'os_name'
         | 
| 184 | 
            +
                  os_name = b.platform.name
         | 
| 185 | 
            +
                  # special case for iOS because value is 'iOS (iPhone)'
         | 
| 186 | 
            +
                  if os_name.include?('iOS') || os_name.include?('ios')
         | 
| 187 | 
            +
                    return 'iOS'
         | 
| 188 | 
            +
                  else
         | 
| 189 | 
            +
                    return os_name
         | 
| 190 | 
            +
                  end
         | 
| 191 | 
            +
                when 'os_version'
         | 
| 192 | 
            +
                  return b.platform.version
         | 
| 193 | 
            +
                when 'browser_name'
         | 
| 194 | 
            +
                  return b.name
         | 
| 195 | 
            +
                when 'browser_version'
         | 
| 196 | 
            +
                  return b.full_version
         | 
| 197 | 
            +
                else
         | 
| 198 | 
            +
                  nil
         | 
| 199 | 
            +
                end
         | 
| 200 | 
            +
              end
         | 
| 201 | 
            +
             | 
| 202 | 
            +
              def eval_pass_percent(user, rule, salt)
         | 
| 203 | 
            +
                return false unless salt.is_a?(String) && !rule['passPercentage'].nil?
         | 
| 204 | 
            +
                begin
         | 
| 205 | 
            +
                  user_id = user.user_id || ''
         | 
| 206 | 
            +
                  hash = Digest::SHA256.digest("#{salt}.#{rule['name']}.#{user_id}").unpack('Q>')[0]
         | 
| 207 | 
            +
                  return hash % 10000 < rule['passPercentage'].to_f * 100
         | 
| 208 | 
            +
                rescue
         | 
| 209 | 
            +
                  return false
         | 
| 210 | 
            +
                end
         | 
| 211 | 
            +
              end
         | 
| 212 | 
            +
            end
         | 
    
        data/lib/network.rb
    ADDED
    
    | @@ -0,0 +1,72 @@ | |
| 1 | 
            +
            require 'concurrent'
         | 
| 2 | 
            +
            require 'http'
         | 
| 3 | 
            +
            require 'json'
         | 
| 4 | 
            +
            require 'dynamic_config'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            class Network
         | 
| 7 | 
            +
              include Concurrent::Async
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              def initialize(server_secret, api)
         | 
| 10 | 
            +
                super()
         | 
| 11 | 
            +
                unless api.end_with?('/')
         | 
| 12 | 
            +
                  api += '/'
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
                @http = HTTP
         | 
| 15 | 
            +
                    .headers({"STATSIG-API-KEY" => server_secret, "Content-Type" => "application/json; charset=UTF-8"})
         | 
| 16 | 
            +
                    .accept(:json)
         | 
| 17 | 
            +
                @api = api
         | 
| 18 | 
            +
                @last_sync_time = 0
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              def check_gate(user, gate_name)
         | 
| 22 | 
            +
                begin
         | 
| 23 | 
            +
                  request_body = JSON.generate({'user' => user&.serialize(), 'gateName' => gate_name})
         | 
| 24 | 
            +
                  response = @http.post(@api + 'check_gate', body: request_body)
         | 
| 25 | 
            +
                  return JSON.parse(response.body)
         | 
| 26 | 
            +
                rescue
         | 
| 27 | 
            +
                  return false
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              def get_config(user, dynamic_config_name)
         | 
| 32 | 
            +
                begin
         | 
| 33 | 
            +
                  request_body = JSON.generate({'user' => user&.serialize(), 'configName' => dynamic_config_name})
         | 
| 34 | 
            +
                  response = @http.post(@api + 'get_config', body: request_body)
         | 
| 35 | 
            +
                  return JSON.parse(response.body)
         | 
| 36 | 
            +
                rescue
         | 
| 37 | 
            +
                  return nil
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              def download_config_specs
         | 
| 42 | 
            +
                begin
         | 
| 43 | 
            +
                  response = @http.post(@api + 'download_config_specs', body: JSON.generate({'sinceTime' => @last_sync_time}))
         | 
| 44 | 
            +
                  json_body = JSON.parse(response.body)
         | 
| 45 | 
            +
                  @last_sync_time = json_body['time']
         | 
| 46 | 
            +
                  return json_body
         | 
| 47 | 
            +
                rescue
         | 
| 48 | 
            +
                  return nil
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              def poll_for_changes(callback)
         | 
| 53 | 
            +
                Thread.new do
         | 
| 54 | 
            +
                  loop do
         | 
| 55 | 
            +
                    sleep 10
         | 
| 56 | 
            +
                    specs = download_config_specs
         | 
| 57 | 
            +
                    unless specs.nil?
         | 
| 58 | 
            +
                      callback.call(specs)
         | 
| 59 | 
            +
                    end
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              def post_logs(events, statsig_metadata)
         | 
| 65 | 
            +
                begin
         | 
| 66 | 
            +
                  json_body = JSON.generate({'events' => events, 'statsigMetadata' => statsig_metadata})
         | 
| 67 | 
            +
                  @http.post(@api + 'log_event', body: json_body)
         | 
| 68 | 
            +
                rescue
         | 
| 69 | 
            +
                  # TODO: retries
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
              end
         | 
| 72 | 
            +
            end
         | 
    
        data/lib/spec_store.rb
    ADDED
    
    | @@ -0,0 +1,51 @@ | |
| 1 | 
            +
            require 'net/http'
         | 
| 2 | 
            +
            require 'uri'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            class SpecStore
         | 
| 5 | 
            +
              def initialize(specs_json)
         | 
| 6 | 
            +
                @last_sync_time = 0
         | 
| 7 | 
            +
                @store = {
         | 
| 8 | 
            +
                  :gates => {},
         | 
| 9 | 
            +
                  :configs => {},
         | 
| 10 | 
            +
                }
         | 
| 11 | 
            +
                process(specs_json)
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def process(specs_json)
         | 
| 15 | 
            +
                if specs_json.nil?
         | 
| 16 | 
            +
                  return
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                @last_sync_time = specs_json['time'] || @last_sync_time
         | 
| 20 | 
            +
                return unless specs_json['has_updates'] == true &&
         | 
| 21 | 
            +
                  !specs_json['feature_gates'].nil? &&
         | 
| 22 | 
            +
                  !specs_json['dynamic_configs'].nil?
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                @store = {
         | 
| 25 | 
            +
                  :gates => {},
         | 
| 26 | 
            +
                  :configs => {},
         | 
| 27 | 
            +
                }
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                specs_json['feature_gates'].map{|gate|  @store[:gates][gate['name']] = gate }
         | 
| 30 | 
            +
                specs_json['dynamic_configs'].map{|config|  @store[:configs][config['name']] = config }
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
              def has_gate?(gate_name)
         | 
| 34 | 
            +
                return @store[:gates].key?(gate_name)
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              def has_config?(config_name)
         | 
| 38 | 
            +
                return @store[:configs].key?(config_name)
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              def get_gate(gate_name)
         | 
| 42 | 
            +
                return nil unless has_gate?(gate_name)
         | 
| 43 | 
            +
                @store[:gates][gate_name]
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              def get_config(config_name)
         | 
| 47 | 
            +
                return nil unless has_config?(config_name)
         | 
| 48 | 
            +
                @store[:configs][config_name]
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            end
         | 
    
        data/lib/statsig.rb
    ADDED
    
    | @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            require 'statsig_driver'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Statsig
         | 
| 4 | 
            +
              def self.initialize(secret_key)
         | 
| 5 | 
            +
                unless @shared_instance.nil?
         | 
| 6 | 
            +
                  puts 'Statsig already initialized.'
         | 
| 7 | 
            +
                  return @shared_instance
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                @shared_instance = StatsigDriver.new(secret_key)
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              def self.check_gate(user, gate_name)
         | 
| 14 | 
            +
                self.ensure_initialized
         | 
| 15 | 
            +
                @shared_instance.check_gate(user, gate_name)
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              def self.get_config(user, dynamic_config_name)
         | 
| 19 | 
            +
                self.ensure_initialized
         | 
| 20 | 
            +
                @shared_instance.get_config(user, dynamic_config_name)
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              def self.log_event(user, event_name, value, metadata)
         | 
| 24 | 
            +
                self.ensure_initialized
         | 
| 25 | 
            +
                @shared_instance.log_event(user, event_name, value, metadata)
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              def self.shutdown
         | 
| 29 | 
            +
                unless @shared_instance.nil?
         | 
| 30 | 
            +
                  @shared_instance.shutdown
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
                @shared_instance = nil
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              private
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              def self.ensure_initialized
         | 
| 38 | 
            +
                if @shared_instance.nil?
         | 
| 39 | 
            +
                  raise 'Must call initialize first.'
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
            end
         | 
| @@ -0,0 +1,146 @@ | |
| 1 | 
            +
            require 'concurrent'
         | 
| 2 | 
            +
            require 'config_result'
         | 
| 3 | 
            +
            require 'evaluator'
         | 
| 4 | 
            +
            require 'network'
         | 
| 5 | 
            +
            require 'statsig_event'
         | 
| 6 | 
            +
            require 'statsig_logger'
         | 
| 7 | 
            +
            require 'statsig_user'
         | 
| 8 | 
            +
            require 'spec_store'
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            class StatsigDriver
         | 
| 11 | 
            +
                include Concurrent::Async
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def initialize(secret_key)
         | 
| 14 | 
            +
                    super()
         | 
| 15 | 
            +
                    if !secret_key.is_a?(String) || !secret_key.start_with?('secret-')
         | 
| 16 | 
            +
                      raise 'Invalid secret key provided. Provide your project secret key from the Statsig console'
         | 
| 17 | 
            +
                    end
         | 
| 18 | 
            +
                    @shutdown = false
         | 
| 19 | 
            +
                    @secret_key = secret_key
         | 
| 20 | 
            +
                    @net = Network.new(secret_key, 'https://api.statsig.com/v1/')
         | 
| 21 | 
            +
                    @statsig_metadata = {
         | 
| 22 | 
            +
                      'sdkType' => 'ruby-server',
         | 
| 23 | 
            +
                      'sdkVersion' => Gem::Specification::load('statsig.gemspec')&.version,
         | 
| 24 | 
            +
                    }
         | 
| 25 | 
            +
                    @logger = StatsigLogger.new(@net, @statsig_metadata)
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    downloaded_specs = @net.download_config_specs
         | 
| 28 | 
            +
                    unless downloaded_specs.nil?
         | 
| 29 | 
            +
                      @initialized = true
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    @store = SpecStore.new(downloaded_specs)
         | 
| 33 | 
            +
                    @evaluator = Evaluator.new(@store)
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    @polling_thread = @net.poll_for_changes(-> (config_specs) { @store.process(config_specs) })
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              
         | 
| 38 | 
            +
                def check_gate(user, gate_name)
         | 
| 39 | 
            +
                  if !user.nil? && !user.instance_of?(StatsigUser)
         | 
| 40 | 
            +
                    raise 'Must provide a valid StatsigUser'
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                  if !gate_name.is_a?(String) || gate_name.empty?
         | 
| 43 | 
            +
                    raise 'Invalid gate_name provided'
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                  check_shutdown
         | 
| 46 | 
            +
                  unless @initialized
         | 
| 47 | 
            +
                    return false
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  res = @evaluator.check_gate(user, gate_name)
         | 
| 51 | 
            +
                  if res.nil?
         | 
| 52 | 
            +
                    res = ConfigResult.new(gate_name)
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  if res == $fetch_from_server
         | 
| 56 | 
            +
                    res = check_gate_fallback(user, gate_name)
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  @logger.logGateExposure(user, res.name, res.gate_value, res.rule_id)
         | 
| 60 | 
            +
                  res.gate_value
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def get_config(user, dynamic_config_name)
         | 
| 64 | 
            +
                  if !user.nil? && !user.instance_of?(StatsigUser)
         | 
| 65 | 
            +
                    raise 'Must provide a valid StatsigUser or nil'
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                  if !dynamic_config_name.is_a?(String) || dynamic_config_name.empty?
         | 
| 68 | 
            +
                    raise "Invalid dynamic_config_name provided"
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
                  check_shutdown
         | 
| 71 | 
            +
                  unless @initialized
         | 
| 72 | 
            +
                    return DynamicConfig.new(dynamic_config_name)
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  res = @evaluator.get_config(user, dynamic_config_name)
         | 
| 76 | 
            +
                  if res.nil?
         | 
| 77 | 
            +
                    res = ConfigResult.new(dynamic_config_name)
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  if res == $fetch_from_server
         | 
| 81 | 
            +
                    res = get_config_fallback(user, dynamic_config_name)
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  result_config = DynamicConfig.new(res.name, res.json_value, res.rule_id)
         | 
| 85 | 
            +
                  @logger.logConfigExposure(user, result_config.name, result_config.rule_id)
         | 
| 86 | 
            +
                  result_config
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                def log_event(user, event_name, value = nil, metadata = nil)
         | 
| 90 | 
            +
                  if !user.nil? && !user.instance_of?(StatsigUser)
         | 
| 91 | 
            +
                    raise 'Must provide a valid StatsigUser or nil'
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
                  check_shutdown
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  event = StatsigEvent.new(event_name)
         | 
| 96 | 
            +
                  event.user = user&.serialize
         | 
| 97 | 
            +
                  event.value = value
         | 
| 98 | 
            +
                  event.metadata = metadata
         | 
| 99 | 
            +
                  event.statsig_metadata = @statsig_metadata
         | 
| 100 | 
            +
                  @logger.log_event(event)
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                def shutdown
         | 
| 104 | 
            +
                  @shutdown = true
         | 
| 105 | 
            +
                  @logger.flush
         | 
| 106 | 
            +
                  @polling_thread&.exit
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                private
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                def check_shutdown
         | 
| 112 | 
            +
                  if @shutdown
         | 
| 113 | 
            +
                    puts 'SDK has been shutdown.  Updates in the Statsig Console will no longer reflect.'
         | 
| 114 | 
            +
                  end
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def check_gate_fallback(user, gate_name)
         | 
| 118 | 
            +
                  network_result = @net.check_gate(user, gate_name)
         | 
| 119 | 
            +
                  if network_result.nil?
         | 
| 120 | 
            +
                    config_result = ConfigResult.new(gate_name)
         | 
| 121 | 
            +
                    return config_result
         | 
| 122 | 
            +
                  end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  ConfigResult.new(
         | 
| 125 | 
            +
                    network_result['name'],
         | 
| 126 | 
            +
                    network_result['value'],
         | 
| 127 | 
            +
                    {},
         | 
| 128 | 
            +
                    network_result['rule_id'],
         | 
| 129 | 
            +
                  )
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                def get_config_fallback(user, dynamic_config_name)
         | 
| 133 | 
            +
                  network_result = @net.get_config(user, dynamic_config_name)
         | 
| 134 | 
            +
                  if network_result.nil?
         | 
| 135 | 
            +
                    config_result = ConfigResult.new(dynamic_config_name)
         | 
| 136 | 
            +
                    return config_result
         | 
| 137 | 
            +
                  end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                  ConfigResult.new(
         | 
| 140 | 
            +
                    network_result['name'],
         | 
| 141 | 
            +
                    false,
         | 
| 142 | 
            +
                    network_result['value'],
         | 
| 143 | 
            +
                    network_result['rule_id'],
         | 
| 144 | 
            +
                  )
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
              end
         | 
| @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            class StatsigEvent
         | 
| 2 | 
            +
              attr_accessor :value
         | 
| 3 | 
            +
              attr_accessor :user
         | 
| 4 | 
            +
              attr_accessor :metadata
         | 
| 5 | 
            +
              attr_accessor :statsig_metadata
         | 
| 6 | 
            +
              def initialize(event_name)
         | 
| 7 | 
            +
                @event_name = event_name
         | 
| 8 | 
            +
                @time = Time.now.to_i * 1000
         | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              def serialize
         | 
| 12 | 
            +
                return {
         | 
| 13 | 
            +
                  'eventName' => @event_name,
         | 
| 14 | 
            +
                  'metadata' => @metadata,
         | 
| 15 | 
            +
                  'value' => @value,
         | 
| 16 | 
            +
                  'user' => @user,
         | 
| 17 | 
            +
                  'time' => @time,
         | 
| 18 | 
            +
                  'statsigMetadata' => @statsig_metadata,
         | 
| 19 | 
            +
                }
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
            end
         | 
| @@ -0,0 +1,52 @@ | |
| 1 | 
            +
            require 'statsig_event'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            $gate_exposure_event = 'statsig::gate_exposure'
         | 
| 4 | 
            +
            $config_exposure_event = 'statsig::config_exposure'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            class StatsigLogger
         | 
| 7 | 
            +
              def initialize(network, statsig_metadata)
         | 
| 8 | 
            +
                @network = network
         | 
| 9 | 
            +
                @statsig_metadata = statsig_metadata
         | 
| 10 | 
            +
                @events = []
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              def log_event(event)
         | 
| 14 | 
            +
                @events.push(event)
         | 
| 15 | 
            +
                if @events.length >= 500
         | 
| 16 | 
            +
                  flush()
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              def logGateExposure(user, gate_name, value, rule_id)
         | 
| 21 | 
            +
                event = StatsigEvent.new($gate_exposure_event)
         | 
| 22 | 
            +
                event.user = user
         | 
| 23 | 
            +
                event.metadata = {
         | 
| 24 | 
            +
                  'gate' => gate_name,
         | 
| 25 | 
            +
                  'gateValue' => value.to_s,
         | 
| 26 | 
            +
                  'ruleID' => rule_id
         | 
| 27 | 
            +
                }
         | 
| 28 | 
            +
                event.statsig_metadata = @statsig_metadata
         | 
| 29 | 
            +
                log_event(event)
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
              def logConfigExposure(user, config_name, rule_id)
         | 
| 33 | 
            +
                event = StatsigEvent.new($config_exposure_event)
         | 
| 34 | 
            +
                event.user = user
         | 
| 35 | 
            +
                event.metadata = {
         | 
| 36 | 
            +
                  'config' => config_name,
         | 
| 37 | 
            +
                  'ruleID' => rule_id
         | 
| 38 | 
            +
                }
         | 
| 39 | 
            +
                event.statsig_metadata = @statsig_metadata
         | 
| 40 | 
            +
                log_event(event)
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              def flush
         | 
| 44 | 
            +
                if @events.length == 0
         | 
| 45 | 
            +
                  return
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
                flush_events = @events.map { |e| e.serialize() }
         | 
| 48 | 
            +
                @events = []
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                @network.post_logs(flush_events, @statsig_metadata)
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
            end
         | 
    
        data/lib/statsig_user.rb
    ADDED
    
    | @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            class StatsigUser
         | 
| 2 | 
            +
              attr_accessor :user_id
         | 
| 3 | 
            +
              attr_accessor :email
         | 
| 4 | 
            +
              attr_accessor :ip
         | 
| 5 | 
            +
              attr_accessor :user_agent
         | 
| 6 | 
            +
              attr_accessor :country
         | 
| 7 | 
            +
              attr_accessor :locale
         | 
| 8 | 
            +
              attr_accessor :client_version
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def custom
         | 
| 11 | 
            +
                @custom
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def custom=(value)
         | 
| 15 | 
            +
                @custom = value.is_a?(Hash) ? value : Hash.new
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              def serialize
         | 
| 19 | 
            +
                {
         | 
| 20 | 
            +
                  'userID' => @user_id,
         | 
| 21 | 
            +
                  'email' => @email,
         | 
| 22 | 
            +
                  'ip' => @ip,
         | 
| 23 | 
            +
                  'userAgent' => @user_agent,
         | 
| 24 | 
            +
                  'country' => @country,
         | 
| 25 | 
            +
                  'locale' => @locale,
         | 
| 26 | 
            +
                  'clientVersion' => @client_version,
         | 
| 27 | 
            +
                  'custom' => @custom,
         | 
| 28 | 
            +
                }
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              def value_lookup
         | 
| 32 | 
            +
                {
         | 
| 33 | 
            +
                  'userID' => @user_id,
         | 
| 34 | 
            +
                  'userid' => @user_id,
         | 
| 35 | 
            +
                  'user_id' => @user_id,
         | 
| 36 | 
            +
                  'email' => @email,
         | 
| 37 | 
            +
                  'ip' => @ip,
         | 
| 38 | 
            +
                  'userAgent' => @user_agent,
         | 
| 39 | 
            +
                  'useragent' => @user_agent,
         | 
| 40 | 
            +
                  'user_agent' => @user_agent,
         | 
| 41 | 
            +
                  'country' => @country,
         | 
| 42 | 
            +
                  'locale' => @locale,
         | 
| 43 | 
            +
                  'clientVersion' => @client_version,
         | 
| 44 | 
            +
                  'clientversion' => @client_version,
         | 
| 45 | 
            +
                  'client_version' => @client_version,
         | 
| 46 | 
            +
                  'custom' => @custom,
         | 
| 47 | 
            +
                }
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,173 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: statsig
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Statsig, Inc
         | 
| 8 | 
            +
            autorequire:
         | 
| 9 | 
            +
            bindir: bin
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2021-06-01 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: concurrent-ruby
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - "~>"
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: '1.1'
         | 
| 20 | 
            +
                - - ">="
         | 
| 21 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 22 | 
            +
                    version: 1.1.0
         | 
| 23 | 
            +
              type: :development
         | 
| 24 | 
            +
              prerelease: false
         | 
| 25 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 26 | 
            +
                requirements:
         | 
| 27 | 
            +
                - - "~>"
         | 
| 28 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 29 | 
            +
                    version: '1.1'
         | 
| 30 | 
            +
                - - ">="
         | 
| 31 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 32 | 
            +
                    version: 1.1.0
         | 
| 33 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 34 | 
            +
              name: http
         | 
| 35 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 36 | 
            +
                requirements:
         | 
| 37 | 
            +
                - - "~>"
         | 
| 38 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 39 | 
            +
                    version: '4.4'
         | 
| 40 | 
            +
                - - ">="
         | 
| 41 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 42 | 
            +
                    version: 4.4.1
         | 
| 43 | 
            +
              type: :development
         | 
| 44 | 
            +
              prerelease: false
         | 
| 45 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 46 | 
            +
                requirements:
         | 
| 47 | 
            +
                - - "~>"
         | 
| 48 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 49 | 
            +
                    version: '4.4'
         | 
| 50 | 
            +
                - - ">="
         | 
| 51 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 52 | 
            +
                    version: 4.4.1
         | 
| 53 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 54 | 
            +
              name: browser
         | 
| 55 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 56 | 
            +
                requirements:
         | 
| 57 | 
            +
                - - "~>"
         | 
| 58 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 59 | 
            +
                    version: '5.3'
         | 
| 60 | 
            +
                - - ">="
         | 
| 61 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 62 | 
            +
                    version: 5.3.1
         | 
| 63 | 
            +
              type: :development
         | 
| 64 | 
            +
              prerelease: false
         | 
| 65 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 66 | 
            +
                requirements:
         | 
| 67 | 
            +
                - - "~>"
         | 
| 68 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 69 | 
            +
                    version: '5.3'
         | 
| 70 | 
            +
                - - ">="
         | 
| 71 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 72 | 
            +
                    version: 5.3.1
         | 
| 73 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 74 | 
            +
              name: browser
         | 
| 75 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 76 | 
            +
                requirements:
         | 
| 77 | 
            +
                - - "~>"
         | 
| 78 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 79 | 
            +
                    version: '5.3'
         | 
| 80 | 
            +
                - - ">="
         | 
| 81 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 82 | 
            +
                    version: 5.3.1
         | 
| 83 | 
            +
              type: :runtime
         | 
| 84 | 
            +
              prerelease: false
         | 
| 85 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 86 | 
            +
                requirements:
         | 
| 87 | 
            +
                - - "~>"
         | 
| 88 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 89 | 
            +
                    version: '5.3'
         | 
| 90 | 
            +
                - - ">="
         | 
| 91 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 92 | 
            +
                    version: 5.3.1
         | 
| 93 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 94 | 
            +
              name: concurrent-ruby
         | 
| 95 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 96 | 
            +
                requirements:
         | 
| 97 | 
            +
                - - "~>"
         | 
| 98 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 99 | 
            +
                    version: '1.1'
         | 
| 100 | 
            +
                - - ">="
         | 
| 101 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 102 | 
            +
                    version: 1.1.0
         | 
| 103 | 
            +
              type: :runtime
         | 
| 104 | 
            +
              prerelease: false
         | 
| 105 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 106 | 
            +
                requirements:
         | 
| 107 | 
            +
                - - "~>"
         | 
| 108 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 109 | 
            +
                    version: '1.1'
         | 
| 110 | 
            +
                - - ">="
         | 
| 111 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 112 | 
            +
                    version: 1.1.0
         | 
| 113 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 114 | 
            +
              name: http
         | 
| 115 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 116 | 
            +
                requirements:
         | 
| 117 | 
            +
                - - "~>"
         | 
| 118 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 119 | 
            +
                    version: '4.4'
         | 
| 120 | 
            +
                - - ">="
         | 
| 121 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 122 | 
            +
                    version: 4.4.1
         | 
| 123 | 
            +
              type: :runtime
         | 
| 124 | 
            +
              prerelease: false
         | 
| 125 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 126 | 
            +
                requirements:
         | 
| 127 | 
            +
                - - "~>"
         | 
| 128 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 129 | 
            +
                    version: '4.4'
         | 
| 130 | 
            +
                - - ">="
         | 
| 131 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 132 | 
            +
                    version: 4.4.1
         | 
| 133 | 
            +
            description: Statsig server SDK for feature gates and experimentation in Ruby
         | 
| 134 | 
            +
            email: support@statsig.com
         | 
| 135 | 
            +
            executables: []
         | 
| 136 | 
            +
            extensions: []
         | 
| 137 | 
            +
            extra_rdoc_files: []
         | 
| 138 | 
            +
            files:
         | 
| 139 | 
            +
            - lib/config_result.rb
         | 
| 140 | 
            +
            - lib/dynamic_config.rb
         | 
| 141 | 
            +
            - lib/evaluation_helpers.rb
         | 
| 142 | 
            +
            - lib/evaluator.rb
         | 
| 143 | 
            +
            - lib/network.rb
         | 
| 144 | 
            +
            - lib/spec_store.rb
         | 
| 145 | 
            +
            - lib/statsig.rb
         | 
| 146 | 
            +
            - lib/statsig_driver.rb
         | 
| 147 | 
            +
            - lib/statsig_event.rb
         | 
| 148 | 
            +
            - lib/statsig_logger.rb
         | 
| 149 | 
            +
            - lib/statsig_user.rb
         | 
| 150 | 
            +
            homepage: https://rubygems.org/gems/statsig
         | 
| 151 | 
            +
            licenses:
         | 
| 152 | 
            +
            - ISC
         | 
| 153 | 
            +
            metadata: {}
         | 
| 154 | 
            +
            post_install_message:
         | 
| 155 | 
            +
            rdoc_options: []
         | 
| 156 | 
            +
            require_paths:
         | 
| 157 | 
            +
            - lib
         | 
| 158 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 159 | 
            +
              requirements:
         | 
| 160 | 
            +
              - - ">="
         | 
| 161 | 
            +
                - !ruby/object:Gem::Version
         | 
| 162 | 
            +
                  version: '0'
         | 
| 163 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 164 | 
            +
              requirements:
         | 
| 165 | 
            +
              - - ">="
         | 
| 166 | 
            +
                - !ruby/object:Gem::Version
         | 
| 167 | 
            +
                  version: '0'
         | 
| 168 | 
            +
            requirements: []
         | 
| 169 | 
            +
            rubygems_version: 3.2.3
         | 
| 170 | 
            +
            signing_key:
         | 
| 171 | 
            +
            specification_version: 4
         | 
| 172 | 
            +
            summary: Statsig server SDK for Ruby
         | 
| 173 | 
            +
            test_files: []
         |