featurevisor 2.0.0 → 3.0.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/README.md +62 -20
- data/bin/cli.rb +2 -2
- data/bin/commands/benchmark.rb +14 -4
- data/bin/commands/test.rb +45 -4
- data/lib/featurevisor/child_instance.rb +64 -289
- data/lib/featurevisor/emitter.rb +1 -1
- data/lib/featurevisor/evaluate.rb +59 -61
- data/lib/featurevisor/evaluation_data_provider.rb +15 -3
- data/lib/featurevisor/events.rb +118 -2
- data/lib/featurevisor/instance.rb +153 -39
- data/lib/featurevisor/modules.rb +51 -6
- data/lib/featurevisor/version.rb +1 -1
- metadata +1 -1
data/lib/featurevisor/events.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
module Featurevisor
|
|
4
6
|
# Events module for generating event parameters
|
|
5
7
|
module Events
|
|
@@ -8,7 +10,7 @@ module Featurevisor
|
|
|
8
10
|
# @param new_sticky [Hash] New sticky features
|
|
9
11
|
# @param replace [Boolean] Whether features were replaced
|
|
10
12
|
# @return [Hash] Event parameters
|
|
11
|
-
def self.
|
|
13
|
+
def self.get_params_for_sticky_features_set_event(previous_sticky = {}, new_sticky = {}, replace = false)
|
|
12
14
|
keys_before = previous_sticky.keys
|
|
13
15
|
keys_after = new_sticky.keys
|
|
14
16
|
|
|
@@ -20,6 +22,13 @@ module Featurevisor
|
|
|
20
22
|
}
|
|
21
23
|
end
|
|
22
24
|
|
|
25
|
+
def self.get_params_for_sticky_variables_set_event(previous_sticky = {}, new_sticky = {}, replace = false)
|
|
26
|
+
{
|
|
27
|
+
variables: (previous_sticky.keys + new_sticky.keys).uniq,
|
|
28
|
+
replaced: replace
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
23
32
|
# Get parameters for datafile set event
|
|
24
33
|
# @param previous_reader [InstanceEvaluationDataProvider] Previous datafile
|
|
25
34
|
# @param new_reader [InstanceEvaluationDataProvider] New datafile
|
|
@@ -30,6 +39,8 @@ module Featurevisor
|
|
|
30
39
|
|
|
31
40
|
new_revision = new_reader.get_revision
|
|
32
41
|
new_feature_keys = new_reader.get_feature_keys
|
|
42
|
+
previous_variable_keys = previous_reader.get_variable_keys
|
|
43
|
+
new_variable_keys = new_reader.get_variable_keys
|
|
33
44
|
|
|
34
45
|
# results
|
|
35
46
|
removed_features = []
|
|
@@ -48,7 +59,8 @@ module Featurevisor
|
|
|
48
59
|
previous_feature = previous_reader.get_feature(previous_feature_key)
|
|
49
60
|
new_feature = new_reader.get_feature(previous_feature_key)
|
|
50
61
|
|
|
51
|
-
if previous_feature && new_feature &&
|
|
62
|
+
if previous_feature && new_feature &&
|
|
63
|
+
(previous_feature[:hash].nil? || new_feature[:hash].nil? || previous_feature[:hash] != new_feature[:hash])
|
|
52
64
|
# feature was changed in new datafile
|
|
53
65
|
changed_features << previous_feature_key
|
|
54
66
|
end
|
|
@@ -64,14 +76,118 @@ module Featurevisor
|
|
|
64
76
|
|
|
65
77
|
# combine all affected feature keys
|
|
66
78
|
all_affected_features = (removed_features + changed_features + added_features).uniq
|
|
79
|
+
all_affected_variables = (previous_variable_keys + new_variable_keys).uniq.select do |key|
|
|
80
|
+
previous = previous_reader.get_global_variable(key)
|
|
81
|
+
current = new_reader.get_global_variable(key)
|
|
82
|
+
previous.nil? || current.nil? || previous[:hash].nil? || current[:hash].nil? || previous[:hash] != current[:hash]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
changed_segments = (previous_reader.get_segment_keys + new_reader.get_segment_keys).uniq.select do |key|
|
|
87
|
+
previous_reader.get_segment(key) != new_reader.get_segment(key)
|
|
88
|
+
end.map(&:to_s)
|
|
89
|
+
feature_keys = (previous_feature_keys + new_feature_keys).uniq
|
|
90
|
+
loop do
|
|
91
|
+
before = all_affected_features.length
|
|
92
|
+
feature_keys.each do |key|
|
|
93
|
+
next if all_affected_features.include?(key)
|
|
94
|
+
|
|
95
|
+
candidates = [previous_reader.get_feature(key), new_reader.get_feature(key)].compact
|
|
96
|
+
all_affected_features << key if candidates.any? do |feature|
|
|
97
|
+
dependency = feature_dependencies(feature)
|
|
98
|
+
!(dependency[:segments].map(&:to_s) & changed_segments).empty? ||
|
|
99
|
+
!(dependency[:features].map(&:to_s) & all_affected_features.map(&:to_s)).empty?
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
break if before == all_affected_features.length
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
(previous_variable_keys + new_variable_keys).uniq.each do |key|
|
|
106
|
+
next if all_affected_variables.include?(key)
|
|
107
|
+
|
|
108
|
+
candidates = [previous_reader.get_global_variable(key), new_reader.get_global_variable(key)].compact
|
|
109
|
+
all_affected_variables << key if candidates.any? do |variable|
|
|
110
|
+
dependency = global_variable_dependencies(variable)
|
|
111
|
+
!(dependency[:segments].map(&:to_s) & changed_segments).empty? ||
|
|
112
|
+
!(dependency[:features].map(&:to_s) & all_affected_features.map(&:to_s)).empty?
|
|
113
|
+
end
|
|
114
|
+
end
|
|
67
115
|
|
|
68
116
|
{
|
|
69
117
|
revision: new_revision,
|
|
70
118
|
previousRevision: previous_revision,
|
|
71
119
|
revisionChanged: previous_revision != new_revision,
|
|
72
120
|
features: all_affected_features,
|
|
121
|
+
variables: all_affected_variables,
|
|
73
122
|
replaced: replace
|
|
74
123
|
}
|
|
75
124
|
end
|
|
125
|
+
|
|
126
|
+
def self.required_feature_keys(values)
|
|
127
|
+
items = values.is_a?(Array) ? values : [values].compact
|
|
128
|
+
items.filter_map do |required|
|
|
129
|
+
required.is_a?(String) ? required : (required[:feature] || required[:key])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
private_class_method :required_feature_keys
|
|
133
|
+
|
|
134
|
+
def self.segment_keys(value)
|
|
135
|
+
return [] if value.nil? || value == "*"
|
|
136
|
+
if value.is_a?(String)
|
|
137
|
+
if value.start_with?("{", "[")
|
|
138
|
+
begin
|
|
139
|
+
return segment_keys(JSON.parse(value, symbolize_names: true))
|
|
140
|
+
rescue JSON::ParserError
|
|
141
|
+
return []
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
return [value]
|
|
145
|
+
end
|
|
146
|
+
return value.flat_map { |item| segment_keys(item) }.uniq if value.is_a?(Array)
|
|
147
|
+
return [] unless value.is_a?(Hash)
|
|
148
|
+
|
|
149
|
+
%i[and or not].flat_map { |key| segment_keys(value[key] || value[key.to_s]) }.uniq
|
|
150
|
+
end
|
|
151
|
+
private_class_method :segment_keys
|
|
152
|
+
|
|
153
|
+
def self.override_dependencies(groups)
|
|
154
|
+
overrides = groups.is_a?(Hash) ? groups.values.flatten : []
|
|
155
|
+
{
|
|
156
|
+
segments: overrides.flat_map { |override| segment_keys(override[:segments]) }.uniq,
|
|
157
|
+
features: overrides.flat_map { |override| required_feature_keys(override[:requiredFeatures]) }.uniq
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
private_class_method :override_dependencies
|
|
161
|
+
|
|
162
|
+
def self.feature_dependencies(feature)
|
|
163
|
+
requirements = feature.key?(:requiredFeatures) ? feature[:requiredFeatures] : feature[:required]
|
|
164
|
+
segments = []
|
|
165
|
+
features = required_feature_keys(requirements)
|
|
166
|
+
Array(feature[:traffic]).each do |traffic|
|
|
167
|
+
segments.concat(segment_keys(traffic[:segments]))
|
|
168
|
+
nested = override_dependencies(traffic[:variableOverrides])
|
|
169
|
+
segments.concat(nested[:segments])
|
|
170
|
+
features.concat(nested[:features])
|
|
171
|
+
end
|
|
172
|
+
Array(feature[:force]).each { |force| segments.concat(segment_keys(force[:segments])) }
|
|
173
|
+
Array(feature[:variations]).each do |variation|
|
|
174
|
+
nested = override_dependencies(variation[:variableOverrides])
|
|
175
|
+
segments.concat(nested[:segments])
|
|
176
|
+
features.concat(nested[:features])
|
|
177
|
+
end
|
|
178
|
+
{ segments: segments.uniq, features: features.uniq }
|
|
179
|
+
end
|
|
180
|
+
private_class_method :feature_dependencies
|
|
181
|
+
|
|
182
|
+
def self.global_variable_dependencies(variable)
|
|
183
|
+
segments = []
|
|
184
|
+
features = required_feature_keys(variable[:requiredFeatures])
|
|
185
|
+
Array(variable[:overrides]).each do |override|
|
|
186
|
+
segments.concat(segment_keys(override[:segments]))
|
|
187
|
+
features.concat(required_feature_keys(override[:requiredFeatures]))
|
|
188
|
+
end
|
|
189
|
+
{ segments: segments.uniq, features: features.uniq }
|
|
190
|
+
end
|
|
191
|
+
private_class_method :global_variable_dependencies
|
|
76
192
|
end
|
|
77
193
|
end
|
|
@@ -11,7 +11,8 @@ module Featurevisor
|
|
|
11
11
|
schemaVersion: "2",
|
|
12
12
|
revision: "unknown",
|
|
13
13
|
segments: {},
|
|
14
|
-
features: {}
|
|
14
|
+
features: {},
|
|
15
|
+
variables: {}
|
|
15
16
|
}.freeze
|
|
16
17
|
|
|
17
18
|
# Initialize a new Featurevisor instance
|
|
@@ -19,7 +20,7 @@ module Featurevisor
|
|
|
19
20
|
# @option options [Hash, String] :datafile Datafile content or JSON string
|
|
20
21
|
# @option options [Hash] :context Initial context
|
|
21
22
|
# @option options [String] :log_level Log level
|
|
22
|
-
# @option options [Hash] :
|
|
23
|
+
# @option options [Hash] :sticky_features Sticky features
|
|
23
24
|
# @option options [Array<Hash, FeaturevisorModule>] :modules Array of modules
|
|
24
25
|
# @option options [Proc] :on_diagnostic Diagnostic handler
|
|
25
26
|
def initialize(options = {})
|
|
@@ -31,7 +32,8 @@ module Featurevisor
|
|
|
31
32
|
)
|
|
32
33
|
@on_diagnostic = options[:on_diagnostic] || options[:onDiagnostic]
|
|
33
34
|
@emitter = Featurevisor::Emitter.new
|
|
34
|
-
@
|
|
35
|
+
@sticky_features = options[:sticky_features] || options[:stickyFeatures] || {}
|
|
36
|
+
@sticky_variables = options[:sticky_variables] || options[:stickyVariables] || {}
|
|
35
37
|
@closed = false
|
|
36
38
|
@module_diagnostic_subscriptions = []
|
|
37
39
|
|
|
@@ -108,7 +110,8 @@ module Featurevisor
|
|
|
108
110
|
end
|
|
109
111
|
unless parsed_datafile.is_a?(Hash) && parsed_datafile[:schemaVersion].is_a?(String) &&
|
|
110
112
|
parsed_datafile[:revision].is_a?(String) && parsed_datafile[:segments].is_a?(Hash) &&
|
|
111
|
-
parsed_datafile[:features].is_a?(Hash)
|
|
113
|
+
parsed_datafile[:features].is_a?(Hash) &&
|
|
114
|
+
(!parsed_datafile.key?(:variables) || parsed_datafile[:variables].is_a?(Hash))
|
|
112
115
|
raise ArgumentError, "Invalid datafile"
|
|
113
116
|
end
|
|
114
117
|
next_datafile = replace ? parsed_datafile : merge_datafiles(@datafile.get_datafile, parsed_datafile)
|
|
@@ -140,27 +143,35 @@ module Featurevisor
|
|
|
140
143
|
# Set sticky features
|
|
141
144
|
# @param sticky [Hash] Sticky features
|
|
142
145
|
# @param replace [Boolean] Whether to replace existing sticky features
|
|
143
|
-
def
|
|
144
|
-
previous_sticky_features = @
|
|
146
|
+
def set_sticky_features(sticky, replace = false)
|
|
147
|
+
previous_sticky_features = @sticky_features || {}
|
|
145
148
|
|
|
146
149
|
if replace
|
|
147
|
-
@
|
|
150
|
+
@sticky_features = sticky
|
|
148
151
|
else
|
|
149
|
-
@
|
|
150
|
-
**@
|
|
152
|
+
@sticky_features = {
|
|
153
|
+
**@sticky_features,
|
|
151
154
|
**sticky
|
|
152
155
|
}
|
|
153
156
|
end
|
|
154
157
|
|
|
155
|
-
params = Featurevisor::Events.
|
|
158
|
+
params = Featurevisor::Events.get_params_for_sticky_features_set_event(previous_sticky_features, @sticky_features, replace)
|
|
156
159
|
|
|
157
160
|
report_diagnostic(
|
|
158
161
|
level: "info",
|
|
159
|
-
code: "
|
|
162
|
+
code: "sticky_features_set",
|
|
160
163
|
message: "Sticky features set",
|
|
161
164
|
details: params
|
|
162
165
|
)
|
|
163
|
-
@emitter.trigger("
|
|
166
|
+
@emitter.trigger("sticky_features_set", params)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def set_sticky_variables(sticky, replace = false)
|
|
170
|
+
previous = @sticky_variables || {}
|
|
171
|
+
@sticky_variables = replace ? sticky : { **@sticky_variables, **sticky }
|
|
172
|
+
params = Featurevisor::Events.get_params_for_sticky_variables_set_event(previous, @sticky_variables, replace)
|
|
173
|
+
report_diagnostic(level: "info", code: "sticky_variables_set", message: "Sticky variables set", details: params)
|
|
174
|
+
@emitter.trigger("sticky_variables_set", params)
|
|
164
175
|
end
|
|
165
176
|
|
|
166
177
|
# Get the revision
|
|
@@ -181,7 +192,7 @@ module Featurevisor
|
|
|
181
192
|
@datafile.get_feature_keys
|
|
182
193
|
end
|
|
183
194
|
|
|
184
|
-
def get_variable_keys(feature_key)
|
|
195
|
+
def get_variable_keys(feature_key = nil)
|
|
185
196
|
@datafile.get_variable_keys(feature_key)
|
|
186
197
|
end
|
|
187
198
|
|
|
@@ -275,7 +286,8 @@ module Featurevisor
|
|
|
275
286
|
Featurevisor::ChildInstance.new(
|
|
276
287
|
parent: self,
|
|
277
288
|
context: get_context(context),
|
|
278
|
-
|
|
289
|
+
sticky_features: options[:sticky_features] || options[:stickyFeatures],
|
|
290
|
+
sticky_variables: options[:sticky_variables] || options[:stickyVariables]
|
|
279
291
|
)
|
|
280
292
|
end
|
|
281
293
|
|
|
@@ -350,12 +362,17 @@ module Featurevisor
|
|
|
350
362
|
# @param context [Hash] Context
|
|
351
363
|
# @param options [Hash] Override options
|
|
352
364
|
# @return [Hash] Evaluation result
|
|
353
|
-
def evaluate_variable(
|
|
365
|
+
def evaluate_variable(feature_or_variable_key, variable_key_or_context = nil, context_or_options = {}, options = {})
|
|
366
|
+
if variable_key_or_context.nil? || variable_key_or_context.is_a?(Hash)
|
|
367
|
+
context = variable_key_or_context || {}
|
|
368
|
+
return evaluate_variable_without_feature(feature_or_variable_key, context, context_or_options)
|
|
369
|
+
end
|
|
370
|
+
|
|
354
371
|
Featurevisor::Evaluate.evaluate_with_modules(
|
|
355
|
-
get_evaluation_dependencies(
|
|
372
|
+
get_evaluation_dependencies(context_or_options, options).merge(
|
|
356
373
|
type: "variable",
|
|
357
|
-
feature_key:
|
|
358
|
-
variable_key:
|
|
374
|
+
feature_key: feature_or_variable_key,
|
|
375
|
+
variable_key: variable_key_or_context
|
|
359
376
|
)
|
|
360
377
|
)
|
|
361
378
|
end
|
|
@@ -366,13 +383,13 @@ module Featurevisor
|
|
|
366
383
|
# @param context [Hash] Context
|
|
367
384
|
# @param options [Hash] Override options
|
|
368
385
|
# @return [Object, nil] Variable value or nil
|
|
369
|
-
def get_variable(
|
|
386
|
+
def get_variable(feature_or_variable_key, variable_key_or_context = nil, context_or_options = {}, options = {})
|
|
370
387
|
begin
|
|
371
|
-
evaluation = evaluate_variable(
|
|
388
|
+
evaluation = evaluate_variable(feature_or_variable_key, variable_key_or_context, context_or_options, options)
|
|
372
389
|
|
|
373
390
|
if evaluation.key?(:variable_value)
|
|
374
|
-
|
|
375
|
-
|
|
391
|
+
variable_type = evaluation.dig(:variable_schema, :type) || evaluation.dig(:variable, :type)
|
|
392
|
+
if variable_type == "json" &&
|
|
376
393
|
evaluation[:variable_value].is_a?(String)
|
|
377
394
|
JSON.parse(evaluation[:variable_value], symbolize_names: true)
|
|
378
395
|
else
|
|
@@ -382,7 +399,7 @@ module Featurevisor
|
|
|
382
399
|
nil
|
|
383
400
|
end
|
|
384
401
|
rescue => e
|
|
385
|
-
report_diagnostic(level: "error", code: "evaluation_error", message: "getVariable failed", originalError: e, details: {
|
|
402
|
+
report_diagnostic(level: "error", code: "evaluation_error", message: "getVariable failed", originalError: e, details: { variableKey: variable_key_or_context || feature_or_variable_key })
|
|
386
403
|
nil
|
|
387
404
|
end
|
|
388
405
|
end
|
|
@@ -393,8 +410,8 @@ module Featurevisor
|
|
|
393
410
|
# @param context [Hash] Context
|
|
394
411
|
# @param options [Hash] Override options
|
|
395
412
|
# @return [Boolean, nil] Boolean value or nil
|
|
396
|
-
def get_variable_boolean(
|
|
397
|
-
variable_value = get_variable(
|
|
413
|
+
def get_variable_boolean(*args)
|
|
414
|
+
variable_value = get_variable(*args)
|
|
398
415
|
get_value_by_type(variable_value, "boolean")
|
|
399
416
|
end
|
|
400
417
|
|
|
@@ -404,8 +421,8 @@ module Featurevisor
|
|
|
404
421
|
# @param context [Hash] Context
|
|
405
422
|
# @param options [Hash] Override options
|
|
406
423
|
# @return [String, nil] String value or nil
|
|
407
|
-
def get_variable_string(
|
|
408
|
-
variable_value = get_variable(
|
|
424
|
+
def get_variable_string(*args)
|
|
425
|
+
variable_value = get_variable(*args)
|
|
409
426
|
get_value_by_type(variable_value, "string")
|
|
410
427
|
end
|
|
411
428
|
|
|
@@ -415,8 +432,8 @@ module Featurevisor
|
|
|
415
432
|
# @param context [Hash] Context
|
|
416
433
|
# @param options [Hash] Override options
|
|
417
434
|
# @return [Integer, nil] Integer value or nil
|
|
418
|
-
def get_variable_integer(
|
|
419
|
-
variable_value = get_variable(
|
|
435
|
+
def get_variable_integer(*args)
|
|
436
|
+
variable_value = get_variable(*args)
|
|
420
437
|
get_value_by_type(variable_value, "integer")
|
|
421
438
|
end
|
|
422
439
|
|
|
@@ -426,8 +443,8 @@ module Featurevisor
|
|
|
426
443
|
# @param context [Hash] Context
|
|
427
444
|
# @param options [Hash] Override options
|
|
428
445
|
# @return [Float, nil] Float value or nil
|
|
429
|
-
def get_variable_double(
|
|
430
|
-
variable_value = get_variable(
|
|
446
|
+
def get_variable_double(*args)
|
|
447
|
+
variable_value = get_variable(*args)
|
|
431
448
|
get_value_by_type(variable_value, "double")
|
|
432
449
|
end
|
|
433
450
|
|
|
@@ -437,8 +454,8 @@ module Featurevisor
|
|
|
437
454
|
# @param context [Hash] Context
|
|
438
455
|
# @param options [Hash] Override options
|
|
439
456
|
# @return [Array, nil] Array value or nil
|
|
440
|
-
def get_variable_array(
|
|
441
|
-
variable_value = get_variable(
|
|
457
|
+
def get_variable_array(*args)
|
|
458
|
+
variable_value = get_variable(*args)
|
|
442
459
|
get_value_by_type(variable_value, "array")
|
|
443
460
|
end
|
|
444
461
|
|
|
@@ -448,8 +465,8 @@ module Featurevisor
|
|
|
448
465
|
# @param context [Hash] Context
|
|
449
466
|
# @param options [Hash] Override options
|
|
450
467
|
# @return [Hash, nil] Object value or nil
|
|
451
|
-
def get_variable_object(
|
|
452
|
-
variable_value = get_variable(
|
|
468
|
+
def get_variable_object(*args)
|
|
469
|
+
variable_value = get_variable(*args)
|
|
453
470
|
get_value_by_type(variable_value, "object")
|
|
454
471
|
end
|
|
455
472
|
|
|
@@ -459,8 +476,8 @@ module Featurevisor
|
|
|
459
476
|
# @param context [Hash] Context
|
|
460
477
|
# @param options [Hash] Override options
|
|
461
478
|
# @return [Object, nil] JSON value or nil
|
|
462
|
-
def get_variable_json(
|
|
463
|
-
variable_value = get_variable(
|
|
479
|
+
def get_variable_json(*args)
|
|
480
|
+
variable_value = get_variable(*args)
|
|
464
481
|
get_value_by_type(variable_value, "json")
|
|
465
482
|
end
|
|
466
483
|
|
|
@@ -469,7 +486,7 @@ module Featurevisor
|
|
|
469
486
|
# @param feature_keys [Array<String>] Feature keys to evaluate
|
|
470
487
|
# @param options [Hash] Override options
|
|
471
488
|
# @return [Hash] All evaluations
|
|
472
|
-
def
|
|
489
|
+
def get_feature_evaluations(context = {}, feature_keys = [], options = {})
|
|
473
490
|
result = {}
|
|
474
491
|
|
|
475
492
|
keys = feature_keys.size > 0 ? feature_keys : @datafile.get_feature_keys
|
|
@@ -510,8 +527,100 @@ module Featurevisor
|
|
|
510
527
|
result
|
|
511
528
|
end
|
|
512
529
|
|
|
530
|
+
def get_variable_evaluations(context = {}, variable_keys = [], options = {})
|
|
531
|
+
keys = variable_keys.empty? ? @datafile.get_variable_keys : variable_keys
|
|
532
|
+
keys.to_h { |key| [key, get_variable(key.to_s, context, options)] }
|
|
533
|
+
end
|
|
534
|
+
|
|
513
535
|
private
|
|
514
536
|
|
|
537
|
+
def evaluate_variable_without_feature(variable_key, context = {}, options = {})
|
|
538
|
+
evaluation_options = {
|
|
539
|
+
type: "variable",
|
|
540
|
+
variable_key: variable_key.to_s,
|
|
541
|
+
context: get_context(context)
|
|
542
|
+
}
|
|
543
|
+
evaluation_options[:default_variable_value] = options[:default_variable_value] if options.key?(:default_variable_value)
|
|
544
|
+
begin
|
|
545
|
+
evaluation_options = @modules_manager.run_before_evaluation_modules(evaluation_options)
|
|
546
|
+
resolved_key = evaluation_options[:variable_key]
|
|
547
|
+
variable = @datafile.get_global_variable(resolved_key)
|
|
548
|
+
sticky = options[:__featurevisor_child_sticky_variables] || @sticky_variables
|
|
549
|
+
evaluation = { type: "variable", variable_key: resolved_key, reason: Featurevisor::EvaluationReason::VARIABLE_NOT_FOUND }
|
|
550
|
+
|
|
551
|
+
if sticky.key?(resolved_key) || sticky.key?(resolved_key.to_sym)
|
|
552
|
+
sticky_value = sticky.key?(resolved_key) ? sticky[resolved_key] : sticky[resolved_key.to_sym]
|
|
553
|
+
evaluation.merge!(reason: Featurevisor::EvaluationReason::STICKY, variable: variable,
|
|
554
|
+
variable_value: sticky_value)
|
|
555
|
+
elsif variable
|
|
556
|
+
unless required_features_are_matched(variable[:requiredFeatures], evaluation_options[:context], options)
|
|
557
|
+
value_key = variable[:useDefaultWhenDisabled] ? :defaultValue : :disabledValue
|
|
558
|
+
evaluation.merge!(reason: Featurevisor::EvaluationReason::REQUIRED_FEATURES_UNMET,
|
|
559
|
+
variable: variable)
|
|
560
|
+
evaluation[:variable_value] = variable[value_key] if variable.key?(value_key)
|
|
561
|
+
else
|
|
562
|
+
(variable[:overrides] || []).each_with_index do |override, index|
|
|
563
|
+
next unless required_features_are_matched(override[:requiredFeatures], evaluation_options[:context], options)
|
|
564
|
+
conditions_match = !override[:conditions] || @datafile.all_conditions_are_matched(
|
|
565
|
+
@datafile.parse_conditions_if_stringified(override[:conditions]), evaluation_options[:context]
|
|
566
|
+
)
|
|
567
|
+
segments_match = !override[:segments] || @datafile.all_segments_are_matched(
|
|
568
|
+
@datafile.parse_segments_if_stringified(override[:segments]), evaluation_options[:context]
|
|
569
|
+
)
|
|
570
|
+
next unless conditions_match && segments_match
|
|
571
|
+
|
|
572
|
+
evaluation.merge!(reason: Featurevisor::EvaluationReason::VARIABLE_OVERRIDE_RULE,
|
|
573
|
+
variable: variable,
|
|
574
|
+
variable_override_index: index, variable_override_key: override[:key],
|
|
575
|
+
variable_override_path: override[:keyPath])
|
|
576
|
+
evaluation[:variable_value] = override[:value] if override.key?(:value)
|
|
577
|
+
break
|
|
578
|
+
end
|
|
579
|
+
if evaluation[:reason] == Featurevisor::EvaluationReason::VARIABLE_NOT_FOUND
|
|
580
|
+
evaluation.merge!(reason: Featurevisor::EvaluationReason::VARIABLE_DEFAULT,
|
|
581
|
+
variable: variable)
|
|
582
|
+
evaluation[:variable_value] = variable[:defaultValue] if variable.key?(:defaultValue)
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
report_diagnostic(level: "warn", code: "variable_deprecated", message: "Variable \"#{resolved_key}\" is deprecated",
|
|
586
|
+
details: { variableKey: resolved_key, evaluation: evaluation }) if variable[:deprecated]
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
if !evaluation.key?(:variable_value) && evaluation_options.key?(:default_variable_value)
|
|
590
|
+
evaluation[:variable_value] = evaluation_options[:default_variable_value]
|
|
591
|
+
end
|
|
592
|
+
evaluation = @modules_manager.run_global_after_modules(evaluation, evaluation_options)
|
|
593
|
+
report_diagnostic(level: "debug", code: evaluation[:reason], message: "Global variable evaluated", details: evaluation)
|
|
594
|
+
evaluation
|
|
595
|
+
rescue => e
|
|
596
|
+
evaluation = { type: "variable", variable_key: evaluation_options[:variable_key],
|
|
597
|
+
reason: Featurevisor::EvaluationReason::ERROR, error: e }
|
|
598
|
+
report_diagnostic(level: "error", code: "evaluation_error", message: "Global variable evaluation failed",
|
|
599
|
+
originalError: e, details: evaluation)
|
|
600
|
+
evaluation
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def required_features_are_matched(requirements, context, options)
|
|
605
|
+
return true if requirements.nil?
|
|
606
|
+
|
|
607
|
+
items = requirements.is_a?(Array) ? requirements : [requirements]
|
|
608
|
+
clean_options = options.reject { |key, _| %i[default_variation_value default_variable_value].include?(key) }
|
|
609
|
+
items.all? do |required|
|
|
610
|
+
if required.is_a?(String)
|
|
611
|
+
key = required
|
|
612
|
+
expected_enabled = true
|
|
613
|
+
expected_variation = nil
|
|
614
|
+
else
|
|
615
|
+
key = required[:feature]
|
|
616
|
+
expected_enabled = required.key?(:enabled) ? required[:enabled] : true
|
|
617
|
+
expected_variation = required[:variation]
|
|
618
|
+
end
|
|
619
|
+
next false unless is_enabled(key, context, clean_options) == expected_enabled
|
|
620
|
+
expected_variation.nil? || get_variation(key, context, clean_options) == expected_variation
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
515
624
|
# Get evaluation dependencies
|
|
516
625
|
# @param context [Hash] Context
|
|
517
626
|
# @param options [Hash] Override options
|
|
@@ -522,7 +631,8 @@ module Featurevisor
|
|
|
522
631
|
diagnostics: @diagnostics,
|
|
523
632
|
modules_manager: @modules_manager,
|
|
524
633
|
datafile: @datafile,
|
|
525
|
-
sticky: options[:
|
|
634
|
+
sticky: options[:__featurevisor_child_sticky_features] || @sticky_features,
|
|
635
|
+
sticky_variables: options[:__featurevisor_child_sticky_variables] || @sticky_variables,
|
|
526
636
|
}.tap do |dependencies|
|
|
527
637
|
dependencies[:default_variation_value] = options[:default_variation_value] if options.key?(:default_variation_value)
|
|
528
638
|
dependencies[:default_variable_value] = options[:default_variable_value] if options.key?(:default_variable_value)
|
|
@@ -573,6 +683,10 @@ module Featurevisor
|
|
|
573
683
|
features: {
|
|
574
684
|
**(previous[:features] || {}),
|
|
575
685
|
**(incoming[:features] || {})
|
|
686
|
+
},
|
|
687
|
+
variables: {
|
|
688
|
+
**(previous[:variables] || {}),
|
|
689
|
+
**(incoming[:variables] || {})
|
|
576
690
|
}
|
|
577
691
|
}.compact
|
|
578
692
|
end
|
data/lib/featurevisor/modules.rb
CHANGED
|
@@ -13,9 +13,11 @@ module Featurevisor
|
|
|
13
13
|
@name = options[:name]
|
|
14
14
|
@setup = options[:setup]
|
|
15
15
|
@before = options[:before]
|
|
16
|
+
@before_evaluation = options[:before_evaluation] || options[:beforeEvaluation]
|
|
16
17
|
@bucket_key = options[:bucket_key]
|
|
17
18
|
@bucket_value = options[:bucket_value]
|
|
18
19
|
@after = options[:after]
|
|
20
|
+
@after_evaluation = options[:after_evaluation] || options[:afterEvaluation]
|
|
19
21
|
@close = options[:close]
|
|
20
22
|
end
|
|
21
23
|
|
|
@@ -29,6 +31,12 @@ module Featurevisor
|
|
|
29
31
|
@before.call(options)
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
def call_before_evaluation(options)
|
|
35
|
+
return options unless @before_evaluation
|
|
36
|
+
|
|
37
|
+
@before_evaluation.call(options)
|
|
38
|
+
end
|
|
39
|
+
|
|
32
40
|
def call_bucket_key(options)
|
|
33
41
|
return options[:bucket_key] unless @bucket_key
|
|
34
42
|
|
|
@@ -47,6 +55,12 @@ module Featurevisor
|
|
|
47
55
|
@after.call(evaluation, options)
|
|
48
56
|
end
|
|
49
57
|
|
|
58
|
+
def call_after_evaluation(evaluation, options)
|
|
59
|
+
return evaluation unless @after_evaluation
|
|
60
|
+
|
|
61
|
+
@after_evaluation.call(evaluation, options)
|
|
62
|
+
end
|
|
63
|
+
|
|
50
64
|
def call_close
|
|
51
65
|
@close.call if @close
|
|
52
66
|
end
|
|
@@ -124,6 +138,43 @@ module Featurevisor
|
|
|
124
138
|
end
|
|
125
139
|
|
|
126
140
|
def run_before_modules(options)
|
|
141
|
+
result = @modules.reduce(options) do |current, mod|
|
|
142
|
+
mod.call_before(current)
|
|
143
|
+
end
|
|
144
|
+
@modules.reduce(result) do |current, mod|
|
|
145
|
+
mod.call_before_evaluation(current)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def run_before_evaluation_modules(options)
|
|
150
|
+
@modules.reduce(options) do |result, mod|
|
|
151
|
+
mod.call_before_evaluation(result)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def run_after_evaluation_modules(evaluation, options)
|
|
156
|
+
@modules.reduce(evaluation) do |result, mod|
|
|
157
|
+
mod.call_after_evaluation(result, options)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def run_after_modules(evaluation, options)
|
|
162
|
+
result = @modules.reduce(evaluation) do |current, mod|
|
|
163
|
+
mod.call_after_evaluation(current, options)
|
|
164
|
+
end
|
|
165
|
+
@modules.reduce(result) do |current, mod|
|
|
166
|
+
mod.call_after(current, options)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Deprecated feature-only callbacks are intentionally excluded here.
|
|
171
|
+
def run_global_after_modules(evaluation, options)
|
|
172
|
+
@modules.reduce(evaluation) do |result, mod|
|
|
173
|
+
mod.call_after_evaluation(result, options)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def run_legacy_before_modules(options)
|
|
127
178
|
@modules.reduce(options) do |result, mod|
|
|
128
179
|
mod.call_before(result)
|
|
129
180
|
end
|
|
@@ -145,12 +196,6 @@ module Featurevisor
|
|
|
145
196
|
bucket_value
|
|
146
197
|
end
|
|
147
198
|
|
|
148
|
-
def run_after_modules(evaluation, options)
|
|
149
|
-
@modules.reduce(evaluation) do |result, mod|
|
|
150
|
-
mod.call_after(result, options)
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
199
|
def close_all
|
|
155
200
|
@modules.each do |mod|
|
|
156
201
|
@clear_module_diagnostic_subscriptions.call(mod) if @clear_module_diagnostic_subscriptions
|
data/lib/featurevisor/version.rb
CHANGED