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
|
@@ -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,26 +20,27 @@ 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 = {})
|
|
26
27
|
# from options
|
|
27
28
|
@context = options[:context] || {}
|
|
28
|
-
@
|
|
29
|
+
@diagnostics = DiagnosticReporter.new(
|
|
29
30
|
level: options[:log_level] || "info",
|
|
30
|
-
handler: method(:
|
|
31
|
+
handler: method(:handle_evaluation_diagnostic)
|
|
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
|
|
|
38
40
|
# datafile
|
|
39
|
-
@
|
|
41
|
+
@datafile = InstanceEvaluationDataProvider.new(
|
|
40
42
|
datafile: EMPTY_DATAFILE,
|
|
41
|
-
|
|
43
|
+
diagnostics: @diagnostics
|
|
42
44
|
)
|
|
43
45
|
|
|
44
46
|
@modules_manager = Featurevisor::Modules::ModulesManager.new(
|
|
@@ -62,21 +64,35 @@ module Featurevisor
|
|
|
62
64
|
# Set the log level
|
|
63
65
|
# @param level [String] Log level
|
|
64
66
|
def set_log_level(level)
|
|
65
|
-
@
|
|
67
|
+
@diagnostics.set_level(level)
|
|
66
68
|
end
|
|
67
69
|
|
|
68
|
-
def
|
|
70
|
+
def handle_evaluation_diagnostic(level, message, details = nil)
|
|
69
71
|
details = (details || {}).dup
|
|
70
|
-
code = details[:reason] || details["reason"] || message
|
|
72
|
+
code = details.delete(:code) || details.delete("code") || details[:reason] || details["reason"] || message
|
|
71
73
|
code = "deprecated_feature" if message == "feature is deprecated"
|
|
72
74
|
code = "deprecated_variable" if message == "variable is deprecated"
|
|
73
75
|
code = "feature_not_found" if message == "feature not found"
|
|
74
76
|
code = "variable_not_found" if message == "variable schema not found"
|
|
75
77
|
code = "no_variations" if message == "no variations"
|
|
76
78
|
code = "invalid_bucket_by" if message == "invalid bucketBy"
|
|
77
|
-
|
|
79
|
+
code = "evaluation_error" if message == "Error during evaluation"
|
|
80
|
+
code = "conditions_parse_error" if message == "Error parsing conditions"
|
|
81
|
+
original_error = details.delete(:error) || details.delete("error")
|
|
82
|
+
nested_details = details.delete(:details) || details.delete("details")
|
|
83
|
+
details.merge!(nested_details) if nested_details.is_a?(Hash)
|
|
84
|
+
if details.key?(:feature_key) && details.key?(:reason)
|
|
85
|
+
evaluation = details.dup
|
|
86
|
+
details = {
|
|
87
|
+
featureKey: evaluation[:feature_key],
|
|
88
|
+
variableKey: evaluation[:variable_key],
|
|
89
|
+
reason: evaluation[:reason],
|
|
90
|
+
evaluation: camelize_diagnostic_value(evaluation)
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
report_diagnostic(level: level, code: code.to_s, message: message, details: details, originalError: original_error)
|
|
78
94
|
end
|
|
79
|
-
private :
|
|
95
|
+
private :handle_evaluation_diagnostic
|
|
80
96
|
|
|
81
97
|
# Set the datafile
|
|
82
98
|
# @param datafile [Hash, String] Datafile content or JSON string
|
|
@@ -94,17 +110,18 @@ module Featurevisor
|
|
|
94
110
|
end
|
|
95
111
|
unless parsed_datafile.is_a?(Hash) && parsed_datafile[:schemaVersion].is_a?(String) &&
|
|
96
112
|
parsed_datafile[:revision].is_a?(String) && parsed_datafile[:segments].is_a?(Hash) &&
|
|
97
|
-
parsed_datafile[:features].is_a?(Hash)
|
|
113
|
+
parsed_datafile[:features].is_a?(Hash) &&
|
|
114
|
+
(!parsed_datafile.key?(:variables) || parsed_datafile[:variables].is_a?(Hash))
|
|
98
115
|
raise ArgumentError, "Invalid datafile"
|
|
99
116
|
end
|
|
100
|
-
next_datafile = replace ? parsed_datafile : merge_datafiles(@
|
|
101
|
-
|
|
117
|
+
next_datafile = replace ? parsed_datafile : merge_datafiles(@datafile.get_datafile, parsed_datafile)
|
|
118
|
+
new_datafile = InstanceEvaluationDataProvider.new(
|
|
102
119
|
datafile: next_datafile,
|
|
103
|
-
|
|
120
|
+
diagnostics: @diagnostics
|
|
104
121
|
)
|
|
105
122
|
|
|
106
|
-
details = Featurevisor::Events.get_params_for_datafile_set_event(@
|
|
107
|
-
@
|
|
123
|
+
details = Featurevisor::Events.get_params_for_datafile_set_event(@datafile, new_datafile, replace)
|
|
124
|
+
@datafile = new_datafile
|
|
108
125
|
|
|
109
126
|
report_diagnostic(
|
|
110
127
|
level: "info",
|
|
@@ -126,60 +143,68 @@ module Featurevisor
|
|
|
126
143
|
# Set sticky features
|
|
127
144
|
# @param sticky [Hash] Sticky features
|
|
128
145
|
# @param replace [Boolean] Whether to replace existing sticky features
|
|
129
|
-
def
|
|
130
|
-
previous_sticky_features = @
|
|
146
|
+
def set_sticky_features(sticky, replace = false)
|
|
147
|
+
previous_sticky_features = @sticky_features || {}
|
|
131
148
|
|
|
132
149
|
if replace
|
|
133
|
-
@
|
|
150
|
+
@sticky_features = sticky
|
|
134
151
|
else
|
|
135
|
-
@
|
|
136
|
-
**@
|
|
152
|
+
@sticky_features = {
|
|
153
|
+
**@sticky_features,
|
|
137
154
|
**sticky
|
|
138
155
|
}
|
|
139
156
|
end
|
|
140
157
|
|
|
141
|
-
params = Featurevisor::Events.
|
|
158
|
+
params = Featurevisor::Events.get_params_for_sticky_features_set_event(previous_sticky_features, @sticky_features, replace)
|
|
142
159
|
|
|
143
160
|
report_diagnostic(
|
|
144
161
|
level: "info",
|
|
145
|
-
code: "
|
|
162
|
+
code: "sticky_features_set",
|
|
146
163
|
message: "Sticky features set",
|
|
147
164
|
details: params
|
|
148
165
|
)
|
|
149
|
-
@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)
|
|
150
175
|
end
|
|
151
176
|
|
|
152
177
|
# Get the revision
|
|
153
178
|
# @return [String] Revision string
|
|
154
179
|
def get_revision
|
|
155
|
-
@
|
|
180
|
+
@datafile.get_revision
|
|
156
181
|
end
|
|
157
182
|
|
|
158
183
|
def get_schema_version
|
|
159
|
-
@
|
|
184
|
+
@datafile.get_schema_version
|
|
160
185
|
end
|
|
161
186
|
|
|
162
187
|
def get_segment(segment_key)
|
|
163
|
-
@
|
|
188
|
+
@datafile.get_segment(segment_key)
|
|
164
189
|
end
|
|
165
190
|
|
|
166
191
|
def get_feature_keys
|
|
167
|
-
@
|
|
192
|
+
@datafile.get_feature_keys
|
|
168
193
|
end
|
|
169
194
|
|
|
170
|
-
def get_variable_keys(feature_key)
|
|
171
|
-
@
|
|
195
|
+
def get_variable_keys(feature_key = nil)
|
|
196
|
+
@datafile.get_variable_keys(feature_key)
|
|
172
197
|
end
|
|
173
198
|
|
|
174
199
|
def has_variations?(feature_key)
|
|
175
|
-
@
|
|
200
|
+
@datafile.has_variations?(feature_key)
|
|
176
201
|
end
|
|
177
202
|
|
|
178
203
|
# Get a feature by key
|
|
179
204
|
# @param feature_key [String] Feature key
|
|
180
205
|
# @return [Hash, nil] Feature data or nil if not found
|
|
181
206
|
def get_feature(feature_key)
|
|
182
|
-
@
|
|
207
|
+
@datafile.get_feature(feature_key)
|
|
183
208
|
end
|
|
184
209
|
|
|
185
210
|
# Add a module
|
|
@@ -189,8 +214,8 @@ module Featurevisor
|
|
|
189
214
|
@modules_manager.add(mod)
|
|
190
215
|
end
|
|
191
216
|
|
|
192
|
-
def remove_module(
|
|
193
|
-
@modules_manager.remove(
|
|
217
|
+
def remove_module(name)
|
|
218
|
+
@modules_manager.remove(name)
|
|
194
219
|
end
|
|
195
220
|
|
|
196
221
|
# Subscribe to an event
|
|
@@ -198,11 +223,15 @@ module Featurevisor
|
|
|
198
223
|
# @param callback [Proc] Callback function
|
|
199
224
|
# @return [Proc] Unsubscribe function
|
|
200
225
|
def on(event_name, callback)
|
|
226
|
+
return -> {} if @closed
|
|
227
|
+
|
|
201
228
|
@emitter.on(event_name, callback)
|
|
202
229
|
end
|
|
203
230
|
|
|
204
231
|
# Close the instance
|
|
205
232
|
def close
|
|
233
|
+
return if @closed
|
|
234
|
+
|
|
206
235
|
@closed = true
|
|
207
236
|
@modules_manager.close_all
|
|
208
237
|
@module_diagnostic_subscriptions = []
|
|
@@ -257,7 +286,8 @@ module Featurevisor
|
|
|
257
286
|
Featurevisor::ChildInstance.new(
|
|
258
287
|
parent: self,
|
|
259
288
|
context: get_context(context),
|
|
260
|
-
|
|
289
|
+
sticky_features: options[:sticky_features] || options[:stickyFeatures],
|
|
290
|
+
sticky_variables: options[:sticky_variables] || options[:stickyVariables]
|
|
261
291
|
)
|
|
262
292
|
end
|
|
263
293
|
|
|
@@ -285,7 +315,7 @@ module Featurevisor
|
|
|
285
315
|
evaluation = evaluate_flag(feature_key, context, options)
|
|
286
316
|
evaluation[:enabled] == true
|
|
287
317
|
rescue => e
|
|
288
|
-
|
|
318
|
+
report_diagnostic(level: "error", code: "evaluation_error", message: "isEnabled failed", originalError: e, details: { featureKey: feature_key })
|
|
289
319
|
false
|
|
290
320
|
end
|
|
291
321
|
end
|
|
@@ -313,7 +343,7 @@ module Featurevisor
|
|
|
313
343
|
begin
|
|
314
344
|
evaluation = evaluate_variation(feature_key, context, options)
|
|
315
345
|
|
|
316
|
-
if evaluation
|
|
346
|
+
if evaluation.key?(:variation_value)
|
|
317
347
|
evaluation[:variation_value]
|
|
318
348
|
elsif evaluation[:variation]
|
|
319
349
|
evaluation[:variation][:value]
|
|
@@ -321,7 +351,7 @@ module Featurevisor
|
|
|
321
351
|
nil
|
|
322
352
|
end
|
|
323
353
|
rescue => e
|
|
324
|
-
|
|
354
|
+
report_diagnostic(level: "error", code: "evaluation_error", message: "getVariation failed", originalError: e, details: { featureKey: feature_key })
|
|
325
355
|
nil
|
|
326
356
|
end
|
|
327
357
|
end
|
|
@@ -332,12 +362,17 @@ module Featurevisor
|
|
|
332
362
|
# @param context [Hash] Context
|
|
333
363
|
# @param options [Hash] Override options
|
|
334
364
|
# @return [Hash] Evaluation result
|
|
335
|
-
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
|
+
|
|
336
371
|
Featurevisor::Evaluate.evaluate_with_modules(
|
|
337
|
-
get_evaluation_dependencies(
|
|
372
|
+
get_evaluation_dependencies(context_or_options, options).merge(
|
|
338
373
|
type: "variable",
|
|
339
|
-
feature_key:
|
|
340
|
-
variable_key:
|
|
374
|
+
feature_key: feature_or_variable_key,
|
|
375
|
+
variable_key: variable_key_or_context
|
|
341
376
|
)
|
|
342
377
|
)
|
|
343
378
|
end
|
|
@@ -348,13 +383,13 @@ module Featurevisor
|
|
|
348
383
|
# @param context [Hash] Context
|
|
349
384
|
# @param options [Hash] Override options
|
|
350
385
|
# @return [Object, nil] Variable value or nil
|
|
351
|
-
def get_variable(
|
|
386
|
+
def get_variable(feature_or_variable_key, variable_key_or_context = nil, context_or_options = {}, options = {})
|
|
352
387
|
begin
|
|
353
|
-
evaluation = evaluate_variable(
|
|
388
|
+
evaluation = evaluate_variable(feature_or_variable_key, variable_key_or_context, context_or_options, options)
|
|
354
389
|
|
|
355
|
-
if
|
|
356
|
-
|
|
357
|
-
|
|
390
|
+
if evaluation.key?(:variable_value)
|
|
391
|
+
variable_type = evaluation.dig(:variable_schema, :type) || evaluation.dig(:variable, :type)
|
|
392
|
+
if variable_type == "json" &&
|
|
358
393
|
evaluation[:variable_value].is_a?(String)
|
|
359
394
|
JSON.parse(evaluation[:variable_value], symbolize_names: true)
|
|
360
395
|
else
|
|
@@ -364,7 +399,7 @@ module Featurevisor
|
|
|
364
399
|
nil
|
|
365
400
|
end
|
|
366
401
|
rescue => e
|
|
367
|
-
|
|
402
|
+
report_diagnostic(level: "error", code: "evaluation_error", message: "getVariable failed", originalError: e, details: { variableKey: variable_key_or_context || feature_or_variable_key })
|
|
368
403
|
nil
|
|
369
404
|
end
|
|
370
405
|
end
|
|
@@ -375,8 +410,8 @@ module Featurevisor
|
|
|
375
410
|
# @param context [Hash] Context
|
|
376
411
|
# @param options [Hash] Override options
|
|
377
412
|
# @return [Boolean, nil] Boolean value or nil
|
|
378
|
-
def get_variable_boolean(
|
|
379
|
-
variable_value = get_variable(
|
|
413
|
+
def get_variable_boolean(*args)
|
|
414
|
+
variable_value = get_variable(*args)
|
|
380
415
|
get_value_by_type(variable_value, "boolean")
|
|
381
416
|
end
|
|
382
417
|
|
|
@@ -386,8 +421,8 @@ module Featurevisor
|
|
|
386
421
|
# @param context [Hash] Context
|
|
387
422
|
# @param options [Hash] Override options
|
|
388
423
|
# @return [String, nil] String value or nil
|
|
389
|
-
def get_variable_string(
|
|
390
|
-
variable_value = get_variable(
|
|
424
|
+
def get_variable_string(*args)
|
|
425
|
+
variable_value = get_variable(*args)
|
|
391
426
|
get_value_by_type(variable_value, "string")
|
|
392
427
|
end
|
|
393
428
|
|
|
@@ -397,8 +432,8 @@ module Featurevisor
|
|
|
397
432
|
# @param context [Hash] Context
|
|
398
433
|
# @param options [Hash] Override options
|
|
399
434
|
# @return [Integer, nil] Integer value or nil
|
|
400
|
-
def get_variable_integer(
|
|
401
|
-
variable_value = get_variable(
|
|
435
|
+
def get_variable_integer(*args)
|
|
436
|
+
variable_value = get_variable(*args)
|
|
402
437
|
get_value_by_type(variable_value, "integer")
|
|
403
438
|
end
|
|
404
439
|
|
|
@@ -408,8 +443,8 @@ module Featurevisor
|
|
|
408
443
|
# @param context [Hash] Context
|
|
409
444
|
# @param options [Hash] Override options
|
|
410
445
|
# @return [Float, nil] Float value or nil
|
|
411
|
-
def get_variable_double(
|
|
412
|
-
variable_value = get_variable(
|
|
446
|
+
def get_variable_double(*args)
|
|
447
|
+
variable_value = get_variable(*args)
|
|
413
448
|
get_value_by_type(variable_value, "double")
|
|
414
449
|
end
|
|
415
450
|
|
|
@@ -419,8 +454,8 @@ module Featurevisor
|
|
|
419
454
|
# @param context [Hash] Context
|
|
420
455
|
# @param options [Hash] Override options
|
|
421
456
|
# @return [Array, nil] Array value or nil
|
|
422
|
-
def get_variable_array(
|
|
423
|
-
variable_value = get_variable(
|
|
457
|
+
def get_variable_array(*args)
|
|
458
|
+
variable_value = get_variable(*args)
|
|
424
459
|
get_value_by_type(variable_value, "array")
|
|
425
460
|
end
|
|
426
461
|
|
|
@@ -430,8 +465,8 @@ module Featurevisor
|
|
|
430
465
|
# @param context [Hash] Context
|
|
431
466
|
# @param options [Hash] Override options
|
|
432
467
|
# @return [Hash, nil] Object value or nil
|
|
433
|
-
def get_variable_object(
|
|
434
|
-
variable_value = get_variable(
|
|
468
|
+
def get_variable_object(*args)
|
|
469
|
+
variable_value = get_variable(*args)
|
|
435
470
|
get_value_by_type(variable_value, "object")
|
|
436
471
|
end
|
|
437
472
|
|
|
@@ -441,8 +476,8 @@ module Featurevisor
|
|
|
441
476
|
# @param context [Hash] Context
|
|
442
477
|
# @param options [Hash] Override options
|
|
443
478
|
# @return [Object, nil] JSON value or nil
|
|
444
|
-
def get_variable_json(
|
|
445
|
-
variable_value = get_variable(
|
|
479
|
+
def get_variable_json(*args)
|
|
480
|
+
variable_value = get_variable(*args)
|
|
446
481
|
get_value_by_type(variable_value, "json")
|
|
447
482
|
end
|
|
448
483
|
|
|
@@ -451,10 +486,10 @@ module Featurevisor
|
|
|
451
486
|
# @param feature_keys [Array<String>] Feature keys to evaluate
|
|
452
487
|
# @param options [Hash] Override options
|
|
453
488
|
# @return [Hash] All evaluations
|
|
454
|
-
def
|
|
489
|
+
def get_feature_evaluations(context = {}, feature_keys = [], options = {})
|
|
455
490
|
result = {}
|
|
456
491
|
|
|
457
|
-
|
|
492
|
+
keys = feature_keys.size > 0 ? feature_keys : @datafile.get_feature_keys
|
|
458
493
|
|
|
459
494
|
keys.each do |feature_key|
|
|
460
495
|
# Convert symbol keys to strings for evaluation functions
|
|
@@ -466,13 +501,13 @@ module Featurevisor
|
|
|
466
501
|
}
|
|
467
502
|
|
|
468
503
|
# variation
|
|
469
|
-
if @
|
|
504
|
+
if @datafile.has_variations?(feature_key_str)
|
|
470
505
|
variation = get_variation(feature_key_str, context, options)
|
|
471
|
-
evaluated_feature[:variation] = variation
|
|
506
|
+
evaluated_feature[:variation] = variation unless variation.nil?
|
|
472
507
|
end
|
|
473
508
|
|
|
474
509
|
# variables
|
|
475
|
-
variable_keys = @
|
|
510
|
+
variable_keys = @datafile.get_variable_keys(feature_key_str)
|
|
476
511
|
if variable_keys.size > 0
|
|
477
512
|
evaluated_feature[:variables] = {}
|
|
478
513
|
|
|
@@ -492,8 +527,100 @@ module Featurevisor
|
|
|
492
527
|
result
|
|
493
528
|
end
|
|
494
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
|
+
|
|
495
535
|
private
|
|
496
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
|
+
|
|
497
624
|
# Get evaluation dependencies
|
|
498
625
|
# @param context [Hash] Context
|
|
499
626
|
# @param options [Hash] Override options
|
|
@@ -501,13 +628,15 @@ module Featurevisor
|
|
|
501
628
|
def get_evaluation_dependencies(context, options = {})
|
|
502
629
|
{
|
|
503
630
|
context: get_context(context),
|
|
504
|
-
|
|
631
|
+
diagnostics: @diagnostics,
|
|
505
632
|
modules_manager: @modules_manager,
|
|
506
|
-
|
|
507
|
-
sticky: options[:
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
633
|
+
datafile: @datafile,
|
|
634
|
+
sticky: options[:__featurevisor_child_sticky_features] || @sticky_features,
|
|
635
|
+
sticky_variables: options[:__featurevisor_child_sticky_variables] || @sticky_variables,
|
|
636
|
+
}.tap do |dependencies|
|
|
637
|
+
dependencies[:default_variation_value] = options[:default_variation_value] if options.key?(:default_variation_value)
|
|
638
|
+
dependencies[:default_variable_value] = options[:default_variable_value] if options.key?(:default_variable_value)
|
|
639
|
+
end
|
|
511
640
|
end
|
|
512
641
|
|
|
513
642
|
# Get value by type
|
|
@@ -554,6 +683,10 @@ module Featurevisor
|
|
|
554
683
|
features: {
|
|
555
684
|
**(previous[:features] || {}),
|
|
556
685
|
**(incoming[:features] || {})
|
|
686
|
+
},
|
|
687
|
+
variables: {
|
|
688
|
+
**(previous[:variables] || {}),
|
|
689
|
+
**(incoming[:variables] || {})
|
|
557
690
|
}
|
|
558
691
|
}.compact
|
|
559
692
|
end
|
|
@@ -584,7 +717,7 @@ module Featurevisor
|
|
|
584
717
|
diagnostic = (diagnostic || {}).dup
|
|
585
718
|
diagnostic[:level] ||= "info"
|
|
586
719
|
diagnostic[:module] = source_module.name if source_module && source_module.name
|
|
587
|
-
details = (diagnostic[:details] || {}).dup
|
|
720
|
+
details = camelize_diagnostic_value((diagnostic[:details] || {}).dup)
|
|
588
721
|
legacy_module_name = diagnostic.delete(:module_name)
|
|
589
722
|
legacy_original_error = diagnostic.delete(:original_error)
|
|
590
723
|
diagnostic[:moduleName] = legacy_module_name if !diagnostic.key?(:moduleName) && !legacy_module_name.nil?
|
|
@@ -609,7 +742,7 @@ module Featurevisor
|
|
|
609
742
|
end
|
|
610
743
|
|
|
611
744
|
if @on_diagnostic
|
|
612
|
-
if should_report_diagnostic?(diagnostic[:level], @
|
|
745
|
+
if should_report_diagnostic?(diagnostic[:level], @diagnostics.level)
|
|
613
746
|
begin
|
|
614
747
|
@on_diagnostic.call(diagnostic)
|
|
615
748
|
rescue => e
|
|
@@ -617,7 +750,7 @@ module Featurevisor
|
|
|
617
750
|
end
|
|
618
751
|
end
|
|
619
752
|
else
|
|
620
|
-
|
|
753
|
+
DiagnosticReporter.new(level: @diagnostics.level).log(
|
|
621
754
|
diagnostic[:level],
|
|
622
755
|
diagnostic[:message],
|
|
623
756
|
diagnostic
|
|
@@ -638,6 +771,21 @@ module Featurevisor
|
|
|
638
771
|
|
|
639
772
|
subscriber_index >= diagnostic_index
|
|
640
773
|
end
|
|
774
|
+
|
|
775
|
+
def camelize_diagnostic_value(value)
|
|
776
|
+
case value
|
|
777
|
+
when Hash
|
|
778
|
+
value.each_with_object({}) do |(key, item), result|
|
|
779
|
+
normalized_key = key.to_s.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }.to_sym
|
|
780
|
+
result[normalized_key] = camelize_diagnostic_value(item)
|
|
781
|
+
end
|
|
782
|
+
when Array
|
|
783
|
+
value.map { |item| camelize_diagnostic_value(item) }
|
|
784
|
+
else
|
|
785
|
+
value
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
private :camelize_diagnostic_value
|
|
641
789
|
end
|
|
642
790
|
|
|
643
791
|
# Create a new Featurevisor instance
|
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
|