featurevisor 1.1.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/LICENSE +1 -1
- data/README.md +68 -19
- data/bin/cli.rb +2 -2
- data/bin/commands/benchmark.rb +14 -4
- data/bin/commands/test.rb +46 -5
- data/lib/featurevisor/bucketer.rb +62 -8
- data/lib/featurevisor/child_instance.rb +73 -256
- data/lib/featurevisor/conditions.rb +63 -34
- data/lib/featurevisor/{logger.rb → diagnostics.rb} +19 -52
- data/lib/featurevisor/emitter.rb +1 -1
- data/lib/featurevisor/evaluate.rb +104 -105
- data/lib/featurevisor/{datafile_reader.rb → evaluation_data_provider.rb} +41 -18
- data/lib/featurevisor/events.rb +120 -4
- data/lib/featurevisor/instance.rb +226 -78
- data/lib/featurevisor/modules.rb +51 -6
- data/lib/featurevisor/version.rb +1 -1
- data/lib/featurevisor.rb +4 -4
- metadata +3 -3
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,9 +22,16 @@ 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
|
-
# @param previous_reader [
|
|
25
|
-
# @param new_reader [
|
|
33
|
+
# @param previous_reader [InstanceEvaluationDataProvider] Previous datafile
|
|
34
|
+
# @param new_reader [InstanceEvaluationDataProvider] New datafile
|
|
26
35
|
# @return [Hash] Event parameters
|
|
27
36
|
def self.get_params_for_datafile_set_event(previous_reader, new_reader, replace = false)
|
|
28
37
|
previous_revision = previous_reader.get_revision
|
|
@@ -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
|