statsig 2.1.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd554df4ba15f0d1addaecc5e76502c1218c1b08842152eeeffd0f6e121dbcba
4
- data.tar.gz: b1d19d91608685488db9705e213559c4a36c21731b0822137b55fab0693a871d
3
+ metadata.gz: 20ee1f5defc13286856fb81276a04db5e1078cc0b0c88223a4a361e71e8c2949
4
+ data.tar.gz: fd32bbe80668331c751091a7d9ddfad05c50eb1c2c37c037a50672731415af30
5
5
  SHA512:
6
- metadata.gz: b360f7fb18b06e87b538eb005ce0f7344f8a633a1cb9915d3e6f74e5b0fab3b7d87d46ab4b4498548e66766d00a4f60a9bb5f502c270e09925e67c24575da7ca
7
- data.tar.gz: 337e0fd5ecfc11fa5565980df91d21ee17cc6586d49f1a18b333eaa4de1da29094c453a00daf1671ce8e6872d03882b85c169df6709aa41e543500cdc5c78057
6
+ metadata.gz: c42d83f65d9689eb181d54eb23e703962550270fd82950c808f34d5e1ce45098ebaa1e89860cc35e0fff20c5efd528bf335cb06a897ef7b0486159c4a3a0af22
7
+ data.tar.gz: 0c41f82dc93999a11e9419cd008d7dcdeb74c42c2d11208ce3ed13168ec3e13a42d2fc4fd6314b5717a51ea1d57ba2c6880b010b4ae3fb131a59bbf2b9def55e
data/lib/api_config.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'constants'
1
+ require_relative 'constants'
2
2
 
3
3
  class UnsupportedConfigException < StandardError
4
4
  end
@@ -1,7 +1,6 @@
1
+ require_relative 'constants'
1
2
  require_relative 'hash_utils'
2
3
 
3
- require 'constants'
4
-
5
4
  module Statsig
6
5
  class ResponseFormatter
7
6
  def self.get_responses(
@@ -31,7 +30,6 @@ module Statsig
31
30
  end
32
31
 
33
32
  def self.to_response(config_name, config_spec, evaluator, user, client_sdk_key, hash_algo, include_exposures, include_local_overrides)
34
- config_name_str = config_name.to_s
35
33
  category = config_spec[:type]
36
34
  entity_type = config_spec[:entity]
37
35
  if entity_type == Const::TYPE_SEGMENT || entity_type == Const::TYPE_HOLDOUT
@@ -47,11 +45,13 @@ module Statsig
47
45
  end
48
46
  end
49
47
 
48
+ config_name_str = config_name.to_s
50
49
  if local_override.nil?
51
50
  eval_result = ConfigResult.new(
52
51
  name: config_name,
53
52
  disable_evaluation_details: true,
54
- disable_exposures: !include_exposures
53
+ disable_exposures: !include_exposures,
54
+ include_local_overrides: include_local_overrides
55
55
  )
56
56
  evaluator.eval_spec(config_name_str, user, config_spec, eval_result)
57
57
  else
@@ -73,6 +73,7 @@ module Statsig
73
73
  result[:value] = eval_result.json_value
74
74
  result[:group] = eval_result.rule_id
75
75
  result[:is_device_based] = id_type.is_a?(String) && id_type.downcase == Statsig::Const::STABLEID
76
+ result[:passed] = eval_result.gate_value
76
77
  else
77
78
  return nil
78
79
  end
@@ -122,14 +123,6 @@ module Statsig
122
123
 
123
124
  result[:is_in_layer] = true
124
125
  result[:explicit_parameters] = config_spec[:explicitParameters] || []
125
-
126
- layer_name = evaluator.spec_store.experiment_to_layer[config_name]
127
- if layer_name.nil? || evaluator.spec_store.layers[layer_name].nil?
128
- return
129
- end
130
-
131
- layer = evaluator.spec_store.layers[layer_name]
132
- result[:value] = layer[:defaultValue].merge(result[:value])
133
126
  end
134
127
 
135
128
  def self.populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
@@ -151,13 +144,15 @@ module Statsig
151
144
  end
152
145
 
153
146
  def self.hash_name(name, hash_algo)
154
- case hash_algo
155
- when Statsig::Const::NONE
156
- return name
157
- when Statsig::Const::DJB2
158
- return Statsig::HashUtils.djb2(name)
159
- else
160
- return Statsig::HashUtils.sha256(name)
147
+ Statsig::Memo.for_global(:hash_name, "#{hash_algo}|#{name}") do
148
+ case hash_algo
149
+ when Statsig::Const::NONE
150
+ name
151
+ when Statsig::Const::DJB2
152
+ Statsig::HashUtils.djb2(name)
153
+ else
154
+ Statsig::HashUtils.sha256(name)
155
+ end
161
156
  end
162
157
  end
163
158
  end
data/lib/config_result.rb CHANGED
@@ -16,6 +16,12 @@ module Statsig
16
16
  attr_accessor :target_app_ids
17
17
  attr_accessor :disable_evaluation_details
18
18
  attr_accessor :disable_exposures
19
+ attr_accessor :config_version
20
+ attr_accessor :include_local_overrides
21
+ attr_accessor :forward_all_exposures
22
+ attr_accessor :sampling_rate
23
+ attr_accessor :has_seen_analytical_gates
24
+ attr_accessor :override_config_name
19
25
 
20
26
  def initialize(
21
27
  name:,
@@ -31,7 +37,13 @@ module Statsig
31
37
  id_type: nil,
32
38
  target_app_ids: nil,
33
39
  disable_evaluation_details: false,
34
- disable_exposures: false
40
+ disable_exposures: false,
41
+ config_version: nil,
42
+ include_local_overrides: true,
43
+ forward_all_exposures: false,
44
+ sampling_rate: nil,
45
+ has_seen_analytical_gates: false,
46
+ override_config_name: nil
35
47
  )
36
48
  @name = name
37
49
  @gate_value = gate_value
@@ -48,6 +60,12 @@ module Statsig
48
60
  @target_app_ids = target_app_ids
49
61
  @disable_evaluation_details = disable_evaluation_details
50
62
  @disable_exposures = disable_exposures
63
+ @config_version = config_version
64
+ @include_local_overrides = include_local_overrides
65
+ @forward_all_exposures = forward_all_exposures
66
+ @sampling_rate = sampling_rate
67
+ @has_seen_analytical_gates = has_seen_analytical_gates
68
+ @override_config_name = override_config_name
51
69
  end
52
70
 
53
71
  def self.from_user_persisted_values(config_name, user_persisted_values)
@@ -67,7 +85,8 @@ module Statsig
67
85
  init_time: @init_time,
68
86
  group_name: @group_name,
69
87
  id_type: @id_type,
70
- target_app_ids: @target_app_ids
88
+ target_app_ids: @target_app_ids,
89
+ override_config_name: @override_config_name
71
90
  }
72
91
  end
73
92
  end
data/lib/constants.rb CHANGED
@@ -29,6 +29,8 @@ module Statsig
29
29
  DISABLED = 'disabled'.freeze
30
30
  DJB2 = 'djb2'.freeze
31
31
  EMAIL = 'email'.freeze
32
+ EXPLORE = ':explore'.freeze
33
+ FAILS_TARGETING = 'inlineTargetingRules'.freeze
32
34
  FALSE = 'false'.freeze
33
35
  IP = 'ip'.freeze
34
36
  LAYER = :layer
@@ -39,6 +41,7 @@ module Statsig
39
41
  OSNAME = 'osname'.freeze
40
42
  OSVERSION = 'osversion'.freeze
41
43
  OVERRIDE = 'override'.freeze
44
+ PRESTART = 'prestart'.freeze
42
45
  Q_RIGHT_CHEVRON = 'Q>'.freeze
43
46
  STABLEID = 'stableid'.freeze
44
47
  STATSIG_RUBY_SDK = 'statsig-ruby-sdk'.freeze
@@ -1,4 +1,4 @@
1
- require 'statsig_errors'
1
+ require_relative 'statsig_errors'
2
2
 
3
3
  $endpoint = 'https://statsigapi.net/v1/sdk_exception'
4
4
 
data/lib/evaluator.rb CHANGED
@@ -1,15 +1,15 @@
1
- require 'config_result'
2
1
  require 'country_lookup'
3
2
  require 'digest'
4
- require 'evaluation_helpers'
5
- require 'client_initialize_helpers'
6
- require 'spec_store'
7
3
  require 'time'
8
- require 'ua_parser'
9
- require 'evaluation_details'
10
4
  require 'user_agent_parser/operating_system'
11
- require 'user_persistent_storage_utils'
12
- require 'constants'
5
+ require_relative 'client_initialize_helpers'
6
+ require_relative 'config_result'
7
+ require_relative 'constants'
8
+ require_relative 'evaluation_details'
9
+ require_relative 'evaluation_helpers'
10
+ require_relative 'spec_store'
11
+ require_relative 'ua_parser'
12
+ require_relative 'user_persistent_storage_utils'
13
13
 
14
14
  module Statsig
15
15
  class Evaluator
@@ -20,6 +20,8 @@ module Statsig
20
20
 
21
21
  attr_accessor :config_overrides
22
22
 
23
+ attr_accessor :experiment_overrides
24
+
23
25
  attr_accessor :options
24
26
 
25
27
  attr_accessor :persistent_storage_utils
@@ -31,6 +33,7 @@ module Statsig
31
33
  @spec_store = store
32
34
  @gate_overrides = {}
33
35
  @config_overrides = {}
36
+ @experiment_overrides = {}
34
37
  @options = options
35
38
  @persistent_storage_utils = persistent_storage_utils
36
39
  end
@@ -58,19 +61,76 @@ module Statsig
58
61
 
59
62
  def lookup_config_override(config_name)
60
63
  config_name_sym = config_name.to_sym
64
+ if @experiment_overrides.key?(config_name_sym)
65
+ override = @experiment_overrides[config_name_sym]
66
+ return ConfigResult.new(
67
+ name: config_name,
68
+ json_value: override[:value],
69
+ group_name: override[:group_name],
70
+ rule_id: override[:rule_id],
71
+ evaluation_details: EvaluationDetails.local_override(
72
+ @spec_store.last_config_sync_time,
73
+ @spec_store.initial_config_sync_time
74
+ )
75
+ )
76
+ end
61
77
  if @config_overrides.key?(config_name_sym)
78
+ override = @config_overrides[config_name_sym]
62
79
  return ConfigResult.new(
63
80
  name: config_name,
64
- json_value: @config_overrides[config_name_sym],
81
+ json_value: override,
65
82
  rule_id: Const::OVERRIDE,
66
- id_type: @spec_store.has_config?(config_name) ? @spec_store.get_config(config_name)[:idType] : Const::EMPTY_STR,
67
83
  evaluation_details: EvaluationDetails.local_override(
68
84
  @spec_store.last_config_sync_time,
69
85
  @spec_store.initial_config_sync_time
70
86
  )
71
87
  )
72
88
  end
73
- return nil
89
+ nil
90
+ end
91
+
92
+ def try_apply_config_mapping(gate_name, user, end_result, type, salt)
93
+ gate_name_sym = gate_name.to_sym
94
+ return false unless @spec_store.overrides.key?(gate_name_sym)
95
+
96
+ mapping_list = @spec_store.overrides[gate_name_sym]
97
+ mapping_list.each do |mapping|
98
+ rules = mapping[:rules]
99
+ rules.each do |rule|
100
+ start_time = rule[:start_time]
101
+ if !start_time.nil? && start_time > (Time.now.to_i * 1000)
102
+ next
103
+ end
104
+
105
+ rule_name = rule[:rule_name].to_sym
106
+ next unless @spec_store.override_rules.key?(rule_name)
107
+
108
+ override_rule = @spec_store.override_rules[rule_name]
109
+ eval_rule(user, override_rule, end_result)
110
+ unless end_result.gate_value
111
+ next
112
+ end
113
+
114
+ pass = eval_pass_percent(user, override_rule, salt)
115
+ unless pass
116
+ next
117
+ end
118
+
119
+ new_config_name = mapping[:new_config_name]
120
+ end_result.override_config_name = new_config_name
121
+ if type == Const::TYPE_FEATURE_GATE
122
+ check_gate(user, new_config_name, end_result)
123
+ end
124
+ if [Const::TYPE_EXPERIMENT, Const::TYPE_DYNAMIC_CONFIG, Const::TYPE_AUTOTUNE].include?(type)
125
+ get_config(user, new_config_name, end_result)
126
+ end
127
+ if type == Const::TYPE_LAYER
128
+ get_layer(user, new_config_name, end_result)
129
+ end
130
+ return true
131
+ end
132
+ end
133
+ false
74
134
  end
75
135
 
76
136
  def check_gate(user, gate_name, end_result, ignore_local_overrides: false, is_nested: false)
@@ -87,6 +147,7 @@ module Statsig
87
147
  end
88
148
 
89
149
  if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
150
+ end_result.gate_value = false
90
151
  unless end_result.disable_evaluation_details
91
152
  end_result.evaluation_details = EvaluationDetails.uninitialized
92
153
  end
@@ -94,11 +155,20 @@ module Statsig
94
155
  end
95
156
 
96
157
  unless @spec_store.has_gate?(gate_name)
158
+ if try_apply_config_mapping(gate_name, user, end_result, Const::TYPE_FEATURE_GATE, Const::EMPTY_STR)
159
+ return
160
+ end
161
+
97
162
  unsupported_or_unrecognized(gate_name, end_result)
98
163
  return
99
164
  end
100
165
 
101
- eval_spec(gate_name, user, @spec_store.get_gate(gate_name), end_result, is_nested: is_nested)
166
+ spec = @spec_store.get_gate(gate_name)
167
+ if try_apply_config_mapping(gate_name, user, end_result, spec[:entity], spec[:salt])
168
+ return
169
+ end
170
+
171
+ eval_spec(gate_name, user, spec, end_result, is_nested: is_nested)
102
172
  end
103
173
 
104
174
  def get_config(user, config_name, end_result, user_persisted_values: nil, ignore_local_overrides: false)
@@ -108,6 +178,8 @@ module Statsig
108
178
  end_result.id_type = local_override.id_type
109
179
  end_result.rule_id = local_override.rule_id
110
180
  end_result.json_value = local_override.json_value
181
+ end_result.group_name = local_override.group_name
182
+ end_result.is_experiment_group = local_override.is_experiment_group
111
183
  unless end_result.disable_evaluation_details
112
184
  end_result.evaluation_details = local_override.evaluation_details
113
185
  end
@@ -122,13 +194,25 @@ module Statsig
122
194
  return
123
195
  end
124
196
 
197
+ if eval_cmab(config_name, user, end_result)
198
+ return
199
+ end
200
+
125
201
  unless @spec_store.has_config?(config_name)
202
+ if try_apply_config_mapping(config_name, user, end_result, Const::TYPE_DYNAMIC_CONFIG, Const::EMPTY_STR)
203
+ return
204
+ end
205
+
126
206
  unsupported_or_unrecognized(config_name, end_result)
127
207
  return
128
208
  end
129
209
 
130
210
  config = @spec_store.get_config(config_name)
131
211
 
212
+ if try_apply_config_mapping(config_name, user, end_result, config[:entity], config[:salt])
213
+ return
214
+ end
215
+
132
216
  # If persisted values is provided and the experiment is active, return sticky values if exists.
133
217
  if !user_persisted_values.nil? && config[:isActive] == true
134
218
  sticky_values = user_persisted_values[config_name]
@@ -163,6 +247,149 @@ module Statsig
163
247
  end
164
248
  end
165
249
 
250
+ def eval_cmab(config_name, user, end_result)
251
+ return false unless @spec_store.has_cmab_config?(config_name)
252
+
253
+ cmab = @spec_store.get_cmab_config(config_name)
254
+
255
+ if !cmab[:enabled] || cmab[:groups].length.zero?
256
+ end_result.rule_id = Const::PRESTART
257
+ end_result.json_value = cmab[:defaultValue]
258
+ finalize_cmab_eval_result(cmab, end_result, did_pass: false)
259
+ return true
260
+ end
261
+
262
+ targeting_gate = cmab[:targetingGateName]
263
+ unless targeting_gate.nil?
264
+ check_gate(user, targeting_gate, end_result, is_nested: true)
265
+
266
+ gate_value = end_result.gate_value
267
+
268
+ unless end_result.disable_exposures
269
+ new_exposure = {
270
+ gate: targeting_gate,
271
+ gateValue: gate_value ? Const::TRUE : Const::FALSE,
272
+ ruleID: end_result.rule_id
273
+ }
274
+ end_result.secondary_exposures.append(new_exposure)
275
+ end
276
+
277
+ if gate_value == false
278
+ end_result.rule_id = Const::FAILS_TARGETING
279
+ end_result.json_value = cmab[:defaultValue]
280
+ finalize_cmab_eval_result(cmab, end_result, did_pass: false)
281
+ return true
282
+ end
283
+ end
284
+
285
+ cmab_config = cmab[:config]
286
+ unit_id = user.get_unit_id(cmab[:idType]) || Const::EMPTY_STR
287
+ salt = cmab[:salt] || config_name
288
+ hash = compute_user_hash("#{salt}.#{unit_id}")
289
+
290
+ # If there is no config assign the user to a random group
291
+ if cmab_config.nil?
292
+ group_size = 10_000.0 / cmab[:groups].length
293
+ group = cmab[:groups][(hash % 10_000) / group_size]
294
+ end_result.json_value = group[:parameterValues]
295
+ end_result.rule_id = group[:id] + Const::EXPLORE
296
+ end_result.group_name = group[:name]
297
+ end_result.is_experiment_group = true
298
+ finalize_cmab_eval_result(cmab, end_result, did_pass: true)
299
+ return true
300
+ end
301
+
302
+ should_sample = (hash % 10_000) < cmab[:sampleRate] * 10_000
303
+ if should_sample && apply_cmab_sampling(cmab, cmab_config, end_result)
304
+ finalize_cmab_eval_result(cmab, end_result, did_pass: true)
305
+ return true
306
+ end
307
+ apply_cmab_best_group(cmab, cmab_config, user, end_result)
308
+ finalize_cmab_eval_result(cmab, end_result, did_pass: true)
309
+ true
310
+ end
311
+
312
+ def apply_cmab_best_group(cmab, cmab_config, user, end_result)
313
+ higher_better = cmab[:higherIsBetter]
314
+ best_score = higher_better ? -1_000_000_000 : 1_000_000_000
315
+ has_score = false
316
+ best_group = nil
317
+ cmab[:groups].each do |group|
318
+ group_id = group[:id]
319
+ config = cmab_config[group_id.to_sym]
320
+ next if config.nil?
321
+
322
+ weights_numerical = config[:weightsNumerical]
323
+ weights_categorical = config[:weightsCategorical]
324
+
325
+ next if weights_numerical.length.zero? && weights_categorical.length.zero?
326
+
327
+ score = 0
328
+ score += config[:alpha] + config[:intercept]
329
+
330
+ weights_categorical.each do |key, weights|
331
+ value = get_value_from_user(user, key.to_s)
332
+ next if value.nil?
333
+
334
+ if weights.key?(value.to_sym)
335
+ score += weights[value.to_sym]
336
+ end
337
+ end
338
+
339
+ weights_numerical.each do |key, weight|
340
+ value = get_value_from_user(user, key.to_s)
341
+ if value.is_a?(Numeric)
342
+ score += weight * value
343
+ end
344
+ end
345
+ if !has_score || (higher_better && score > best_score) || (!higher_better && score < best_score)
346
+ best_score = score
347
+ best_group = group
348
+ end
349
+ has_score = true
350
+ end
351
+ if best_group.nil?
352
+ best_group = cmab[:groups][Random.rand(cmab[:groups].length)]
353
+ end
354
+ end_result.json_value = best_group[:parameterValues]
355
+ end_result.rule_id = best_group[:id]
356
+ end_result.group_name = best_group[:name]
357
+ end_result.is_experiment_group = true
358
+ end
359
+
360
+ def apply_cmab_sampling(cmab, cmab_config, end_result)
361
+ total_records = 0.0
362
+ cmab[:groups].each do |group|
363
+ group_id = group[:id]
364
+ config = cmab_config[group_id.to_sym]
365
+ cur_count = 1.0
366
+ unless config.nil?
367
+ cur_count += config[:records]
368
+ end
369
+ total_records += 1.0 / cur_count
370
+ end
371
+
372
+ sum = 0.0
373
+ value = Random.rand
374
+ cmab[:groups].each do |group|
375
+ group_id = group[:id]
376
+ config = cmab_config[group_id.to_sym]
377
+ cur_count = 1.0
378
+ unless config.nil?
379
+ cur_count += config[:records]
380
+ end
381
+ sum += 1.0 / (cur_count / total_records)
382
+ next unless value < sum
383
+
384
+ end_result.json_value = group[:parameterValues]
385
+ end_result.rule_id = group[:id] + Const::EXPLORE
386
+ end_result.group_name = group[:name]
387
+ end_result.is_experiment_group = true
388
+ return true
389
+ end
390
+ false
391
+ end
392
+
166
393
  def get_layer(user, layer_name, end_result)
167
394
  if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
168
395
  unless end_result.disable_evaluation_details
@@ -172,11 +399,20 @@ module Statsig
172
399
  end
173
400
 
174
401
  unless @spec_store.has_layer?(layer_name)
402
+ if try_apply_config_mapping(layer_name, user, end_result, Const::LAYER, Const::EMPTY_STR)
403
+ return
404
+ end
405
+
175
406
  unsupported_or_unrecognized(layer_name, end_result)
176
407
  return
177
408
  end
178
409
 
179
- eval_spec(layer_name, user, @spec_store.get_layer(layer_name), end_result)
410
+ layer = @spec_store.get_layer(layer_name)
411
+ if try_apply_config_mapping(layer_name, user, end_result, layer[:entity], layer[:salt])
412
+ return
413
+ end
414
+
415
+ eval_spec(layer_name, user, layer, end_result)
180
416
  end
181
417
 
182
418
  def list_gates
@@ -257,6 +493,7 @@ module Statsig
257
493
 
258
494
  def unsupported_or_unrecognized(config_name, end_result)
259
495
  end_result.rule_id = Const::EMPTY_STR
496
+ end_result.gate_value = false
260
497
 
261
498
  if end_result.disable_evaluation_details
262
499
  return
@@ -300,8 +537,44 @@ module Statsig
300
537
  @config_overrides.clear
301
538
  end
302
539
 
540
+ def override_experiment_by_group_name(experiment_name, group_name)
541
+ return unless @spec_store.has_config?(experiment_name)
542
+
543
+ config = @spec_store.get_config(experiment_name)
544
+ return unless config[:entity] == Const::TYPE_EXPERIMENT
545
+
546
+ config[:rules].each do |rule|
547
+ if rule[:groupName] == group_name
548
+ @experiment_overrides[experiment_name.to_sym] = {
549
+ value: rule[:returnValue],
550
+ group_name: rule[:groupName],
551
+ rule_id: rule[:id],
552
+ evaluation_details: EvaluationDetails.local_override(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time)
553
+ }
554
+ return
555
+ end
556
+ end
557
+
558
+ # If no matching rule is found, create a default override with empty value
559
+ @experiment_overrides[experiment_name.to_sym] = {
560
+ value: {},
561
+ group_name: group_name,
562
+ rule_id: "#{experiment_name}:override",
563
+ evaluation_details: EvaluationDetails.local_override(@spec_store.last_config_sync_time, @spec_store.initial_config_sync_time)
564
+ }
565
+ end
566
+
567
+ def clear_experiment_overrides
568
+ @experiment_overrides.clear
569
+ end
570
+
571
+ def remove_experiment_override(experiment_name)
572
+ @experiment_overrides.delete(experiment_name.to_sym)
573
+ end
574
+
303
575
  def eval_spec(config_name, user, config, end_result, is_nested: false)
304
576
  config[:rules].each do |rule|
577
+ end_result.sampling_rate = rule[:samplingRate]
305
578
  eval_rule(user, rule, end_result)
306
579
 
307
580
  if end_result.gate_value
@@ -324,7 +597,12 @@ module Statsig
324
597
  def finalize_eval_result(config, end_result, did_pass:, rule:, is_nested: false)
325
598
  end_result.id_type = config[:idType]
326
599
  end_result.target_app_ids = config[:targetAppIDs]
327
- end_result.gate_value = did_pass ? rule[:returnValue] == true : config[:defaultValue] == true
600
+ end_result.gate_value = did_pass
601
+ end_result.forward_all_exposures = config[:forwardAllExposures]
602
+ if config[:entity] == Const::TYPE_FEATURE_GATE
603
+ end_result.gate_value = did_pass ? rule[:returnValue] == true : config[:defaultValue] == true
604
+ end
605
+ end_result.config_version = config[:version]
328
606
 
329
607
  if rule.nil?
330
608
  end_result.json_value = config[:defaultValue]
@@ -336,6 +614,7 @@ module Statsig
336
614
  end_result.group_name = rule[:groupName]
337
615
  end_result.is_experiment_group = rule[:isExperimentGroup] == true
338
616
  end_result.rule_id = rule[:id]
617
+ end_result.sampling_rate = rule[:samplingRate]
339
618
  end
340
619
 
341
620
  unless end_result.disable_evaluation_details
@@ -351,6 +630,21 @@ module Statsig
351
630
  end
352
631
  end
353
632
 
633
+ def finalize_cmab_eval_result(config, end_result, did_pass:)
634
+ end_result.id_type = config[:idType]
635
+ end_result.target_app_ids = config[:targetAppIDs]
636
+ end_result.gate_value = did_pass
637
+
638
+ unless end_result.disable_evaluation_details
639
+ end_result.evaluation_details = EvaluationDetails.new(
640
+ @spec_store.last_config_sync_time,
641
+ @spec_store.initial_config_sync_time,
642
+ @spec_store.init_reason
643
+ )
644
+ end
645
+ end_result.config_version = config[:version]
646
+ end
647
+
354
648
  def finalize_secondary_exposures(end_result)
355
649
  end_result.secondary_exposures = clean_exposures(end_result.secondary_exposures)
356
650
  end_result.undelegated_sec_exps = clean_exposures(end_result.undelegated_sec_exps)
@@ -373,11 +667,35 @@ module Statsig
373
667
  def eval_rule(user, rule, end_result)
374
668
  pass = true
375
669
  i = 0
670
+ memo = user.get_memo
376
671
 
377
672
  until i >= rule[:conditions].length
378
673
  condition_hash = rule[:conditions][i]
674
+
675
+ eval_rule_memo = memo[:eval_rule] || {}
676
+ result = eval_rule_memo[condition_hash]
677
+
678
+ if !result.nil?
679
+ pass = false if result != true
680
+ i += 1
681
+ next
682
+ end
683
+
379
684
  condition = @spec_store.get_condition(condition_hash)
380
- result = eval_condition(user, condition, end_result)
685
+ result = if condition.nil?
686
+ puts "[Statsig]: Warning - Condition with hash #{condition_hash} could not be found."
687
+ false
688
+ else
689
+ eval_condition(user, condition, end_result)
690
+ end
691
+
692
+ if !@options.disable_evaluation_memoization &&
693
+ condition && condition[:type] != Const::CND_PASS_GATE && condition[:type] != Const::CND_FAIL_GATE
694
+ eval_rule_memo[condition_hash] = result
695
+ end
696
+
697
+ memo[:eval_rule] = eval_rule_memo
698
+
381
699
  pass = false if result != true
382
700
  i += 1
383
701
  end
@@ -413,7 +731,14 @@ module Statsig
413
731
  when Const::CND_PUBLIC
414
732
  return true
415
733
  when Const::CND_PASS_GATE, Const::CND_FAIL_GATE
734
+ if !target.is_a?(String) || target.empty?
735
+ return type == Const::CND_FAIL_GATE
736
+ end
737
+
416
738
  result = eval_nested_gate(target, user, end_result)
739
+ if end_result.sampling_rate == nil && !target.start_with?("segment")
740
+ end_result.has_seen_analytical_gates = true
741
+ end
417
742
  return type == Const::CND_PASS_GATE ? result : !result
418
743
  when Const::CND_MULTI_PASS_GATE, Const::CND_MULTI_FAIL_GATE
419
744
  return eval_nested_gates(target, type, user, end_result)
@@ -580,7 +905,8 @@ module Statsig
580
905
  end
581
906
 
582
907
  def eval_nested_gate(gate_name, user, end_result)
583
- check_gate(user, gate_name, end_result, is_nested: true)
908
+ check_gate(user, gate_name, end_result, is_nested: true,
909
+ ignore_local_overrides: !end_result.include_local_overrides)
584
910
  gate_value = end_result.gate_value
585
911
 
586
912
  unless end_result.disable_exposures
@@ -600,7 +926,9 @@ module Statsig
600
926
  is_multi_pass_gate_type = condition_type == Const::CND_MULTI_PASS_GATE
601
927
  gate_names.each { |gate_name|
602
928
  result = eval_nested_gate(gate_name, user, end_result)
603
-
929
+ if end_result.sampling_rate == nil && !target.start_with?("segment")
930
+ end_result.has_seen_analytical_gates = true
931
+ end
604
932
  if is_multi_pass_gate_type == result
605
933
  has_passing_gate = true
606
934
  break
@@ -691,7 +1019,7 @@ module Statsig
691
1019
  pass_percentage = rule[:passPercentage]
692
1020
  return true if pass_percentage == 100.0
693
1021
  return false if pass_percentage == 0.0
694
-
1022
+
695
1023
  unit_id = user.get_unit_id(rule[:idType]) || Const::EMPTY_STR
696
1024
  rule_salt = rule[:salt] || rule[:id] || Const::EMPTY_STR
697
1025
  hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")