statsig 1.25.2 → 1.33.0

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