statsig 1.8.0 → 1.8.4.beta.pre.1

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: 4268658d92961175796c877e357cc3cf18f40656cf8aa272034fd4953bdb641e
4
- data.tar.gz: e0a62e60cdca3b8b7ef8fe0302032badb88f8b9b5d86635bf20f79dc7fc011da
3
+ metadata.gz: 9e0d1d1530c323ccc58ea83da7a0c305692b51defa32071ee3f842d1519bb16b
4
+ data.tar.gz: e0d5018bbfd6f8c3dbe4fc099039d729d270d1ea5df8d5f59dbb72e56772da30
5
5
  SHA512:
6
- metadata.gz: e40a7d816b2c36ec211272bfa21e1bacb035ae7a6a9a1366eb4bae9b0af917f1694f66a1d35f146018b49be6c0df1ec07fe549612d3a49e54c69ffeeb7e3e733
7
- data.tar.gz: a53f398b7574bdaa47f26b5dcda0d2e11f83fbc85401b071592198ae24b59dbaec21cc74ec04f102ad909a06aeb0f552be1a49838241b035a2153b81c3250c04
6
+ metadata.gz: 9aa5bcf5d281709982ab3c325043cfcea99118e36bf2d82d11fd75995ad4a1e1f8f3e3e1a087652432ae5b6d0f15ee3a1a5deebff6f8d557ede16167edf72250
7
+ data.tar.gz: b1d95b28521991e9a247f181b16ce7652158bf713e230aca0a2c4ebc1752c6a2e317223e50e69caaa2b1b9751d85b7e6db3de5d70e5dc1750ca41009e52ec0a8
data/lib/config_result.rb CHANGED
@@ -1,15 +1,18 @@
1
- class ConfigResult
2
- attr_accessor :name
3
- attr_accessor :gate_value
4
- attr_accessor :json_value
5
- attr_accessor :rule_id
6
- attr_accessor :secondary_exposures
7
1
 
8
- def initialize(name, gate_value = false, json_value = {}, rule_id = '', secondary_exposures = [])
9
- @name = name
10
- @gate_value = gate_value
11
- @json_value = json_value
12
- @rule_id = rule_id
13
- @secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
2
+ module Statsig
3
+ class ConfigResult
4
+ attr_accessor :name
5
+ attr_accessor :gate_value
6
+ attr_accessor :json_value
7
+ attr_accessor :rule_id
8
+ attr_accessor :secondary_exposures
9
+
10
+ def initialize(name, gate_value = false, json_value = {}, rule_id = '', secondary_exposures = [])
11
+ @name = name
12
+ @gate_value = gate_value
13
+ @json_value = json_value
14
+ @rule_id = rule_id
15
+ @secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
16
+ end
14
17
  end
15
18
  end
data/lib/evaluator.rb CHANGED
@@ -10,300 +10,302 @@ require 'user_agent_parser/operating_system'
10
10
  $fetch_from_server = :fetch_from_server
11
11
  $type_dynamic_config = 'dynamic_config'
12
12
 
13
- class Evaluator
14
- def initialize(network, error_callback)
15
- @spec_store = SpecStore.new(network, error_callback)
16
- @ua_parser = UserAgentParser::Parser.new
17
- CountryLookup.initialize
18
- @initialized = true
19
- end
13
+ module Statsig
14
+ class Evaluator
15
+ def initialize(network, error_callback)
16
+ @spec_store = Statsig::SpecStore.new(network, error_callback)
17
+ @ua_parser = UserAgentParser::Parser.new
18
+ CountryLookup.initialize
19
+ @initialized = true
20
+ end
20
21
 
21
- def check_gate(user, gate_name)
22
- return nil unless @initialized && @spec_store.has_gate?(gate_name)
23
- eval_spec(user, @spec_store.get_gate(gate_name))
24
- end
22
+ def check_gate(user, gate_name)
23
+ return nil unless @initialized && @spec_store.has_gate?(gate_name)
24
+ eval_spec(user, @spec_store.get_gate(gate_name))
25
+ end
25
26
 
26
- def get_config(user, config_name)
27
- return nil unless @initialized && @spec_store.has_config?(config_name)
28
- eval_spec(user, @spec_store.get_config(config_name))
29
- end
27
+ def get_config(user, config_name)
28
+ return nil unless @initialized && @spec_store.has_config?(config_name)
29
+ eval_spec(user, @spec_store.get_config(config_name))
30
+ end
30
31
 
31
- def shutdown
32
- @spec_store.shutdown
33
- end
32
+ def shutdown
33
+ @spec_store.shutdown
34
+ end
35
+
36
+ private
37
+
38
+ def eval_spec(user, config)
39
+ default_rule_id = 'default'
40
+ exposures = []
41
+ if config['enabled']
42
+ i = 0
43
+ until i >= config['rules'].length do
44
+ rule = config['rules'][i]
45
+ result = eval_rule(user, rule)
46
+ return $fetch_from_server if result == $fetch_from_server
47
+ exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
48
+ if result['value']
49
+ pass = eval_pass_percent(user, rule, config['salt'])
50
+ return Statsig::ConfigResult.new(
51
+ config['name'],
52
+ pass,
53
+ pass ? rule['returnValue'] : config['defaultValue'],
54
+ rule['id'],
55
+ exposures
56
+ )
57
+ end
58
+
59
+ i += 1
60
+ end
61
+ else
62
+ default_rule_id = 'disabled'
63
+ end
34
64
 
35
- private
65
+ Statsig::ConfigResult.new(config['name'], false, config['defaultValue'], default_rule_id, exposures)
66
+ end
36
67
 
37
- def eval_spec(user, config)
38
- default_rule_id = 'default'
39
- exposures = []
40
- if config['enabled']
68
+ def eval_rule(user, rule)
69
+ exposures = []
70
+ pass = true
41
71
  i = 0
42
- until i >= config['rules'].length do
43
- rule = config['rules'][i]
44
- result = eval_rule(user, rule)
45
- return $fetch_from_server if result == $fetch_from_server
46
- exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
47
- if result['value']
48
- pass = eval_pass_percent(user, rule, config['salt'])
49
- return ConfigResult.new(
50
- config['name'],
51
- pass,
52
- pass ? rule['returnValue'] : config['defaultValue'],
53
- rule['id'],
54
- exposures
55
- )
72
+ until i >= rule['conditions'].length do
73
+ result = eval_condition(user, rule['conditions'][i])
74
+ if result == $fetch_from_server
75
+ return $fetch_from_server
56
76
  end
57
77
 
78
+ if result.is_a?(Hash)
79
+ exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
80
+ pass = false if result['value'] == false
81
+ elsif result == false
82
+ pass = false
83
+ end
58
84
  i += 1
59
85
  end
60
- else
61
- default_rule_id = 'disabled'
86
+ { 'value' => pass, 'exposures' => exposures }
62
87
  end
63
88
 
64
- ConfigResult.new(config['name'], false, config['defaultValue'], default_rule_id, exposures)
65
- end
66
-
67
- def eval_rule(user, rule)
68
- exposures = []
69
- pass = true
70
- i = 0
71
- until i >= rule['conditions'].length do
72
- result = eval_condition(user, rule['conditions'][i])
73
- if result == $fetch_from_server
89
+ def eval_condition(user, condition)
90
+ value = nil
91
+ field = condition['field']
92
+ target = condition['targetValue']
93
+ type = condition['type']
94
+ operator = condition['operator']
95
+ additional_values = condition['additionalValues']
96
+ additional_values = Hash.new unless additional_values.is_a? Hash
97
+ idType = condition['idType']
98
+
99
+ return $fetch_from_server unless type.is_a? String
100
+ type = type.downcase
101
+
102
+ case type
103
+ when 'public'
104
+ return true
105
+ when 'fail_gate', 'pass_gate'
106
+ other_gate_result = check_gate(user, target)
107
+ return $fetch_from_server if other_gate_result == $fetch_from_server
108
+
109
+ gate_value = other_gate_result&.gate_value == true
110
+ new_exposure = {
111
+ 'gate' => target,
112
+ 'gateValue' => gate_value ? 'true' : 'false',
113
+ 'ruleID' => other_gate_result&.rule_id
114
+ }
115
+ exposures = other_gate_result&.secondary_exposures&.append(new_exposure)
116
+ return {
117
+ 'value' => type == 'pass_gate' ? gate_value : !gate_value,
118
+ 'exposures' => exposures
119
+ }
120
+ when 'ip_based'
121
+ value = get_value_from_user(user, field) || get_value_from_ip(user, field)
122
+ return $fetch_from_server if value == $fetch_from_server
123
+ when 'ua_based'
124
+ value = get_value_from_user(user, field) || get_value_from_ua(user, field)
125
+ return $fetch_from_server if value == $fetch_from_server
126
+ when 'user_field'
127
+ value = get_value_from_user(user, field)
128
+ when 'environment_field'
129
+ value = get_value_from_environment(user, field)
130
+ when 'current_time'
131
+ value = Time.now.to_f # epoch time in seconds
132
+ when 'user_bucket'
133
+ begin
134
+ salt = additional_values['salt']
135
+ unit_id = get_unit_id(user, idType) || ''
136
+ # there are only 1000 user buckets as opposed to 10k for gate pass %
137
+ value = compute_user_hash("#{salt}.#{unit_id}") % 1000
138
+ rescue
139
+ return false
140
+ end
141
+ when 'unit_id'
142
+ value = get_unit_id(user, idType)
143
+ else
74
144
  return $fetch_from_server
75
145
  end
76
146
 
77
- if result.is_a?(Hash)
78
- exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
79
- pass = false if result['value'] == false
80
- elsif result == false
81
- pass = false
82
- end
83
- i += 1
84
- end
85
- { 'value' => pass, 'exposures' => exposures }
86
- end
87
-
88
- def eval_condition(user, condition)
89
- value = nil
90
- field = condition['field']
91
- target = condition['targetValue']
92
- type = condition['type']
93
- operator = condition['operator']
94
- additional_values = condition['additionalValues']
95
- additional_values = Hash.new unless additional_values.is_a? Hash
96
- idType = condition['idType']
97
-
98
- return $fetch_from_server unless type.is_a? String
99
- type = type.downcase
100
-
101
- case type
102
- when 'public'
103
- return true
104
- when 'fail_gate', 'pass_gate'
105
- other_gate_result = check_gate(user, target)
106
- return $fetch_from_server if other_gate_result == $fetch_from_server
107
-
108
- gate_value = other_gate_result&.gate_value == true
109
- new_exposure = {
110
- 'gate' => target,
111
- 'gateValue' => gate_value ? 'true' : 'false',
112
- 'ruleID' => other_gate_result&.rule_id
113
- }
114
- exposures = other_gate_result&.secondary_exposures&.append(new_exposure)
115
- return {
116
- 'value' => type == 'pass_gate' ? gate_value : !gate_value,
117
- 'exposures' => exposures
118
- }
119
- when 'ip_based'
120
- value = get_value_from_user(user, field) || get_value_from_ip(user, field)
121
- return $fetch_from_server if value == $fetch_from_server
122
- when 'ua_based'
123
- value = get_value_from_user(user, field) || get_value_from_ua(user, field)
124
- return $fetch_from_server if value == $fetch_from_server
125
- when 'user_field'
126
- value = get_value_from_user(user, field)
127
- when 'environment_field'
128
- value = get_value_from_environment(user, field)
129
- when 'current_time'
130
- value = Time.now.to_f # epoch time in seconds
131
- when 'user_bucket'
132
- begin
133
- salt = additional_values['salt']
134
- unit_id = get_unit_id(user, idType) || ''
135
- # there are only 1000 user buckets as opposed to 10k for gate pass %
136
- value = compute_user_hash("#{salt}.#{unit_id}") % 1000
137
- rescue
138
- return false
147
+ return $fetch_from_server if value == $fetch_from_server || !operator.is_a?(String)
148
+ operator = operator.downcase
149
+
150
+ case operator
151
+ # numerical comparison
152
+ when 'gt'
153
+ return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a > b })
154
+ when 'gte'
155
+ return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a >= b })
156
+ when 'lt'
157
+ return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a < b })
158
+ when 'lte'
159
+ return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a <= b })
160
+
161
+ # version comparison
162
+ when 'version_gt'
163
+ return (Gem::Version.new(value) > Gem::Version.new(target) rescue false)
164
+ when 'version_gte'
165
+ return (Gem::Version.new(value) >= Gem::Version.new(target) rescue false)
166
+ when 'version_lt'
167
+ return (Gem::Version.new(value) < Gem::Version.new(target) rescue false)
168
+ when 'version_lte'
169
+ return (Gem::Version.new(value) <= Gem::Version.new(target) rescue false)
170
+ when 'version_eq'
171
+ return (Gem::Version.new(value) == Gem::Version.new(target) rescue false)
172
+ when 'version_neq'
173
+ return (Gem::Version.new(value) != Gem::Version.new(target) rescue false)
174
+
175
+ # array operations
176
+ when 'any'
177
+ return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
178
+ when 'none'
179
+ return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
180
+ when 'any_case_sensitive'
181
+ return EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
182
+ when 'none_case_sensitive'
183
+ return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
184
+
185
+ #string
186
+ when 'str_starts_with_any'
187
+ return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
188
+ when 'str_ends_with_any'
189
+ return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
190
+ when 'str_contains_any'
191
+ return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
192
+ when 'str_contains_none'
193
+ return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
194
+ when 'str_matches'
195
+ return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
196
+ when 'eq'
197
+ return value == target
198
+ when 'neq'
199
+ return value != target
200
+
201
+ # dates
202
+ when 'before'
203
+ return EvaluationHelpers::compare_times(value, target, ->(a, b) { a < b })
204
+ when 'after'
205
+ return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
206
+ when 'on'
207
+ return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
208
+ when 'in_segment_list', 'not_in_segment_list'
209
+ begin
210
+ id_list = (@spec_store.get_id_list(target) || {:ids => {}})[:ids]
211
+ hashed_id = Digest::SHA256.base64digest(value.to_s)[0, 8]
212
+ is_in_list = id_list.is_a?(Hash) && id_list[hashed_id] == true
213
+
214
+ return is_in_list if operator == 'in_segment_list'
215
+ return !is_in_list
216
+ rescue StandardError => e
217
+ return false
218
+ end
219
+ else
220
+ return $fetch_from_server
139
221
  end
140
- when 'unit_id'
141
- value = get_unit_id(user, idType)
142
- else
143
- return $fetch_from_server
144
222
  end
145
223
 
146
- return $fetch_from_server if value == $fetch_from_server || !operator.is_a?(String)
147
- operator = operator.downcase
148
-
149
- case operator
150
- # numerical comparison
151
- when 'gt'
152
- return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a > b })
153
- when 'gte'
154
- return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a >= b })
155
- when 'lt'
156
- return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a < b })
157
- when 'lte'
158
- return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a <= b })
159
-
160
- # version comparison
161
- when 'version_gt'
162
- return (Gem::Version.new(value) > Gem::Version.new(target) rescue false)
163
- when 'version_gte'
164
- return (Gem::Version.new(value) >= Gem::Version.new(target) rescue false)
165
- when 'version_lt'
166
- return (Gem::Version.new(value) < Gem::Version.new(target) rescue false)
167
- when 'version_lte'
168
- return (Gem::Version.new(value) <= Gem::Version.new(target) rescue false)
169
- when 'version_eq'
170
- return (Gem::Version.new(value) == Gem::Version.new(target) rescue false)
171
- when 'version_neq'
172
- return (Gem::Version.new(value) != Gem::Version.new(target) rescue false)
173
-
174
- # array operations
175
- when 'any'
176
- return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
177
- when 'none'
178
- return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
179
- when 'any_case_sensitive'
180
- return EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
181
- when 'none_case_sensitive'
182
- return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
183
-
184
- #string
185
- when 'str_starts_with_any'
186
- return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
187
- when 'str_ends_with_any'
188
- return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
189
- when 'str_contains_any'
190
- return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
191
- when 'str_contains_none'
192
- return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
193
- when 'str_matches'
194
- return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
195
- when 'eq'
196
- return value == target
197
- when 'neq'
198
- return value != target
199
-
200
- # dates
201
- when 'before'
202
- return EvaluationHelpers::compare_times(value, target, ->(a, b) { a < b })
203
- when 'after'
204
- return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
205
- when 'on'
206
- return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
207
- when 'in_segment_list', 'not_in_segment_list'
208
- begin
209
- id_list = (@spec_store.get_id_list(target) || {:ids => {}})[:ids]
210
- hashed_id = Digest::SHA256.base64digest(value.to_s)[0, 8]
211
- is_in_list = id_list.is_a?(Hash) && id_list[hashed_id] == true
212
-
213
- return is_in_list if operator == 'in_segment_list'
214
- return !is_in_list
215
- rescue StandardError => e
216
- return false
217
- end
218
- else
219
- return $fetch_from_server
220
- end
221
- end
224
+ def get_value_from_user(user, field)
225
+ return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
222
226
 
223
- def get_value_from_user(user, field)
224
- return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
227
+ user_lookup_table = user&.value_lookup
228
+ return nil unless user_lookup_table.is_a?(Hash)
229
+ return user_lookup_table[field.downcase] if user_lookup_table.has_key?(field.downcase) && !user_lookup_table[field.downcase].nil?
225
230
 
226
- user_lookup_table = user&.value_lookup
227
- return nil unless user_lookup_table.is_a?(Hash)
228
- return user_lookup_table[field.downcase] if user_lookup_table.has_key?(field.downcase) && !user_lookup_table[field.downcase].nil?
231
+ user_custom = user_lookup_table['custom']
232
+ if user_custom.is_a?(Hash)
233
+ user_custom.each do |key, value|
234
+ return value if key.downcase.casecmp?(field.downcase) && !value.nil?
235
+ end
236
+ end
229
237
 
230
- user_custom = user_lookup_table['custom']
231
- if user_custom.is_a?(Hash)
232
- user_custom.each do |key, value|
233
- return value if key.downcase.casecmp?(field.downcase) && !value.nil?
238
+ private_attributes = user_lookup_table['privateAttributes']
239
+ if private_attributes.is_a?(Hash)
240
+ private_attributes.each do |key, value|
241
+ return value if key.downcase.casecmp?(field.downcase) && !value.nil?
242
+ end
234
243
  end
244
+
245
+ nil
235
246
  end
236
247
 
237
- private_attributes = user_lookup_table['privateAttributes']
238
- if private_attributes.is_a?(Hash)
239
- private_attributes.each do |key, value|
240
- return value if key.downcase.casecmp?(field.downcase) && !value.nil?
248
+ def get_value_from_environment(user, field)
249
+ return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
250
+ field = field.downcase
251
+ return nil unless user.statsig_environment.is_a? Hash
252
+ user.statsig_environment.each do |key, value|
253
+ return value if key.downcase == (field)
241
254
  end
255
+ nil
242
256
  end
243
257
 
244
- nil
245
- end
258
+ def get_value_from_ip(user, field)
259
+ return nil unless user.is_a?(StatsigUser) && field.is_a?(String) && field.downcase == 'country'
260
+ ip = get_value_from_user(user, 'ip')
261
+ return nil unless ip.is_a?(String)
246
262
 
247
- def get_value_from_environment(user, field)
248
- return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
249
- field = field.downcase
250
- return nil unless user.statsig_environment.is_a? Hash
251
- user.statsig_environment.each do |key, value|
252
- return value if key.downcase == (field)
263
+ CountryLookup.lookup_ip_string(ip)
253
264
  end
254
- nil
255
- end
256
-
257
- def get_value_from_ip(user, field)
258
- return nil unless user.is_a?(StatsigUser) && field.is_a?(String) && field.downcase == 'country'
259
- ip = get_value_from_user(user, 'ip')
260
- return nil unless ip.is_a?(String)
261
265
 
262
- CountryLookup.lookup_ip_string(ip)
263
- end
264
-
265
- def get_value_from_ua(user, field)
266
- return nil unless user.is_a?(StatsigUser) && field.is_a?(String)
267
- ua = get_value_from_user(user, 'userAgent')
268
- return nil unless ua.is_a?(String)
269
-
270
- parsed = @ua_parser.parse ua
271
- os = parsed.os
272
- case field.downcase
273
- when 'os_name', 'osname'
274
- return os&.family
275
- when 'os_version', 'osversion'
276
- return os&.version unless os&.version.nil?
277
- when 'browser_name', 'browsername'
278
- return parsed.family
279
- when 'browser_version', 'browserversion'
280
- return parsed.version.to_s
281
- else
282
- nil
266
+ def get_value_from_ua(user, field)
267
+ return nil unless user.is_a?(StatsigUser) && field.is_a?(String)
268
+ ua = get_value_from_user(user, 'userAgent')
269
+ return nil unless ua.is_a?(String)
270
+
271
+ parsed = @ua_parser.parse ua
272
+ os = parsed.os
273
+ case field.downcase
274
+ when 'os_name', 'osname'
275
+ return os&.family
276
+ when 'os_version', 'osversion'
277
+ return os&.version unless os&.version.nil?
278
+ when 'browser_name', 'browsername'
279
+ return parsed.family
280
+ when 'browser_version', 'browserversion'
281
+ return parsed.version.to_s
282
+ else
283
+ nil
284
+ end
283
285
  end
284
- end
285
286
 
286
- def eval_pass_percent(user, rule, config_salt)
287
- return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
288
- begin
289
- unit_id = get_unit_id(user, rule['id_type']) || ''
290
- rule_salt = rule['salt'] || rule['id'] || ''
291
- hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
292
- return (hash % 10000) < (rule['passPercentage'].to_f * 100)
293
- rescue
294
- return false
287
+ def eval_pass_percent(user, rule, config_salt)
288
+ return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
289
+ begin
290
+ unit_id = get_unit_id(user, rule['id_type']) || ''
291
+ rule_salt = rule['salt'] || rule['id'] || ''
292
+ hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
293
+ return (hash % 10000) < (rule['passPercentage'].to_f * 100)
294
+ rescue
295
+ return false
296
+ end
295
297
  end
296
- end
297
298
 
298
- def get_unit_id(user, id_type)
299
- if id_type.is_a?(String) && id_type.downcase != 'userid'
300
- return nil unless user&.custom_ids.is_a? Hash
301
- return user.custom_ids[id_type] || user.custom_ids[id_type.downcase]
299
+ def get_unit_id(user, id_type)
300
+ if id_type.is_a?(String) && id_type.downcase != 'userid'
301
+ return nil unless user&.custom_ids.is_a? Hash
302
+ return user.custom_ids[id_type] || user.custom_ids[id_type.downcase]
303
+ end
304
+ user.user_id
302
305
  end
303
- user.user_id
304
- end
305
306
 
306
- def compute_user_hash(user_hash)
307
- Digest::SHA256.digest(user_hash).unpack('Q>')[0]
307
+ def compute_user_hash(user_hash)
308
+ Digest::SHA256.digest(user_hash).unpack('Q>')[0]
309
+ end
308
310
  end
309
311
  end
data/lib/network.rb CHANGED
@@ -4,65 +4,67 @@ require 'dynamic_config'
4
4
 
5
5
  $retry_codes = [408, 500, 502, 503, 504, 522, 524, 599]
6
6
 
7
- class Network
8
- def initialize(server_secret, api, backoff_mult = 10)
9
- super()
10
- unless api.end_with?('/')
11
- api += '/'
7
+ module Statsig
8
+ class Network
9
+ def initialize(server_secret, api, backoff_mult = 10)
10
+ super()
11
+ unless api.end_with?('/')
12
+ api += '/'
13
+ end
14
+ @server_secret = server_secret
15
+ @api = api
16
+ @backoff_multiplier = backoff_mult
12
17
  end
13
- @server_secret = server_secret
14
- @api = api
15
- @backoff_multiplier = backoff_mult
16
- end
17
18
 
18
- def post_helper(endpoint, body, retries = 0, backoff = 1)
19
- http = HTTP.headers(
20
- {"STATSIG-API-KEY" => @server_secret,
21
- "STATSIG-CLIENT-TIME" => (Time.now.to_f * 1000).to_s,
22
- "Content-Type" => "application/json; charset=UTF-8"
23
- }).accept(:json)
24
- begin
25
- res = http.post(@api + endpoint, body: body)
26
- rescue StandardError => e
27
- ## network error retry
28
- return nil, e unless retries > 0
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 StandardError => e
28
+ ## network error retry
29
+ return nil, e unless retries > 0
30
+ sleep backoff
31
+ return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
32
+ end
33
+ return res, nil unless !res.status.success?
34
+ return nil, StandardError.new("Got an exception when making request to #{@api + endpoint}: #{res.to_s}") unless retries > 0 && $retry_codes.include?(res.code)
35
+ ## status code retry
29
36
  sleep backoff
30
- return post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
37
+ post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
31
38
  end
32
- return res, nil unless !res.status.success?
33
- return nil, StandardError.new("Got an exception when making request to #{@api + endpoint}: #{res.to_s}") unless retries > 0 && $retry_codes.include?(res.code)
34
- ## status code retry
35
- sleep backoff
36
- post_helper(endpoint, body, retries - 1, backoff * @backoff_multiplier)
37
- end
38
39
 
39
- def check_gate(user, gate_name)
40
- begin
41
- request_body = JSON.generate({'user' => user&.serialize(false), 'gateName' => gate_name})
42
- response, _ = post_helper('check_gate', request_body)
43
- return JSON.parse(response.body) unless response.nil?
44
- false
45
- rescue
46
- return false
40
+ def check_gate(user, gate_name)
41
+ begin
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
46
+ rescue
47
+ return false
48
+ end
47
49
  end
48
- end
49
50
 
50
- def get_config(user, dynamic_config_name)
51
- begin
52
- request_body = JSON.generate({'user' => user&.serialize(false), 'configName' => dynamic_config_name})
53
- response, _ = post_helper('get_config', request_body)
54
- return JSON.parse(response.body) unless response.nil?
55
- nil
56
- rescue
57
- return nil
51
+ def get_config(user, dynamic_config_name)
52
+ begin
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
57
+ rescue
58
+ return nil
59
+ end
58
60
  end
59
- end
60
61
 
61
- def post_logs(events)
62
- begin
63
- json_body = JSON.generate({'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata})
64
- post_helper('log_event', json_body, retries: 5)
65
- rescue
62
+ def post_logs(events)
63
+ begin
64
+ json_body = JSON.generate({'events' => events, 'statsigMetadata' => Statsig.get_statsig_metadata})
65
+ post_helper('log_event', json_body, retries: 5)
66
+ rescue
67
+ end
66
68
  end
67
69
  end
68
70
  end
data/lib/spec_store.rb CHANGED
@@ -1,150 +1,151 @@
1
1
  require 'net/http'
2
2
  require 'uri'
3
3
 
4
- class SpecStore
5
- def initialize(network, error_callback = nil, config_sync_interval = 10, id_lists_sync_interval = 60)
6
- @network = network
7
- @last_sync_time = 0
8
- @config_sync_interval = config_sync_interval
9
- @id_lists_sync_interval = id_lists_sync_interval
10
- @store = {
11
- :gates => {},
12
- :configs => {},
13
- :id_lists => {},
14
- }
15
- e = download_config_specs
16
- error_callback.call(e) unless error_callback.nil?
17
- download_id_lists
18
-
19
- @config_sync_thread = sync_config_specs
20
- @id_lists_sync_thread = sync_id_lists
21
- end
4
+ module Statsig
5
+ class SpecStore
6
+ def initialize(network, error_callback = nil, config_sync_interval = 10, id_lists_sync_interval = 60)
7
+ @network = network
8
+ @last_sync_time = 0
9
+ @config_sync_interval = config_sync_interval
10
+ @id_lists_sync_interval = id_lists_sync_interval
11
+ @store = {
12
+ :gates => {},
13
+ :configs => {},
14
+ :id_lists => {},
15
+ }
16
+ e = download_config_specs
17
+ error_callback.call(e) unless error_callback.nil?
18
+ download_id_lists
19
+
20
+ @config_sync_thread = sync_config_specs
21
+ @id_lists_sync_thread = sync_id_lists
22
+ end
22
23
 
23
- def shutdown
24
- @config_sync_thread&.exit
25
- @id_lists_sync_thread&.exit
26
- end
24
+ def shutdown
25
+ @config_sync_thread&.exit
26
+ @id_lists_sync_thread&.exit
27
+ end
27
28
 
28
- def has_gate?(gate_name)
29
- @store[:gates].key?(gate_name)
30
- end
29
+ def has_gate?(gate_name)
30
+ @store[:gates].key?(gate_name)
31
+ end
31
32
 
32
- def has_config?(config_name)
33
- @store[:configs].key?(config_name)
34
- end
33
+ def has_config?(config_name)
34
+ @store[:configs].key?(config_name)
35
+ end
35
36
 
36
- def get_gate(gate_name)
37
- return nil unless has_gate?(gate_name)
38
- @store[:gates][gate_name]
39
- end
37
+ def get_gate(gate_name)
38
+ return nil unless has_gate?(gate_name)
39
+ @store[:gates][gate_name]
40
+ end
40
41
 
41
- def get_config(config_name)
42
- return nil unless has_config?(config_name)
43
- @store[:configs][config_name]
44
- end
42
+ def get_config(config_name)
43
+ return nil unless has_config?(config_name)
44
+ @store[:configs][config_name]
45
+ end
45
46
 
46
- def get_id_list(list_name)
47
- @store[:id_lists][list_name]
48
- end
47
+ def get_id_list(list_name)
48
+ @store[:id_lists][list_name]
49
+ end
49
50
 
50
- private
51
+ private
51
52
 
52
- def sync_config_specs
53
- Thread.new do
54
- loop do
55
- sleep @config_sync_interval
56
- download_config_specs
53
+ def sync_config_specs
54
+ Thread.new do
55
+ loop do
56
+ sleep @config_sync_interval
57
+ download_config_specs
58
+ end
57
59
  end
58
60
  end
59
- end
60
61
 
61
- def sync_id_lists
62
- Thread.new do
63
- loop do
64
- sleep @id_lists_sync_interval
65
- download_id_lists
62
+ def sync_id_lists
63
+ Thread.new do
64
+ loop do
65
+ sleep @id_lists_sync_interval
66
+ download_id_lists
67
+ end
66
68
  end
67
69
  end
68
- end
69
70
 
70
- def download_config_specs
71
- begin
72
- response, e = @network.post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
73
- if e.nil?
74
- process(JSON.parse(response.body))
75
- else
71
+ def download_config_specs
72
+ begin
73
+ response, e = @network.post_helper('download_config_specs', JSON.generate({'sinceTime' => @last_sync_time}))
74
+ if e.nil?
75
+ process(JSON.parse(response.body))
76
+ else
77
+ e
78
+ end
79
+ rescue StandardError => e
76
80
  e
77
81
  end
78
- rescue StandardError => e
79
- e
80
82
  end
81
- end
82
83
 
83
- def process(specs_json)
84
- if specs_json.nil?
85
- return
86
- end
84
+ def process(specs_json)
85
+ if specs_json.nil?
86
+ return
87
+ end
87
88
 
88
- @last_sync_time = specs_json['time'] || @last_sync_time
89
- return unless specs_json['has_updates'] == true &&
90
- !specs_json['feature_gates'].nil? &&
91
- !specs_json['dynamic_configs'].nil?
89
+ @last_sync_time = specs_json['time'] || @last_sync_time
90
+ return unless specs_json['has_updates'] == true &&
91
+ !specs_json['feature_gates'].nil? &&
92
+ !specs_json['dynamic_configs'].nil?
92
93
 
93
- new_gates = {}
94
- new_configs = {}
94
+ new_gates = {}
95
+ new_configs = {}
95
96
 
96
- specs_json['feature_gates'].map{|gate| new_gates[gate['name']] = gate }
97
- specs_json['dynamic_configs'].map{|config| new_configs[config['name']] = config }
98
- @store[:gates] = new_gates
99
- @store[:configs] = new_configs
97
+ specs_json['feature_gates'].map{|gate| new_gates[gate['name']] = gate }
98
+ specs_json['dynamic_configs'].map{|config| new_configs[config['name']] = config }
99
+ @store[:gates] = new_gates
100
+ @store[:configs] = new_configs
100
101
 
101
- new_id_lists = specs_json['id_lists']
102
- if new_id_lists.is_a? Hash
103
- new_id_lists.each do |list_name, _|
104
- unless @store[:id_lists].key?(list_name)
105
- @store[:id_lists][list_name] = { :ids => {}, :time => 0 }
102
+ new_id_lists = specs_json['id_lists']
103
+ if new_id_lists.is_a? Hash
104
+ new_id_lists.each do |list_name, _|
105
+ unless @store[:id_lists].key?(list_name)
106
+ @store[:id_lists][list_name] = { :ids => {}, :time => 0 }
107
+ end
106
108
  end
107
- end
108
109
 
109
- @store[:id_lists].each do |list_name, _|
110
- unless new_id_lists.key?(list_name)
111
- @store[:id_lists].delete(list_name)
110
+ @store[:id_lists].each do |list_name, _|
111
+ unless new_id_lists.key?(list_name)
112
+ @store[:id_lists].delete(list_name)
113
+ end
112
114
  end
113
115
  end
114
116
  end
115
- end
116
117
 
117
- def download_id_lists
118
- if @store[:id_lists].is_a? Hash
119
- threads = []
120
- id_lists = @store[:id_lists]
121
- id_lists.each do |list_name, list|
122
- threads << Thread.new do
123
- response, e = @network.post_helper('download_id_list', JSON.generate({'listName' => list_name, 'statsigMetadata' => Statsig.get_statsig_metadata, 'sinceTime' => list['time'] || 0 }))
124
- if e.nil? && !response.nil?
125
- begin
126
- data = JSON.parse(response)
127
- if data['add_ids'].is_a? Array
128
- data['add_ids'].each do |id|
129
- list[:ids][id] = true
118
+ def download_id_lists
119
+ if @store[:id_lists].is_a? Hash
120
+ threads = []
121
+ id_lists = @store[:id_lists]
122
+ id_lists.each do |list_name, list|
123
+ threads << Thread.new do
124
+ response, e = @network.post_helper('download_id_list', JSON.generate({'listName' => list_name, 'statsigMetadata' => Statsig.get_statsig_metadata, 'sinceTime' => list['time'] || 0 }))
125
+ if e.nil? && !response.nil?
126
+ begin
127
+ data = JSON.parse(response)
128
+ if data['add_ids'].is_a? Array
129
+ data['add_ids'].each do |id|
130
+ list[:ids][id] = true
131
+ end
130
132
  end
131
- end
132
- if data['remove_ids'].is_a? Array
133
- data['remove_ids'].each do |id|
134
- list[:ids]&.delete(id)
133
+ if data['remove_ids'].is_a? Array
134
+ data['remove_ids'].each do |id|
135
+ list[:ids]&.delete(id)
136
+ end
135
137
  end
138
+ if data['time'].is_a? Numeric
139
+ list[:time] = data['time']
140
+ end
141
+ rescue
142
+ # Ignored
136
143
  end
137
- if data['time'].is_a? Numeric
138
- list[:time] = data['time']
139
- end
140
- rescue
141
- # Ignored
142
144
  end
143
145
  end
144
146
  end
147
+ threads.each(&:join)
145
148
  end
146
- threads.each(&:join)
147
149
  end
148
150
  end
149
-
150
- end
151
+ end
data/lib/statsig.rb CHANGED
@@ -40,7 +40,7 @@ module Statsig
40
40
  def self.get_statsig_metadata
41
41
  {
42
42
  'sdkType' => 'ruby-server',
43
- 'sdkVersion' => '1.8.0',
43
+ 'sdkVersion' => '1.8.4.beta-1`',
44
44
  }
45
45
  end
46
46
 
@@ -20,9 +20,9 @@ class StatsigDriver
20
20
  @options = options || StatsigOptions.new
21
21
  @shutdown = false
22
22
  @secret_key = secret_key
23
- @net = Network.new(secret_key, @options.api_url_base)
24
- @logger = StatsigLogger.new(@net)
25
- @evaluator = Evaluator.new(@net, error_callback)
23
+ @net = Statsig::Network.new(secret_key, @options.api_url_base)
24
+ @logger = Statsig::StatsigLogger.new(@net)
25
+ @evaluator = Statsig::Evaluator.new(@net, error_callback)
26
26
  end
27
27
 
28
28
  def check_gate(user, gate_name)
@@ -35,7 +35,7 @@ class StatsigDriver
35
35
 
36
36
  res = @evaluator.check_gate(user, gate_name)
37
37
  if res.nil?
38
- res = ConfigResult.new(gate_name)
38
+ res = Statsig::ConfigResult.new(gate_name)
39
39
  end
40
40
 
41
41
  if res == $fetch_from_server
@@ -58,7 +58,7 @@ class StatsigDriver
58
58
 
59
59
  res = @evaluator.get_config(user, dynamic_config_name)
60
60
  if res.nil?
61
- res = ConfigResult.new(dynamic_config_name)
61
+ res = Statsig::ConfigResult.new(dynamic_config_name)
62
62
  end
63
63
 
64
64
  if res == $fetch_from_server
@@ -124,11 +124,11 @@ class StatsigDriver
124
124
  def check_gate_fallback(user, gate_name)
125
125
  network_result = @net.check_gate(user, gate_name)
126
126
  if network_result.nil?
127
- config_result = ConfigResult.new(gate_name)
127
+ config_result = Statsig::ConfigResult.new(gate_name)
128
128
  return config_result
129
129
  end
130
130
 
131
- ConfigResult.new(
131
+ Statsig::ConfigResult.new(
132
132
  network_result['name'],
133
133
  network_result['value'],
134
134
  {},
@@ -139,11 +139,11 @@ class StatsigDriver
139
139
  def get_config_fallback(user, dynamic_config_name)
140
140
  network_result = @net.get_config(user, dynamic_config_name)
141
141
  if network_result.nil?
142
- config_result = ConfigResult.new(dynamic_config_name)
142
+ config_result = Statsig::ConfigResult.new(dynamic_config_name)
143
143
  return config_result
144
144
  end
145
145
 
146
- ConfigResult.new(
146
+ Statsig::ConfigResult.new(
147
147
  network_result['name'],
148
148
  false,
149
149
  network_result['value'],
@@ -3,58 +3,71 @@ require 'statsig_event'
3
3
  $gate_exposure_event = 'statsig::gate_exposure'
4
4
  $config_exposure_event = 'statsig::config_exposure'
5
5
 
6
- class StatsigLogger
7
- def initialize(network)
8
- @network = network
9
- @events = []
10
- @background_flush = Thread.new do
11
- sleep 60
12
- flush
6
+ module Statsig
7
+ class StatsigLogger
8
+ def initialize(network)
9
+ @network = network
10
+ @events = []
11
+ @background_flush = periodic_flush
13
12
  end
14
- end
15
13
 
16
- def log_event(event)
17
- @events.push(event)
18
- if @events.length >= 500
19
- flush
14
+ def log_event(event)
15
+ @events.push(event)
16
+ if @events.length >= 500
17
+ flush
18
+ end
20
19
  end
21
- end
22
20
 
23
- def log_gate_exposure(user, gate_name, value, rule_id, secondary_exposures)
24
- event = StatsigEvent.new($gate_exposure_event)
25
- event.user = user
26
- event.metadata = {
27
- 'gate' => gate_name,
28
- 'gateValue' => value.to_s,
29
- 'ruleID' => rule_id
30
- }
31
- event.statsig_metadata = Statsig.get_statsig_metadata
32
- event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
33
- log_event(event)
34
- end
35
-
36
- def log_config_exposure(user, config_name, rule_id, secondary_exposures)
37
- event = StatsigEvent.new($config_exposure_event)
38
- event.user = user
39
- event.metadata = {
40
- 'config' => config_name,
41
- 'ruleID' => rule_id
42
- }
43
- event.statsig_metadata = Statsig.get_statsig_metadata
44
- event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
45
- log_event(event)
46
- end
21
+ def log_gate_exposure(user, gate_name, value, rule_id, secondary_exposures)
22
+ event = StatsigEvent.new($gate_exposure_event)
23
+ event.user = user
24
+ event.metadata = {
25
+ 'gate' => gate_name,
26
+ 'gateValue' => value.to_s,
27
+ 'ruleID' => rule_id
28
+ }
29
+ event.statsig_metadata = Statsig.get_statsig_metadata
30
+ event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
31
+ log_event(event)
32
+ end
47
33
 
48
- def flush(closing = false)
49
- if closing
50
- @background_flush.exit
34
+ def log_config_exposure(user, config_name, rule_id, secondary_exposures)
35
+ event = StatsigEvent.new($config_exposure_event)
36
+ event.user = user
37
+ event.metadata = {
38
+ 'config' => config_name,
39
+ 'ruleID' => rule_id
40
+ }
41
+ event.statsig_metadata = Statsig.get_statsig_metadata
42
+ event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
43
+ log_event(event)
51
44
  end
52
- if @events.length == 0
53
- return
45
+
46
+ def periodic_flush
47
+ puts "Statsig:: starting a thread"
48
+ Thread.new do
49
+ loop do
50
+ puts "Statsig:: beginning to sleep for 60 seconds"
51
+ sleep 60
52
+ puts "Statsig:: finished sleeping"
53
+ flush
54
+ puts "Statsig:: flushed"
55
+ end
56
+ end
54
57
  end
55
- flush_events = @events.map { |e| e.serialize }
56
- @events = []
57
58
 
58
- @network.post_logs(flush_events)
59
+ def flush(closing = false)
60
+ if closing
61
+ @background_flush&.exit
62
+ end
63
+ if @events.length == 0
64
+ return
65
+ end
66
+ puts "Statsig:: flushing events, count: #{@events.length.to_s}"
67
+ flush_events = @events.map { |e| e.serialize }
68
+ @events = []
69
+
70
+ @network.post_logs(flush_events)
71
+ end
59
72
  end
60
73
  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: 1.8.0
4
+ version: 1.8.4.beta.pre.1
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-12-18 00:00:00.000000000 Z
11
+ date: 2022-01-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -141,9 +141,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
141
141
  version: '0'
142
142
  required_rubygems_version: !ruby/object:Gem::Requirement
143
143
  requirements:
144
- - - ">="
144
+ - - ">"
145
145
  - !ruby/object:Gem::Version
146
- version: '0'
146
+ version: 1.3.1
147
147
  requirements: []
148
148
  rubygems_version: 3.2.3
149
149
  signing_key: