statsig 2.0.1 → 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.
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)
@@ -542,6 +867,24 @@ module Statsig
542
867
  a.year == b.year && a.month == b.month && a.day == b.day
543
868
  })
544
869
 
870
+ # array
871
+ when Const::OP_ARRAY_CONTAINS_ANY
872
+ if value.is_a?(Array) && target.is_a?(Array)
873
+ return EvaluationHelpers.array_contains_any(value, target)
874
+ end
875
+ when Const::OP_ARRAY_CONTAINS_NONE
876
+ if value.is_a?(Array) && target.is_a?(Array)
877
+ return !EvaluationHelpers.array_contains_any(value, target)
878
+ end
879
+ when Const::OP_ARRAY_CONTAINS_ALL
880
+ if value.is_a?(Array) && target.is_a?(Array)
881
+ return EvaluationHelpers.array_contains_all(value, target)
882
+ end
883
+ when Const::OP_NOT_ARRAY_CONTAINS_ALL
884
+ if value.is_a?(Array) && target.is_a?(Array)
885
+ return !EvaluationHelpers.array_contains_all(value, target)
886
+ end
887
+
545
888
  # segments
546
889
  when Const::OP_IN_SEGMENT_LIST, Const::OP_NOT_IN_SEGMENT_LIST
547
890
  begin
@@ -562,7 +905,8 @@ module Statsig
562
905
  end
563
906
 
564
907
  def eval_nested_gate(gate_name, user, end_result)
565
- 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)
566
910
  gate_value = end_result.gate_value
567
911
 
568
912
  unless end_result.disable_exposures
@@ -582,7 +926,9 @@ module Statsig
582
926
  is_multi_pass_gate_type = condition_type == Const::CND_MULTI_PASS_GATE
583
927
  gate_names.each { |gate_name|
584
928
  result = eval_nested_gate(gate_name, user, end_result)
585
-
929
+ if end_result.sampling_rate == nil && !target.start_with?("segment")
930
+ end_result.has_seen_analytical_gates = true
931
+ end
586
932
  if is_multi_pass_gate_type == result
587
933
  has_passing_gate = true
588
934
  break
@@ -670,10 +1016,14 @@ module Statsig
670
1016
  end
671
1017
 
672
1018
  def eval_pass_percent(user, rule, config_salt)
1019
+ pass_percentage = rule[:passPercentage]
1020
+ return true if pass_percentage == 100.0
1021
+ return false if pass_percentage == 0.0
1022
+
673
1023
  unit_id = user.get_unit_id(rule[:idType]) || Const::EMPTY_STR
674
1024
  rule_salt = rule[:salt] || rule[:id] || Const::EMPTY_STR
675
1025
  hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
676
- return (hash % 10_000) < (rule[:passPercentage] * 100)
1026
+ return (hash % 10_000) < (pass_percentage * 100)
677
1027
  end
678
1028
 
679
1029
  def compute_user_hash(user_hash)
data/lib/hash_utils.rb CHANGED
@@ -1,11 +1,15 @@
1
+ require 'digest'
1
2
  require 'json'
3
+
4
+ TWO_TO_THE_63 = 1 << 63
5
+ TWO_TO_THE_64 = 1 << 64
2
6
  module Statsig
3
7
  class HashUtils
4
8
  def self.djb2(input_str)
5
9
  hash = 0
6
- input_str.each_char.each do |c|
7
- hash = (hash << 5) - hash + c.ord
8
- hash &= hash
10
+ input_str.each_codepoint.each do |c|
11
+ hash = (hash << 5) - hash + c
12
+ hash &= 0xFFFFFFFF
9
13
  end
10
14
  hash &= 0xFFFFFFFF # Convert to unsigned 32-bit integer
11
15
  return hash.to_s
@@ -32,5 +36,46 @@ module Statsig
32
36
  end
33
37
  return dictionary
34
38
  end
39
+
40
+ def self.bigquery_hash(string)
41
+ digest = Digest::SHA256.digest(string)
42
+ num = digest[0...8].unpack('Q>')[0]
43
+
44
+ if num >= TWO_TO_THE_63
45
+ num - TWO_TO_THE_64
46
+ else
47
+ num
48
+ end
49
+ end
50
+
51
+ def self.is_hash_in_sampling_rate(key, sampling_rate)
52
+ hash_key = bigquery_hash(key)
53
+ hash_key % sampling_rate == 0
54
+ end
55
+
56
+ def self.compute_dedupe_key_for_gate(gate_name, rule_id, value, user_id, custom_ids = nil)
57
+ user_key = compute_user_key(user_id, custom_ids)
58
+ "n:#{gate_name};u:#{user_key}r:#{rule_id};v:#{value}"
59
+ end
60
+
61
+ def self.compute_dedupe_key_for_config(config_name, rule_id, user_id, custom_ids = nil)
62
+ user_key = compute_user_key(user_id, custom_ids)
63
+ "n:#{config_name};u:#{user_key}r:#{rule_id}"
64
+ end
65
+
66
+ def self.compute_dedupe_key_for_layer(layer_name, experiment_name, parameter_name, rule_id, user_id, custom_ids = nil)
67
+ user_key = compute_user_key(user_id, custom_ids)
68
+ "n:#{layer_name};e:#{experiment_name};p:#{parameter_name};u:#{user_key}r:#{rule_id}"
69
+ end
70
+
71
+ def self.compute_user_key(user_id, custom_ids = nil)
72
+ user_key = "u:#{user_id};"
73
+ if custom_ids
74
+ custom_ids.each do |k, v|
75
+ user_key += "#{k}:#{v};"
76
+ end
77
+ end
78
+ user_key
79
+ end
35
80
  end
36
81
  end
data/lib/layer.rb CHANGED
@@ -52,12 +52,31 @@ class Layer
52
52
  index_sym = index.to_sym
53
53
  return default_value unless @value.key?(index_sym)
54
54
 
55
- return default_value if @value[index_sym].class != default_value.class and default_value.class != TrueClass and default_value.class != FalseClass
55
+ value = @value[index_sym]
56
56
 
57
- if @exposure_log_func.is_a? Proc
58
- @exposure_log_func.call(self, index)
57
+ case default_value
58
+ when Integer
59
+ if @exposure_log_func.is_a? Proc
60
+ @exposure_log_func.call(self, index)
61
+ end
62
+ return value.to_i if value.is_a?(Numeric) && default_value.is_a?(Integer)
63
+ when Float
64
+ if @exposure_log_func.is_a? Proc
65
+ @exposure_log_func.call(self, index)
66
+ end
67
+ return value.to_f if value.is_a?(Numeric) && default_value.is_a?(Float)
68
+ when TrueClass, FalseClass
69
+ if @exposure_log_func.is_a? Proc
70
+ @exposure_log_func.call(self, index)
71
+ end
72
+ return value if [true, false].include?(value)
73
+ else
74
+ if @exposure_log_func.is_a? Proc
75
+ @exposure_log_func.call(self, index)
76
+ end
77
+ return value if value.class == default_value.class
59
78
  end
60
79
 
61
- @value[index_sym]
80
+ default_value
62
81
  end
63
82
  end
data/lib/memo.rb CHANGED
@@ -3,7 +3,11 @@ module Statsig
3
3
 
4
4
  @global_memo = {}
5
5
 
6
- def self.for(hash, method, key)
6
+ def self.for(hash, method, key, disable_evaluation_memoization: false)
7
+ if disable_evaluation_memoization
8
+ return yield
9
+ end
10
+
7
11
  if key != nil
8
12
  method_hash = hash[method]
9
13
  unless method_hash