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