statsig 1.31.1 → 1.32.0

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.
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