statsig 2.1.0 → 2.5.5

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: c340de8172bac9c10e0af76b1b34dce4dc812b19f9e33fbf19e67b54ddb90f4a
4
+ data.tar.gz: a49b6298cc34ff28a2133ecc96518e66f4c622bb65f163a6468eb6310e53f027
5
5
  SHA512:
6
- metadata.gz: b360f7fb18b06e87b538eb005ce0f7344f8a633a1cb9915d3e6f74e5b0fab3b7d87d46ab4b4498548e66766d00a4f60a9bb5f502c270e09925e67c24575da7ca
7
- data.tar.gz: 337e0fd5ecfc11fa5565980df91d21ee17cc6586d49f1a18b333eaa4de1da29094c453a00daf1671ce8e6872d03882b85c169df6709aa41e543500cdc5c78057
6
+ metadata.gz: f4182f35cff27c25b80d1add5f4e7482021f834711a9a31d99ade432b9366a0f56da18b8a5022797fd9105a87efee40ef19ed3e243ce9d17d94259613f34dec9
7
+ data.tar.gz: 456e3f74f8cbb17f0be3aa256381349214df363298603da260bae951b570c8ffe4eac9bf2bc8156af31e6a6c4404e349e3bbbb664606c81f4fc23b1265b99ea5
@@ -31,7 +31,6 @@ module Statsig
31
31
  end
32
32
 
33
33
  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
34
  category = config_spec[:type]
36
35
  entity_type = config_spec[:entity]
37
36
  if entity_type == Const::TYPE_SEGMENT || entity_type == Const::TYPE_HOLDOUT
@@ -47,11 +46,13 @@ module Statsig
47
46
  end
48
47
  end
49
48
 
49
+ config_name_str = config_name.to_s
50
50
  if local_override.nil?
51
51
  eval_result = ConfigResult.new(
52
52
  name: config_name,
53
53
  disable_evaluation_details: true,
54
- disable_exposures: !include_exposures
54
+ disable_exposures: !include_exposures,
55
+ include_local_overrides: include_local_overrides
55
56
  )
56
57
  evaluator.eval_spec(config_name_str, user, config_spec, eval_result)
57
58
  else
@@ -73,6 +74,7 @@ module Statsig
73
74
  result[:value] = eval_result.json_value
74
75
  result[:group] = eval_result.rule_id
75
76
  result[:is_device_based] = id_type.is_a?(String) && id_type.downcase == Statsig::Const::STABLEID
77
+ result[:passed] = eval_result.gate_value
76
78
  else
77
79
  return nil
78
80
  end
@@ -122,14 +124,6 @@ module Statsig
122
124
 
123
125
  result[:is_in_layer] = true
124
126
  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
127
  end
134
128
 
135
129
  def self.populate_layer_fields(config_spec, eval_result, result, evaluator, hash_algo, include_exposures)
@@ -151,13 +145,15 @@ module Statsig
151
145
  end
152
146
 
153
147
  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)
148
+ Statsig::Memo.for_global(:hash_name, "#{hash_algo}|#{name}") do
149
+ case hash_algo
150
+ when Statsig::Const::NONE
151
+ name
152
+ when Statsig::Const::DJB2
153
+ Statsig::HashUtils.djb2(name)
154
+ else
155
+ Statsig::HashUtils.sha256(name)
156
+ end
161
157
  end
162
158
  end
163
159
  end
data/lib/config_result.rb CHANGED
@@ -16,6 +16,11 @@ 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
19
24
 
20
25
  def initialize(
21
26
  name:,
@@ -31,7 +36,12 @@ module Statsig
31
36
  id_type: nil,
32
37
  target_app_ids: nil,
33
38
  disable_evaluation_details: false,
34
- disable_exposures: false
39
+ disable_exposures: false,
40
+ config_version: nil,
41
+ include_local_overrides: true,
42
+ forward_all_exposures: false,
43
+ sampling_rate: nil,
44
+ has_seen_analytical_gates: false
35
45
  )
36
46
  @name = name
37
47
  @gate_value = gate_value
@@ -48,6 +58,11 @@ module Statsig
48
58
  @target_app_ids = target_app_ids
49
59
  @disable_evaluation_details = disable_evaluation_details
50
60
  @disable_exposures = disable_exposures
61
+ @config_version = config_version
62
+ @include_local_overrides = include_local_overrides
63
+ @forward_all_exposures = forward_all_exposures
64
+ @sampling_rate = sampling_rate
65
+ @has_seen_analytical_gates = has_seen_analytical_gates
51
66
  end
52
67
 
53
68
  def self.from_user_persisted_values(config_name, user_persisted_values)
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
data/lib/evaluator.rb CHANGED
@@ -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,32 @@ 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
74
90
  end
75
91
 
76
92
  def check_gate(user, gate_name, end_result, ignore_local_overrides: false, is_nested: false)
@@ -87,6 +103,7 @@ module Statsig
87
103
  end
88
104
 
89
105
  if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
106
+ end_result.gate_value = false
90
107
  unless end_result.disable_evaluation_details
91
108
  end_result.evaluation_details = EvaluationDetails.uninitialized
92
109
  end
@@ -108,6 +125,8 @@ module Statsig
108
125
  end_result.id_type = local_override.id_type
109
126
  end_result.rule_id = local_override.rule_id
110
127
  end_result.json_value = local_override.json_value
128
+ end_result.group_name = local_override.group_name
129
+ end_result.is_experiment_group = local_override.is_experiment_group
111
130
  unless end_result.disable_evaluation_details
112
131
  end_result.evaluation_details = local_override.evaluation_details
113
132
  end
@@ -122,6 +141,10 @@ module Statsig
122
141
  return
123
142
  end
124
143
 
144
+ if eval_cmab(config_name, user, end_result)
145
+ return
146
+ end
147
+
125
148
  unless @spec_store.has_config?(config_name)
126
149
  unsupported_or_unrecognized(config_name, end_result)
127
150
  return
@@ -163,6 +186,149 @@ module Statsig
163
186
  end
164
187
  end
165
188
 
189
+ def eval_cmab(config_name, user, end_result)
190
+ return false unless @spec_store.has_cmab_config?(config_name)
191
+
192
+ cmab = @spec_store.get_cmab_config(config_name)
193
+
194
+ if !cmab[:enabled] || cmab[:groups].length.zero?
195
+ end_result.rule_id = Const::PRESTART
196
+ end_result.json_value = cmab[:defaultValue]
197
+ finalize_cmab_eval_result(cmab, end_result, did_pass: false)
198
+ return true
199
+ end
200
+
201
+ targeting_gate = cmab[:targetingGateName]
202
+ unless targeting_gate.nil?
203
+ check_gate(user, targeting_gate, end_result, is_nested: true)
204
+
205
+ gate_value = end_result.gate_value
206
+
207
+ unless end_result.disable_exposures
208
+ new_exposure = {
209
+ gate: targeting_gate,
210
+ gateValue: gate_value ? Const::TRUE : Const::FALSE,
211
+ ruleID: end_result.rule_id
212
+ }
213
+ end_result.secondary_exposures.append(new_exposure)
214
+ end
215
+
216
+ if gate_value == false
217
+ end_result.rule_id = Const::FAILS_TARGETING
218
+ end_result.json_value = cmab[:defaultValue]
219
+ finalize_cmab_eval_result(cmab, end_result, did_pass: false)
220
+ return true
221
+ end
222
+ end
223
+
224
+ cmab_config = cmab[:config]
225
+ unit_id = user.get_unit_id(cmab[:idType]) || Const::EMPTY_STR
226
+ salt = cmab[:salt] || config_name
227
+ hash = compute_user_hash("#{salt}.#{unit_id}")
228
+
229
+ # If there is no config assign the user to a random group
230
+ if cmab_config.nil?
231
+ group_size = 10_000.0 / cmab[:groups].length
232
+ group = cmab[:groups][(hash % 10_000) / group_size]
233
+ end_result.json_value = group[:parameterValues]
234
+ end_result.rule_id = group[:id] + Const::EXPLORE
235
+ end_result.group_name = group[:name]
236
+ end_result.is_experiment_group = true
237
+ finalize_cmab_eval_result(cmab, end_result, did_pass: true)
238
+ return true
239
+ end
240
+
241
+ should_sample = (hash % 10_000) < cmab[:sampleRate] * 10_000
242
+ if should_sample && apply_cmab_sampling(cmab, cmab_config, end_result)
243
+ finalize_cmab_eval_result(cmab, end_result, did_pass: true)
244
+ return true
245
+ end
246
+ apply_cmab_best_group(cmab, cmab_config, user, end_result)
247
+ finalize_cmab_eval_result(cmab, end_result, did_pass: true)
248
+ true
249
+ end
250
+
251
+ def apply_cmab_best_group(cmab, cmab_config, user, end_result)
252
+ higher_better = cmab[:higherIsBetter]
253
+ best_score = higher_better ? -1_000_000_000 : 1_000_000_000
254
+ has_score = false
255
+ best_group = nil
256
+ cmab[:groups].each do |group|
257
+ group_id = group[:id]
258
+ config = cmab_config[group_id.to_sym]
259
+ next if config.nil?
260
+
261
+ weights_numerical = config[:weightsNumerical]
262
+ weights_categorical = config[:weightsCategorical]
263
+
264
+ next if weights_numerical.length.zero? && weights_categorical.length.zero?
265
+
266
+ score = 0
267
+ score += config[:alpha] + config[:intercept]
268
+
269
+ weights_categorical.each do |key, weights|
270
+ value = get_value_from_user(user, key.to_s)
271
+ next if value.nil?
272
+
273
+ if weights.key?(value.to_sym)
274
+ score += weights[value.to_sym]
275
+ end
276
+ end
277
+
278
+ weights_numerical.each do |key, weight|
279
+ value = get_value_from_user(user, key.to_s)
280
+ if value.is_a?(Numeric)
281
+ score += weight * value
282
+ end
283
+ end
284
+ if !has_score || (higher_better && score > best_score) || (!higher_better && score < best_score)
285
+ best_score = score
286
+ best_group = group
287
+ end
288
+ has_score = true
289
+ end
290
+ if best_group.nil?
291
+ best_group = cmab[:groups][Random.rand(cmab[:groups].length)]
292
+ end
293
+ end_result.json_value = best_group[:parameterValues]
294
+ end_result.rule_id = best_group[:id]
295
+ end_result.group_name = best_group[:name]
296
+ end_result.is_experiment_group = true
297
+ end
298
+
299
+ def apply_cmab_sampling(cmab, cmab_config, end_result)
300
+ total_records = 0.0
301
+ cmab[:groups].each do |group|
302
+ group_id = group[:id]
303
+ config = cmab_config[group_id.to_sym]
304
+ cur_count = 1.0
305
+ unless config.nil?
306
+ cur_count += config[:records]
307
+ end
308
+ total_records += 1.0 / cur_count
309
+ end
310
+
311
+ sum = 0.0
312
+ value = Random.rand
313
+ cmab[:groups].each do |group|
314
+ group_id = group[:id]
315
+ config = cmab_config[group_id.to_sym]
316
+ cur_count = 1.0
317
+ unless config.nil?
318
+ cur_count += config[:records]
319
+ end
320
+ sum += 1.0 / (cur_count / total_records)
321
+ next unless value < sum
322
+
323
+ end_result.json_value = group[:parameterValues]
324
+ end_result.rule_id = group[:id] + Const::EXPLORE
325
+ end_result.group_name = group[:name]
326
+ end_result.is_experiment_group = true
327
+ return true
328
+ end
329
+ false
330
+ end
331
+
166
332
  def get_layer(user, layer_name, end_result)
167
333
  if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
168
334
  unless end_result.disable_evaluation_details
@@ -257,6 +423,7 @@ module Statsig
257
423
 
258
424
  def unsupported_or_unrecognized(config_name, end_result)
259
425
  end_result.rule_id = Const::EMPTY_STR
426
+ end_result.gate_value = false
260
427
 
261
428
  if end_result.disable_evaluation_details
262
429
  return
@@ -300,8 +467,40 @@ module Statsig
300
467
  @config_overrides.clear
301
468
  end
302
469
 
470
+ def override_experiment_by_group_name(experiment_name, group_name)
471
+ return unless @spec_store.has_config?(experiment_name)
472
+
473
+ config = @spec_store.get_config(experiment_name)
474
+ return unless config[:entity] == Const::TYPE_EXPERIMENT
475
+
476
+ config[:rules].each do |rule|
477
+ if rule[:groupName] == group_name
478
+ @experiment_overrides[experiment_name.to_sym] = {
479
+ value: rule[:returnValue],
480
+ group_name: rule[:groupName],
481
+ rule_id: rule[:id],
482
+ evaluation_details: EvaluationDetails.local_override(@config_sync_time, @init_time)
483
+ }
484
+ return
485
+ end
486
+ end
487
+
488
+ # If no matching rule is found, create a default override with empty value
489
+ @experiment_overrides[experiment_name.to_sym] = {
490
+ value: {},
491
+ group_name: group_name,
492
+ rule_id: "#{experiment_name}:override",
493
+ evaluation_details: EvaluationDetails.local_override(@config_sync_time, @init_time)
494
+ }
495
+ end
496
+
497
+ def clear_experiment_overrides
498
+ @experiment_overrides.clear
499
+ end
500
+
303
501
  def eval_spec(config_name, user, config, end_result, is_nested: false)
304
502
  config[:rules].each do |rule|
503
+ end_result.sampling_rate = rule[:samplingRate]
305
504
  eval_rule(user, rule, end_result)
306
505
 
307
506
  if end_result.gate_value
@@ -324,7 +523,12 @@ module Statsig
324
523
  def finalize_eval_result(config, end_result, did_pass:, rule:, is_nested: false)
325
524
  end_result.id_type = config[:idType]
326
525
  end_result.target_app_ids = config[:targetAppIDs]
327
- end_result.gate_value = did_pass ? rule[:returnValue] == true : config[:defaultValue] == true
526
+ end_result.gate_value = did_pass
527
+ end_result.forward_all_exposures = config[:forwardAllExposures]
528
+ if config[:entity] == Const::TYPE_FEATURE_GATE
529
+ end_result.gate_value = did_pass ? rule[:returnValue] == true : config[:defaultValue] == true
530
+ end
531
+ end_result.config_version = config[:version]
328
532
 
329
533
  if rule.nil?
330
534
  end_result.json_value = config[:defaultValue]
@@ -336,6 +540,7 @@ module Statsig
336
540
  end_result.group_name = rule[:groupName]
337
541
  end_result.is_experiment_group = rule[:isExperimentGroup] == true
338
542
  end_result.rule_id = rule[:id]
543
+ end_result.sampling_rate = rule[:samplingRate]
339
544
  end
340
545
 
341
546
  unless end_result.disable_evaluation_details
@@ -351,6 +556,21 @@ module Statsig
351
556
  end
352
557
  end
353
558
 
559
+ def finalize_cmab_eval_result(config, end_result, did_pass:)
560
+ end_result.id_type = config[:idType]
561
+ end_result.target_app_ids = config[:targetAppIDs]
562
+ end_result.gate_value = did_pass
563
+
564
+ unless end_result.disable_evaluation_details
565
+ end_result.evaluation_details = EvaluationDetails.new(
566
+ @spec_store.last_config_sync_time,
567
+ @spec_store.initial_config_sync_time,
568
+ @spec_store.init_reason
569
+ )
570
+ end
571
+ end_result.config_version = config[:version]
572
+ end
573
+
354
574
  def finalize_secondary_exposures(end_result)
355
575
  end_result.secondary_exposures = clean_exposures(end_result.secondary_exposures)
356
576
  end_result.undelegated_sec_exps = clean_exposures(end_result.undelegated_sec_exps)
@@ -373,11 +593,35 @@ module Statsig
373
593
  def eval_rule(user, rule, end_result)
374
594
  pass = true
375
595
  i = 0
596
+ memo = user.get_memo
376
597
 
377
598
  until i >= rule[:conditions].length
378
599
  condition_hash = rule[:conditions][i]
600
+
601
+ eval_rule_memo = memo[:eval_rule] || {}
602
+ result = eval_rule_memo[condition_hash]
603
+
604
+ if !result.nil?
605
+ pass = false if result != true
606
+ i += 1
607
+ next
608
+ end
609
+
379
610
  condition = @spec_store.get_condition(condition_hash)
380
- result = eval_condition(user, condition, end_result)
611
+ result = if condition.nil?
612
+ puts "[Statsig]: Warning - Condition with hash #{condition_hash} could not be found."
613
+ false
614
+ else
615
+ eval_condition(user, condition, end_result)
616
+ end
617
+
618
+ if !@options.disable_evaluation_memoization &&
619
+ condition && condition[:type] != Const::CND_PASS_GATE && condition[:type] != Const::CND_FAIL_GATE
620
+ eval_rule_memo[condition_hash] = result
621
+ end
622
+
623
+ memo[:eval_rule] = eval_rule_memo
624
+
381
625
  pass = false if result != true
382
626
  i += 1
383
627
  end
@@ -414,6 +658,9 @@ module Statsig
414
658
  return true
415
659
  when Const::CND_PASS_GATE, Const::CND_FAIL_GATE
416
660
  result = eval_nested_gate(target, user, end_result)
661
+ if end_result.sampling_rate == nil && !target.start_with?("segment")
662
+ end_result.has_seen_analytical_gates = true
663
+ end
417
664
  return type == Const::CND_PASS_GATE ? result : !result
418
665
  when Const::CND_MULTI_PASS_GATE, Const::CND_MULTI_FAIL_GATE
419
666
  return eval_nested_gates(target, type, user, end_result)
@@ -580,7 +827,8 @@ module Statsig
580
827
  end
581
828
 
582
829
  def eval_nested_gate(gate_name, user, end_result)
583
- check_gate(user, gate_name, end_result, is_nested: true)
830
+ check_gate(user, gate_name, end_result, is_nested: true,
831
+ ignore_local_overrides: !end_result.include_local_overrides)
584
832
  gate_value = end_result.gate_value
585
833
 
586
834
  unless end_result.disable_exposures
@@ -600,7 +848,9 @@ module Statsig
600
848
  is_multi_pass_gate_type = condition_type == Const::CND_MULTI_PASS_GATE
601
849
  gate_names.each { |gate_name|
602
850
  result = eval_nested_gate(gate_name, user, end_result)
603
-
851
+ if end_result.sampling_rate == nil && !target.start_with?("segment")
852
+ end_result.has_seen_analytical_gates = true
853
+ end
604
854
  if is_multi_pass_gate_type == result
605
855
  has_passing_gate = true
606
856
  break
@@ -691,7 +941,7 @@ module Statsig
691
941
  pass_percentage = rule[:passPercentage]
692
942
  return true if pass_percentage == 100.0
693
943
  return false if pass_percentage == 0.0
694
-
944
+
695
945
  unit_id = user.get_unit_id(rule[:idType]) || Const::EMPTY_STR
696
946
  rule_salt = rule[:salt] || rule[:id] || Const::EMPTY_STR
697
947
  hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
data/lib/hash_utils.rb CHANGED
@@ -1,11 +1,15 @@
1
1
  require 'json'
2
+ require 'digest'
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/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
@@ -0,0 +1,37 @@
1
+ require 'concurrent-ruby'
2
+
3
+ module Statsig
4
+ class SDKConfigs
5
+ def initialize
6
+ @configs = Concurrent::Hash.new
7
+ @flags = Concurrent::Hash.new
8
+ end
9
+
10
+ def set_flags(new_flags)
11
+ @flags = new_flags || Concurrent::Hash.new
12
+ end
13
+
14
+ def set_configs(new_configs)
15
+ @configs = new_configs || Concurrent::Hash.new
16
+ end
17
+
18
+ def on(flag)
19
+ @flags[flag.to_sym] == true
20
+ end
21
+
22
+ def get_config_num_value(config)
23
+ value = @configs[config.to_sym]
24
+ value.is_a?(Numeric) ? value.to_f : nil
25
+ end
26
+
27
+ def get_config_string_value(config)
28
+ value = @configs[config.to_sym]
29
+ value.is_a?(String) ? value : nil
30
+ end
31
+
32
+ def get_config_int_value(config)
33
+ value = @configs[config.to_sym]
34
+ value.is_a?(Integer) ? value : nil
35
+ end
36
+ end
37
+ end