statsig 0.1.5 → 1.6.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: a1985e842f0301387596fbd3d8db298ce485c013a502c09a1aff00916882afc0
4
+ data.tar.gz: 9cf814e0def7dc3fefd2db9d80f102ec318a41703e6638f66b8a8d844c751959
5
5
  SHA512:
6
- metadata.gz: b8cf69b8e91ae507188de12dd3addd8ccbc7389bc630d175ee940f00a12aea28adf1080dc335229170ff95db330a04e4c74d42446ff29439c4de9ba6c75506d2
7
- data.tar.gz: fd23228461be6058ead19eaa9b4fce3705a0717b8eb8970ddcdd0b06bdb4734e9d103e2a910aaa09c31ab175d2d1776db199122dcf4c6d38fc77884b3fef65da
6
+ metadata.gz: ba2661bcd29ca31b3c458f8046add904029b03ecc0eeeb0c358760467389e58c5f48b5bc39456df1ea2e30051df951359a37e1045283a5d77f911291b0f7bfcd
7
+ data.tar.gz: 2631c284b8b792101558e90689353c79aa043a22cbf2f995f045efc4333f74d3d80fcca639ece7f11d734cc4a39b459edd08ce14f6603f86f09ad57afadf2c08
@@ -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&.value_lookup['ip'], field)
87
+ value = get_value_from_user(user, field) || get_value_from_ip(user, 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&.value_lookup['userAgent'], field)
90
+ value = get_value_from_user(user, field) || get_value_from_ua(user, 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,23 @@ 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) })
156
+ when 'str_contains_none'
157
+ return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
138
158
  when 'str_matches'
139
159
  return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
140
160
  when 'eq'
@@ -144,11 +164,11 @@ class Evaluator
144
164
 
145
165
  # dates
146
166
  when 'before'
147
- # TODO - planned future conditions
167
+ return EvaluationHelpers::compare_times(value, target, ->(a, b) { a < b })
148
168
  when 'after'
149
- # TODO - planned future conditions
169
+ return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
150
170
  when 'on'
151
- # TODO - planned future conditions
171
+ return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
152
172
  else
153
173
  return $fetch_from_server
154
174
  end
@@ -159,52 +179,77 @@ class Evaluator
159
179
 
160
180
  user_lookup_table = user&.value_lookup
161
181
  return nil unless user_lookup_table.is_a?(Hash)
162
- return user_lookup_table[field.downcase] if user_lookup_table.has_key?(field.downcase)
182
+ return user_lookup_table[field.downcase] if user_lookup_table.has_key?(field.downcase) && !user_lookup_table[field.downcase].nil?
163
183
 
164
184
  user_custom = user_lookup_table['custom']
165
- return nil unless user_custom.is_a?(Hash)
166
- user_custom.each do |key, value|
167
- return value if key.downcase.casecmp(field.downcase)
185
+ if user_custom.is_a?(Hash)
186
+ user_custom.each do |key, value|
187
+ return value if key.downcase.casecmp?(field.downcase) && !value.nil?
188
+ end
189
+ end
190
+
191
+ private_attributes = user_lookup_table['privateAttributes']
192
+ if private_attributes.is_a?(Hash)
193
+ private_attributes.each do |key, value|
194
+ return value if key.downcase.casecmp?(field.downcase) && !value.nil?
195
+ end
196
+ end
197
+
198
+ nil
199
+ end
200
+
201
+ def get_value_from_environment(user, field)
202
+ return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
203
+ field = field.downcase
204
+ return nil unless user.statsig_environment.is_a? Hash
205
+ user.statsig_environment.each do |key, value|
206
+ return value if key.downcase == (field)
168
207
  end
208
+ nil
169
209
  end
170
210
 
171
- def get_value_from_ip(ip, field)
172
- return nil unless ip.is_a?(String) && field.is_a?(String)
173
- # TODO
174
- $fetch_from_server
211
+ def get_value_from_ip(user, field)
212
+ return nil unless user.is_a?(StatsigUser) && field.is_a?(String) && field.downcase == 'country'
213
+ ip = get_value_from_user(user, 'ip')
214
+ return nil unless ip.is_a?(String)
215
+
216
+ CountryLookup.lookup_ip_string(ip)
175
217
  end
176
218
 
177
- def get_value_from_ua(ua, field)
178
- return nil unless ua.is_a?(String) && field.is_a?(String)
179
- b = Browser.new(ua)
219
+ def get_value_from_ua(user, field)
220
+ return nil unless user.is_a?(StatsigUser) && field.is_a?(String)
221
+ ua = get_value_from_user(user, 'userAgent')
222
+ return nil unless ua.is_a?(String)
223
+
224
+ parsed = @ua_parser.parse ua
225
+ os = parsed.os
180
226
  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
227
+ when 'os_name', 'osname'
228
+ return os&.family
229
+ when 'os_version', 'osversion'
230
+ return os&.version unless os&.version.nil?
231
+ when 'browser_name', 'browsername'
232
+ return parsed.family
233
+ when 'browser_version', 'browserversion'
234
+ return parsed.version.to_s
195
235
  else
196
236
  nil
197
237
  end
198
238
  end
199
239
 
200
- def eval_pass_percent(user, rule, salt)
201
- return false unless salt.is_a?(String) && !rule['passPercentage'].nil?
240
+ def eval_pass_percent(user, rule, config_salt)
241
+ return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
202
242
  begin
203
243
  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
244
+ rule_salt = rule['salt'] || rule['id'] || ''
245
+ hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{user_id}")
246
+ return (hash % 10000) < (rule['passPercentage'].to_f * 100)
206
247
  rescue
207
248
  return false
208
249
  end
209
250
  end
251
+
252
+ def compute_user_hash(user_hash)
253
+ Digest::SHA256.digest(user_hash).unpack('Q>')[0]
254
+ end
210
255
  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
- 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)
42
+ request_body = JSON.generate({'user' => user&.serialize(false), 'gateName' => gate_name})
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
@@ -27,9 +50,10 @@ class Network
27
50
 
28
51
  def get_config(user, dynamic_config_name)
29
52
  begin
30
- 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)
53
+ request_body = JSON.generate({'user' => user&.serialize(false), 'configName' => dynamic_config_name})
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
@@ -51,16 +56,17 @@ class StatsigDriver
51
56
 
52
57
  if res == $fetch_from_server
53
58
  res = check_gate_fallback(user, gate_name)
59
+ # exposure logged by the server
60
+ else
61
+ @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id)
54
62
  end
55
63
 
56
- @logger.log_gate_exposure(user, res.name, res.gate_value, res.rule_id)
57
64
  res.gate_value
58
65
  end
59
66
 
60
67
  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
68
+ validate_user(user)
69
+ user = normalize_user(user)
64
70
  if !dynamic_config_name.is_a?(String) || dynamic_config_name.empty?
65
71
  raise "Invalid dynamic_config_name provided"
66
72
  end
@@ -76,11 +82,12 @@ class StatsigDriver
76
82
 
77
83
  if res == $fetch_from_server
78
84
  res = get_config_fallback(user, dynamic_config_name)
85
+ # exposure logged by the server
86
+ else
87
+ @logger.log_config_exposure(user, res.name, res.rule_id)
79
88
  end
80
89
 
81
- result_config = DynamicConfig.new(res.name, res.json_value, res.rule_id)
82
- @logger.log_config_exposure(user, result_config.name, result_config.rule_id)
83
- result_config
90
+ DynamicConfig.new(res.name, res.json_value, res.rule_id)
84
91
  end
85
92
 
86
93
  def log_event(user, event_name, value = nil, metadata = nil)
@@ -89,8 +96,10 @@ class StatsigDriver
89
96
  end
90
97
  check_shutdown
91
98
 
99
+ user = normalize_user(user)
100
+
92
101
  event = StatsigEvent.new(event_name)
93
- event.user = user&.serialize
102
+ event.user = user
94
103
  event.value = value
95
104
  event.metadata = metadata
96
105
  event.statsig_metadata = @statsig_metadata
@@ -99,12 +108,25 @@ class StatsigDriver
99
108
 
100
109
  def shutdown
101
110
  @shutdown = true
102
- @logger.flush
111
+ @logger.flush(true)
103
112
  @polling_thread&.exit
104
113
  end
105
114
 
106
115
  private
107
116
 
117
+ def validate_user(user)
118
+ if user.nil? || !user.instance_of?(StatsigUser) || !user.user_id.is_a?(String)
119
+ 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.'
120
+ end
121
+ end
122
+
123
+ def normalize_user(user)
124
+ if !@options&.environment.nil?
125
+ user.statsig_environment = @options.environment
126
+ end
127
+ user
128
+ end
129
+
108
130
  def check_shutdown
109
131
  if @shutdown
110
132
  puts 'SDK has been shutdown. Updates in the Statsig Console will no longer reflect.'
data/lib/statsig_event.rb CHANGED
@@ -1,15 +1,22 @@
1
1
  class StatsigEvent
2
2
  attr_accessor :value
3
- attr_accessor :user
4
3
  attr_accessor :metadata
5
4
  attr_accessor :statsig_metadata
5
+ attr_reader :user
6
+
6
7
  def initialize(event_name)
7
8
  @event_name = event_name
8
- @time = Time.now.to_i * 1000
9
+ @time = Time.now.to_f * 1000
10
+ end
11
+
12
+ def user=(value)
13
+ if value.is_a?(StatsigUser)
14
+ @user = value.serialize(true)
15
+ end
9
16
  end
10
17
 
11
18
  def serialize
12
- return {
19
+ {
13
20
  'eventName' => @event_name,
14
21
  'metadata' => @metadata,
15
22
  'value' => @value,
@@ -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,9 @@ 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
10
+ attr_accessor :private_attributes
9
11
 
10
12
  def custom
11
13
  @custom
@@ -15,30 +17,40 @@ class StatsigUser
15
17
  @custom = value.is_a?(Hash) ? value : Hash.new
16
18
  end
17
19
 
18
- def initialize(user_hash = nil)
20
+ def initialize(user_hash)
21
+ @statsig_environment = Hash.new
19
22
  if user_hash.is_a?(Hash)
20
- @user_id = user_hash['userID']
23
+ @user_id = user_hash['userID'] || user_hash['user_id']
24
+ @user_id = @user_id.to_s unless @user_id.nil?
21
25
  @email = user_hash['email']
22
26
  @ip = user_hash['ip']
23
- @user_agent = user_hash['userAgent']
27
+ @user_agent = user_hash['userAgent'] || user_hash['user_agent']
24
28
  @country = user_hash['country']
25
29
  @locale = user_hash['locale']
26
- @client_version = user_hash['clientVersion']
30
+ @app_version = user_hash['appVersion'] || user_hash['app_version']
27
31
  @custom = user_hash['custom']
32
+ @statsig_environment = user_hash['statsigEnvironment']
33
+ @private_attributes = user_hash['privateAttributes']
28
34
  end
29
35
  end
30
36
 
31
- def serialize
32
- {
37
+ def serialize(for_logging)
38
+ hash = {
33
39
  'userID' => @user_id,
34
40
  'email' => @email,
35
41
  'ip' => @ip,
36
42
  'userAgent' => @user_agent,
37
43
  'country' => @country,
38
44
  'locale' => @locale,
39
- 'clientVersion' => @client_version,
45
+ 'appVersion' => @app_version,
40
46
  'custom' => @custom,
47
+ 'statsigEnvironment' => @statsig_environment,
48
+ 'privateAttributes' => @private_attributes,
41
49
  }
50
+ if for_logging
51
+ hash.delete('privateAttributes')
52
+ end
53
+ hash
42
54
  end
43
55
 
44
56
  def value_lookup
@@ -53,10 +65,11 @@ class StatsigUser
53
65
  'user_agent' => @user_agent,
54
66
  'country' => @country,
55
67
  'locale' => @locale,
56
- 'clientVersion' => @client_version,
57
- 'clientversion' => @client_version,
58
- 'client_version' => @client_version,
68
+ 'appVersion' => @app_version,
69
+ 'appversion' => @app_version,
70
+ 'app_version' => @app_version,
59
71
  'custom' => @custom,
72
+ 'privateAttributes' => @private_attributes,
60
73
  }
61
74
  end
62
75
  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.6.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-09-09 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: