statsig 1.31.1 → 1.32.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/api_config.rb +128 -0
- data/lib/client_initialize_helpers.rb +78 -88
- data/lib/config_result.rb +17 -32
- data/lib/constants.rb +60 -0
- data/lib/diagnostics.rb +1 -38
- data/lib/dynamic_config.rb +1 -24
- data/lib/error_boundary.rb +0 -5
- data/lib/evaluation_details.rb +4 -1
- data/lib/evaluation_helpers.rb +35 -3
- data/lib/evaluator.rb +332 -327
- data/lib/feature_gate.rb +0 -24
- data/lib/id_list.rb +1 -1
- data/lib/interfaces/data_store.rb +1 -1
- data/lib/interfaces/user_persistent_storage.rb +1 -1
- data/lib/layer.rb +1 -20
- data/lib/network.rb +6 -33
- data/lib/spec_store.rb +86 -74
- data/lib/statsig.rb +72 -97
- data/lib/statsig_driver.rb +63 -70
- data/lib/statsig_errors.rb +1 -1
- data/lib/statsig_event.rb +8 -8
- data/lib/statsig_logger.rb +21 -21
- data/lib/statsig_options.rb +0 -50
- data/lib/statsig_user.rb +27 -68
- data/lib/ua_parser.rb +1 -1
- data/lib/uri_helper.rb +1 -9
- data/lib/user_persistent_storage_utils.rb +0 -17
- metadata +8 -6
data/lib/evaluator.rb
CHANGED
@@ -1,6 +1,3 @@
|
|
1
|
-
# typed: false
|
2
|
-
|
3
|
-
require 'sorbet-runtime'
|
4
1
|
require 'config_result'
|
5
2
|
require 'country_lookup'
|
6
3
|
require 'digest'
|
@@ -12,29 +9,17 @@ require 'ua_parser'
|
|
12
9
|
require 'evaluation_details'
|
13
10
|
require 'user_agent_parser/operating_system'
|
14
11
|
require 'user_persistent_storage_utils'
|
12
|
+
require 'constants'
|
15
13
|
|
16
14
|
module Statsig
|
17
15
|
class Evaluator
|
18
|
-
extend T::Sig
|
19
|
-
|
20
|
-
UNSUPPORTED_EVALUATION = :unsupported_eval
|
21
16
|
|
22
|
-
sig { returns(SpecStore) }
|
23
17
|
attr_accessor :spec_store
|
24
18
|
|
25
|
-
sig { returns(StatsigOptions) }
|
26
19
|
attr_accessor :options
|
27
20
|
|
28
|
-
sig { returns(UserPersistentStorageUtils) }
|
29
21
|
attr_accessor :persistent_storage_utils
|
30
22
|
|
31
|
-
sig do
|
32
|
-
params(
|
33
|
-
store: SpecStore,
|
34
|
-
options: StatsigOptions,
|
35
|
-
persistent_storage_utils: UserPersistentStorageUtils,
|
36
|
-
).void
|
37
|
-
end
|
38
23
|
def initialize(store, options, persistent_storage_utils)
|
39
24
|
UAParser.initialize_async
|
40
25
|
CountryLookup.initialize_async
|
@@ -50,92 +35,111 @@ module Statsig
|
|
50
35
|
@spec_store.maybe_restart_background_threads
|
51
36
|
end
|
52
37
|
|
53
|
-
def check_gate(user, gate_name)
|
54
|
-
if @gate_overrides.
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
38
|
+
def check_gate(user, gate_name, end_result)
|
39
|
+
if @gate_overrides.key?(gate_name)
|
40
|
+
end_result.gate_value = @gate_overrides[gate_name]
|
41
|
+
end_result.rule_id = Const::OVERRIDE
|
42
|
+
unless end_result.disable_evaluation_details
|
43
|
+
end_result.evaluation_details = EvaluationDetails.local_override(
|
44
|
+
@spec_store.last_config_sync_time,
|
45
|
+
@spec_store.initial_config_sync_time
|
46
|
+
)
|
47
|
+
end
|
48
|
+
return
|
62
49
|
end
|
63
50
|
|
64
51
|
if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
|
65
|
-
|
52
|
+
unless end_result.disable_evaluation_details
|
53
|
+
end_result.evaluation_details = EvaluationDetails.uninitialized
|
54
|
+
end
|
55
|
+
return
|
66
56
|
end
|
67
57
|
|
68
58
|
unless @spec_store.has_gate?(gate_name)
|
69
|
-
|
59
|
+
unsupported_or_unrecognized(gate_name, end_result)
|
60
|
+
return
|
70
61
|
end
|
71
62
|
|
72
|
-
eval_spec(user, @spec_store.get_gate(gate_name))
|
63
|
+
eval_spec(user, @spec_store.get_gate(gate_name), end_result)
|
73
64
|
end
|
74
65
|
|
75
|
-
|
76
|
-
def get_config(user, config_name, user_persisted_values: nil)
|
66
|
+
def get_config(user, config_name, end_result, user_persisted_values: nil)
|
77
67
|
if @config_overrides.key?(config_name)
|
78
|
-
id_type = @spec_store.has_config?(config_name) ? @spec_store.get_config(config_name)
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
[],
|
85
|
-
evaluation_details: EvaluationDetails.local_override(
|
68
|
+
id_type = @spec_store.has_config?(config_name) ? @spec_store.get_config(config_name).id_type : Const::EMPTY_STR
|
69
|
+
end_result.id_type = id_type
|
70
|
+
end_result.rule_id = Const::OVERRIDE
|
71
|
+
end_result.json_value = @config_overrides[config_name]
|
72
|
+
unless end_result.disable_evaluation_details
|
73
|
+
end_result.evaluation_details = EvaluationDetails.local_override(
|
86
74
|
@spec_store.last_config_sync_time,
|
87
75
|
@spec_store.initial_config_sync_time
|
88
|
-
)
|
89
|
-
|
90
|
-
|
76
|
+
)
|
77
|
+
end
|
78
|
+
return
|
91
79
|
end
|
92
80
|
|
93
81
|
if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
|
94
|
-
|
82
|
+
unless end_result.disable_evaluation_details
|
83
|
+
end_result.evaluation_details = EvaluationDetails.uninitialized
|
84
|
+
end
|
85
|
+
return
|
95
86
|
end
|
96
87
|
|
97
88
|
unless @spec_store.has_config?(config_name)
|
98
|
-
|
99
|
-
|
100
|
-
evaluation_details: EvaluationDetails.unrecognized(
|
101
|
-
@spec_store.last_config_sync_time,
|
102
|
-
@spec_store.initial_config_sync_time
|
103
|
-
)
|
104
|
-
)
|
89
|
+
unsupported_or_unrecognized(config_name, end_result)
|
90
|
+
return
|
105
91
|
end
|
106
92
|
|
107
93
|
config = @spec_store.get_config(config_name)
|
108
94
|
|
109
95
|
# If persisted values is provided and the experiment is active, return sticky values if exists.
|
110
|
-
if !user_persisted_values.nil? && config
|
111
|
-
|
112
|
-
|
96
|
+
if !user_persisted_values.nil? && config.is_active == true
|
97
|
+
sticky_values = user_persisted_values[config_name]
|
98
|
+
unless sticky_values.nil?
|
99
|
+
end_result.gate_value = sticky_values[Statsig::Const::GATE_VALUE]
|
100
|
+
end_result.json_value = sticky_values[Statsig::Const::JSON_VALUE]
|
101
|
+
end_result.rule_id = sticky_values[Statsig::Const::RULE_ID]
|
102
|
+
end_result.secondary_exposures = sticky_values[Statsig::Const::SECONDARY_EXPOSURES]
|
103
|
+
end_result.group_name = sticky_values[Statsig::Const::GROUP_NAME]
|
104
|
+
end_result.id_type = sticky_values[Statsig::Const::ID_TYPE]
|
105
|
+
end_result.target_app_ids = sticky_values[Statsig::Const::TARGET_APP_IDS]
|
106
|
+
unless end_result.disable_evaluation_details
|
107
|
+
end_result.evaluation_details = EvaluationDetails.persisted(
|
108
|
+
sticky_values[Statsig::Const::CONFIG_SYNC_TIME],
|
109
|
+
sticky_values[Statsig::Const::INIT_TIME]
|
110
|
+
)
|
111
|
+
end
|
112
|
+
return
|
113
|
+
end
|
113
114
|
|
114
115
|
# If it doesn't exist, then save to persisted storage if the user was assigned to an experiment group.
|
115
|
-
|
116
|
-
if
|
117
|
-
@persistent_storage_utils.add_evaluation_to_user_persisted_values(user_persisted_values, config_name,
|
118
|
-
|
116
|
+
eval_spec(user, config, end_result)
|
117
|
+
if end_result.is_experiment_group
|
118
|
+
@persistent_storage_utils.add_evaluation_to_user_persisted_values(user_persisted_values, config_name,
|
119
|
+
end_result)
|
120
|
+
@persistent_storage_utils.save_to_storage(user, config.id_type, user_persisted_values)
|
119
121
|
end
|
120
122
|
# Otherwise, remove from persisted storage
|
121
123
|
else
|
122
|
-
@persistent_storage_utils.remove_experiment_from_storage(user, config
|
123
|
-
|
124
|
+
@persistent_storage_utils.remove_experiment_from_storage(user, config.id_type, config_name)
|
125
|
+
eval_spec(user, config, end_result)
|
124
126
|
end
|
125
|
-
|
126
|
-
return evaluation
|
127
127
|
end
|
128
128
|
|
129
|
-
def get_layer(user, layer_name)
|
129
|
+
def get_layer(user, layer_name, end_result)
|
130
130
|
if @spec_store.init_reason == EvaluationReason::UNINITIALIZED
|
131
|
-
|
131
|
+
unless end_result.disable_evaluation_details
|
132
|
+
end_result.evaluation_details = EvaluationDetails.uninitialized
|
133
|
+
end
|
134
|
+
return
|
132
135
|
end
|
133
136
|
|
134
137
|
unless @spec_store.has_layer?(layer_name)
|
135
|
-
|
138
|
+
unsupported_or_unrecognized(layer_name, end_result)
|
139
|
+
return
|
136
140
|
end
|
137
141
|
|
138
|
-
eval_spec(user, @spec_store.get_layer(layer_name))
|
142
|
+
eval_spec(user, @spec_store.get_layer(layer_name), end_result)
|
139
143
|
end
|
140
144
|
|
141
145
|
def list_gates
|
@@ -143,65 +147,77 @@ module Statsig
|
|
143
147
|
end
|
144
148
|
|
145
149
|
def list_configs
|
146
|
-
@spec_store.configs.map { |name, config| name if config
|
150
|
+
@spec_store.configs.map { |name, config| name if config.entity == :dynamic_config }.compact
|
147
151
|
end
|
148
152
|
|
149
153
|
def list_experiments
|
150
|
-
@spec_store.configs.map { |name, config| name if config
|
154
|
+
@spec_store.configs.map { |name, config| name if config.entity == :experiment }.compact
|
151
155
|
end
|
152
156
|
|
153
157
|
def list_autotunes
|
154
|
-
@spec_store.configs.map { |name, config| name if config
|
158
|
+
@spec_store.configs.map { |name, config| name if config.entity == :autotune }.compact
|
155
159
|
end
|
156
160
|
|
157
161
|
def list_layers
|
158
162
|
@spec_store.layers.map { |name, _| name }
|
159
163
|
end
|
160
164
|
|
161
|
-
def get_client_initialize_response(user,
|
165
|
+
def get_client_initialize_response(user, hash_algo, client_sdk_key)
|
162
166
|
if @spec_store.is_ready_for_checks == false
|
163
167
|
return nil
|
164
168
|
end
|
165
169
|
|
166
|
-
formatter = ClientInitializeHelpers::ResponseFormatter.new(self, user, hash, client_sdk_key)
|
167
|
-
|
168
170
|
evaluated_keys = {}
|
169
171
|
if user.user_id.nil? == false
|
170
|
-
evaluated_keys[
|
172
|
+
evaluated_keys[:userID] = user.user_id
|
171
173
|
end
|
172
174
|
|
173
175
|
if user.custom_ids.nil? == false
|
174
|
-
evaluated_keys[
|
176
|
+
evaluated_keys[:customIDs] = user.custom_ids
|
175
177
|
end
|
176
178
|
|
177
179
|
{
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
180
|
+
feature_gates: Statsig::ResponseFormatter
|
181
|
+
.get_responses(@spec_store.gates, self, user, client_sdk_key, hash_algo),
|
182
|
+
dynamic_configs: Statsig::ResponseFormatter
|
183
|
+
.get_responses(@spec_store.configs, self, user, client_sdk_key, hash_algo),
|
184
|
+
layer_configs: Statsig::ResponseFormatter
|
185
|
+
.get_responses(@spec_store.layers, self, user, client_sdk_key, hash_algo),
|
186
|
+
sdkParams: {},
|
187
|
+
has_updates: true,
|
188
|
+
generator: Const::STATSIG_RUBY_SDK,
|
189
|
+
evaluated_keys: evaluated_keys,
|
190
|
+
time: 0,
|
191
|
+
hash_used: hash_algo,
|
192
|
+
user_hash: user.to_hash_without_stable_id
|
188
193
|
}
|
189
194
|
end
|
190
195
|
|
191
|
-
def clean_exposures(exposures)
|
192
|
-
seen = {}
|
193
|
-
exposures.reject do |exposure|
|
194
|
-
key = "#{exposure["gate"]}|#{exposure["gateValue"]}|#{exposure["ruleID"]}}"
|
195
|
-
should_reject = seen[key]
|
196
|
-
seen[key] = true
|
197
|
-
should_reject == true
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
196
|
def shutdown
|
202
197
|
@spec_store.shutdown
|
203
198
|
end
|
204
199
|
|
200
|
+
def unsupported_or_unrecognized(config_name, end_result)
|
201
|
+
end_result.rule_id = Const::EMPTY_STR
|
202
|
+
|
203
|
+
if end_result.disable_evaluation_details
|
204
|
+
return
|
205
|
+
end
|
206
|
+
|
207
|
+
if @spec_store.unsupported_configs.include?(config_name)
|
208
|
+
end_result.evaluation_details = EvaluationDetails.unsupported(
|
209
|
+
@spec_store.last_config_sync_time,
|
210
|
+
@spec_store.initial_config_sync_time
|
211
|
+
)
|
212
|
+
return
|
213
|
+
end
|
214
|
+
|
215
|
+
end_result.evaluation_details = EvaluationDetails.unrecognized(
|
216
|
+
@spec_store.last_config_sync_time,
|
217
|
+
@spec_store.initial_config_sync_time
|
218
|
+
)
|
219
|
+
end
|
220
|
+
|
205
221
|
def override_gate(gate, value)
|
206
222
|
@gate_overrides[gate] = value
|
207
223
|
end
|
@@ -210,256 +226,236 @@ module Statsig
|
|
210
226
|
@config_overrides[config] = value
|
211
227
|
end
|
212
228
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
config['defaultValue'],
|
226
|
-
'',
|
227
|
-
exposures,
|
228
|
-
evaluation_details: EvaluationDetails.new(
|
229
|
-
@spec_store.last_config_sync_time,
|
230
|
-
@spec_store.initial_config_sync_time,
|
231
|
-
EvaluationReason::UNSUPPORTED,
|
232
|
-
),
|
233
|
-
group_name: nil,
|
234
|
-
id_type: config['idType'],
|
235
|
-
target_app_ids: config['targetAppIDs']
|
236
|
-
) if result == UNSUPPORTED_EVALUATION
|
237
|
-
exposures = exposures + result.secondary_exposures
|
238
|
-
if result.gate_value
|
239
|
-
|
240
|
-
if (delegated_result = eval_delegate(config['name'], user, rule, exposures))
|
241
|
-
return delegated_result
|
242
|
-
end
|
243
|
-
|
244
|
-
pass = eval_pass_percent(user, rule, config['salt'])
|
245
|
-
return Statsig::ConfigResult.new(
|
246
|
-
config['name'],
|
247
|
-
pass,
|
248
|
-
pass ? result.json_value : config['defaultValue'],
|
249
|
-
result.rule_id,
|
250
|
-
exposures,
|
251
|
-
evaluation_details: EvaluationDetails.new(
|
252
|
-
@spec_store.last_config_sync_time,
|
253
|
-
@spec_store.initial_config_sync_time,
|
254
|
-
@spec_store.init_reason
|
255
|
-
),
|
256
|
-
is_experiment_group: result.is_experiment_group,
|
257
|
-
group_name: result.group_name,
|
258
|
-
id_type: config['idType'],
|
259
|
-
target_app_ids: config['targetAppIDs']
|
260
|
-
)
|
229
|
+
def eval_spec(user, config, end_result)
|
230
|
+
unless config.enabled
|
231
|
+
finalize_eval_result(config, end_result, did_pass: false, rule: nil)
|
232
|
+
return
|
233
|
+
end
|
234
|
+
|
235
|
+
config.rules.each do |rule|
|
236
|
+
eval_rule(user, rule, end_result)
|
237
|
+
|
238
|
+
if end_result.gate_value
|
239
|
+
if eval_delegate(config.name, user, rule, end_result)
|
240
|
+
return
|
261
241
|
end
|
262
242
|
|
263
|
-
|
243
|
+
pass = eval_pass_percent(user, rule, config.salt)
|
244
|
+
finalize_eval_result(config, end_result, did_pass: pass, rule: rule)
|
245
|
+
return
|
264
246
|
end
|
247
|
+
end
|
248
|
+
|
249
|
+
finalize_eval_result(config, end_result, did_pass: false, rule: nil)
|
250
|
+
end
|
251
|
+
|
252
|
+
private
|
253
|
+
|
254
|
+
def finalize_eval_result(config, end_result, did_pass:, rule:)
|
255
|
+
end_result.id_type = config.id_type
|
256
|
+
end_result.target_app_ids = config.target_app_ids
|
257
|
+
end_result.gate_value = did_pass
|
258
|
+
|
259
|
+
if rule.nil?
|
260
|
+
end_result.json_value = config.default_value
|
261
|
+
end_result.group_name = nil
|
262
|
+
end_result.is_experiment_group = false
|
263
|
+
end_result.rule_id = config.enabled ? Const::DEFAULT : Const::DISABLED
|
265
264
|
else
|
266
|
-
|
265
|
+
end_result.json_value = did_pass ? rule.return_value : config.default_value
|
266
|
+
end_result.group_name = rule.group_name
|
267
|
+
end_result.is_experiment_group = rule.is_experiment_group == true
|
268
|
+
end_result.rule_id = rule.id
|
267
269
|
end
|
268
270
|
|
269
|
-
|
270
|
-
|
271
|
-
false,
|
272
|
-
config['defaultValue'],
|
273
|
-
default_rule_id,
|
274
|
-
exposures,
|
275
|
-
evaluation_details: EvaluationDetails.new(
|
271
|
+
unless end_result.disable_evaluation_details
|
272
|
+
end_result.evaluation_details = EvaluationDetails.new(
|
276
273
|
@spec_store.last_config_sync_time,
|
277
274
|
@spec_store.initial_config_sync_time,
|
278
275
|
@spec_store.init_reason
|
279
|
-
)
|
280
|
-
|
281
|
-
id_type: config['idType'],
|
282
|
-
target_app_ids: config['targetAppIDs']
|
283
|
-
)
|
276
|
+
)
|
277
|
+
end
|
284
278
|
end
|
285
279
|
|
286
|
-
|
287
|
-
|
288
|
-
def eval_rule(user, rule)
|
289
|
-
exposures = []
|
280
|
+
def eval_rule(user, rule, end_result)
|
290
281
|
pass = true
|
291
282
|
i = 0
|
292
|
-
until i >= rule
|
293
|
-
result = eval_condition(user, rule
|
294
|
-
if result == UNSUPPORTED_EVALUATION
|
295
|
-
return UNSUPPORTED_EVALUATION
|
296
|
-
end
|
283
|
+
until i >= rule.conditions.length
|
284
|
+
result = eval_condition(user, rule.conditions[i], end_result)
|
297
285
|
|
298
|
-
if result
|
299
|
-
exposures = exposures + result['exposures'] if result['exposures'].is_a? Array
|
300
|
-
pass = false if result['value'] == false
|
301
|
-
elsif result == false
|
302
|
-
pass = false
|
303
|
-
end
|
286
|
+
pass = false if result != true
|
304
287
|
i += 1
|
305
288
|
end
|
306
289
|
|
307
|
-
|
308
|
-
'',
|
309
|
-
pass,
|
310
|
-
rule['returnValue'],
|
311
|
-
rule['id'],
|
312
|
-
exposures,
|
313
|
-
evaluation_details: EvaluationDetails.new(
|
314
|
-
@spec_store.last_config_sync_time,
|
315
|
-
@spec_store.initial_config_sync_time,
|
316
|
-
@spec_store.init_reason
|
317
|
-
),
|
318
|
-
is_experiment_group: rule["isExperimentGroup"] == true,
|
319
|
-
group_name: rule['groupName']
|
320
|
-
)
|
290
|
+
end_result.gate_value = pass
|
321
291
|
end
|
322
292
|
|
323
|
-
def eval_delegate(name, user, rule,
|
324
|
-
return
|
325
|
-
return
|
293
|
+
def eval_delegate(name, user, rule, end_result)
|
294
|
+
return false unless (delegate = rule.config_delegate)
|
295
|
+
return false unless (config = @spec_store.get_config(delegate))
|
296
|
+
|
297
|
+
end_result.undelegated_sec_exps = end_result.secondary_exposures.dup
|
298
|
+
|
299
|
+
eval_spec(user, config, end_result)
|
326
300
|
|
327
|
-
|
328
|
-
|
301
|
+
end_result.name = name
|
302
|
+
end_result.config_delegate = delegate
|
303
|
+
end_result.explicit_parameters = config.explicit_parameters
|
329
304
|
|
330
|
-
|
331
|
-
delegated_result.config_delegate = delegate
|
332
|
-
delegated_result.secondary_exposures = exposures + delegated_result.secondary_exposures
|
333
|
-
delegated_result.undelegated_sec_exps = exposures
|
334
|
-
delegated_result.explicit_parameters = config['explicitParameters']
|
335
|
-
delegated_result
|
305
|
+
true
|
336
306
|
end
|
337
307
|
|
338
|
-
def eval_condition(user, condition)
|
308
|
+
def eval_condition(user, condition, end_result)
|
339
309
|
value = nil
|
340
|
-
field = condition
|
341
|
-
target = condition
|
342
|
-
type = condition
|
343
|
-
operator = condition
|
344
|
-
additional_values = condition
|
345
|
-
|
346
|
-
id_type = condition['idType']
|
347
|
-
|
348
|
-
return UNSUPPORTED_EVALUATION unless type.is_a? String
|
349
|
-
type = type.downcase
|
310
|
+
field = condition.field
|
311
|
+
target = condition.target_value
|
312
|
+
type = condition.type
|
313
|
+
operator = condition.operator
|
314
|
+
additional_values = condition.additional_values
|
315
|
+
id_type = condition.id_type
|
350
316
|
|
351
317
|
case type
|
352
|
-
when
|
318
|
+
when :public
|
353
319
|
return true
|
354
|
-
when
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
when 'ip_based'
|
320
|
+
when :fail_gate, :pass_gate
|
321
|
+
check_gate(user, target, end_result)
|
322
|
+
|
323
|
+
gate_value = end_result.gate_value
|
324
|
+
|
325
|
+
unless end_result.disable_exposures
|
326
|
+
new_exposure = {
|
327
|
+
gate: target,
|
328
|
+
gateValue: gate_value ? Const::TRUE : Const::FALSE,
|
329
|
+
ruleID: end_result.rule_id
|
330
|
+
}
|
331
|
+
end_result.secondary_exposures.append(new_exposure)
|
332
|
+
end
|
333
|
+
return type == :pass_gate ? gate_value : !gate_value
|
334
|
+
when :ip_based
|
370
335
|
value = get_value_from_user(user, field) || get_value_from_ip(user, field)
|
371
|
-
when
|
336
|
+
when :ua_based
|
372
337
|
value = get_value_from_user(user, field) || get_value_from_ua(user, field)
|
373
|
-
when
|
338
|
+
when :user_field
|
374
339
|
value = get_value_from_user(user, field)
|
375
|
-
when
|
340
|
+
when :environment_field
|
376
341
|
value = get_value_from_environment(user, field)
|
377
|
-
when
|
342
|
+
when :current_time
|
378
343
|
value = Time.now.to_i # epoch time in seconds
|
379
|
-
when
|
344
|
+
when :user_bucket
|
380
345
|
begin
|
381
|
-
salt = additional_values[
|
382
|
-
unit_id = user.get_unit_id(id_type) ||
|
346
|
+
salt = additional_values[:salt]
|
347
|
+
unit_id = user.get_unit_id(id_type) || Const::EMPTY_STR
|
383
348
|
# there are only 1000 user buckets as opposed to 10k for gate pass %
|
384
349
|
value = compute_user_hash("#{salt}.#{unit_id}") % 1000
|
385
|
-
rescue
|
350
|
+
rescue StandardError
|
386
351
|
return false
|
387
352
|
end
|
388
|
-
when
|
353
|
+
when :unit_id
|
389
354
|
value = user.get_unit_id(id_type)
|
390
|
-
else
|
391
|
-
return UNSUPPORTED_EVALUATION
|
392
355
|
end
|
393
356
|
|
394
|
-
return UNSUPPORTED_EVALUATION if !operator.is_a?(String)
|
395
|
-
operator = operator.downcase
|
396
|
-
|
397
357
|
case operator
|
398
358
|
# numerical comparison
|
399
|
-
when
|
400
|
-
return EvaluationHelpers
|
401
|
-
when
|
402
|
-
return EvaluationHelpers
|
403
|
-
when
|
404
|
-
return EvaluationHelpers
|
405
|
-
when
|
406
|
-
return EvaluationHelpers
|
359
|
+
when :gt
|
360
|
+
return EvaluationHelpers.compare_numbers(value, target, ->(a, b) { a > b })
|
361
|
+
when :gte
|
362
|
+
return EvaluationHelpers.compare_numbers(value, target, ->(a, b) { a >= b })
|
363
|
+
when :lt
|
364
|
+
return EvaluationHelpers.compare_numbers(value, target, ->(a, b) { a < b })
|
365
|
+
when :lte
|
366
|
+
return EvaluationHelpers.compare_numbers(value, target, ->(a, b) { a <= b })
|
407
367
|
|
408
368
|
# version comparison
|
409
369
|
# need to check for nil or empty value because Version takes them as valid values
|
410
|
-
when
|
370
|
+
when :version_gt
|
411
371
|
return false if value.to_s.empty?
|
412
|
-
|
413
|
-
|
372
|
+
|
373
|
+
return begin
|
374
|
+
Gem::Version.new(value) > Gem::Version.new(target)
|
375
|
+
rescue StandardError
|
376
|
+
false
|
377
|
+
end
|
378
|
+
when :version_gte
|
414
379
|
return false if value.to_s.empty?
|
415
|
-
|
416
|
-
|
380
|
+
|
381
|
+
return begin
|
382
|
+
Gem::Version.new(value) >= Gem::Version.new(target)
|
383
|
+
rescue StandardError
|
384
|
+
false
|
385
|
+
end
|
386
|
+
when :version_lt
|
417
387
|
return false if value.to_s.empty?
|
418
|
-
|
419
|
-
|
388
|
+
|
389
|
+
return begin
|
390
|
+
Gem::Version.new(value) < Gem::Version.new(target)
|
391
|
+
rescue StandardError
|
392
|
+
false
|
393
|
+
end
|
394
|
+
when :version_lte
|
420
395
|
return false if value.to_s.empty?
|
421
|
-
|
422
|
-
|
396
|
+
|
397
|
+
return begin
|
398
|
+
Gem::Version.new(value) <= Gem::Version.new(target)
|
399
|
+
rescue StandardError
|
400
|
+
false
|
401
|
+
end
|
402
|
+
when :version_eq
|
423
403
|
return false if value.to_s.empty?
|
424
|
-
|
425
|
-
|
404
|
+
|
405
|
+
return begin
|
406
|
+
Gem::Version.new(value) == Gem::Version.new(target)
|
407
|
+
rescue StandardError
|
408
|
+
false
|
409
|
+
end
|
410
|
+
when :version_neq
|
426
411
|
return false if value.to_s.empty?
|
427
|
-
|
412
|
+
|
413
|
+
return begin
|
414
|
+
Gem::Version.new(value) != Gem::Version.new(target)
|
415
|
+
rescue StandardError
|
416
|
+
false
|
417
|
+
end
|
428
418
|
|
429
419
|
# array operations
|
430
|
-
when
|
431
|
-
return EvaluationHelpers::
|
432
|
-
when
|
433
|
-
return !EvaluationHelpers::
|
434
|
-
when
|
435
|
-
return EvaluationHelpers::
|
436
|
-
when
|
437
|
-
return !EvaluationHelpers::
|
420
|
+
when :any
|
421
|
+
return EvaluationHelpers::equal_string_in_array(target, value, true)
|
422
|
+
when :none
|
423
|
+
return !EvaluationHelpers::equal_string_in_array(target, value, true)
|
424
|
+
when :any_case_sensitive
|
425
|
+
return EvaluationHelpers::equal_string_in_array(target, value, false)
|
426
|
+
when :none_case_sensitive
|
427
|
+
return !EvaluationHelpers::equal_string_in_array(target, value, false)
|
438
428
|
|
439
429
|
# string
|
440
|
-
when
|
441
|
-
return EvaluationHelpers
|
442
|
-
when
|
443
|
-
return EvaluationHelpers
|
444
|
-
when
|
445
|
-
return EvaluationHelpers
|
446
|
-
when
|
447
|
-
return !EvaluationHelpers
|
448
|
-
when
|
449
|
-
return
|
450
|
-
|
430
|
+
when :str_starts_with_any
|
431
|
+
return EvaluationHelpers.match_string_in_array(target, value, true, ->(a, b) { a.start_with?(b) })
|
432
|
+
when :str_ends_with_any
|
433
|
+
return EvaluationHelpers.match_string_in_array(target, value, true, ->(a, b) { a.end_with?(b) })
|
434
|
+
when :str_contains_any
|
435
|
+
return EvaluationHelpers.match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
|
436
|
+
when :str_contains_none
|
437
|
+
return !EvaluationHelpers.match_string_in_array(target, value, true, ->(a, b) { a.include?(b) })
|
438
|
+
when :str_matches
|
439
|
+
return begin
|
440
|
+
value&.is_a?(String) && !(value =~ Regexp.new(target)).nil?
|
441
|
+
rescue StandardError
|
442
|
+
false
|
443
|
+
end
|
444
|
+
when :eq
|
451
445
|
return value == target
|
452
|
-
when
|
446
|
+
when :neq
|
453
447
|
return value != target
|
454
448
|
|
455
449
|
# dates
|
456
|
-
when
|
457
|
-
return EvaluationHelpers
|
458
|
-
when
|
459
|
-
return EvaluationHelpers
|
460
|
-
when
|
461
|
-
return EvaluationHelpers
|
462
|
-
|
450
|
+
when :before
|
451
|
+
return EvaluationHelpers.compare_times(value, target, ->(a, b) { a < b })
|
452
|
+
when :after
|
453
|
+
return EvaluationHelpers.compare_times(value, target, ->(a, b) { a > b })
|
454
|
+
when :on
|
455
|
+
return EvaluationHelpers.compare_times(value, target, lambda { |a, b|
|
456
|
+
a.year == b.year && a.month == b.month && a.day == b.day
|
457
|
+
})
|
458
|
+
when :in_segment_list, :not_in_segment_list
|
463
459
|
begin
|
464
460
|
is_in_list = false
|
465
461
|
id_list = @spec_store.get_id_list(target)
|
@@ -467,44 +463,57 @@ module Statsig
|
|
467
463
|
hashed_id = Digest::SHA256.base64digest(value.to_s)[0, 8]
|
468
464
|
is_in_list = id_list.ids.include?(hashed_id)
|
469
465
|
end
|
470
|
-
return is_in_list if operator ==
|
466
|
+
return is_in_list if operator == :in_segment_list
|
467
|
+
|
471
468
|
return !is_in_list
|
472
|
-
rescue
|
469
|
+
rescue StandardError
|
473
470
|
return false
|
474
471
|
end
|
475
|
-
else
|
476
|
-
return UNSUPPORTED_EVALUATION
|
477
472
|
end
|
473
|
+
return false
|
478
474
|
end
|
479
475
|
|
480
476
|
def get_value_from_user(user, field)
|
481
|
-
return nil unless
|
477
|
+
return nil unless field.is_a?(String)
|
482
478
|
|
483
|
-
|
484
|
-
|
485
|
-
return user_lookup_table[field.downcase] if user_lookup_table.has_key?(field.downcase) && !user_lookup_table[field.downcase].nil?
|
479
|
+
value = get_value_from_user_field(user, field)
|
480
|
+
value ||= get_value_from_user_field(user, field.downcase)
|
486
481
|
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
482
|
+
if value.nil?
|
483
|
+
value = user.custom[field] if user.custom.is_a?(Hash)
|
484
|
+
value = user.custom[field.to_sym] if value.nil? && user.custom.is_a?(Hash)
|
485
|
+
value = user.private_attributes[field] if value.nil? && user.private_attributes.is_a?(Hash)
|
486
|
+
value = user.private_attributes[field.to_sym] if value.nil? && user.private_attributes.is_a?(Hash)
|
492
487
|
end
|
488
|
+
value
|
489
|
+
end
|
493
490
|
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
491
|
+
def get_value_from_user_field(user, field)
|
492
|
+
return nil unless field.is_a?(String)
|
493
|
+
|
494
|
+
case field
|
495
|
+
when Const::USERID, Const::USER_ID
|
496
|
+
user.user_id
|
497
|
+
when Const::EMAIL
|
498
|
+
user.email
|
499
|
+
when Const::IP
|
500
|
+
user.ip
|
501
|
+
when Const::USERAGENT, Const::USER_AGENT
|
502
|
+
user.user_agent
|
503
|
+
when Const::COUNTRY
|
504
|
+
user.country
|
505
|
+
when Const::LOCALE
|
506
|
+
user.locale
|
507
|
+
when Const::APPVERSION, Const::APP_VERSION
|
508
|
+
user.app_version
|
509
|
+
else
|
510
|
+
nil
|
499
511
|
end
|
500
|
-
|
501
|
-
nil
|
502
512
|
end
|
503
513
|
|
504
514
|
def get_value_from_environment(user, field)
|
505
|
-
return nil unless user.
|
506
|
-
|
507
|
-
return nil unless user.statsig_environment.is_a? Hash
|
515
|
+
return nil unless user.statsig_environment.is_a?(Hash) && field.is_a?(String)
|
516
|
+
|
508
517
|
user.statsig_environment.each do |key, value|
|
509
518
|
return value if key.to_s.downcase == (field)
|
510
519
|
end
|
@@ -512,50 +521,46 @@ module Statsig
|
|
512
521
|
end
|
513
522
|
|
514
523
|
def get_value_from_ip(user, field)
|
515
|
-
return nil unless
|
516
|
-
|
524
|
+
return nil unless field == Const::COUNTRY
|
525
|
+
|
526
|
+
ip = get_value_from_user(user, Const::IP)
|
517
527
|
return nil unless ip.is_a?(String)
|
518
528
|
|
519
529
|
CountryLookup.lookup_ip_string(ip)
|
520
530
|
end
|
521
531
|
|
522
532
|
def get_value_from_ua(user, field)
|
523
|
-
return nil unless
|
524
|
-
|
533
|
+
return nil unless field.is_a?(String)
|
534
|
+
|
535
|
+
ua = get_value_from_user(user, Const::USER_AGENT)
|
525
536
|
return nil unless ua.is_a?(String)
|
526
537
|
|
527
538
|
case field.downcase
|
528
|
-
when
|
539
|
+
when Const::OSNAME, Const::OS_NAME
|
529
540
|
os = UAParser.parse_os(ua)
|
530
541
|
return os&.family
|
531
|
-
when
|
542
|
+
when Const::OS_VERSION, Const::OSVERSION
|
532
543
|
os = UAParser.parse_os(ua)
|
533
544
|
return os&.version unless os&.version.nil?
|
534
|
-
when
|
545
|
+
when Const::BROWSERNAME, Const::BROWSER_NAME
|
535
546
|
parsed = UAParser.parse_ua(ua)
|
536
547
|
return parsed.family
|
537
|
-
when
|
548
|
+
when Const::BROWSERVERSION, Const::BROWSER_VERSION
|
538
549
|
parsed = UAParser.parse_ua(ua)
|
539
550
|
return parsed.version.to_s
|
540
|
-
else
|
541
|
-
nil
|
542
551
|
end
|
543
552
|
end
|
544
553
|
|
545
554
|
def eval_pass_percent(user, rule, config_salt)
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
|
551
|
-
return (hash % 10000) < (rule['passPercentage'].to_f * 100)
|
552
|
-
rescue
|
553
|
-
return false
|
554
|
-
end
|
555
|
+
unit_id = user.get_unit_id(rule.id_type) || Const::EMPTY_STR
|
556
|
+
rule_salt = rule.salt || rule.id || Const::EMPTY_STR
|
557
|
+
hash = compute_user_hash("#{config_salt}.#{rule_salt}.#{unit_id}")
|
558
|
+
return (hash % 10_000) < (rule.pass_percentage * 100)
|
555
559
|
end
|
556
560
|
|
557
561
|
def compute_user_hash(user_hash)
|
558
|
-
Digest::SHA256.digest(user_hash).
|
562
|
+
Digest::SHA256.digest(user_hash).unpack1(Const::Q_RIGHT_CHEVRON)
|
559
563
|
end
|
564
|
+
|
560
565
|
end
|
561
|
-
end
|
566
|
+
end
|