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 +4 -4
- data/lib/api_config.rb +1 -1
- data/lib/client_initialize_helpers.rb +14 -19
- data/lib/config_result.rb +21 -2
- data/lib/constants.rb +3 -0
- data/lib/error_boundary.rb +1 -1
- data/lib/evaluator.rb +346 -18
- data/lib/hash_utils.rb +48 -3
- data/lib/memo.rb +5 -1
- data/lib/network.rb +10 -3
- data/lib/sdk_configs.rb +37 -0
- data/lib/spec_store.rb +97 -49
- data/lib/statsig.rb +59 -8
- data/lib/statsig_driver.rb +76 -28
- data/lib/statsig_event.rb +1 -2
- data/lib/statsig_logger.rb +137 -17
- data/lib/statsig_options.rb +7 -2
- data/lib/statsig_user.rb +1 -1
- data/lib/ttl_set.rb +36 -0
- data/lib/user_persistent_storage_utils.rb +27 -5
- 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: 20ee1f5defc13286856fb81276a04db5e1078cc0b0c88223a4a361e71e8c2949
|
|
4
|
+
data.tar.gz: fd32bbe80668331c751091a7d9ddfad05c50eb1c2c37c037a50672731415af30
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c42d83f65d9689eb181d54eb23e703962550270fd82950c808f34d5e1ce45098ebaa1e89860cc35e0fff20c5efd528bf335cb06a897ef7b0486159c4a3a0af22
|
|
7
|
+
data.tar.gz: 0c41f82dc93999a11e9419cd008d7dcdeb74c42c2d11208ce3ed13168ec3e13a42d2fc4fd6314b5717a51ea1d57ba2c6880b010b4ae3fb131a59bbf2b9def55e
|
data/lib/api_config.rb
CHANGED
|
@@ -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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
data/lib/error_boundary.rb
CHANGED
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
|
-
|
|
12
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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}")
|