statsig 0.1.5 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dfe488b916b3d18b4f227e9783251537b8197ba6e449521bec56489a1c871988
4
- data.tar.gz: 49bda9345b2436a8715997b3a57330f10ceb304448106a38519c6d5a8f2ffe01
3
+ metadata.gz: 90f13c55a79aa919b94e633f69d39b3cb997a92cceec3e27bdb3e00e00e8e3ef
4
+ data.tar.gz: cd40c863516da1b3ff0730959ccbc3fa664afe0e613775f0369a1377ccc06123
5
5
  SHA512:
6
- metadata.gz: b8cf69b8e91ae507188de12dd3addd8ccbc7389bc630d175ee940f00a12aea28adf1080dc335229170ff95db330a04e4c74d42446ff29439c4de9ba6c75506d2
7
- data.tar.gz: fd23228461be6058ead19eaa9b4fce3705a0717b8eb8970ddcdd0b06bdb4734e9d103e2a910aaa09c31ab175d2d1776db199122dcf4c6d38fc77884b3fef65da
6
+ metadata.gz: a4c73b125853c8128d5aa57a60acfe1e80a0aa8ffc9f1fdcd89ea74e9a6ac033c93262103850d0c13ec84052ca9b9a464627836a1af945e6100c036c0fbc1359
7
+ data.tar.gz: 86bf2fe76cdc5cdc29126bd188a33e746f2a143e56fce1cd2e7d5a242622acb4b3783694f01e11a6e8ce10c707bc38a57a055694e15c965ee584bb951c6acb0d
@@ -1,3 +1,5 @@
1
+ require 'time'
2
+
1
3
  module EvaluationHelpers
2
4
  def self.compare_numbers(a, b, func)
3
5
  return false unless self.is_numeric(a) && self.is_numeric(b)
@@ -5,16 +7,28 @@ module EvaluationHelpers
5
7
  end
6
8
 
7
9
  # returns true if array contains value, ignoring case when comparing strings
8
- def self.array_contains(array, value)
10
+ def self.array_contains(array, value, ignore_case)
9
11
  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
+ if value.is_a?(String) && match_string_in_array(array, value, ignore_case, ->(a, b) { a == b })
13
+ return true
14
+ end
15
+ return array.include?(value)
12
16
  end
13
17
 
14
18
  # 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)
19
+ def self.match_string_in_array(array, value, ignore_case, func)
16
20
  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
21
+ array.any?{ |s| s.is_a?(String) && ((ignore_case && func.call(value.downcase, s.downcase)) || func.call(value, s)) } rescue false
22
+ end
23
+
24
+ def self.compare_times(a, b, func)
25
+ begin
26
+ time_1 = self.get_epoch_time(a)
27
+ time_2 = self.get_epoch_time(b)
28
+ func.call(time_1, time_2)
29
+ rescue
30
+ false
31
+ end
18
32
  end
19
33
 
20
34
  private
@@ -22,4 +36,13 @@ module EvaluationHelpers
22
36
  def self.is_numeric(v)
23
37
  !(v.to_s =~ /\A[-+]?\d*\.?\d+\z/).nil?
24
38
  end
39
+
40
+ def self.get_epoch_time(v)
41
+ time = self.is_numeric(v) ? Time.at(v.to_f) : Time.parse(v)
42
+ if time.year > Time.now.year + 100
43
+ # divide by 1000 when the epoch time is in milliseconds instead of seconds
44
+ return time.to_i / 1000
45
+ end
46
+ return time.to_i
47
+ end
25
48
  end
data/lib/evaluator.rb CHANGED
@@ -1,8 +1,11 @@
1
- require 'browser'
2
1
  require 'config_result'
2
+ require 'country_lookup'
3
3
  require 'digest'
4
4
  require 'evaluation_helpers'
5
5
  require 'spec_store'
6
+ require 'time'
7
+ require 'user_agent_parser'
8
+ require 'user_agent_parser/operating_system'
6
9
 
7
10
  $fetch_from_server = :fetch_from_server
8
11
  $type_dynamic_config = 'dynamic_config'
@@ -11,6 +14,8 @@ class Evaluator
11
14
  def initialize(store)
12
15
  @spec_store = store
13
16
  @initialized = true
17
+ @ua_parser = UserAgentParser::Parser.new
18
+ CountryLookup.initialize
14
19
  end
15
20
 
16
21
  def check_gate(user, gate_name)
@@ -65,8 +70,10 @@ class Evaluator
65
70
  target = condition['targetValue']
66
71
  type = condition['type']
67
72
  operator = condition['operator']
73
+ additional_values = condition['additionalValues']
74
+ additional_values = Hash.new unless additional_values.is_a? Hash
68
75
 
69
- return $fetch_from_server unless type.is_a?(String)
76
+ return $fetch_from_server unless type.is_a? String
70
77
  type = type.downcase
71
78
 
72
79
  case type
@@ -76,7 +83,7 @@ class Evaluator
76
83
  when 'pass_gate'
77
84
  other_gate_result = self.check_gate(user, target)
78
85
  return $fetch_from_server if other_gate_result == $fetch_from_server
79
- return type == 'pass_gate' ? other_gate_result[:gate_value] : !other_gate_result[:gate_value]
86
+ return type == 'pass_gate' ? other_gate_result.gate_value : !other_gate_result.gate_value
80
87
  when 'ip_based'
81
88
  value = get_value_from_user(user, field) || get_value_from_ip(user&.value_lookup['ip'], field)
82
89
  return $fetch_from_server if value == $fetch_from_server
@@ -85,8 +92,19 @@ class Evaluator
85
92
  return $fetch_from_server if value == $fetch_from_server
86
93
  when 'user_field'
87
94
  value = get_value_from_user(user, field)
95
+ when 'environment_field'
96
+ value = get_value_from_environment(user, field)
88
97
  when 'current_time'
89
98
  value = Time.now.to_f # epoch time in seconds
99
+ when 'user_bucket'
100
+ begin
101
+ salt = additional_values['salt']
102
+ user_id = user.user_id || ''
103
+ # there are only 1000 user buckets as opposed to 10k for gate pass %
104
+ value = compute_user_hash("#{salt}.#{user_id}") % 1000
105
+ rescue
106
+ return false
107
+ end
90
108
  else
91
109
  return $fetch_from_server
92
110
  end
@@ -124,17 +142,21 @@ class Evaluator
124
142
 
125
143
  # array operations
126
144
  when 'any'
127
- return EvaluationHelpers::array_contains(target, value)
145
+ return EvaluationHelpers::array_contains(target, value, true)
128
146
  when 'none'
129
- return !EvaluationHelpers::array_contains(target, value)
147
+ return !EvaluationHelpers::array_contains(target, value, true)
148
+ when 'any_case_sensitive'
149
+ return EvaluationHelpers::array_contains(target, value, false)
150
+ when 'none_case_sensitive'
151
+ return !EvaluationHelpers::array_contains(target, value, false)
130
152
 
131
153
  #string
132
154
  when 'str_starts_with_any'
133
- return EvaluationHelpers::match_string_in_array(target, value, ->(a, b) { a.start_with?(b) })
155
+ return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
134
156
  when 'str_ends_with_any'
135
- return EvaluationHelpers::match_string_in_array(target, value, ->(a, b) { a.end_with?(b) })
157
+ return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
136
158
  when 'str_contains_any'
137
- return EvaluationHelpers::match_string_in_array(target, value, ->(a, b) { a.include?(b) })
159
+ return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
138
160
  when 'str_matches'
139
161
  return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
140
162
  when 'eq'
@@ -144,11 +166,11 @@ class Evaluator
144
166
 
145
167
  # dates
146
168
  when 'before'
147
- # TODO - planned future conditions
169
+ return EvaluationHelpers::compare_times(value, target, ->(a, b) { a < b })
148
170
  when 'after'
149
- # TODO - planned future conditions
171
+ return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
150
172
  when 'on'
151
- # TODO - planned future conditions
173
+ return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
152
174
  else
153
175
  return $fetch_from_server
154
176
  end
@@ -164,47 +186,61 @@ class Evaluator
164
186
  user_custom = user_lookup_table['custom']
165
187
  return nil unless user_custom.is_a?(Hash)
166
188
  user_custom.each do |key, value|
167
- return value if key.downcase.casecmp(field.downcase)
189
+ return value if key.downcase.casecmp?(field.downcase)
190
+ end
191
+ nil
192
+ end
193
+
194
+ def get_value_from_environment(user, field)
195
+ return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
196
+ field = field.downcase
197
+ return nil unless user.statsig_environment.is_a? Hash
198
+ user.statsig_environment.each do |key, value|
199
+ return value if key.downcase == (field)
168
200
  end
201
+ nil
169
202
  end
170
203
 
171
204
  def get_value_from_ip(ip, field)
172
205
  return nil unless ip.is_a?(String) && field.is_a?(String)
173
- # TODO
174
- $fetch_from_server
206
+
207
+ if field.downcase != 'country'
208
+ return $fetch_from_server
209
+ end
210
+ CountryLookup.lookup_ip_string(ip)
175
211
  end
176
212
 
177
213
  def get_value_from_ua(ua, field)
178
214
  return nil unless ua.is_a?(String) && field.is_a?(String)
179
- b = Browser.new(ua)
215
+ parsed = @ua_parser.parse ua
216
+ os = parsed.os
180
217
  case field.downcase
181
- when 'os_name'
182
- os_name = b.platform.name
183
- # special case for iOS because value is 'iOS (iPhone)'
184
- if os_name.include?('iOS') || os_name.include?('ios')
185
- return 'iOS'
186
- else
187
- return os_name
188
- end
189
- when 'os_version'
190
- return b.platform.version
191
- when 'browser_name'
192
- return b.name
193
- when 'browser_version'
194
- return b.full_version
218
+ when 'os_name', 'osname'
219
+ return os&.family
220
+ when 'os_version', 'osversion'
221
+ return os&.version unless os&.version.nil?
222
+ when 'browser_name', 'browsername'
223
+ return parsed.family
224
+ when 'browser_version', 'browserversion'
225
+ return parsed.version.to_s
195
226
  else
196
227
  nil
197
228
  end
198
229
  end
199
230
 
200
- def eval_pass_percent(user, rule, salt)
201
- return false unless salt.is_a?(String) && !rule['passPercentage'].nil?
231
+ def eval_pass_percent(user, rule, config_salt)
232
+ return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
202
233
  begin
203
234
  user_id = user.user_id || ''
204
- hash = Digest::SHA256.digest("#{salt}.#{rule['name']}.#{user_id}").unpack('Q>')[0]
205
- return hash % 10000 < rule['passPercentage'].to_f * 100
235
+ rule_salt = rule['salt'] || rule['id'] || ''
236
+ hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{user_id}")
237
+ return (hash % 10000) < (rule['passPercentage'].to_f * 100)
206
238
  rescue
207
239
  return false
208
240
  end
209
241
  end
242
+
243
+ def compute_user_hash(user_hash)
244
+ Digest::SHA256.digest(user_hash).unpack('Q>')[0]
245
+ end
210
246
  end
data/lib/network.rb CHANGED
@@ -8,17 +8,24 @@ class Network
8
8
  unless api.end_with?('/')
9
9
  api += '/'
10
10
  end
11
- @http = HTTP
12
- .headers({"STATSIG-API-KEY" => server_secret, "Content-Type" => "application/json; charset=UTF-8"})
13
- .accept(:json)
11
+ @server_secret = server_secret
14
12
  @api = api
15
13
  @last_sync_time = 0
16
14
  end
17
15
 
16
+ def post_helper(endpoint, body)
17
+ http = HTTP.headers(
18
+ {"STATSIG-API-KEY" => @server_secret,
19
+ "STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_s,
20
+ "Content-Type" => "application/json; charset=UTF-8"
21
+ }).accept(:json)
22
+ http.post(@api + endpoint, body: body)
23
+ end
24
+
18
25
  def check_gate(user, gate_name)
19
26
  begin
20
27
  request_body = JSON.generate({'user' => user&.serialize, 'gateName' => gate_name})
21
- response = @http.post(@api + 'check_gate', body: request_body)
28
+ response = post_helper('check_gate', request_body)
22
29
  return JSON.parse(response.body)
23
30
  rescue
24
31
  return false
@@ -28,7 +35,7 @@ class Network
28
35
  def get_config(user, dynamic_config_name)
29
36
  begin
30
37
  request_body = JSON.generate({'user' => user&.serialize, 'configName' => dynamic_config_name})
31
- response = @http.post(@api + 'get_config', body: request_body)
38
+ response = post_helper('get_config', request_body)
32
39
  return JSON.parse(response.body)
33
40
  rescue
34
41
  return nil
@@ -37,7 +44,7 @@ class Network
37
44
 
38
45
  def download_config_specs
39
46
  begin
40
- response = @http.post(@api + 'download_config_specs', body: JSON.generate({'sinceTime' => @last_sync_time}))
47
+ response = post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
41
48
  json_body = JSON.parse(response.body)
42
49
  @last_sync_time = json_body['time']
43
50
  return json_body
@@ -61,7 +68,7 @@ class Network
61
68
  def post_logs(events, statsig_metadata)
62
69
  begin
63
70
  json_body = JSON.generate({'events' => events, 'statsigMetadata' => statsig_metadata})
64
- @http.post(@api + 'log_event', body: json_body)
71
+ post_helper('log_event', body: json_body)
65
72
  rescue
66
73
  # TODO: retries
67
74
  end
data/lib/statsig.rb CHANGED
@@ -1,28 +1,33 @@
1
1
  require 'statsig_driver'
2
2
 
3
3
  module Statsig
4
- def self.initialize(secret_key)
4
+ def self.initialize(secret_key, options = nil)
5
5
  unless @shared_instance.nil?
6
6
  puts 'Statsig already initialized.'
7
7
  return @shared_instance
8
8
  end
9
9
 
10
- @shared_instance = StatsigDriver.new(secret_key)
10
+ @shared_instance = StatsigDriver.new(secret_key, options)
11
11
  end
12
12
 
13
13
  def self.check_gate(user, gate_name)
14
14
  self.ensure_initialized
15
- @shared_instance.check_gate(user, gate_name)
15
+ @shared_instance&.check_gate(user, gate_name)
16
16
  end
17
17
 
18
18
  def self.get_config(user, dynamic_config_name)
19
19
  self.ensure_initialized
20
- @shared_instance.get_config(user, dynamic_config_name)
20
+ @shared_instance&.get_config(user, dynamic_config_name)
21
+ end
22
+
23
+ def self.get_experiment(user, experiment_name)
24
+ self.ensure_initialized
25
+ @shared_instance&.get_config(user, experiment_name)
21
26
  end
22
27
 
23
28
  def self.log_event(user, event_name, value, metadata)
24
29
  self.ensure_initialized
25
- @shared_instance.log_event(user, event_name, value, metadata)
30
+ @shared_instance&.log_event(user, event_name, value, metadata)
26
31
  end
27
32
 
28
33
  def self.shutdown
@@ -3,18 +3,24 @@ require 'evaluator'
3
3
  require 'network'
4
4
  require 'statsig_event'
5
5
  require 'statsig_logger'
6
+ require 'statsig_options'
6
7
  require 'statsig_user'
7
8
  require 'spec_store'
8
9
 
9
10
  class StatsigDriver
10
- def initialize(secret_key)
11
+ def initialize(secret_key, options = nil)
11
12
  super()
12
13
  if !secret_key.is_a?(String) || !secret_key.start_with?('secret-')
13
14
  raise 'Invalid secret key provided. Provide your project secret key from the Statsig console'
14
15
  end
16
+ if !options.nil? && !options.instance_of?(StatsigOptions)
17
+ raise 'Invalid options provided. Either provide a valid StatsigOptions object or nil'
18
+ end
19
+
20
+ @options = options || StatsigOptions.new()
15
21
  @shutdown = false
16
22
  @secret_key = secret_key
17
- @net = Network.new(secret_key, 'https://api.statsig.com/v1/')
23
+ @net = Network.new(secret_key, @options.api_url_base)
18
24
  @statsig_metadata = {
19
25
  'sdkType' => 'ruby-server',
20
26
  'sdkVersion' => Gem::Specification::load('statsig.gemspec')&.version,
@@ -33,9 +39,8 @@ class StatsigDriver
33
39
  end
34
40
 
35
41
  def check_gate(user, gate_name)
36
- if !user.nil? && !user.instance_of?(StatsigUser)
37
- raise 'Must provide a valid StatsigUser'
38
- end
42
+ validate_user(user)
43
+ user = normalize_user(user)
39
44
  if !gate_name.is_a?(String) || gate_name.empty?
40
45
  raise 'Invalid gate_name provided'
41
46
  end
@@ -58,9 +63,8 @@ class StatsigDriver
58
63
  end
59
64
 
60
65
  def get_config(user, dynamic_config_name)
61
- if !user.nil? && !user.instance_of?(StatsigUser)
62
- raise 'Must provide a valid StatsigUser or nil'
63
- end
66
+ validate_user(user)
67
+ user = normalize_user(user)
64
68
  if !dynamic_config_name.is_a?(String) || dynamic_config_name.empty?
65
69
  raise "Invalid dynamic_config_name provided"
66
70
  end
@@ -89,6 +93,8 @@ class StatsigDriver
89
93
  end
90
94
  check_shutdown
91
95
 
96
+ user = normalize_user(user)
97
+
92
98
  event = StatsigEvent.new(event_name)
93
99
  event.user = user&.serialize
94
100
  event.value = value
@@ -105,6 +111,19 @@ class StatsigDriver
105
111
 
106
112
  private
107
113
 
114
+ def validate_user(user)
115
+ if user.nil? || !user.instance_of?(StatsigUser) || !user.user_id.is_a?(String)
116
+ raise 'Must provide a valid StatsigUser with a user_id to use the server SDK. See https://docs.statsig.com/messages/serverRequiredUserID/ for more details.'
117
+ end
118
+ end
119
+
120
+ def normalize_user(user)
121
+ if !@options&.environment.nil?
122
+ user.statsig_environment = @options.environment
123
+ end
124
+ user
125
+ end
126
+
108
127
  def check_shutdown
109
128
  if @shutdown
110
129
  puts 'SDK has been shutdown. Updates in the Statsig Console will no longer reflect.'
data/lib/statsig_event.rb CHANGED
@@ -5,7 +5,7 @@ class StatsigEvent
5
5
  attr_accessor :statsig_metadata
6
6
  def initialize(event_name)
7
7
  @event_name = event_name
8
- @time = Time.now.to_i * 1000
8
+ @time = Time.now.to_f * 1000
9
9
  end
10
10
 
11
11
  def serialize
@@ -0,0 +1,9 @@
1
+ class StatsigOptions
2
+ attr_reader :environment
3
+ attr_reader :api_url_base
4
+
5
+ def initialize(environment = nil, api_url_base = 'https://api.statsig.com/v1')
6
+ @environment = environment.is_a?(Hash) ? environment : nil
7
+ @api_url_base = api_url_base
8
+ end
9
+ end
data/lib/statsig_user.rb CHANGED
@@ -5,7 +5,8 @@ class StatsigUser
5
5
  attr_accessor :user_agent
6
6
  attr_accessor :country
7
7
  attr_accessor :locale
8
- attr_accessor :client_version
8
+ attr_accessor :app_version
9
+ attr_accessor :statsig_environment
9
10
 
10
11
  def custom
11
12
  @custom
@@ -15,16 +16,19 @@ class StatsigUser
15
16
  @custom = value.is_a?(Hash) ? value : Hash.new
16
17
  end
17
18
 
18
- def initialize(user_hash = nil)
19
+ def initialize(user_hash)
20
+ @statsig_environment = Hash.new
19
21
  if user_hash.is_a?(Hash)
20
- @user_id = user_hash['userID']
22
+ @user_id = user_hash['userID'] || user_hash['user_id']
23
+ @user_id = @user_id.to_s unless @user_id.nil?
21
24
  @email = user_hash['email']
22
25
  @ip = user_hash['ip']
23
- @user_agent = user_hash['userAgent']
26
+ @user_agent = user_hash['userAgent'] || user_hash['user_agent']
24
27
  @country = user_hash['country']
25
28
  @locale = user_hash['locale']
26
- @client_version = user_hash['clientVersion']
29
+ @app_version = user_hash['appVersion'] || user_hash['app_version']
27
30
  @custom = user_hash['custom']
31
+ @statsig_environment = user_hash['statsigEnvironment']
28
32
  end
29
33
  end
30
34
 
@@ -36,8 +40,9 @@ class StatsigUser
36
40
  'userAgent' => @user_agent,
37
41
  'country' => @country,
38
42
  'locale' => @locale,
39
- 'clientVersion' => @client_version,
43
+ 'appVersion' => @app_version,
40
44
  'custom' => @custom,
45
+ 'statsigEnvironment' => @statsig_environment,
41
46
  }
42
47
  end
43
48
 
@@ -53,9 +58,9 @@ class StatsigUser
53
58
  'user_agent' => @user_agent,
54
59
  'country' => @country,
55
60
  'locale' => @locale,
56
- 'clientVersion' => @client_version,
57
- 'clientversion' => @client_version,
58
- 'client_version' => @client_version,
61
+ 'appVersion' => @app_version,
62
+ 'appversion' => @app_version,
63
+ 'app_version' => @app_version,
59
64
  'custom' => @custom,
60
65
  }
61
66
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statsig
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Statsig, Inc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-05 00:00:00.000000000 Z
11
+ date: 2021-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -25,25 +25,19 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.1'
27
27
  - !ruby/object:Gem::Dependency
28
- name: browser
28
+ name: user_agent_parser
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '5.3'
34
- - - ">="
35
- - !ruby/object:Gem::Version
36
- version: 5.3.1
33
+ version: '2.7'
37
34
  type: :runtime
38
35
  prerelease: false
39
36
  version_requirements: !ruby/object:Gem::Requirement
40
37
  requirements:
41
38
  - - "~>"
42
39
  - !ruby/object:Gem::Version
43
- version: '5.3'
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: 5.3.1
40
+ version: '2.7'
47
41
  - !ruby/object:Gem::Dependency
48
42
  name: http
49
43
  requirement: !ruby/object:Gem::Requirement
@@ -51,9 +45,6 @@ dependencies:
51
45
  - - "~>"
52
46
  - !ruby/object:Gem::Version
53
47
  version: '4.4'
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- version: 4.4.1
57
48
  type: :runtime
58
49
  prerelease: false
59
50
  version_requirements: !ruby/object:Gem::Requirement
@@ -61,9 +52,20 @@ dependencies:
61
52
  - - "~>"
62
53
  - !ruby/object:Gem::Version
63
54
  version: '4.4'
64
- - - ">="
55
+ - !ruby/object:Gem::Dependency
56
+ name: ip3country
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
65
67
  - !ruby/object:Gem::Version
66
- version: 4.4.1
68
+ version: '0.1'
67
69
  description: Statsig server SDK for feature gates and experimentation in Ruby
68
70
  email: support@statsig.com
69
71
  executables: []
@@ -80,6 +82,7 @@ files:
80
82
  - lib/statsig_driver.rb
81
83
  - lib/statsig_event.rb
82
84
  - lib/statsig_logger.rb
85
+ - lib/statsig_options.rb
83
86
  - lib/statsig_user.rb
84
87
  homepage: https://rubygems.org/gems/statsig
85
88
  licenses: