statsig 1.31.1 → 1.32.0
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 +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
|