statsig 1.31.1 → 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,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,21 @@ 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) }
19
+ attr_accessor :gate_overrides
20
+
21
+ attr_accessor :config_overrides
22
+
26
23
  attr_accessor :options
27
24
 
28
- sig { returns(UserPersistentStorageUtils) }
29
25
  attr_accessor :persistent_storage_utils
30
26
 
31
- sig do
32
- params(
33
- store: SpecStore,
34
- options: StatsigOptions,
35
- persistent_storage_utils: UserPersistentStorageUtils,
36
- ).void
37
- end
38
27
  def initialize(store, options, persistent_storage_utils)
39
28
  UAParser.initialize_async
40
29
  CountryLookup.initialize_async
@@ -50,92 +39,142 @@ module Statsig
50
39
  @spec_store.maybe_restart_background_threads
51
40
  end
52
41
 
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))
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
62
85
  end
63
86
 
64
87
  if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
65
- 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
66
92
  end
67
93
 
68
94
  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))
95
+ unsupported_or_unrecognized(gate_name, end_result)
96
+ return
70
97
  end
71
98
 
72
- eval_spec(user, @spec_store.get_gate(gate_name))
99
+ eval_spec(user, @spec_store.get_gate(gate_name), end_result)
73
100
  end
74
101
 
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)
77
- 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(
86
- @spec_store.last_config_sync_time,
87
- @spec_store.initial_config_sync_time
88
- ),
89
- id_type: id_type
90
- )
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
91
114
  end
92
115
 
93
116
  if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
94
- 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
95
121
  end
96
122
 
97
123
  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
- )
124
+ unsupported_or_unrecognized(config_name, end_result)
125
+ return
105
126
  end
106
127
 
107
128
  config = @spec_store.get_config(config_name)
108
129
 
109
130
  # 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?
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
113
149
 
114
150
  # 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)
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)
119
156
  end
120
157
  # Otherwise, remove from persisted storage
121
158
  else
122
- @persistent_storage_utils.remove_experiment_from_storage(user, config['idType'], config_name)
123
- evaluation = eval_spec(user, config)
159
+ @persistent_storage_utils.remove_experiment_from_storage(user, config.id_type, config_name)
160
+ eval_spec(user, config, end_result)
124
161
  end
125
-
126
- return evaluation
127
162
  end
128
163
 
129
- def get_layer(user, layer_name)
164
+ def get_layer(user, layer_name, end_result)
130
165
  if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
131
- 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
132
170
  end
133
171
 
134
172
  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))
173
+ unsupported_or_unrecognized(layer_name, end_result)
174
+ return
136
175
  end
137
176
 
138
- eval_spec(user, @spec_store.get_layer(layer_name))
177
+ eval_spec(user, @spec_store.get_layer(layer_name), end_result)
139
178
  end
140
179
 
141
180
  def list_gates
@@ -143,323 +182,331 @@ module Statsig
143
182
  end
144
183
 
145
184
  def list_configs
146
- @spec_store.configs.map { |name, config| name if config['entity'] == 'dynamic_config' }.compact
185
+ @spec_store.configs.map { |name, config| name if config.entity == :dynamic_config }.compact
147
186
  end
148
187
 
149
188
  def list_experiments
150
- @spec_store.configs.map { |name, config| name if config['entity'] == 'experiment' }.compact
189
+ @spec_store.configs.map { |name, config| name if config.entity == :experiment }.compact
151
190
  end
152
191
 
153
192
  def list_autotunes
154
- @spec_store.configs.map { |name, config| name if config['entity'] == 'autotune' }.compact
193
+ @spec_store.configs.map { |name, config| name if config.entity == :autotune }.compact
155
194
  end
156
195
 
157
196
  def list_layers
158
197
  @spec_store.layers.map { |name, _| name }
159
198
  end
160
199
 
161
- def get_client_initialize_response(user, hash, client_sdk_key)
200
+ def get_client_initialize_response(user, hash_algo, client_sdk_key, include_local_overrides)
162
201
  if @spec_store.is_ready_for_checks == false
163
202
  return nil
164
203
  end
165
204
 
166
- formatter = ClientInitializeHelpers::ResponseFormatter.new(self, user, hash, client_sdk_key)
167
-
168
205
  evaluated_keys = {}
169
206
  if user.user_id.nil? == false
170
- evaluated_keys['userID'] = user.user_id
207
+ evaluated_keys[:userID] = user.user_id
171
208
  end
172
209
 
173
210
  if user.custom_ids.nil? == false
174
- evaluated_keys['customIDs'] = user.custom_ids
211
+ evaluated_keys[:customIDs] = user.custom_ids
175
212
  end
176
213
 
177
214
  {
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()
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
188
228
  }
189
229
  end
190
230
 
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
231
  def shutdown
202
232
  @spec_store.shutdown
203
233
  end
204
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
+
205
256
  def override_gate(gate, value)
206
257
  @gate_overrides[gate] = value
207
258
  end
208
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
+
209
268
  def override_config(config, value)
210
269
  @config_overrides[config] = value
211
270
  end
212
271
 
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
- )
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
261
292
  end
262
293
 
263
- 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
264
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
265
315
  else
266
- 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
267
320
  end
268
321
 
269
- Statsig::ConfigResult.new(
270
- config['name'],
271
- false,
272
- config['defaultValue'],
273
- default_rule_id,
274
- exposures,
275
- evaluation_details: EvaluationDetails.new(
322
+ unless end_result.disable_evaluation_details
323
+ end_result.evaluation_details = EvaluationDetails.new(
276
324
  @spec_store.last_config_sync_time,
277
325
  @spec_store.initial_config_sync_time,
278
326
  @spec_store.init_reason
279
- ),
280
- group_name: nil,
281
- id_type: config['idType'],
282
- target_app_ids: config['targetAppIDs']
283
- )
327
+ )
328
+ end
284
329
  end
285
330
 
286
- private
287
-
288
- def eval_rule(user, rule)
289
- exposures = []
331
+ def eval_rule(user, rule, end_result)
290
332
  pass = true
291
333
  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
334
+ until i >= rule.conditions.length
335
+ result = eval_condition(user, rule.conditions[i], end_result)
297
336
 
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
337
+ pass = false if result != true
304
338
  i += 1
305
339
  end
306
340
 
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
- )
341
+ end_result.gate_value = pass
321
342
  end
322
343
 
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))
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))
326
347
 
327
- delegated_result = self.eval_spec(user, config)
328
- return UNSUPPORTED_EVALUATION if delegated_result == UNSUPPORTED_EVALUATION
348
+ end_result.undelegated_sec_exps = end_result.secondary_exposures.dup
329
349
 
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
350
+ eval_spec(user, config, end_result)
351
+
352
+ end_result.name = name
353
+ end_result.config_delegate = delegate
354
+ end_result.explicit_parameters = config.explicit_parameters
355
+
356
+ true
336
357
  end
337
358
 
338
- def eval_condition(user, condition)
359
+ def eval_condition(user, condition, end_result)
339
360
  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
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
350
367
 
351
368
  case type
352
- when 'public'
369
+ when :public
353
370
  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'
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
370
386
  value = get_value_from_user(user, field) || get_value_from_ip(user, field)
371
- when 'ua_based'
387
+ when :ua_based
372
388
  value = get_value_from_user(user, field) || get_value_from_ua(user, field)
373
- when 'user_field'
389
+ when :user_field
374
390
  value = get_value_from_user(user, field)
375
- when 'environment_field'
391
+ when :environment_field
376
392
  value = get_value_from_environment(user, field)
377
- when 'current_time'
393
+ when :current_time
378
394
  value = Time.now.to_i # epoch time in seconds
379
- when 'user_bucket'
395
+ when :user_bucket
380
396
  begin
381
- salt = additional_values['salt']
382
- unit_id = user.get_unit_id(id_type) || ''
397
+ salt = additional_values[:salt]
398
+ unit_id = user.get_unit_id(id_type) || Const::EMPTY_STR
383
399
  # there are only 1000 user buckets as opposed to 10k for gate pass %
384
400
  value = compute_user_hash("#{salt}.#{unit_id}") % 1000
385
- rescue
401
+ rescue StandardError
386
402
  return false
387
403
  end
388
- when 'unit_id'
404
+ when :unit_id
389
405
  value = user.get_unit_id(id_type)
390
- else
391
- return UNSUPPORTED_EVALUATION
392
406
  end
393
407
 
394
- return UNSUPPORTED_EVALUATION if !operator.is_a?(String)
395
- operator = operator.downcase
396
-
397
408
  case operator
398
409
  # 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 })
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 })
407
418
 
408
419
  # version comparison
409
420
  # need to check for nil or empty value because Version takes them as valid values
410
- when 'version_gt'
421
+ when :version_gt
411
422
  return false if value.to_s.empty?
412
- return (Gem::Version.new(value) > Gem::Version.new(target) rescue false)
413
- 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
414
430
  return false if value.to_s.empty?
415
- return (Gem::Version.new(value) >= Gem::Version.new(target) rescue false)
416
- 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
417
438
  return false if value.to_s.empty?
418
- return (Gem::Version.new(value) < Gem::Version.new(target) rescue false)
419
- 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
420
446
  return false if value.to_s.empty?
421
- return (Gem::Version.new(value) <= Gem::Version.new(target) rescue false)
422
- 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
423
454
  return false if value.to_s.empty?
424
- return (Gem::Version.new(value) == Gem::Version.new(target) rescue false)
425
- 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
426
462
  return false if value.to_s.empty?
427
- 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
428
469
 
429
470
  # 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 })
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)
438
479
 
439
480
  # 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'
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
451
496
  return value == target
452
- when 'neq'
497
+ when :neq
453
498
  return value != target
454
499
 
455
500
  # 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'
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
463
510
  begin
464
511
  is_in_list = false
465
512
  id_list = @spec_store.get_id_list(target)
@@ -467,44 +514,57 @@ module Statsig
467
514
  hashed_id = Digest::SHA256.base64digest(value.to_s)[0, 8]
468
515
  is_in_list = id_list.ids.include?(hashed_id)
469
516
  end
470
- return is_in_list if operator == 'in_segment_list'
517
+ return is_in_list if operator == :in_segment_list
518
+
471
519
  return !is_in_list
472
- rescue
520
+ rescue StandardError
473
521
  return false
474
522
  end
475
- else
476
- return UNSUPPORTED_EVALUATION
477
523
  end
524
+ return false
478
525
  end
479
526
 
480
527
  def get_value_from_user(user, field)
481
- return nil unless user.instance_of?(StatsigUser) && field.is_a?(String)
528
+ return nil unless field.is_a?(String)
482
529
 
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?
530
+ value = get_value_from_user_field(user, field)
531
+ value ||= get_value_from_user_field(user, field.downcase)
486
532
 
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
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)
492
538
  end
539
+ value
540
+ end
493
541
 
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
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
499
562
  end
500
-
501
- nil
502
563
  end
503
564
 
504
565
  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
566
+ return nil unless user.statsig_environment.is_a?(Hash) && field.is_a?(String)
567
+
508
568
  user.statsig_environment.each do |key, value|
509
569
  return value if key.to_s.downcase == (field)
510
570
  end
@@ -512,50 +572,46 @@ module Statsig
512
572
  end
513
573
 
514
574
  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')
575
+ return nil unless field == Const::COUNTRY
576
+
577
+ ip = get_value_from_user(user, Const::IP)
517
578
  return nil unless ip.is_a?(String)
518
579
 
519
580
  CountryLookup.lookup_ip_string(ip)
520
581
  end
521
582
 
522
583
  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')
584
+ return nil unless field.is_a?(String)
585
+
586
+ ua = get_value_from_user(user, Const::USER_AGENT)
525
587
  return nil unless ua.is_a?(String)
526
588
 
527
589
  case field.downcase
528
- when 'os_name', 'osname'
590
+ when Const::OSNAME, Const::OS_NAME
529
591
  os = UAParser.parse_os(ua)
530
592
  return os&.family
531
- when 'os_version', 'osversion'
593
+ when Const::OS_VERSION, Const::OSVERSION
532
594
  os = UAParser.parse_os(ua)
533
595
  return os&.version unless os&.version.nil?
534
- when 'browser_name', 'browsername'
596
+ when Const::BROWSERNAME, Const::BROWSER_NAME
535
597
  parsed = UAParser.parse_ua(ua)
536
598
  return parsed.family
537
- when 'browser_version', 'browserversion'
599
+ when Const::BROWSERVERSION, Const::BROWSER_VERSION
538
600
  parsed = UAParser.parse_ua(ua)
539
601
  return parsed.version.to_s
540
- else
541
- nil
542
602
  end
543
603
  end
544
604
 
545
605
  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
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)
555
610
  end
556
611
 
557
612
  def compute_user_hash(user_hash)
558
- Digest::SHA256.digest(user_hash).unpack('Q>')[0]
613
+ Digest::SHA256.digest(user_hash).unpack1(Const::Q_RIGHT_CHEVRON)
559
614
  end
615
+
560
616
  end
561
- end
617
+ end