statsig 1.31.1 → 1.32.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/evaluator.rb CHANGED
@@ -1,6 +1,3 @@
1
- # typed: false
2
-
3
- require 'sorbet-runtime'
4
1
  require 'config_result'
5
2
  require 'country_lookup'
6
3
  require 'digest'
@@ -12,29 +9,17 @@ require 'ua_parser'
12
9
  require 'evaluation_details'
13
10
  require 'user_agent_parser/operating_system'
14
11
  require 'user_persistent_storage_utils'
12
+ require 'constants'
15
13
 
16
14
  module Statsig
17
15
  class Evaluator
18
- extend T::Sig
19
-
20
- UNSUPPORTED_EVALUATION = :unsupported_eval
21
16
 
22
- sig { returns(SpecStore) }
23
17
  attr_accessor :spec_store
24
18
 
25
- sig { returns(StatsigOptions) }
26
19
  attr_accessor :options
27
20
 
28
- sig { returns(UserPersistentStorageUtils) }
29
21
  attr_accessor :persistent_storage_utils
30
22
 
31
- sig do
32
- params(
33
- store: SpecStore,
34
- options: StatsigOptions,
35
- persistent_storage_utils: UserPersistentStorageUtils,
36
- ).void
37
- end
38
23
  def initialize(store, options, persistent_storage_utils)
39
24
  UAParser.initialize_async
40
25
  CountryLookup.initialize_async
@@ -50,92 +35,111 @@ module Statsig
50
35
  @spec_store.maybe_restart_background_threads
51
36
  end
52
37
 
53
- def check_gate(user, gate_name)
54
- if @gate_overrides.has_key?(gate_name)
55
- return Statsig::ConfigResult.new(
56
- gate_name,
57
- @gate_overrides[gate_name],
58
- @gate_overrides[gate_name],
59
- 'override',
60
- [],
61
- evaluation_details: EvaluationDetails.local_override(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
38
+ def check_gate(user, gate_name, end_result)
39
+ if @gate_overrides.key?(gate_name)
40
+ end_result.gate_value = @gate_overrides[gate_name]
41
+ end_result.rule_id = Const::OVERRIDE
42
+ unless end_result.disable_evaluation_details
43
+ end_result.evaluation_details = EvaluationDetails.local_override(
44
+ @spec_store.last_config_sync_time,
45
+ @spec_store.initial_config_sync_time
46
+ )
47
+ end
48
+ return
62
49
  end
63
50
 
64
51
  if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
65
- return Statsig::ConfigResult.new(gate_name, evaluation_details: EvaluationDetails.uninitialized)
52
+ unless end_result.disable_evaluation_details
53
+ end_result.evaluation_details = EvaluationDetails.uninitialized
54
+ end
55
+ return
66
56
  end
67
57
 
68
58
  unless @spec_store.has_gate?(gate_name)
69
- return Statsig::ConfigResult.new(gate_name, evaluation_details: EvaluationDetails.unrecognized(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
59
+ unsupported_or_unrecognized(gate_name, end_result)
60
+ return
70
61
  end
71
62
 
72
- eval_spec(user, @spec_store.get_gate(gate_name))
63
+ eval_spec(user, @spec_store.get_gate(gate_name), end_result)
73
64
  end
74
65
 
75
- sig { params(user: StatsigUser, config_name: String, user_persisted_values: T.nilable(UserPersistedValues)).returns(ConfigResult) }
76
- def get_config(user, config_name, user_persisted_values: nil)
66
+ def get_config(user, config_name, end_result, user_persisted_values: nil)
77
67
  if @config_overrides.key?(config_name)
78
- id_type = @spec_store.has_config?(config_name) ? @spec_store.get_config(config_name)['idType'] : ''
79
- return Statsig::ConfigResult.new(
80
- config_name,
81
- false,
82
- @config_overrides[config_name],
83
- 'override',
84
- [],
85
- evaluation_details: EvaluationDetails.local_override(
68
+ id_type = @spec_store.has_config?(config_name) ? @spec_store.get_config(config_name).id_type : Const::EMPTY_STR
69
+ end_result.id_type = id_type
70
+ end_result.rule_id = Const::OVERRIDE
71
+ end_result.json_value = @config_overrides[config_name]
72
+ unless end_result.disable_evaluation_details
73
+ end_result.evaluation_details = EvaluationDetails.local_override(
86
74
  @spec_store.last_config_sync_time,
87
75
  @spec_store.initial_config_sync_time
88
- ),
89
- id_type: id_type
90
- )
76
+ )
77
+ end
78
+ return
91
79
  end
92
80
 
93
81
  if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
94
- return Statsig::ConfigResult.new(config_name, evaluation_details: EvaluationDetails.uninitialized)
82
+ unless end_result.disable_evaluation_details
83
+ end_result.evaluation_details = EvaluationDetails.uninitialized
84
+ end
85
+ return
95
86
  end
96
87
 
97
88
  unless @spec_store.has_config?(config_name)
98
- return Statsig::ConfigResult.new(
99
- config_name,
100
- evaluation_details: EvaluationDetails.unrecognized(
101
- @spec_store.last_config_sync_time,
102
- @spec_store.initial_config_sync_time
103
- )
104
- )
89
+ unsupported_or_unrecognized(config_name, end_result)
90
+ return
105
91
  end
106
92
 
107
93
  config = @spec_store.get_config(config_name)
108
94
 
109
95
  # If persisted values is provided and the experiment is active, return sticky values if exists.
110
- if !user_persisted_values.nil? && config['isActive'] == true
111
- sticky_result = Statsig::ConfigResult.from_user_persisted_values(config_name, user_persisted_values)
112
- return sticky_result unless sticky_result.nil?
96
+ if !user_persisted_values.nil? && config.is_active == true
97
+ sticky_values = user_persisted_values[config_name]
98
+ unless sticky_values.nil?
99
+ end_result.gate_value = sticky_values[Statsig::Const::GATE_VALUE]
100
+ end_result.json_value = sticky_values[Statsig::Const::JSON_VALUE]
101
+ end_result.rule_id = sticky_values[Statsig::Const::RULE_ID]
102
+ end_result.secondary_exposures = sticky_values[Statsig::Const::SECONDARY_EXPOSURES]
103
+ end_result.group_name = sticky_values[Statsig::Const::GROUP_NAME]
104
+ end_result.id_type = sticky_values[Statsig::Const::ID_TYPE]
105
+ end_result.target_app_ids = sticky_values[Statsig::Const::TARGET_APP_IDS]
106
+ unless end_result.disable_evaluation_details
107
+ end_result.evaluation_details = EvaluationDetails.persisted(
108
+ sticky_values[Statsig::Const::CONFIG_SYNC_TIME],
109
+ sticky_values[Statsig::Const::INIT_TIME]
110
+ )
111
+ end
112
+ return
113
+ end
113
114
 
114
115
  # If it doesn't exist, then save to persisted storage if the user was assigned to an experiment group.
115
- evaluation = eval_spec(user, config)
116
- if evaluation.is_experiment_group
117
- @persistent_storage_utils.add_evaluation_to_user_persisted_values(user_persisted_values, config_name, evaluation)
118
- @persistent_storage_utils.save_to_storage(user, config['idType'], user_persisted_values)
116
+ eval_spec(user, config, end_result)
117
+ if end_result.is_experiment_group
118
+ @persistent_storage_utils.add_evaluation_to_user_persisted_values(user_persisted_values, config_name,
119
+ end_result)
120
+ @persistent_storage_utils.save_to_storage(user, config.id_type, user_persisted_values)
119
121
  end
120
122
  # Otherwise, remove from persisted storage
121
123
  else
122
- @persistent_storage_utils.remove_experiment_from_storage(user, config['idType'], config_name)
123
- evaluation = eval_spec(user, config)
124
+ @persistent_storage_utils.remove_experiment_from_storage(user, config.id_type, config_name)
125
+ eval_spec(user, config, end_result)
124
126
  end
125
-
126
- return evaluation
127
127
  end
128
128
 
129
- def get_layer(user, layer_name)
129
+ def get_layer(user, layer_name, end_result)
130
130
  if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
131
- return Statsig::ConfigResult.new(layer_name, evaluation_details: EvaluationDetails.uninitialized)
131
+ unless end_result.disable_evaluation_details
132
+ end_result.evaluation_details = EvaluationDetails.uninitialized
133
+ end
134
+ return
132
135
  end
133
136
 
134
137
  unless @spec_store.has_layer?(layer_name)
135
- return Statsig::ConfigResult.new(layer_name, evaluation_details: EvaluationDetails.unrecognized(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time))
138
+ unsupported_or_unrecognized(layer_name, end_result)
139
+ return
136
140
  end
137
141
 
138
- eval_spec(user, @spec_store.get_layer(layer_name))
142
+ eval_spec(user, @spec_store.get_layer(layer_name), end_result)
139
143
  end
140
144
 
141
145
  def list_gates
@@ -143,65 +147,77 @@ module Statsig
143
147
  end
144
148
 
145
149
  def list_configs
146
- @spec_store.configs.map { |name, config| name if config['entity'] == 'dynamic_config' }.compact
150
+ @spec_store.configs.map { |name, config| name if config.entity == :dynamic_config }.compact
147
151
  end
148
152
 
149
153
  def list_experiments
150
- @spec_store.configs.map { |name, config| name if config['entity'] == 'experiment' }.compact
154
+ @spec_store.configs.map { |name, config| name if config.entity == :experiment }.compact
151
155
  end
152
156
 
153
157
  def list_autotunes
154
- @spec_store.configs.map { |name, config| name if config['entity'] == 'autotune' }.compact
158
+ @spec_store.configs.map { |name, config| name if config.entity == :autotune }.compact
155
159
  end
156
160
 
157
161
  def list_layers
158
162
  @spec_store.layers.map { |name, _| name }
159
163
  end
160
164
 
161
- def get_client_initialize_response(user, hash, client_sdk_key)
165
+ def get_client_initialize_response(user, hash_algo, client_sdk_key)
162
166
  if @spec_store.is_ready_for_checks == false
163
167
  return nil
164
168
  end
165
169
 
166
- formatter = ClientInitializeHelpers::ResponseFormatter.new(self, user, hash, client_sdk_key)
167
-
168
170
  evaluated_keys = {}
169
171
  if user.user_id.nil? == false
170
- evaluated_keys['userID'] = user.user_id
172
+ evaluated_keys[:userID] = user.user_id
171
173
  end
172
174
 
173
175
  if user.custom_ids.nil? == false
174
- evaluated_keys['customIDs'] = user.custom_ids
176
+ evaluated_keys[:customIDs] = user.custom_ids
175
177
  end
176
178
 
177
179
  {
178
- "feature_gates" => formatter.get_responses(:gates),
179
- "dynamic_configs" => formatter.get_responses(:configs),
180
- "layer_configs" => formatter.get_responses(:layers),
181
- "sdkParams" => {},
182
- "has_updates" => true,
183
- "generator" => "statsig-ruby-sdk",
184
- "evaluated_keys" => evaluated_keys,
185
- "time" => 0,
186
- "hash_used" => hash,
187
- "user_hash" => user.to_hash_without_stable_id()
180
+ feature_gates: Statsig::ResponseFormatter
181
+ .get_responses(@spec_store.gates, self, user, client_sdk_key, hash_algo),
182
+ dynamic_configs: Statsig::ResponseFormatter
183
+ .get_responses(@spec_store.configs, self, user, client_sdk_key, hash_algo),
184
+ layer_configs: Statsig::ResponseFormatter
185
+ .get_responses(@spec_store.layers, self, user, client_sdk_key, hash_algo),
186
+ sdkParams: {},
187
+ has_updates: true,
188
+ generator: Const::STATSIG_RUBY_SDK,
189
+ evaluated_keys: evaluated_keys,
190
+ time: 0,
191
+ hash_used: hash_algo,
192
+ user_hash: user.to_hash_without_stable_id
188
193
  }
189
194
  end
190
195
 
191
- def clean_exposures(exposures)
192
- seen = {}
193
- exposures.reject do |exposure|
194
- key = "#{exposure["gate"]}|#{exposure["gateValue"]}|#{exposure["ruleID"]}}"
195
- should_reject = seen[key]
196
- seen[key] = true
197
- should_reject == true
198
- end
199
- end
200
-
201
196
  def shutdown
202
197
  @spec_store.shutdown
203
198
  end
204
199
 
200
+ def unsupported_or_unrecognized(config_name, end_result)
201
+ end_result.rule_id = Const::EMPTY_STR
202
+
203
+ if end_result.disable_evaluation_details
204
+ return
205
+ end
206
+
207
+ if @spec_store.unsupported_configs.include?(config_name)
208
+ end_result.evaluation_details = EvaluationDetails.unsupported(
209
+ @spec_store.last_config_sync_time,
210
+ @spec_store.initial_config_sync_time
211
+ )
212
+ return
213
+ end
214
+
215
+ end_result.evaluation_details = EvaluationDetails.unrecognized(
216
+ @spec_store.last_config_sync_time,
217
+ @spec_store.initial_config_sync_time
218
+ )
219
+ end
220
+
205
221
  def override_gate(gate, value)
206
222
  @gate_overrides[gate] = value
207
223
  end
@@ -210,256 +226,236 @@ module Statsig
210
226
  @config_overrides[config] = value
211
227
  end
212
228
 
213
- sig { params(user: StatsigUser, config: Hash).returns(ConfigResult) }
214
- def eval_spec(user, config)
215
- default_rule_id = 'default'
216
- exposures = []
217
- if config['enabled']
218
- i = 0
219
- until i >= config['rules'].length do
220
- rule = config['rules'][i]
221
- result = eval_rule(user, rule)
222
- return Statsig::ConfigResult.new(
223
- config['name'],
224
- false,
225
- config['defaultValue'],
226
- '',
227
- exposures,
228
- evaluation_details: EvaluationDetails.new(
229
- @spec_store.last_config_sync_time,
230
- @spec_store.initial_config_sync_time,
231
- EvaluationReason::UNSUPPORTED,
232
- ),
233
- group_name: nil,
234
- id_type: config['idType'],
235
- target_app_ids: config['targetAppIDs']
236
- ) if result == UNSUPPORTED_EVALUATION
237
- exposures = exposures + result.secondary_exposures
238
- if result.gate_value
239
-
240
- if (delegated_result = eval_delegate(config['name'], user, rule, exposures))
241
- return delegated_result
242
- end
243
-
244
- pass = eval_pass_percent(user, rule, config['salt'])
245
- return Statsig::ConfigResult.new(
246
- config['name'],
247
- pass,
248
- pass ? result.json_value : config['defaultValue'],
249
- result.rule_id,
250
- exposures,
251
- evaluation_details: EvaluationDetails.new(
252
- @spec_store.last_config_sync_time,
253
- @spec_store.initial_config_sync_time,
254
- @spec_store.init_reason
255
- ),
256
- is_experiment_group: result.is_experiment_group,
257
- group_name: result.group_name,
258
- id_type: config['idType'],
259
- target_app_ids: config['targetAppIDs']
260
- )
229
+ def eval_spec(user, config, end_result)
230
+ unless config.enabled
231
+ finalize_eval_result(config, end_result, did_pass: false, rule: nil)
232
+ return
233
+ end
234
+
235
+ config.rules.each do |rule|
236
+ eval_rule(user, rule, end_result)
237
+
238
+ if end_result.gate_value
239
+ if eval_delegate(config.name, user, rule, end_result)
240
+ return
261
241
  end
262
242
 
263
- i += 1
243
+ pass = eval_pass_percent(user, rule, config.salt)
244
+ finalize_eval_result(config, end_result, did_pass: pass, rule: rule)
245
+ return
264
246
  end
247
+ end
248
+
249
+ finalize_eval_result(config, end_result, did_pass: false, rule: nil)
250
+ end
251
+
252
+ private
253
+
254
+ def finalize_eval_result(config, end_result, did_pass:, rule:)
255
+ end_result.id_type = config.id_type
256
+ end_result.target_app_ids = config.target_app_ids
257
+ end_result.gate_value = did_pass
258
+
259
+ if rule.nil?
260
+ end_result.json_value = config.default_value
261
+ end_result.group_name = nil
262
+ end_result.is_experiment_group = false
263
+ end_result.rule_id = config.enabled ? Const::DEFAULT : Const::DISABLED
265
264
  else
266
- default_rule_id = 'disabled'
265
+ end_result.json_value = did_pass ? rule.return_value : config.default_value
266
+ end_result.group_name = rule.group_name
267
+ end_result.is_experiment_group = rule.is_experiment_group == true
268
+ end_result.rule_id = rule.id
267
269
  end
268
270
 
269
- Statsig::ConfigResult.new(
270
- config['name'],
271
- false,
272
- config['defaultValue'],
273
- default_rule_id,
274
- exposures,
275
- evaluation_details: EvaluationDetails.new(
271
+ unless end_result.disable_evaluation_details
272
+ end_result.evaluation_details = EvaluationDetails.new(
276
273
  @spec_store.last_config_sync_time,
277
274
  @spec_store.initial_config_sync_time,
278
275
  @spec_store.init_reason
279
- ),
280
- group_name: nil,
281
- id_type: config['idType'],
282
- target_app_ids: config['targetAppIDs']
283
- )
276
+ )
277
+ end
284
278
  end
285
279
 
286
- private
287
-
288
- def eval_rule(user, rule)
289
- exposures = []
280
+ def eval_rule(user, rule, end_result)
290
281
  pass = true
291
282
  i = 0
292
- until i >= rule['conditions'].length do
293
- result = eval_condition(user, rule['conditions'][i])
294
- if result == UNSUPPORTED_EVALUATION
295
- return UNSUPPORTED_EVALUATION
296
- end
283
+ until i >= rule.conditions.length
284
+ result = eval_condition(user, rule.conditions[i], end_result)
297
285
 
298
- if result.is_a?(Hash)
299
- exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
300
- pass = false if result['value'] == false
301
- elsif result == false
302
- pass = false
303
- end
286
+ pass = false if result != true
304
287
  i += 1
305
288
  end
306
289
 
307
- Statsig::ConfigResult.new(
308
- '',
309
- pass,
310
- rule['returnValue'],
311
- rule['id'],
312
- exposures,
313
- evaluation_details: EvaluationDetails.new(
314
- @spec_store.last_config_sync_time,
315
- @spec_store.initial_config_sync_time,
316
- @spec_store.init_reason
317
- ),
318
- is_experiment_group: rule["isExperimentGroup"] == true,
319
- group_name: rule['groupName']
320
- )
290
+ end_result.gate_value = pass
321
291
  end
322
292
 
323
- def eval_delegate(name, user, rule, exposures)
324
- return nil unless (delegate = rule['configDelegate'])
325
- return nil unless (config = @spec_store.get_config(delegate))
293
+ def eval_delegate(name, user, rule, end_result)
294
+ return false unless (delegate = rule.config_delegate)
295
+ return false unless (config = @spec_store.get_config(delegate))
296
+
297
+ end_result.undelegated_sec_exps = end_result.secondary_exposures.dup
298
+
299
+ eval_spec(user, config, end_result)
326
300
 
327
- delegated_result = self.eval_spec(user, config)
328
- return UNSUPPORTED_EVALUATION if delegated_result == UNSUPPORTED_EVALUATION
301
+ end_result.name = name
302
+ end_result.config_delegate = delegate
303
+ end_result.explicit_parameters = config.explicit_parameters
329
304
 
330
- delegated_result.name = name
331
- delegated_result.config_delegate = delegate
332
- delegated_result.secondary_exposures = exposures + delegated_result.secondary_exposures
333
- delegated_result.undelegated_sec_exps = exposures
334
- delegated_result.explicit_parameters = config['explicitParameters']
335
- delegated_result
305
+ true
336
306
  end
337
307
 
338
- def eval_condition(user, condition)
308
+ def eval_condition(user, condition, end_result)
339
309
  value = nil
340
- field = condition['field']
341
- target = condition['targetValue']
342
- type = condition['type']
343
- operator = condition['operator']
344
- additional_values = condition['additionalValues']
345
- additional_values = Hash.new unless additional_values.is_a? Hash
346
- id_type = condition['idType']
347
-
348
- return UNSUPPORTED_EVALUATION unless type.is_a? String
349
- type = type.downcase
310
+ field = condition.field
311
+ target = condition.target_value
312
+ type = condition.type
313
+ operator = condition.operator
314
+ additional_values = condition.additional_values
315
+ id_type = condition.id_type
350
316
 
351
317
  case type
352
- when 'public'
318
+ when :public
353
319
  return true
354
- when 'fail_gate', 'pass_gate'
355
- other_gate_result = check_gate(user, target)
356
- return UNSUPPORTED_EVALUATION if other_gate_result == UNSUPPORTED_EVALUATION
357
-
358
- gate_value = other_gate_result&.gate_value == true
359
- new_exposure = {
360
- 'gate' => target,
361
- 'gateValue' => gate_value ? 'true' : 'false',
362
- 'ruleID' => other_gate_result&.rule_id
363
- }
364
- exposures = other_gate_result&.secondary_exposures&.append(new_exposure)
365
- return {
366
- 'value' => type == 'pass_gate' ? gate_value : !gate_value,
367
- 'exposures' => exposures
368
- }
369
- when 'ip_based'
320
+ when :fail_gate, :pass_gate
321
+ check_gate(user, target, end_result)
322
+
323
+ gate_value = end_result.gate_value
324
+
325
+ unless end_result.disable_exposures
326
+ new_exposure = {
327
+ gate: target,
328
+ gateValue: gate_value ? Const::TRUE : Const::FALSE,
329
+ ruleID: end_result.rule_id
330
+ }
331
+ end_result.secondary_exposures.append(new_exposure)
332
+ end
333
+ return type == :pass_gate ? gate_value : !gate_value
334
+ when :ip_based
370
335
  value = get_value_from_user(user, field) || get_value_from_ip(user, field)
371
- when 'ua_based'
336
+ when :ua_based
372
337
  value = get_value_from_user(user, field) || get_value_from_ua(user, field)
373
- when 'user_field'
338
+ when :user_field
374
339
  value = get_value_from_user(user, field)
375
- when 'environment_field'
340
+ when :environment_field
376
341
  value = get_value_from_environment(user, field)
377
- when 'current_time'
342
+ when :current_time
378
343
  value = Time.now.to_i # epoch time in seconds
379
- when 'user_bucket'
344
+ when :user_bucket
380
345
  begin
381
- salt = additional_values['salt']
382
- unit_id = user.get_unit_id(id_type) || ''
346
+ salt = additional_values[:salt]
347
+ unit_id = user.get_unit_id(id_type) || Const::EMPTY_STR
383
348
  # there are only 1000 user buckets as opposed to 10k for gate pass %
384
349
  value = compute_user_hash("#{salt}.#{unit_id}") % 1000
385
- rescue
350
+ rescue StandardError
386
351
  return false
387
352
  end
388
- when 'unit_id'
353
+ when :unit_id
389
354
  value = user.get_unit_id(id_type)
390
- else
391
- return UNSUPPORTED_EVALUATION
392
355
  end
393
356
 
394
- return UNSUPPORTED_EVALUATION if !operator.is_a?(String)
395
- operator = operator.downcase
396
-
397
357
  case operator
398
358
  # numerical comparison
399
- when 'gt'
400
- return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a > b })
401
- when 'gte'
402
- return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a >= b })
403
- when 'lt'
404
- return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a < b })
405
- when 'lte'
406
- return EvaluationHelpers::compare_numbers(value, target, ->(a, b) { a <= b })
359
+ when :gt
360
+ return EvaluationHelpers.compare_numbers(value, target, ->(a, b) { a > b })
361
+ when :gte
362
+ return EvaluationHelpers.compare_numbers(value, target, ->(a, b) { a >= b })
363
+ when :lt
364
+ return EvaluationHelpers.compare_numbers(value, target, ->(a, b) { a < b })
365
+ when :lte
366
+ return EvaluationHelpers.compare_numbers(value, target, ->(a, b) { a <= b })
407
367
 
408
368
  # version comparison
409
369
  # need to check for nil or empty value because Version takes them as valid values
410
- when 'version_gt'
370
+ when :version_gt
411
371
  return false if value.to_s.empty?
412
- return (Gem::Version.new(value) > Gem::Version.new(target) rescue false)
413
- when 'version_gte'
372
+
373
+ return begin
374
+ Gem::Version.new(value) > Gem::Version.new(target)
375
+ rescue StandardError
376
+ false
377
+ end
378
+ when :version_gte
414
379
  return false if value.to_s.empty?
415
- return (Gem::Version.new(value) >= Gem::Version.new(target) rescue false)
416
- when 'version_lt'
380
+
381
+ return begin
382
+ Gem::Version.new(value) >= Gem::Version.new(target)
383
+ rescue StandardError
384
+ false
385
+ end
386
+ when :version_lt
417
387
  return false if value.to_s.empty?
418
- return (Gem::Version.new(value) < Gem::Version.new(target) rescue false)
419
- when 'version_lte'
388
+
389
+ return begin
390
+ Gem::Version.new(value) < Gem::Version.new(target)
391
+ rescue StandardError
392
+ false
393
+ end
394
+ when :version_lte
420
395
  return false if value.to_s.empty?
421
- return (Gem::Version.new(value) <= Gem::Version.new(target) rescue false)
422
- when 'version_eq'
396
+
397
+ return begin
398
+ Gem::Version.new(value) <= Gem::Version.new(target)
399
+ rescue StandardError
400
+ false
401
+ end
402
+ when :version_eq
423
403
  return false if value.to_s.empty?
424
- return (Gem::Version.new(value) == Gem::Version.new(target) rescue false)
425
- when 'version_neq'
404
+
405
+ return begin
406
+ Gem::Version.new(value) == Gem::Version.new(target)
407
+ rescue StandardError
408
+ false
409
+ end
410
+ when :version_neq
426
411
  return false if value.to_s.empty?
427
- return (Gem::Version.new(value) != Gem::Version.new(target) rescue false)
412
+
413
+ return begin
414
+ Gem::Version.new(value) != Gem::Version.new(target)
415
+ rescue StandardError
416
+ false
417
+ end
428
418
 
429
419
  # array operations
430
- when 'any'
431
- return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
432
- when 'none'
433
- return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a == b })
434
- when 'any_case_sensitive'
435
- return EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
436
- when 'none_case_sensitive'
437
- return !EvaluationHelpers::match_string_in_array(target, value, false, ->(a, b) { a == b })
420
+ when :any
421
+ return EvaluationHelpers::equal_string_in_array(target, value, true)
422
+ when :none
423
+ return !EvaluationHelpers::equal_string_in_array(target, value, true)
424
+ when :any_case_sensitive
425
+ return EvaluationHelpers::equal_string_in_array(target, value, false)
426
+ when :none_case_sensitive
427
+ return !EvaluationHelpers::equal_string_in_array(target, value, false)
438
428
 
439
429
  # string
440
- when 'str_starts_with_any'
441
- return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
442
- when 'str_ends_with_any'
443
- return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
444
- when 'str_contains_any'
445
- return EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
446
- when 'str_contains_none'
447
- return !EvaluationHelpers::match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
448
- when 'str_matches'
449
- return (value.is_a?(String) && !(value =~ Regexp.new(target)).nil? rescue false)
450
- when 'eq'
430
+ when :str_starts_with_any
431
+ return EvaluationHelpers.match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
432
+ when :str_ends_with_any
433
+ return EvaluationHelpers.match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
434
+ when :str_contains_any
435
+ return EvaluationHelpers.match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
436
+ when :str_contains_none
437
+ return !EvaluationHelpers.match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
438
+ when :str_matches
439
+ return begin
440
+ value&.is_a?(String) && !(value =~ Regexp.new(target)).nil?
441
+ rescue StandardError
442
+ false
443
+ end
444
+ when :eq
451
445
  return value == target
452
- when 'neq'
446
+ when :neq
453
447
  return value != target
454
448
 
455
449
  # dates
456
- when 'before'
457
- return EvaluationHelpers::compare_times(value, target, ->(a, b) { a < b })
458
- when 'after'
459
- return EvaluationHelpers::compare_times(value, target, ->(a, b) { a > b })
460
- when 'on'
461
- return EvaluationHelpers::compare_times(value, target, ->(a, b) { a.year == b.year && a.month == b.month && a.day == b.day })
462
- when 'in_segment_list', 'not_in_segment_list'
450
+ when :before
451
+ return EvaluationHelpers.compare_times(value, target, ->(a, b) { a < b })
452
+ when :after
453
+ return EvaluationHelpers.compare_times(value, target, ->(a, b) { a > b })
454
+ when :on
455
+ return EvaluationHelpers.compare_times(value, target, lambda { |a, b|
456
+ a.year == b.year && a.month == b.month && a.day == b.day
457
+ })
458
+ when :in_segment_list, :not_in_segment_list
463
459
  begin
464
460
  is_in_list = false
465
461
  id_list = @spec_store.get_id_list(target)
@@ -467,44 +463,57 @@ module Statsig
467
463
  hashed_id = Digest::SHA256.base64digest(value.to_s)[0, 8]
468
464
  is_in_list = id_list.ids.include?(hashed_id)
469
465
  end
470
- return is_in_list if operator == 'in_segment_list'
466
+ return is_in_list if operator == :in_segment_list
467
+
471
468
  return !is_in_list
472
- rescue
469
+ rescue StandardError
473
470
  return false
474
471
  end
475
- else
476
- return UNSUPPORTED_EVALUATION
477
472
  end
473
+ return false
478
474
  end
479
475
 
480
476
  def get_value_from_user(user, field)
481
- return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
477
+ return nil unless field.is_a?(String)
482
478
 
483
- user_lookup_table = user&.value_lookup
484
- return nil unless user_lookup_table.is_a?(Hash)
485
- return user_lookup_table[field.downcase] if user_lookup_table.has_key?(field.downcase) && !user_lookup_table[field.downcase].nil?
479
+ value = get_value_from_user_field(user, field)
480
+ value ||= get_value_from_user_field(user, field.downcase)
486
481
 
487
- user_custom = user_lookup_table['custom']
488
- if user_custom.is_a?(Hash)
489
- user_custom.each do |key, value|
490
- return value if key.to_s.downcase.casecmp?(field.downcase) && !value.nil?
491
- end
482
+ if value.nil?
483
+ value = user.custom[field] if user.custom.is_a?(Hash)
484
+ value = user.custom[field.to_sym] if value.nil? && user.custom.is_a?(Hash)
485
+ value = user.private_attributes[field] if value.nil? && user.private_attributes.is_a?(Hash)
486
+ value = user.private_attributes[field.to_sym] if value.nil? && user.private_attributes.is_a?(Hash)
492
487
  end
488
+ value
489
+ end
493
490
 
494
- private_attributes = user_lookup_table['privateAttributes']
495
- if private_attributes.is_a?(Hash)
496
- private_attributes.each do |key, value|
497
- return value if key.to_s.downcase.casecmp?(field.downcase) && !value.nil?
498
- end
491
+ def get_value_from_user_field(user, field)
492
+ return nil unless field.is_a?(String)
493
+
494
+ case field
495
+ when Const::USERID, Const::USER_ID
496
+ user.user_id
497
+ when Const::EMAIL
498
+ user.email
499
+ when Const::IP
500
+ user.ip
501
+ when Const::USERAGENT, Const::USER_AGENT
502
+ user.user_agent
503
+ when Const::COUNTRY
504
+ user.country
505
+ when Const::LOCALE
506
+ user.locale
507
+ when Const::APPVERSION, Const::APP_VERSION
508
+ user.app_version
509
+ else
510
+ nil
499
511
  end
500
-
501
- nil
502
512
  end
503
513
 
504
514
  def get_value_from_environment(user, field)
505
- return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
506
- field = field.downcase
507
- return nil unless user.statsig_environment.is_a? Hash
515
+ return nil unless user.statsig_environment.is_a?(Hash) && field.is_a?(String)
516
+
508
517
  user.statsig_environment.each do |key, value|
509
518
  return value if key.to_s.downcase == (field)
510
519
  end
@@ -512,50 +521,46 @@ module Statsig
512
521
  end
513
522
 
514
523
  def get_value_from_ip(user, field)
515
- return nil unless user.is_a?(StatsigUser) && field.is_a?(String) && field.downcase == 'country'
516
- ip = get_value_from_user(user, 'ip')
524
+ return nil unless field == Const::COUNTRY
525
+
526
+ ip = get_value_from_user(user, Const::IP)
517
527
  return nil unless ip.is_a?(String)
518
528
 
519
529
  CountryLookup.lookup_ip_string(ip)
520
530
  end
521
531
 
522
532
  def get_value_from_ua(user, field)
523
- return nil unless user.is_a?(StatsigUser) && field.is_a?(String)
524
- ua = get_value_from_user(user, 'userAgent')
533
+ return nil unless field.is_a?(String)
534
+
535
+ ua = get_value_from_user(user, Const::USER_AGENT)
525
536
  return nil unless ua.is_a?(String)
526
537
 
527
538
  case field.downcase
528
- when 'os_name', 'osname'
539
+ when Const::OSNAME, Const::OS_NAME
529
540
  os = UAParser.parse_os(ua)
530
541
  return os&.family
531
- when 'os_version', 'osversion'
542
+ when Const::OS_VERSION, Const::OSVERSION
532
543
  os = UAParser.parse_os(ua)
533
544
  return os&.version unless os&.version.nil?
534
- when 'browser_name', 'browsername'
545
+ when Const::BROWSERNAME, Const::BROWSER_NAME
535
546
  parsed = UAParser.parse_ua(ua)
536
547
  return parsed.family
537
- when 'browser_version', 'browserversion'
548
+ when Const::BROWSERVERSION, Const::BROWSER_VERSION
538
549
  parsed = UAParser.parse_ua(ua)
539
550
  return parsed.version.to_s
540
- else
541
- nil
542
551
  end
543
552
  end
544
553
 
545
554
  def eval_pass_percent(user, rule, config_salt)
546
- return false unless config_salt.is_a?(String) && !rule['passPercentage'].nil?
547
- begin
548
- unit_id = user.get_unit_id(rule['idType']) || ''
549
- rule_salt = rule['salt'] || rule['id'] || ''
550
- hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
551
- return (hash % 10000) < (rule['passPercentage'].to_f * 100)
552
- rescue
553
- return false
554
- end
555
+ unit_id = user.get_unit_id(rule.id_type) || Const::EMPTY_STR
556
+ rule_salt = rule.salt || rule.id || Const::EMPTY_STR
557
+ hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
558
+ return (hash % 10_000) < (rule.pass_percentage * 100)
555
559
  end
556
560
 
557
561
  def compute_user_hash(user_hash)
558
- Digest::SHA256.digest(user_hash).unpack('Q>')[0]
562
+ Digest::SHA256.digest(user_hash).unpack1(Const::Q_RIGHT_CHEVRON)
559
563
  end
564
+
560
565
  end
561
- end
566
+ end