featurevisor 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +722 -0
- data/bin/cli.rb +142 -0
- data/bin/commands/assess_distribution.rb +236 -0
- data/bin/commands/benchmark.rb +274 -0
- data/bin/commands/test.rb +793 -0
- data/bin/commands.rb +10 -0
- data/bin/featurevisor +18 -0
- data/lib/featurevisor/bucketer.rb +95 -0
- data/lib/featurevisor/child_instance.rb +311 -0
- data/lib/featurevisor/compare_versions.rb +126 -0
- data/lib/featurevisor/conditions.rb +152 -0
- data/lib/featurevisor/datafile_reader.rb +350 -0
- data/lib/featurevisor/emitter.rb +60 -0
- data/lib/featurevisor/evaluate.rb +818 -0
- data/lib/featurevisor/events.rb +76 -0
- data/lib/featurevisor/hooks.rb +159 -0
- data/lib/featurevisor/instance.rb +463 -0
- data/lib/featurevisor/logger.rb +150 -0
- data/lib/featurevisor/murmurhash.rb +69 -0
- data/lib/featurevisor/version.rb +3 -0
- data/lib/featurevisor.rb +17 -0
- metadata +89 -0
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Featurevisor
|
|
4
|
+
# Evaluation reason constants
|
|
5
|
+
module EvaluationReason
|
|
6
|
+
# Feature specific
|
|
7
|
+
FEATURE_NOT_FOUND = "feature_not_found" # feature is not found in datafile
|
|
8
|
+
DISABLED = "disabled" # feature is disabled
|
|
9
|
+
REQUIRED = "required" # required features are not enabled
|
|
10
|
+
OUT_OF_RANGE = "out_of_range" # out of range when mutually exclusive experiments are involved via Groups
|
|
11
|
+
|
|
12
|
+
# Variations specific
|
|
13
|
+
NO_VARIATIONS = "no_variations" # feature has no variations
|
|
14
|
+
VARIATION_DISABLED = "variation_disabled" # feature is disabled, and variation's disabledVariationValue is used
|
|
15
|
+
|
|
16
|
+
# Variable specific
|
|
17
|
+
VARIABLE_NOT_FOUND = "variable_not_found" # variable's schema is not defined in the feature
|
|
18
|
+
VARIABLE_DEFAULT = "variable_default" # default variable value used
|
|
19
|
+
VARIABLE_DISABLED = "variable_disabled" # feature is disabled, and variable's disabledValue is used
|
|
20
|
+
VARIABLE_OVERRIDE = "variable_override" # variable overridden from inside a variation
|
|
21
|
+
|
|
22
|
+
# Common
|
|
23
|
+
NO_MATCH = "no_match" # no rules matched
|
|
24
|
+
FORCED = "forced" # against a forced rule
|
|
25
|
+
STICKY = "sticky" # against a sticky feature
|
|
26
|
+
RULE = "rule" # against a regular rule
|
|
27
|
+
ALLOCATED = "allocated" # regular allocation based on bucketing
|
|
28
|
+
|
|
29
|
+
ERROR = "error" # error
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Evaluation types
|
|
33
|
+
EVALUATION_TYPES = %w[flag variation variable].freeze
|
|
34
|
+
|
|
35
|
+
# Evaluation module for feature flag evaluation
|
|
36
|
+
module Evaluate
|
|
37
|
+
|
|
38
|
+
# Evaluate with hooks
|
|
39
|
+
# @param options [Hash] Evaluation options
|
|
40
|
+
# @return [Hash] Evaluation result
|
|
41
|
+
def self.evaluate_with_hooks(options)
|
|
42
|
+
begin
|
|
43
|
+
hooks_manager = options[:hooks_manager]
|
|
44
|
+
hooks = hooks_manager.get_all
|
|
45
|
+
|
|
46
|
+
# Run before hooks
|
|
47
|
+
result_options = options
|
|
48
|
+
hooks.each do |hook|
|
|
49
|
+
if hook.respond_to?(:call_before)
|
|
50
|
+
result_options = hook.call_before(result_options)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Evaluate
|
|
55
|
+
evaluation = evaluate(result_options)
|
|
56
|
+
|
|
57
|
+
# Default: variation
|
|
58
|
+
if options[:default_variation_value] &&
|
|
59
|
+
evaluation[:type] == "variation" &&
|
|
60
|
+
evaluation[:variation_value].nil?
|
|
61
|
+
evaluation[:variation_value] = options[:default_variation_value]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Default: variable
|
|
65
|
+
if options[:default_variable_value] &&
|
|
66
|
+
evaluation[:type] == "variable" &&
|
|
67
|
+
evaluation[:variable_value].nil?
|
|
68
|
+
evaluation[:variable_value] = options[:default_variable_value]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Run after hooks
|
|
72
|
+
hooks.each do |hook|
|
|
73
|
+
if hook.respond_to?(:call_after)
|
|
74
|
+
evaluation = hook.call_after(evaluation, result_options)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
evaluation
|
|
79
|
+
rescue => e
|
|
80
|
+
type = options[:type]
|
|
81
|
+
feature_key = options[:feature_key]
|
|
82
|
+
variable_key = options[:variable_key]
|
|
83
|
+
logger = options[:logger]
|
|
84
|
+
|
|
85
|
+
evaluation = {
|
|
86
|
+
type: type,
|
|
87
|
+
feature_key: feature_key,
|
|
88
|
+
variable_key: variable_key,
|
|
89
|
+
reason: Featurevisor::EvaluationReason::ERROR,
|
|
90
|
+
error: e
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
logger.error("error during evaluation", evaluation)
|
|
94
|
+
|
|
95
|
+
evaluation
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Main evaluation function
|
|
100
|
+
# @param options [Hash] Evaluation options
|
|
101
|
+
# @return [Hash] Evaluation result
|
|
102
|
+
def self.evaluate(options)
|
|
103
|
+
type = options[:type]
|
|
104
|
+
feature_key = options[:feature_key]
|
|
105
|
+
variable_key = options[:variable_key]
|
|
106
|
+
context = options[:context]
|
|
107
|
+
logger = options[:logger]
|
|
108
|
+
datafile_reader = options[:datafile_reader]
|
|
109
|
+
sticky = options[:sticky]
|
|
110
|
+
hooks_manager = options[:hooks_manager]
|
|
111
|
+
|
|
112
|
+
hooks = hooks_manager.get_all
|
|
113
|
+
evaluation = nil
|
|
114
|
+
|
|
115
|
+
begin
|
|
116
|
+
# Root flag evaluation
|
|
117
|
+
flag = nil
|
|
118
|
+
if type != "flag"
|
|
119
|
+
# needed by variation and variable evaluations
|
|
120
|
+
flag = evaluate(options.merge(type: "flag"))
|
|
121
|
+
|
|
122
|
+
if flag[:enabled] == false
|
|
123
|
+
evaluation = {
|
|
124
|
+
type: type,
|
|
125
|
+
feature_key: feature_key,
|
|
126
|
+
reason: Featurevisor::EvaluationReason::DISABLED
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
feature = datafile_reader.get_feature(feature_key)
|
|
130
|
+
|
|
131
|
+
# serve variable default value if feature is disabled (if explicitly specified)
|
|
132
|
+
if type == "variable"
|
|
133
|
+
if feature && variable_key &&
|
|
134
|
+
feature[:variablesSchema] &&
|
|
135
|
+
(feature[:variablesSchema][variable_key] || feature[:variablesSchema][variable_key.to_sym])
|
|
136
|
+
variable_schema = feature[:variablesSchema][variable_key] || feature[:variablesSchema][variable_key.to_sym]
|
|
137
|
+
|
|
138
|
+
if variable_schema[:disabledValue]
|
|
139
|
+
# disabledValue: <value>
|
|
140
|
+
evaluation = {
|
|
141
|
+
type: type,
|
|
142
|
+
feature_key: feature_key,
|
|
143
|
+
reason: Featurevisor::EvaluationReason::VARIABLE_DISABLED,
|
|
144
|
+
variable_key: variable_key,
|
|
145
|
+
variable_value: variable_schema[:disabledValue],
|
|
146
|
+
variable_schema: variable_schema,
|
|
147
|
+
enabled: false
|
|
148
|
+
}
|
|
149
|
+
elsif variable_schema[:useDefaultWhenDisabled]
|
|
150
|
+
# useDefaultWhenDisabled: true
|
|
151
|
+
evaluation = {
|
|
152
|
+
type: type,
|
|
153
|
+
feature_key: feature_key,
|
|
154
|
+
reason: Featurevisor::EvaluationReason::VARIABLE_DEFAULT,
|
|
155
|
+
variable_key: variable_key,
|
|
156
|
+
variable_value: variable_schema[:defaultValue],
|
|
157
|
+
variable_schema: variable_schema,
|
|
158
|
+
enabled: false
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# serve disabled variation value if feature is disabled (if explicitly specified)
|
|
165
|
+
if type == "variation" && feature && feature[:disabledVariationValue]
|
|
166
|
+
evaluation = {
|
|
167
|
+
type: type,
|
|
168
|
+
feature_key: feature_key,
|
|
169
|
+
reason: Featurevisor::EvaluationReason::VARIATION_DISABLED,
|
|
170
|
+
variation_value: feature[:disabledVariationValue],
|
|
171
|
+
enabled: false
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
logger.debug("feature is disabled", evaluation)
|
|
176
|
+
|
|
177
|
+
return evaluation
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Sticky
|
|
182
|
+
if sticky && (sticky[feature_key] || sticky[feature_key.to_sym])
|
|
183
|
+
sticky_feature = sticky[feature_key] || sticky[feature_key.to_sym]
|
|
184
|
+
|
|
185
|
+
# flag
|
|
186
|
+
if type == "flag" && sticky_feature.key?(:enabled)
|
|
187
|
+
evaluation = {
|
|
188
|
+
type: type,
|
|
189
|
+
feature_key: feature_key,
|
|
190
|
+
reason: Featurevisor::EvaluationReason::STICKY,
|
|
191
|
+
sticky: sticky_feature,
|
|
192
|
+
enabled: sticky_feature[:enabled]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
logger.debug("using sticky enabled", evaluation)
|
|
196
|
+
|
|
197
|
+
return evaluation
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# variation
|
|
201
|
+
if type == "variation"
|
|
202
|
+
variation_value = sticky_feature[:variation]
|
|
203
|
+
|
|
204
|
+
if variation_value
|
|
205
|
+
evaluation = {
|
|
206
|
+
type: type,
|
|
207
|
+
feature_key: feature_key,
|
|
208
|
+
reason: Featurevisor::EvaluationReason::STICKY,
|
|
209
|
+
variation_value: variation_value
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
logger.debug("using sticky variation", evaluation)
|
|
213
|
+
|
|
214
|
+
return evaluation
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# variable
|
|
219
|
+
if type == "variable" && variable_key
|
|
220
|
+
variables = sticky_feature[:variables]
|
|
221
|
+
|
|
222
|
+
if variables && (variables[variable_key] || variables[variable_key.to_sym])
|
|
223
|
+
variable_value = variables[variable_key] || variables[variable_key.to_sym]
|
|
224
|
+
evaluation = {
|
|
225
|
+
type: type,
|
|
226
|
+
feature_key: feature_key,
|
|
227
|
+
reason: Featurevisor::EvaluationReason::STICKY,
|
|
228
|
+
variable_key: variable_key,
|
|
229
|
+
variable_value: variable_value
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
logger.debug("using sticky variable", evaluation)
|
|
233
|
+
|
|
234
|
+
return evaluation
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Feature
|
|
240
|
+
feature = feature_key.is_a?(String) ? datafile_reader.get_feature(feature_key) : feature_key
|
|
241
|
+
|
|
242
|
+
# feature: not found
|
|
243
|
+
unless feature
|
|
244
|
+
evaluation = {
|
|
245
|
+
type: type,
|
|
246
|
+
feature_key: feature_key,
|
|
247
|
+
reason: Featurevisor::EvaluationReason::FEATURE_NOT_FOUND
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
logger.warn("feature not found", evaluation)
|
|
251
|
+
|
|
252
|
+
return evaluation
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# feature: deprecated
|
|
256
|
+
if type == "flag" && feature[:deprecated]
|
|
257
|
+
logger.warn("feature is deprecated", { feature_key: feature_key })
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# variableSchema
|
|
261
|
+
variable_schema = nil
|
|
262
|
+
|
|
263
|
+
if variable_key
|
|
264
|
+
if feature[:variablesSchema] && (feature[:variablesSchema][variable_key] || feature[:variablesSchema][variable_key.to_sym])
|
|
265
|
+
variable_schema = feature[:variablesSchema][variable_key] || feature[:variablesSchema][variable_key.to_sym]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# variable schema not found
|
|
269
|
+
unless variable_schema
|
|
270
|
+
evaluation = {
|
|
271
|
+
type: type,
|
|
272
|
+
feature_key: feature_key,
|
|
273
|
+
reason: Featurevisor::EvaluationReason::VARIABLE_NOT_FOUND,
|
|
274
|
+
variable_key: variable_key
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
logger.warn("variable schema not found", evaluation)
|
|
278
|
+
|
|
279
|
+
return evaluation
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
if variable_schema[:deprecated]
|
|
283
|
+
logger.warn("variable is deprecated", {
|
|
284
|
+
feature_key: feature_key,
|
|
285
|
+
variable_key: variable_key
|
|
286
|
+
})
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# variation: no variations
|
|
291
|
+
if type == "variation" && (!feature[:variations] || feature[:variations].empty?)
|
|
292
|
+
evaluation = {
|
|
293
|
+
type: type,
|
|
294
|
+
feature_key: feature_key,
|
|
295
|
+
reason: Featurevisor::EvaluationReason::NO_VARIATIONS
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
logger.warn("no variations", evaluation)
|
|
299
|
+
|
|
300
|
+
return evaluation
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Forced
|
|
304
|
+
force_result = datafile_reader.get_matched_force(feature, context)
|
|
305
|
+
force = force_result[:force]
|
|
306
|
+
force_index = force_result[:forceIndex]
|
|
307
|
+
|
|
308
|
+
if force
|
|
309
|
+
# flag
|
|
310
|
+
if type == "flag" && force.key?(:enabled)
|
|
311
|
+
evaluation = {
|
|
312
|
+
type: type,
|
|
313
|
+
feature_key: feature_key,
|
|
314
|
+
reason: Featurevisor::EvaluationReason::FORCED,
|
|
315
|
+
force_index: force_index,
|
|
316
|
+
force: force,
|
|
317
|
+
enabled: force[:enabled]
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
logger.debug("forced enabled found", evaluation)
|
|
321
|
+
|
|
322
|
+
return evaluation
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# variation
|
|
326
|
+
if type == "variation" && force[:variation] && feature[:variations]
|
|
327
|
+
variation = feature[:variations].find { |v| v[:value] == force[:variation] }
|
|
328
|
+
|
|
329
|
+
if variation
|
|
330
|
+
evaluation = {
|
|
331
|
+
type: type,
|
|
332
|
+
feature_key: feature_key,
|
|
333
|
+
reason: Featurevisor::EvaluationReason::FORCED,
|
|
334
|
+
force_index: force_index,
|
|
335
|
+
force: force,
|
|
336
|
+
variation: variation
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
logger.debug("forced variation found", evaluation)
|
|
340
|
+
|
|
341
|
+
return evaluation
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# variable
|
|
346
|
+
if variable_key && force[:variables] && (force[:variables][variable_key] || force[:variables][variable_key.to_sym])
|
|
347
|
+
variable_value = force[:variables][variable_key] || force[:variables][variable_key.to_sym]
|
|
348
|
+
evaluation = {
|
|
349
|
+
type: type,
|
|
350
|
+
feature_key: feature_key,
|
|
351
|
+
reason: Featurevisor::EvaluationReason::FORCED,
|
|
352
|
+
force_index: force_index,
|
|
353
|
+
force: force,
|
|
354
|
+
variable_key: variable_key,
|
|
355
|
+
variable_schema: variable_schema,
|
|
356
|
+
variable_value: variable_value
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
logger.debug("forced variable", evaluation)
|
|
360
|
+
|
|
361
|
+
return evaluation
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Required
|
|
366
|
+
if type == "flag" && feature[:required] && feature[:required].length > 0
|
|
367
|
+
required_features_are_enabled = feature[:required].all? do |required|
|
|
368
|
+
required_key = nil
|
|
369
|
+
required_variation = nil
|
|
370
|
+
|
|
371
|
+
if required.is_a?(String)
|
|
372
|
+
required_key = required
|
|
373
|
+
else
|
|
374
|
+
required_key = required[:key]
|
|
375
|
+
required_variation = required[:variation]
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
required_evaluation = evaluate(options.merge(type: "flag", feature_key: required_key))
|
|
379
|
+
required_is_enabled = required_evaluation[:enabled]
|
|
380
|
+
|
|
381
|
+
next false unless required_is_enabled
|
|
382
|
+
|
|
383
|
+
if required_variation
|
|
384
|
+
required_variation_evaluation = evaluate(options.merge(type: "variation", feature_key: required_key))
|
|
385
|
+
|
|
386
|
+
required_variation_value = nil
|
|
387
|
+
|
|
388
|
+
if required_variation_evaluation[:variation_value]
|
|
389
|
+
required_variation_value = required_variation_evaluation[:variation_value]
|
|
390
|
+
elsif required_variation_evaluation[:variation]
|
|
391
|
+
required_variation_value = required_variation_evaluation[:variation][:value]
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
next required_variation_value == required_variation
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
true
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
unless required_features_are_enabled
|
|
401
|
+
evaluation = {
|
|
402
|
+
type: type,
|
|
403
|
+
feature_key: feature_key,
|
|
404
|
+
reason: Featurevisor::EvaluationReason::REQUIRED,
|
|
405
|
+
required: feature[:required],
|
|
406
|
+
enabled: required_features_are_enabled
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
logger.debug("required features not enabled", evaluation)
|
|
410
|
+
|
|
411
|
+
return evaluation
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Bucketing
|
|
416
|
+
# bucketKey
|
|
417
|
+
bucket_key = Featurevisor::Bucketer.get_bucket_key({
|
|
418
|
+
feature_key: feature_key,
|
|
419
|
+
bucket_by: feature[:bucketBy],
|
|
420
|
+
context: context,
|
|
421
|
+
logger: logger
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
# Run bucket key hooks
|
|
425
|
+
bucket_key = hooks_manager.run_bucket_key_hooks({
|
|
426
|
+
feature_key: feature_key,
|
|
427
|
+
context: context,
|
|
428
|
+
bucket_by: feature[:bucketBy],
|
|
429
|
+
bucket_key: bucket_key
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
# bucketValue
|
|
433
|
+
bucket_value = Featurevisor::Bucketer.get_bucketed_number(bucket_key)
|
|
434
|
+
|
|
435
|
+
# Run bucket value hooks
|
|
436
|
+
bucket_value = hooks_manager.run_bucket_value_hooks({
|
|
437
|
+
feature_key: feature_key,
|
|
438
|
+
bucket_key: bucket_key,
|
|
439
|
+
context: context,
|
|
440
|
+
bucket_value: bucket_value
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
matched_traffic = nil
|
|
444
|
+
matched_allocation = nil
|
|
445
|
+
|
|
446
|
+
if type != "flag"
|
|
447
|
+
matched_traffic = datafile_reader.get_matched_traffic(feature[:traffic], context)
|
|
448
|
+
|
|
449
|
+
if matched_traffic
|
|
450
|
+
matched_allocation = datafile_reader.get_matched_allocation(matched_traffic, bucket_value)
|
|
451
|
+
end
|
|
452
|
+
else
|
|
453
|
+
matched_traffic = datafile_reader.get_matched_traffic(feature[:traffic], context)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
if matched_traffic
|
|
457
|
+
# percentage: 0
|
|
458
|
+
if matched_traffic[:percentage] == 0
|
|
459
|
+
evaluation = {
|
|
460
|
+
type: type,
|
|
461
|
+
feature_key: feature_key,
|
|
462
|
+
reason: Featurevisor::EvaluationReason::RULE,
|
|
463
|
+
bucket_key: bucket_key,
|
|
464
|
+
bucket_value: bucket_value,
|
|
465
|
+
rule_key: matched_traffic[:key],
|
|
466
|
+
traffic: matched_traffic,
|
|
467
|
+
enabled: false
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
logger.debug("matched rule with 0 percentage", evaluation)
|
|
471
|
+
|
|
472
|
+
return evaluation
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# flag
|
|
476
|
+
if type == "flag"
|
|
477
|
+
# flag: check if mutually exclusive
|
|
478
|
+
if feature[:ranges] && feature[:ranges].length > 0
|
|
479
|
+
matched_range = feature[:ranges].find do |range|
|
|
480
|
+
bucket_value >= range[0] && bucket_value < range[1]
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# matched
|
|
484
|
+
if matched_range
|
|
485
|
+
evaluation = {
|
|
486
|
+
type: type,
|
|
487
|
+
feature_key: feature_key,
|
|
488
|
+
reason: Featurevisor::EvaluationReason::ALLOCATED,
|
|
489
|
+
bucket_key: bucket_key,
|
|
490
|
+
bucket_value: bucket_value,
|
|
491
|
+
rule_key: matched_traffic[:key],
|
|
492
|
+
traffic: matched_traffic,
|
|
493
|
+
enabled: matched_traffic[:enabled].nil? ? true : matched_traffic[:enabled]
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
logger.debug("matched", evaluation)
|
|
497
|
+
|
|
498
|
+
return evaluation
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# no match
|
|
502
|
+
evaluation = {
|
|
503
|
+
type: type,
|
|
504
|
+
feature_key: feature_key,
|
|
505
|
+
reason: Featurevisor::EvaluationReason::OUT_OF_RANGE,
|
|
506
|
+
bucket_key: bucket_key,
|
|
507
|
+
bucket_value: bucket_value,
|
|
508
|
+
enabled: false
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
logger.debug("not matched", evaluation)
|
|
512
|
+
|
|
513
|
+
return evaluation
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# flag: override from rule
|
|
517
|
+
if matched_traffic.key?(:enabled)
|
|
518
|
+
evaluation = {
|
|
519
|
+
type: type,
|
|
520
|
+
feature_key: feature_key,
|
|
521
|
+
reason: Featurevisor::EvaluationReason::RULE,
|
|
522
|
+
bucket_key: bucket_key,
|
|
523
|
+
bucket_value: bucket_value,
|
|
524
|
+
rule_key: matched_traffic[:key],
|
|
525
|
+
traffic: matched_traffic,
|
|
526
|
+
enabled: matched_traffic[:enabled]
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
logger.debug("override from rule", evaluation)
|
|
530
|
+
|
|
531
|
+
return evaluation
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# treated as enabled because of matched traffic
|
|
535
|
+
if bucket_value <= matched_traffic[:percentage]
|
|
536
|
+
evaluation = {
|
|
537
|
+
type: type,
|
|
538
|
+
feature_key: feature_key,
|
|
539
|
+
reason: Featurevisor::EvaluationReason::RULE,
|
|
540
|
+
bucket_key: bucket_key,
|
|
541
|
+
bucket_value: bucket_value,
|
|
542
|
+
rule_key: matched_traffic[:key],
|
|
543
|
+
traffic: matched_traffic,
|
|
544
|
+
enabled: true
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
logger.debug("matched traffic", evaluation)
|
|
548
|
+
|
|
549
|
+
return evaluation
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# variation
|
|
554
|
+
if type == "variation" && feature[:variations]
|
|
555
|
+
# override from rule
|
|
556
|
+
if matched_traffic[:variation]
|
|
557
|
+
variation = feature[:variations].find { |v| v[:value] == matched_traffic[:variation] }
|
|
558
|
+
|
|
559
|
+
if variation
|
|
560
|
+
evaluation = {
|
|
561
|
+
type: type,
|
|
562
|
+
feature_key: feature_key,
|
|
563
|
+
reason: Featurevisor::EvaluationReason::RULE,
|
|
564
|
+
bucket_key: bucket_key,
|
|
565
|
+
bucket_value: bucket_value,
|
|
566
|
+
rule_key: matched_traffic[:key],
|
|
567
|
+
traffic: matched_traffic,
|
|
568
|
+
variation: variation
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
logger.debug("override from rule", evaluation)
|
|
572
|
+
|
|
573
|
+
return evaluation
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# regular allocation
|
|
578
|
+
if matched_allocation && matched_allocation[:variation]
|
|
579
|
+
variation = feature[:variations].find { |v| v[:value] == matched_allocation[:variation] }
|
|
580
|
+
|
|
581
|
+
if variation
|
|
582
|
+
evaluation = {
|
|
583
|
+
type: type,
|
|
584
|
+
feature_key: feature_key,
|
|
585
|
+
reason: Featurevisor::EvaluationReason::ALLOCATED,
|
|
586
|
+
bucket_key: bucket_key,
|
|
587
|
+
bucket_value: bucket_value,
|
|
588
|
+
rule_key: matched_traffic[:key],
|
|
589
|
+
traffic: matched_traffic,
|
|
590
|
+
variation: variation
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
logger.debug("allocated variation", evaluation)
|
|
594
|
+
|
|
595
|
+
return evaluation
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# variable
|
|
602
|
+
if type == "variable" && variable_key
|
|
603
|
+
# override from rule
|
|
604
|
+
if matched_traffic &&
|
|
605
|
+
matched_traffic[:variables] &&
|
|
606
|
+
(matched_traffic[:variables][variable_key] || matched_traffic[:variables][variable_key.to_sym])
|
|
607
|
+
variable_value = matched_traffic[:variables][variable_key] || matched_traffic[:variables][variable_key.to_sym]
|
|
608
|
+
evaluation = {
|
|
609
|
+
type: type,
|
|
610
|
+
feature_key: feature_key,
|
|
611
|
+
reason: Featurevisor::EvaluationReason::RULE,
|
|
612
|
+
bucket_key: bucket_key,
|
|
613
|
+
bucket_value: bucket_value,
|
|
614
|
+
rule_key: matched_traffic[:key],
|
|
615
|
+
traffic: matched_traffic,
|
|
616
|
+
variable_key: variable_key,
|
|
617
|
+
variable_schema: variable_schema,
|
|
618
|
+
variable_value: variable_value
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
logger.debug("override from rule", evaluation)
|
|
622
|
+
|
|
623
|
+
return evaluation
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# check variations
|
|
627
|
+
variation_value = nil
|
|
628
|
+
|
|
629
|
+
if force && force[:variation]
|
|
630
|
+
variation_value = force[:variation]
|
|
631
|
+
elsif matched_traffic && matched_traffic[:variation]
|
|
632
|
+
variation_value = matched_traffic[:variation]
|
|
633
|
+
elsif matched_allocation && matched_allocation[:variation]
|
|
634
|
+
variation_value = matched_allocation[:variation]
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
if variation_value && feature[:variations].is_a?(Array)
|
|
638
|
+
variation = feature[:variations].find { |v| v[:value] == variation_value }
|
|
639
|
+
|
|
640
|
+
if variation && variation[:variableOverrides] && (variation[:variableOverrides][variable_key] || variation[:variableOverrides][variable_key.to_sym])
|
|
641
|
+
overrides = variation[:variableOverrides][variable_key] || variation[:variableOverrides][variable_key.to_sym]
|
|
642
|
+
|
|
643
|
+
logger.debug("checking variableOverrides", {
|
|
644
|
+
feature_key: feature_key,
|
|
645
|
+
variable_key: variable_key,
|
|
646
|
+
overrides: overrides,
|
|
647
|
+
context: context
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
override = overrides.find do |o|
|
|
651
|
+
logger.debug("evaluating override", {
|
|
652
|
+
feature_key: feature_key,
|
|
653
|
+
variable_key: variable_key,
|
|
654
|
+
override: o,
|
|
655
|
+
context: context
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
result = if o[:conditions]
|
|
659
|
+
matched = datafile_reader.all_conditions_are_matched(
|
|
660
|
+
o[:conditions].is_a?(String) && o[:conditions] != "*" ?
|
|
661
|
+
JSON.parse(o[:conditions]) : o[:conditions],
|
|
662
|
+
context
|
|
663
|
+
)
|
|
664
|
+
logger.debug("conditions match result", {
|
|
665
|
+
feature_key: feature_key,
|
|
666
|
+
variable_key: variable_key,
|
|
667
|
+
conditions: o[:conditions],
|
|
668
|
+
matched: matched
|
|
669
|
+
})
|
|
670
|
+
matched
|
|
671
|
+
elsif o[:segments]
|
|
672
|
+
segments = datafile_reader.parse_segments_if_stringified(o[:segments])
|
|
673
|
+
matched = datafile_reader.all_segments_are_matched(segments, context)
|
|
674
|
+
logger.debug("segments match result", {
|
|
675
|
+
feature_key: feature_key,
|
|
676
|
+
variable_key: variable_key,
|
|
677
|
+
segments: o[:segments],
|
|
678
|
+
parsed_segments: segments,
|
|
679
|
+
matched: matched
|
|
680
|
+
})
|
|
681
|
+
matched
|
|
682
|
+
else
|
|
683
|
+
logger.debug("override has no conditions or segments", {
|
|
684
|
+
feature_key: feature_key,
|
|
685
|
+
variable_key: variable_key,
|
|
686
|
+
override: o
|
|
687
|
+
})
|
|
688
|
+
false
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
logger.debug("override evaluation result", {
|
|
692
|
+
feature_key: feature_key,
|
|
693
|
+
variable_key: variable_key,
|
|
694
|
+
result: result
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
result
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
if override
|
|
701
|
+
evaluation = {
|
|
702
|
+
type: type,
|
|
703
|
+
feature_key: feature_key,
|
|
704
|
+
reason: Featurevisor::EvaluationReason::VARIABLE_OVERRIDE,
|
|
705
|
+
bucket_key: bucket_key,
|
|
706
|
+
bucket_value: bucket_value,
|
|
707
|
+
rule_key: matched_traffic&.[](:key),
|
|
708
|
+
traffic: matched_traffic,
|
|
709
|
+
variable_key: variable_key,
|
|
710
|
+
variable_schema: variable_schema,
|
|
711
|
+
variable_value: override[:value]
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
logger.debug("variable override", evaluation)
|
|
715
|
+
|
|
716
|
+
return evaluation
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
if variation &&
|
|
721
|
+
variation[:variables] &&
|
|
722
|
+
(variation[:variables][variable_key] || variation[:variables][variable_key.to_sym])
|
|
723
|
+
variable_value = variation[:variables][variable_key] || variation[:variables][variable_key.to_sym]
|
|
724
|
+
evaluation = {
|
|
725
|
+
type: type,
|
|
726
|
+
feature_key: feature_key,
|
|
727
|
+
reason: Featurevisor::EvaluationReason::ALLOCATED,
|
|
728
|
+
bucket_key: bucket_key,
|
|
729
|
+
bucket_value: bucket_value,
|
|
730
|
+
rule_key: matched_traffic&.[](:key),
|
|
731
|
+
traffic: matched_traffic,
|
|
732
|
+
variable_key: variable_key,
|
|
733
|
+
variable_schema: variable_schema,
|
|
734
|
+
variable_value: variable_value
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
logger.debug("allocated variable", evaluation)
|
|
738
|
+
|
|
739
|
+
return evaluation
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
# Nothing matched
|
|
745
|
+
if type == "variation"
|
|
746
|
+
evaluation = {
|
|
747
|
+
type: type,
|
|
748
|
+
feature_key: feature_key,
|
|
749
|
+
reason: Featurevisor::EvaluationReason::NO_MATCH,
|
|
750
|
+
bucket_key: bucket_key,
|
|
751
|
+
bucket_value: bucket_value
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
logger.debug("no matched variation", evaluation)
|
|
755
|
+
|
|
756
|
+
return evaluation
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
if type == "variable"
|
|
760
|
+
if variable_schema
|
|
761
|
+
evaluation = {
|
|
762
|
+
type: type,
|
|
763
|
+
feature_key: feature_key,
|
|
764
|
+
reason: Featurevisor::EvaluationReason::VARIABLE_DEFAULT,
|
|
765
|
+
bucket_key: bucket_key,
|
|
766
|
+
bucket_value: bucket_value,
|
|
767
|
+
variable_key: variable_key,
|
|
768
|
+
variable_schema: variable_schema,
|
|
769
|
+
variable_value: variable_schema[:defaultValue]
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
logger.debug("using default value", evaluation)
|
|
773
|
+
|
|
774
|
+
return evaluation
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
evaluation = {
|
|
778
|
+
type: type,
|
|
779
|
+
feature_key: feature_key,
|
|
780
|
+
reason: Featurevisor::EvaluationReason::VARIABLE_NOT_FOUND,
|
|
781
|
+
variable_key: variable_key,
|
|
782
|
+
bucket_key: bucket_key,
|
|
783
|
+
bucket_value: bucket_value
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
logger.debug("variable not found", evaluation)
|
|
787
|
+
|
|
788
|
+
return evaluation
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
evaluation = {
|
|
792
|
+
type: type,
|
|
793
|
+
feature_key: feature_key,
|
|
794
|
+
reason: Featurevisor::EvaluationReason::NO_MATCH,
|
|
795
|
+
bucket_key: bucket_key,
|
|
796
|
+
bucket_value: bucket_value,
|
|
797
|
+
enabled: false
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
logger.debug("nothing matched", evaluation)
|
|
801
|
+
|
|
802
|
+
evaluation
|
|
803
|
+
rescue => e
|
|
804
|
+
evaluation = {
|
|
805
|
+
type: type,
|
|
806
|
+
feature_key: feature_key,
|
|
807
|
+
variable_key: variable_key,
|
|
808
|
+
reason: Featurevisor::EvaluationReason::ERROR,
|
|
809
|
+
error: e
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
logger.error("error", evaluation)
|
|
813
|
+
|
|
814
|
+
evaluation
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
end
|