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 +4 -4
- data/lib/client_initialize_helpers.rb +13 -17
- data/lib/config_result.rb +16 -1
- data/lib/constants.rb +3 -0
- data/lib/evaluator.rb +258 -8
- data/lib/hash_utils.rb +48 -3
- data/lib/memo.rb +5 -1
- data/lib/sdk_configs.rb +37 -0
- data/lib/spec_store.rb +83 -41
- data/lib/statsig.rb +32 -5
- data/lib/statsig_driver.rb +27 -13
- data/lib/statsig_event.rb +1 -2
- data/lib/statsig_logger.rb +129 -15
- data/lib/statsig_options.rb +7 -2
- data/lib/ttl_set.rb +36 -0
- data/lib/user_persistent_storage_utils.rb +26 -4
- metadata +9 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c340de8172bac9c10e0af76b1b34dce4dc812b19f9e33fbf19e67b54ddb90f4a
|
4
|
+
data.tar.gz: a49b6298cc34ff28a2133ecc96518e66f4c622bb65f163a6468eb6310e53f027
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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:
|
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
|
-
|
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
|
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 =
|
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.
|
7
|
-
hash = (hash << 5) - hash + c
|
8
|
-
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
|
data/lib/sdk_configs.rb
ADDED
@@ -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
|