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