statsig 1.8.0 → 1.8.1.beta.1

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: 4268658d92961175796c877e357cc3cf18f40656cf8aa272034fd4953bdb641e
4
- data.tar.gz: e0a62e60cdca3b8b7ef8fe0302032badb88f8b9b5d86635bf20f79dc7fc011da
3
+ metadata.gz: 85cc5f1ff296df6832034a396c9e575cd21e54502cff95467c691289143d0067
4
+ data.tar.gz: a3ca082effe4a16dde1e73b700c30322db55a00e3e8f384015ae8d22640e41f8
5
5
  SHA512:
6
- metadata.gz: e40a7d816b2c36ec211272bfa21e1bacb035ae7a6a9a1366eb4bae9b0af917f1694f66a1d35f146018b49be6c0df1ec07fe549612d3a49e54c69ffeeb7e3e733
7
- data.tar.gz: a53f398b7574bdaa47f26b5dcda0d2e11f83fbc85401b071592198ae24b59dbaec21cc74ec04f102ad909a06aeb0f552be1a49838241b035a2153b81c3250c04
6
+ metadata.gz: 182507d8e780f6d37f0f94fe5e2183ad0c1fb8f6500ea8271203df45debcfa00a9630e03670a2e82e010aa77f41abb1d3d38ca5067c7e5df88dbf697c7aa7716
7
+ data.tar.gz: de3029066c82f7886a55a0c0beecddd65751ebc337d3a6138ccb437c5ba506a4e238f55cca1e301530a4168dd126ea2fba833dfe8af70cc2936bbe7c72f68f3f
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.1.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,60 @@ 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 = Thread.new do
12
+ sleep 60
13
+ flush
14
+ end
13
15
  end
14
- end
15
16
 
16
- def log_event(event)
17
- @events.push(event)
18
- if @events.length >= 500
19
- flush
17
+ def log_event(event)
18
+ @events.push(event)
19
+ if @events.length >= 500
20
+ flush
21
+ end
20
22
  end
21
- end
22
23
 
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
47
-
48
- def flush(closing = false)
49
- if closing
50
- @background_flush.exit
24
+ def log_gate_exposure(user, gate_name, value, rule_id, secondary_exposures)
25
+ event = StatsigEvent.new($gate_exposure_event)
26
+ event.user = user
27
+ event.metadata = {
28
+ 'gate' => gate_name,
29
+ 'gateValue' => value.to_s,
30
+ 'ruleID' => rule_id
31
+ }
32
+ event.statsig_metadata = Statsig.get_statsig_metadata
33
+ event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
34
+ log_event(event)
51
35
  end
52
- if @events.length == 0
53
- return
36
+
37
+ def log_config_exposure(user, config_name, rule_id, secondary_exposures)
38
+ event = StatsigEvent.new($config_exposure_event)
39
+ event.user = user
40
+ event.metadata = {
41
+ 'config' => config_name,
42
+ 'ruleID' => rule_id
43
+ }
44
+ event.statsig_metadata = Statsig.get_statsig_metadata
45
+ event.secondary_exposures = secondary_exposures.is_a?(Array) ? secondary_exposures : []
46
+ log_event(event)
54
47
  end
55
- flush_events = @events.map { |e| e.serialize }
56
- @events = []
57
48
 
58
- @network.post_logs(flush_events)
49
+ def flush(closing = false)
50
+ if closing
51
+ @background_flush.exit
52
+ end
53
+ if @events.length == 0
54
+ return
55
+ end
56
+ flush_events = @events.map { |e| e.serialize }
57
+ @events = []
58
+
59
+ @network.post_logs(flush_events)
60
+ end
59
61
  end
60
62
  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.1.beta.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-12 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: