statsig 0.1.3 → 1.4.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: f41a1d20b9c249d023d561e447fc4f7dc746107e4215074596b405a3c22598bc
4
- data.tar.gz: 3a285af817ab26710aff500afe493d7d79153d652fc62cb1c36b993e98812fb4
3
+ metadata.gz: 3ac164a57fadf37c1c9ce2b978df30da43901cc3b53e03e1b3e52a72e19981cb
4
+ data.tar.gz: aad0387cd6a80bfd3cbf501118466f444790ec387b45a96d562a3256331ae034
5
5
  SHA512:
6
- metadata.gz: e29c7d17b69110935f96141a98faff6fa4644a795c4b7fd6600c04fe3835ad97cbf8928d1acc43742491d0c0c61d3247243c695cadbecf3ec618748377774b90
7
- data.tar.gz: 0dc599774ade558878168fd121c3ceb4df2ca88e51b1bca16c2eb0fce52b8d1b0f23237215e49c92b11b6e07edd1a8d01788bbfe8e251c227fb4d59b35e59ebd
6
+ metadata.gz: f7eeefd82a45217becadee9783d2616de5e6997d29236fecea526947a34e54f921125fbf018f57a1fbf2f7560c00d80721f632a9498e0f065b6914b035beef5f
7
+ data.tar.gz: 24b8c55535b7656109b669d898351e536953c83ecc6858e94eabacbdb36f8c9e600b55bb9cd06fc8b043a84305f351e645c07a673a85c1688668de6181c125f3
@@ -9,8 +9,8 @@ class DynamicConfig
9
9
  @rule_id = rule_id
10
10
  end
11
11
 
12
- def get(index)
13
- return nil if @value.nil?
14
- value[index]
12
+ def get(index, default_value)
13
+ return default_value if @value.nil? || !@value.key?(index)
14
+ @value[index]
15
15
  end
16
16
  end
@@ -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,36 +70,45 @@ 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
73
80
  when 'public'
74
81
  return true
75
- when 'fail_gate'
76
- when 'pass_gate'
82
+ when 'fail_gate', 'pass_gate'
77
83
  other_gate_result = self.check_gate(user, target)
78
84
  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]
85
+ return type == 'pass_gate' ? other_gate_result.gate_value : !other_gate_result.gate_value
80
86
  when 'ip_based'
81
- value = get_value_from_user(user, field) || get_value_from_ip(user['ip'], field)
87
+ value = get_value_from_user(user, field) || get_value_from_ip(user&.value_lookup['ip'], field)
82
88
  return $fetch_from_server if value == $fetch_from_server
83
89
  when 'ua_based'
84
- value = get_value_from_user(user, field) || get_value_from_ua(user['userAgent'], field)
90
+ value = get_value_from_user(user, field) || get_value_from_ua(user&.value_lookup['userAgent'], field)
85
91
  return $fetch_from_server if value == $fetch_from_server
86
92
  when 'user_field'
87
93
  value = get_value_from_user(user, field)
94
+ when 'environment_field'
95
+ value = get_value_from_environment(user, field)
88
96
  when 'current_time'
89
97
  value = Time.now.to_f # epoch time in seconds
98
+ when 'user_bucket'
99
+ begin
100
+ salt = additional_values['salt']
101
+ user_id = user.user_id || ''
102
+ # there are only 1000 user buckets as opposed to 10k for gate pass %
103
+ value = compute_user_hash("#{salt}.#{user_id}") % 1000
104
+ rescue
105
+ return false
106
+ end
90
107
  else
91
108
  return $fetch_from_server
92
109
  end
93
110
 
94
- return $fetch_from_server if value == $fetch_from_server
95
- return false if value.nil?
96
-
97
- return $fetch_from_server unless operator.is_a?(String)
111
+ return $fetch_from_server if value == $fetch_from_server || !operator.is_a?(String)
98
112
  operator = operator.downcase
99
113
 
100
114
  case operator
@@ -124,17 +138,21 @@ class Evaluator
124
138
 
125
139
  # array operations
126
140
  when 'any'
127
- return EvaluationHelpers::array_contains(target, value)
141
+ return EvaluationHelpers::array_contains(target, value, true)
128
142
  when 'none'
129
- return !EvaluationHelpers::array_contains(target, value)
143
+ return !EvaluationHelpers::array_contains(target, value, true)
144
+ when 'any_case_sensitive'
145
+ return EvaluationHelpers::array_contains(target, value, false)
146
+ when 'none_case_sensitive'
147
+ return !EvaluationHelpers::array_contains(target, value, false)
130
148
 
131
149
  #string
132
150
  when 'str_starts_with_any'
133
- return EvaluationHelpers::match_string_in_array(target, value, ->(a, b) { a.start_with?(b) })
151
+ return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
134
152
  when 'str_ends_with_any'
135
- return EvaluationHelpers::match_string_in_array(target, value, ->(a, b) { a.end_with?(b) })
153
+ return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
136
154
  when 'str_contains_any'
137
- return EvaluationHelpers::match_string_in_array(target, value, ->(a, b) { a.include?(b) })
155
+ return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
138
156
  when 'str_matches'
139
157
  return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
140
158
  when 'eq'
@@ -144,11 +162,11 @@ class Evaluator
144
162
 
145
163
  # dates
146
164
  when 'before'
147
- # TODO - planned future conditions
165
+ return EvaluationHelpers::compare_times(value, target, ->(a, b) { a < b })
148
166
  when 'after'
149
- # TODO - planned future conditions
167
+ return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
150
168
  when 'on'
151
- # TODO - planned future conditions
169
+ return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
152
170
  else
153
171
  return $fetch_from_server
154
172
  end
@@ -164,47 +182,61 @@ class Evaluator
164
182
  user_custom = user_lookup_table['custom']
165
183
  return nil unless user_custom.is_a?(Hash)
166
184
  user_custom.each do |key, value|
167
- return value if key.downcase.casecmp(field.downcase)
185
+ return value if key.downcase.casecmp?(field.downcase)
186
+ end
187
+ nil
188
+ end
189
+
190
+ def get_value_from_environment(user, field)
191
+ return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
192
+ field = field.downcase
193
+ return nil unless user.statsig_environment.is_a? Hash
194
+ user.statsig_environment.each do |key, value|
195
+ return value if key.downcase == (field)
168
196
  end
197
+ nil
169
198
  end
170
199
 
171
200
  def get_value_from_ip(ip, field)
172
201
  return nil unless ip.is_a?(String) && field.is_a?(String)
173
- # TODO
174
- $fetch_from_server
202
+
203
+ if field.downcase != 'country'
204
+ return $fetch_from_server
205
+ end
206
+ CountryLookup.lookup_ip_string(ip)
175
207
  end
176
208
 
177
209
  def get_value_from_ua(ua, field)
178
210
  return nil unless ua.is_a?(String) && field.is_a?(String)
179
- b = Browser.new(ua)
211
+ parsed = @ua_parser.parse ua
212
+ os = parsed.os
180
213
  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
214
+ when 'os_name', 'osname'
215
+ return os&.family
216
+ when 'os_version', 'osversion'
217
+ return os&.version unless os&.version.nil?
218
+ when 'browser_name', 'browsername'
219
+ return parsed.family
220
+ when 'browser_version', 'browserversion'
221
+ return parsed.version.to_s
195
222
  else
196
223
  nil
197
224
  end
198
225
  end
199
226
 
200
- def eval_pass_percent(user, rule, salt)
201
- return false unless salt.is_a?(String) && !rule['passPercentage'].nil?
227
+ def eval_pass_percent(user, rule, config_salt)
228
+ return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
202
229
  begin
203
230
  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
231
+ rule_salt = rule['salt'] || rule['id'] || ''
232
+ hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{user_id}")
233
+ return (hash % 10000) < (rule['passPercentage'].to_f * 100)
206
234
  rescue
207
235
  return false
208
236
  end
209
237
  end
238
+
239
+ def compute_user_hash(user_hash)
240
+ Digest::SHA256.digest(user_hash).unpack('Q>')[0]
241
+ end
210
242
  end
data/lib/network.rb CHANGED
@@ -2,24 +2,47 @@ require 'http'
2
2
  require 'json'
3
3
  require 'dynamic_config'
4
4
 
5
+ $retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
6
+
5
7
  class Network
6
- def initialize(server_secret, api)
8
+ def initialize(server_secret, api, backoff_mult = 10)
7
9
  super()
8
10
  unless api.end_with?('/')
9
11
  api += '/'
10
12
  end
11
- @http = HTTP
12
- .headers({"STATSIG-API-KEY" => server_secret, "Content-Type" => "application/json; charset=UTF-8"})
13
- .accept(:json)
13
+ @server_secret = server_secret
14
14
  @api = api
15
15
  @last_sync_time = 0
16
+ @backoff_multiplier = backoff_mult
17
+ end
18
+
19
+ def post_helper(endpoint, body, retries = 0, backoff = 1)
20
+ http = HTTP.headers(
21
+ {"STATSIG-API-KEY" => @server_secret,
22
+ "STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_s,
23
+ "Content-Type" => "application/json; charset=UTF-8"
24
+ }).accept(:json)
25
+ begin
26
+ res = http.post(@api + endpoint, body: body)
27
+ rescue
28
+ ## network error retry
29
+ return nil unless retries > 0
30
+ sleep backoff
31
+ return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
32
+ end
33
+ return res unless !res.status.success?
34
+ return nil unless retries > 0 && $retry_codes.include?(res.code)
35
+ ## status code retry
36
+ sleep backoff
37
+ post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
16
38
  end
17
39
 
18
40
  def check_gate(user, gate_name)
19
41
  begin
20
42
  request_body = JSON.generate({'user' => user&.serialize, 'gateName' => gate_name})
21
- response = @http.post(@api + 'check_gate', body: request_body)
22
- return JSON.parse(response.body)
43
+ response = post_helper('check_gate', request_body)
44
+ return JSON.parse(response.body) unless response.nil?
45
+ false
23
46
  rescue
24
47
  return false
25
48
  end
@@ -28,8 +51,9 @@ class Network
28
51
  def get_config(user, dynamic_config_name)
29
52
  begin
30
53
  request_body = JSON.generate({'user' => user&.serialize, 'configName' => dynamic_config_name})
31
- response = @http.post(@api + 'get_config', body: request_body)
32
- return JSON.parse(response.body)
54
+ response = post_helper('get_config', request_body)
55
+ return JSON.parse(response.body) unless response.nil?
56
+ nil
33
57
  rescue
34
58
  return nil
35
59
  end
@@ -37,7 +61,8 @@ class Network
37
61
 
38
62
  def download_config_specs
39
63
  begin
40
- response = @http.post(@api + 'download_config_specs', body: JSON.generate({'sinceTime' => @last_sync_time}))
64
+ response = post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
65
+ return nil unless !response.nil?
41
66
  json_body = JSON.parse(response.body)
42
67
  @last_sync_time = json_body['time']
43
68
  return json_body
@@ -61,9 +86,8 @@ class Network
61
86
  def post_logs(events, statsig_metadata)
62
87
  begin
63
88
  json_body = JSON.generate({'events' => events, 'statsigMetadata' => statsig_metadata})
64
- @http.post(@api + 'log_event', body: json_body)
89
+ post_helper('log_event', body: json_body, retries: 5)
65
90
  rescue
66
- # TODO: retries
67
91
  end
68
92
  end
69
93
  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
@@ -99,12 +105,25 @@ class StatsigDriver
99
105
 
100
106
  def shutdown
101
107
  @shutdown = true
102
- @logger.flush
108
+ @logger.flush(true)
103
109
  @polling_thread&.exit
104
110
  end
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
@@ -8,6 +8,10 @@ class StatsigLogger
8
8
  @network = network
9
9
  @statsig_metadata = statsig_metadata
10
10
  @events = []
11
+ @background_flush = Thread.new do
12
+ sleep 60
13
+ flush
14
+ end
11
15
  end
12
16
 
13
17
  def log_event(event)
@@ -40,13 +44,18 @@ class StatsigLogger
40
44
  log_event(event)
41
45
  end
42
46
 
43
- def flush
47
+ def flush(closing = false)
48
+ if closing
49
+ @background_flush.exit
50
+ end
44
51
  if @events.length == 0
45
52
  return
46
53
  end
47
54
  flush_events = @events.map { |e| e.serialize() }
48
55
  @events = []
49
56
 
50
- @network.post_logs(flush_events, @statsig_metadata)
57
+ Thread.new do
58
+ @network.post_logs(flush_events, @statsig_metadata)
59
+ end
51
60
  end
52
61
  end
@@ -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['user_id']
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['user_agent']
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['client_version']
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.3
4
+ version: 1.4.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-04 00:00:00.000000000 Z
11
+ date: 2021-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -25,25 +25,61 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.1'
27
27
  - !ruby/object:Gem::Dependency
28
- name: browser
28
+ name: webmock
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '5.3'
34
- - - ">="
33
+ version: '3.13'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
35
39
  - !ruby/object:Gem::Version
36
- version: 5.3.1
37
- type: :runtime
40
+ version: '3.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.14'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.14'
55
+ - !ruby/object:Gem::Dependency
56
+ name: spy
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
38
63
  prerelease: false
39
64
  version_requirements: !ruby/object:Gem::Requirement
40
65
  requirements:
41
66
  - - "~>"
42
67
  - !ruby/object:Gem::Version
43
- version: '5.3'
44
- - - ">="
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: user_agent_parser
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.7'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
45
81
  - !ruby/object:Gem::Version
46
- version: 5.3.1
82
+ version: '2.7'
47
83
  - !ruby/object:Gem::Dependency
48
84
  name: http
49
85
  requirement: !ruby/object:Gem::Requirement
@@ -51,9 +87,6 @@ dependencies:
51
87
  - - "~>"
52
88
  - !ruby/object:Gem::Version
53
89
  version: '4.4'
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- version: 4.4.1
57
90
  type: :runtime
58
91
  prerelease: false
59
92
  version_requirements: !ruby/object:Gem::Requirement
@@ -61,9 +94,20 @@ dependencies:
61
94
  - - "~>"
62
95
  - !ruby/object:Gem::Version
63
96
  version: '4.4'
64
- - - ">="
97
+ - !ruby/object:Gem::Dependency
98
+ name: ip3country
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.1'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
65
109
  - !ruby/object:Gem::Version
66
- version: 4.4.1
110
+ version: '0.1'
67
111
  description: Statsig server SDK for feature gates and experimentation in Ruby
68
112
  email: support@statsig.com
69
113
  executables: []
@@ -80,6 +124,7 @@ files:
80
124
  - lib/statsig_driver.rb
81
125
  - lib/statsig_event.rb
82
126
  - lib/statsig_logger.rb
127
+ - lib/statsig_options.rb
83
128
  - lib/statsig_user.rb
84
129
  homepage: https://rubygems.org/gems/statsig
85
130
  licenses: