statsig 0.1.5 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: