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
|
@@ -19,6 +19,7 @@ module Featurevisor
|
|
|
19
19
|
VARIABLE_DISABLED = "variable_disabled" # feature is disabled, and variable's disabledValue is used
|
|
20
20
|
VARIABLE_OVERRIDE_VARIATION = "variable_override_variation" # variable overridden from inside a variation
|
|
21
21
|
VARIABLE_OVERRIDE_RULE = "variable_override_rule" # variable overridden from inside a rule
|
|
22
|
+
REQUIRED_FEATURES_UNMET = "required_features_unmet"
|
|
22
23
|
|
|
23
24
|
# Common
|
|
24
25
|
NO_MATCH = "no_match" # no rules matched
|
|
@@ -35,6 +36,50 @@ module Featurevisor
|
|
|
35
36
|
|
|
36
37
|
# Evaluation module for feature flag evaluation
|
|
37
38
|
module Evaluate
|
|
39
|
+
def self.required_features_are_matched(requirements, datafile, options)
|
|
40
|
+
return true if requirements.nil?
|
|
41
|
+
|
|
42
|
+
items = requirements.is_a?(Array) ? requirements : [requirements]
|
|
43
|
+
clean_options = options.reject do |key, _|
|
|
44
|
+
%i[feature_key variable_key default_variation_value default_variable_value].include?(key)
|
|
45
|
+
end
|
|
46
|
+
items.all? do |required|
|
|
47
|
+
if required.is_a?(String)
|
|
48
|
+
key = required
|
|
49
|
+
enabled = true
|
|
50
|
+
variation = nil
|
|
51
|
+
elsif required.key?(:feature)
|
|
52
|
+
key = required[:feature]
|
|
53
|
+
enabled = required.key?(:enabled) ? required[:enabled] : true
|
|
54
|
+
variation = required[:variation]
|
|
55
|
+
else
|
|
56
|
+
key = required[:key]
|
|
57
|
+
enabled = true
|
|
58
|
+
variation = required[:variation]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
flag = evaluate_with_modules(clean_options.merge(type: "flag", feature_key: key, datafile: datafile))
|
|
62
|
+
next false unless (flag[:enabled] == true) == enabled
|
|
63
|
+
next true if variation.nil?
|
|
64
|
+
|
|
65
|
+
evaluated_variation = evaluate_with_modules(clean_options.merge(type: "variation", feature_key: key, datafile: datafile))
|
|
66
|
+
value = evaluated_variation.key?(:variation_value) ? evaluated_variation[:variation_value] : evaluated_variation.dig(:variation, :value)
|
|
67
|
+
value == variation
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.variable_override_matches?(override, datafile, context, options)
|
|
72
|
+
return false unless required_features_are_matched(override[:requiredFeatures], datafile, options)
|
|
73
|
+
|
|
74
|
+
conditions_match = !override[:conditions] || datafile.all_conditions_are_matched(
|
|
75
|
+
datafile.parse_conditions_if_stringified(override[:conditions]), context
|
|
76
|
+
)
|
|
77
|
+
segments_match = !override[:segments] || datafile.all_segments_are_matched(
|
|
78
|
+
datafile.parse_segments_if_stringified(override[:segments]), context
|
|
79
|
+
)
|
|
80
|
+
conditions_match && segments_match &&
|
|
81
|
+
(override.key?(:conditions) || override.key?(:segments) || override.key?(:requiredFeatures))
|
|
82
|
+
end
|
|
38
83
|
|
|
39
84
|
# Evaluate with modules
|
|
40
85
|
# @param options [Hash] Evaluation options
|
|
@@ -53,17 +98,18 @@ module Featurevisor
|
|
|
53
98
|
evaluation = evaluate(result_options)
|
|
54
99
|
|
|
55
100
|
# Default: variation
|
|
56
|
-
if
|
|
101
|
+
if result_options.key?(:default_variation_value) &&
|
|
57
102
|
evaluation[:type] == "variation" &&
|
|
58
|
-
evaluation
|
|
59
|
-
|
|
103
|
+
!evaluation.key?(:variation_value) &&
|
|
104
|
+
!evaluation.key?(:variation)
|
|
105
|
+
evaluation[:variation_value] = result_options[:default_variation_value]
|
|
60
106
|
end
|
|
61
107
|
|
|
62
108
|
# Default: variable
|
|
63
|
-
if
|
|
109
|
+
if result_options.key?(:default_variable_value) &&
|
|
64
110
|
evaluation[:type] == "variable" &&
|
|
65
|
-
evaluation
|
|
66
|
-
evaluation[:variable_value] =
|
|
111
|
+
!evaluation.key?(:variable_value)
|
|
112
|
+
evaluation[:variable_value] = result_options[:default_variable_value]
|
|
67
113
|
end
|
|
68
114
|
|
|
69
115
|
# Run after modules
|
|
@@ -76,7 +122,7 @@ module Featurevisor
|
|
|
76
122
|
type = options[:type]
|
|
77
123
|
feature_key = options[:feature_key]
|
|
78
124
|
variable_key = options[:variable_key]
|
|
79
|
-
|
|
125
|
+
diagnostics = options[:diagnostics]
|
|
80
126
|
|
|
81
127
|
evaluation = {
|
|
82
128
|
type: type,
|
|
@@ -86,7 +132,7 @@ module Featurevisor
|
|
|
86
132
|
error: e
|
|
87
133
|
}
|
|
88
134
|
|
|
89
|
-
|
|
135
|
+
diagnostics.error("Error during evaluation", evaluation)
|
|
90
136
|
|
|
91
137
|
evaluation
|
|
92
138
|
end
|
|
@@ -100,8 +146,8 @@ module Featurevisor
|
|
|
100
146
|
feature_key = options[:feature_key]
|
|
101
147
|
variable_key = options[:variable_key]
|
|
102
148
|
context = options[:context]
|
|
103
|
-
|
|
104
|
-
|
|
149
|
+
diagnostics = options[:diagnostics]
|
|
150
|
+
datafile = options[:datafile]
|
|
105
151
|
sticky = options[:sticky]
|
|
106
152
|
modules_manager = options[:modules_manager]
|
|
107
153
|
evaluation = nil
|
|
@@ -120,7 +166,7 @@ module Featurevisor
|
|
|
120
166
|
reason: Featurevisor::EvaluationReason::DISABLED
|
|
121
167
|
}
|
|
122
168
|
|
|
123
|
-
feature =
|
|
169
|
+
feature = datafile.get_feature(feature_key)
|
|
124
170
|
|
|
125
171
|
# serve variable default value if feature is disabled (if explicitly specified)
|
|
126
172
|
if type == "variable"
|
|
@@ -156,7 +202,7 @@ module Featurevisor
|
|
|
156
202
|
end
|
|
157
203
|
|
|
158
204
|
# serve disabled variation value if feature is disabled (if explicitly specified)
|
|
159
|
-
if type == "variation" && feature && feature
|
|
205
|
+
if type == "variation" && feature && feature.key?(:disabledVariationValue)
|
|
160
206
|
evaluation = {
|
|
161
207
|
type: type,
|
|
162
208
|
feature_key: feature_key,
|
|
@@ -166,7 +212,7 @@ module Featurevisor
|
|
|
166
212
|
}
|
|
167
213
|
end
|
|
168
214
|
|
|
169
|
-
|
|
215
|
+
diagnostics.debug("feature is disabled", evaluation)
|
|
170
216
|
|
|
171
217
|
return evaluation
|
|
172
218
|
end
|
|
@@ -186,7 +232,7 @@ module Featurevisor
|
|
|
186
232
|
enabled: sticky_feature[:enabled]
|
|
187
233
|
}
|
|
188
234
|
|
|
189
|
-
|
|
235
|
+
diagnostics.debug("using sticky enabled", evaluation)
|
|
190
236
|
|
|
191
237
|
return evaluation
|
|
192
238
|
end
|
|
@@ -195,7 +241,7 @@ module Featurevisor
|
|
|
195
241
|
if type == "variation"
|
|
196
242
|
variation_value = sticky_feature[:variation]
|
|
197
243
|
|
|
198
|
-
|
|
244
|
+
if sticky_feature.key?(:variation)
|
|
199
245
|
evaluation = {
|
|
200
246
|
type: type,
|
|
201
247
|
feature_key: feature_key,
|
|
@@ -203,7 +249,7 @@ module Featurevisor
|
|
|
203
249
|
variation_value: variation_value
|
|
204
250
|
}
|
|
205
251
|
|
|
206
|
-
|
|
252
|
+
diagnostics.debug("using sticky variation", evaluation)
|
|
207
253
|
|
|
208
254
|
return evaluation
|
|
209
255
|
end
|
|
@@ -223,7 +269,7 @@ module Featurevisor
|
|
|
223
269
|
variable_value: variable_value
|
|
224
270
|
}
|
|
225
271
|
|
|
226
|
-
|
|
272
|
+
diagnostics.debug("using sticky variable", evaluation)
|
|
227
273
|
|
|
228
274
|
return evaluation
|
|
229
275
|
end
|
|
@@ -231,7 +277,7 @@ module Featurevisor
|
|
|
231
277
|
end
|
|
232
278
|
|
|
233
279
|
# Feature
|
|
234
|
-
feature = feature_key.is_a?(String) ?
|
|
280
|
+
feature = feature_key.is_a?(String) ? datafile.get_feature(feature_key) : feature_key
|
|
235
281
|
|
|
236
282
|
# feature: not found
|
|
237
283
|
unless feature
|
|
@@ -241,14 +287,14 @@ module Featurevisor
|
|
|
241
287
|
reason: Featurevisor::EvaluationReason::FEATURE_NOT_FOUND
|
|
242
288
|
}
|
|
243
289
|
|
|
244
|
-
|
|
290
|
+
diagnostics.warn("feature not found", evaluation)
|
|
245
291
|
|
|
246
292
|
return evaluation
|
|
247
293
|
end
|
|
248
294
|
|
|
249
295
|
# feature: deprecated
|
|
250
296
|
if type == "flag" && feature[:deprecated]
|
|
251
|
-
|
|
297
|
+
diagnostics.warn("feature is deprecated", { feature_key: feature_key })
|
|
252
298
|
end
|
|
253
299
|
|
|
254
300
|
# variableSchema
|
|
@@ -268,13 +314,13 @@ module Featurevisor
|
|
|
268
314
|
variable_key: variable_key
|
|
269
315
|
}
|
|
270
316
|
|
|
271
|
-
|
|
317
|
+
diagnostics.warn("variable schema not found", evaluation)
|
|
272
318
|
|
|
273
319
|
return evaluation
|
|
274
320
|
end
|
|
275
321
|
|
|
276
322
|
if variable_schema[:deprecated]
|
|
277
|
-
|
|
323
|
+
diagnostics.warn("variable is deprecated", {
|
|
278
324
|
feature_key: feature_key,
|
|
279
325
|
variable_key: variable_key
|
|
280
326
|
})
|
|
@@ -289,13 +335,13 @@ module Featurevisor
|
|
|
289
335
|
reason: Featurevisor::EvaluationReason::NO_VARIATIONS
|
|
290
336
|
}
|
|
291
337
|
|
|
292
|
-
|
|
338
|
+
diagnostics.warn("no variations", evaluation)
|
|
293
339
|
|
|
294
340
|
return evaluation
|
|
295
341
|
end
|
|
296
342
|
|
|
297
343
|
# Forced
|
|
298
|
-
force_result =
|
|
344
|
+
force_result = datafile.get_matched_force(feature, context)
|
|
299
345
|
force = force_result[:force]
|
|
300
346
|
force_index = force_result[:forceIndex]
|
|
301
347
|
|
|
@@ -311,7 +357,7 @@ module Featurevisor
|
|
|
311
357
|
enabled: force[:enabled]
|
|
312
358
|
}
|
|
313
359
|
|
|
314
|
-
|
|
360
|
+
diagnostics.debug("forced enabled found", evaluation)
|
|
315
361
|
|
|
316
362
|
return evaluation
|
|
317
363
|
end
|
|
@@ -330,7 +376,7 @@ module Featurevisor
|
|
|
330
376
|
variation: variation
|
|
331
377
|
}
|
|
332
378
|
|
|
333
|
-
|
|
379
|
+
diagnostics.debug("forced variation found", evaluation)
|
|
334
380
|
|
|
335
381
|
return evaluation
|
|
336
382
|
end
|
|
@@ -350,46 +396,16 @@ module Featurevisor
|
|
|
350
396
|
variable_value: variable_value
|
|
351
397
|
}
|
|
352
398
|
|
|
353
|
-
|
|
399
|
+
diagnostics.debug("forced variable", evaluation)
|
|
354
400
|
|
|
355
401
|
return evaluation
|
|
356
402
|
end
|
|
357
403
|
end
|
|
358
404
|
|
|
359
405
|
# Required
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
required_variation = nil
|
|
364
|
-
|
|
365
|
-
if required.is_a?(String)
|
|
366
|
-
required_key = required
|
|
367
|
-
else
|
|
368
|
-
required_key = required[:key]
|
|
369
|
-
required_variation = required[:variation]
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
required_evaluation = evaluate(options.merge(type: "flag", feature_key: required_key))
|
|
373
|
-
required_is_enabled = required_evaluation[:enabled]
|
|
374
|
-
|
|
375
|
-
next false unless required_is_enabled
|
|
376
|
-
|
|
377
|
-
if required_variation
|
|
378
|
-
required_variation_evaluation = evaluate(options.merge(type: "variation", feature_key: required_key))
|
|
379
|
-
|
|
380
|
-
required_variation_value = nil
|
|
381
|
-
|
|
382
|
-
if has_key?(required_variation_evaluation, :variation_value)
|
|
383
|
-
required_variation_value = fetch_with_symbol_key(required_variation_evaluation, :variation_value)
|
|
384
|
-
elsif required_variation_evaluation[:variation]
|
|
385
|
-
required_variation_value = required_variation_evaluation[:variation][:value]
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
next required_variation_value == required_variation
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
true
|
|
392
|
-
end
|
|
406
|
+
required_features = feature[:requiredFeatures] || feature[:required]
|
|
407
|
+
if type == "flag" && required_features && !Array(required_features).empty?
|
|
408
|
+
required_features_are_enabled = required_features_are_matched(required_features, datafile, options)
|
|
393
409
|
|
|
394
410
|
unless required_features_are_enabled
|
|
395
411
|
evaluation = {
|
|
@@ -397,10 +413,11 @@ module Featurevisor
|
|
|
397
413
|
feature_key: feature_key,
|
|
398
414
|
reason: Featurevisor::EvaluationReason::REQUIRED,
|
|
399
415
|
required: feature[:required],
|
|
416
|
+
required_features: feature[:requiredFeatures],
|
|
400
417
|
enabled: required_features_are_enabled
|
|
401
418
|
}
|
|
402
419
|
|
|
403
|
-
|
|
420
|
+
diagnostics.debug("required features not enabled", evaluation)
|
|
404
421
|
|
|
405
422
|
return evaluation
|
|
406
423
|
end
|
|
@@ -412,7 +429,7 @@ module Featurevisor
|
|
|
412
429
|
feature_key: feature_key,
|
|
413
430
|
bucket_by: feature[:bucketBy],
|
|
414
431
|
context: context,
|
|
415
|
-
|
|
432
|
+
diagnostics: diagnostics
|
|
416
433
|
})
|
|
417
434
|
|
|
418
435
|
# Run bucket key modules
|
|
@@ -438,13 +455,13 @@ module Featurevisor
|
|
|
438
455
|
matched_allocation = nil
|
|
439
456
|
|
|
440
457
|
if type != "flag"
|
|
441
|
-
matched_traffic =
|
|
458
|
+
matched_traffic = datafile.get_matched_traffic(feature[:traffic], context)
|
|
442
459
|
|
|
443
460
|
if matched_traffic
|
|
444
|
-
matched_allocation =
|
|
461
|
+
matched_allocation = datafile.get_matched_allocation(matched_traffic, bucket_value)
|
|
445
462
|
end
|
|
446
463
|
else
|
|
447
|
-
matched_traffic =
|
|
464
|
+
matched_traffic = datafile.get_matched_traffic(feature[:traffic], context)
|
|
448
465
|
end
|
|
449
466
|
|
|
450
467
|
if matched_traffic
|
|
@@ -461,7 +478,7 @@ module Featurevisor
|
|
|
461
478
|
enabled: false
|
|
462
479
|
}
|
|
463
480
|
|
|
464
|
-
|
|
481
|
+
diagnostics.debug("matched rule with 0 percentage", evaluation)
|
|
465
482
|
|
|
466
483
|
return evaluation
|
|
467
484
|
end
|
|
@@ -487,7 +504,7 @@ module Featurevisor
|
|
|
487
504
|
enabled: matched_traffic[:enabled].nil? ? true : matched_traffic[:enabled]
|
|
488
505
|
}
|
|
489
506
|
|
|
490
|
-
|
|
507
|
+
diagnostics.debug("matched", evaluation)
|
|
491
508
|
|
|
492
509
|
return evaluation
|
|
493
510
|
end
|
|
@@ -502,7 +519,7 @@ module Featurevisor
|
|
|
502
519
|
enabled: false
|
|
503
520
|
}
|
|
504
521
|
|
|
505
|
-
|
|
522
|
+
diagnostics.debug("not matched", evaluation)
|
|
506
523
|
|
|
507
524
|
return evaluation
|
|
508
525
|
end
|
|
@@ -520,7 +537,7 @@ module Featurevisor
|
|
|
520
537
|
enabled: matched_traffic[:enabled]
|
|
521
538
|
}
|
|
522
539
|
|
|
523
|
-
|
|
540
|
+
diagnostics.debug("override from rule", evaluation)
|
|
524
541
|
|
|
525
542
|
return evaluation
|
|
526
543
|
end
|
|
@@ -538,7 +555,7 @@ module Featurevisor
|
|
|
538
555
|
enabled: true
|
|
539
556
|
}
|
|
540
557
|
|
|
541
|
-
|
|
558
|
+
diagnostics.debug("matched traffic", evaluation)
|
|
542
559
|
|
|
543
560
|
return evaluation
|
|
544
561
|
end
|
|
@@ -562,7 +579,7 @@ module Featurevisor
|
|
|
562
579
|
variation: variation
|
|
563
580
|
}
|
|
564
581
|
|
|
565
|
-
|
|
582
|
+
diagnostics.debug("override from rule", evaluation)
|
|
566
583
|
|
|
567
584
|
return evaluation
|
|
568
585
|
end
|
|
@@ -584,7 +601,7 @@ module Featurevisor
|
|
|
584
601
|
variation: variation
|
|
585
602
|
}
|
|
586
603
|
|
|
587
|
-
|
|
604
|
+
diagnostics.debug("allocated variation", evaluation)
|
|
588
605
|
|
|
589
606
|
return evaluation
|
|
590
607
|
end
|
|
@@ -600,17 +617,7 @@ module Featurevisor
|
|
|
600
617
|
has_key?(matched_traffic[:variableOverrides], variable_key)
|
|
601
618
|
overrides = fetch_with_symbol_key(matched_traffic[:variableOverrides], variable_key)
|
|
602
619
|
|
|
603
|
-
override_index = overrides.find_index
|
|
604
|
-
if o[:conditions]
|
|
605
|
-
conditions = o[:conditions].is_a?(String) && o[:conditions] != "*" ? JSON.parse(o[:conditions]) : o[:conditions]
|
|
606
|
-
datafile_reader.all_conditions_are_matched(conditions, context)
|
|
607
|
-
elsif o[:segments]
|
|
608
|
-
segments = datafile_reader.parse_segments_if_stringified(o[:segments])
|
|
609
|
-
datafile_reader.all_segments_are_matched(segments, context)
|
|
610
|
-
else
|
|
611
|
-
false
|
|
612
|
-
end
|
|
613
|
-
end
|
|
620
|
+
override_index = overrides.find_index { |o| variable_override_matches?(o, datafile, context, options) }
|
|
614
621
|
|
|
615
622
|
unless override_index.nil?
|
|
616
623
|
override = overrides[override_index]
|
|
@@ -626,10 +633,11 @@ module Featurevisor
|
|
|
626
633
|
variable_key: variable_key,
|
|
627
634
|
variable_schema: variable_schema,
|
|
628
635
|
variable_value: override[:value],
|
|
629
|
-
variable_override_index: override_index
|
|
636
|
+
variable_override_index: override_index,
|
|
637
|
+
variable_override_key: override[:key]
|
|
630
638
|
}
|
|
631
639
|
|
|
632
|
-
|
|
640
|
+
diagnostics.debug("variable override from rule", evaluation)
|
|
633
641
|
|
|
634
642
|
return evaluation
|
|
635
643
|
end
|
|
@@ -652,7 +660,7 @@ module Featurevisor
|
|
|
652
660
|
variable_value: variable_value
|
|
653
661
|
}
|
|
654
662
|
|
|
655
|
-
|
|
663
|
+
diagnostics.debug("override from rule", evaluation)
|
|
656
664
|
|
|
657
665
|
return evaluation
|
|
658
666
|
end
|
|
@@ -674,17 +682,7 @@ module Featurevisor
|
|
|
674
682
|
if variation && variation[:variableOverrides] && has_key?(variation[:variableOverrides], variable_key)
|
|
675
683
|
overrides = fetch_with_symbol_key(variation[:variableOverrides], variable_key)
|
|
676
684
|
|
|
677
|
-
override_index = overrides.find_index
|
|
678
|
-
if o[:conditions]
|
|
679
|
-
conditions = o[:conditions].is_a?(String) && o[:conditions] != "*" ? JSON.parse(o[:conditions]) : o[:conditions]
|
|
680
|
-
datafile_reader.all_conditions_are_matched(conditions, context)
|
|
681
|
-
elsif o[:segments]
|
|
682
|
-
segments = datafile_reader.parse_segments_if_stringified(o[:segments])
|
|
683
|
-
datafile_reader.all_segments_are_matched(segments, context)
|
|
684
|
-
else
|
|
685
|
-
false
|
|
686
|
-
end
|
|
687
|
-
end
|
|
685
|
+
override_index = overrides.find_index { |o| variable_override_matches?(o, datafile, context, options) }
|
|
688
686
|
|
|
689
687
|
unless override_index.nil?
|
|
690
688
|
override = overrides[override_index]
|
|
@@ -699,10 +697,11 @@ module Featurevisor
|
|
|
699
697
|
variable_key: variable_key,
|
|
700
698
|
variable_schema: variable_schema,
|
|
701
699
|
variable_value: override[:value],
|
|
702
|
-
variable_override_index: override_index
|
|
700
|
+
variable_override_index: override_index,
|
|
701
|
+
variable_override_key: override[:key]
|
|
703
702
|
}
|
|
704
703
|
|
|
705
|
-
|
|
704
|
+
diagnostics.debug("variable override from variation", evaluation)
|
|
706
705
|
|
|
707
706
|
return evaluation
|
|
708
707
|
end
|
|
@@ -725,7 +724,7 @@ module Featurevisor
|
|
|
725
724
|
variable_value: variable_value
|
|
726
725
|
}
|
|
727
726
|
|
|
728
|
-
|
|
727
|
+
diagnostics.debug("allocated variable", evaluation)
|
|
729
728
|
|
|
730
729
|
return evaluation
|
|
731
730
|
end
|
|
@@ -742,7 +741,7 @@ module Featurevisor
|
|
|
742
741
|
bucket_value: bucket_value
|
|
743
742
|
}
|
|
744
743
|
|
|
745
|
-
|
|
744
|
+
diagnostics.debug("no matched variation", evaluation)
|
|
746
745
|
|
|
747
746
|
return evaluation
|
|
748
747
|
end
|
|
@@ -760,7 +759,7 @@ module Featurevisor
|
|
|
760
759
|
variable_value: variable_schema[:defaultValue]
|
|
761
760
|
}
|
|
762
761
|
|
|
763
|
-
|
|
762
|
+
diagnostics.debug("using default value", evaluation)
|
|
764
763
|
|
|
765
764
|
return evaluation
|
|
766
765
|
end
|
|
@@ -774,7 +773,7 @@ module Featurevisor
|
|
|
774
773
|
bucket_value: bucket_value
|
|
775
774
|
}
|
|
776
775
|
|
|
777
|
-
|
|
776
|
+
diagnostics.debug("variable not found", evaluation)
|
|
778
777
|
|
|
779
778
|
return evaluation
|
|
780
779
|
end
|
|
@@ -788,7 +787,7 @@ module Featurevisor
|
|
|
788
787
|
enabled: false
|
|
789
788
|
}
|
|
790
789
|
|
|
791
|
-
|
|
790
|
+
diagnostics.debug("nothing matched", evaluation)
|
|
792
791
|
|
|
793
792
|
evaluation
|
|
794
793
|
rescue => e
|
|
@@ -800,7 +799,7 @@ module Featurevisor
|
|
|
800
799
|
error: e
|
|
801
800
|
}
|
|
802
801
|
|
|
803
|
-
|
|
802
|
+
diagnostics.error("Error during evaluation", evaluation)
|
|
804
803
|
|
|
805
804
|
evaluation
|
|
806
805
|
end
|
|
@@ -3,23 +3,24 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
|
|
5
5
|
module Featurevisor
|
|
6
|
-
#
|
|
7
|
-
class
|
|
8
|
-
attr_reader :schema_version, :revision, :featurevisor_version, :segments, :features, :
|
|
6
|
+
# Private datafile and matching adapter used by the evaluator.
|
|
7
|
+
class InstanceEvaluationDataProvider
|
|
8
|
+
attr_reader :schema_version, :revision, :featurevisor_version, :segments, :features, :variables, :diagnostics, :regex_cache
|
|
9
9
|
|
|
10
|
-
# Initialize a new
|
|
11
|
-
# @param options [Hash] Options hash containing datafile and
|
|
10
|
+
# Initialize a new evaluation data provider.
|
|
11
|
+
# @param options [Hash] Options hash containing datafile and diagnostics
|
|
12
12
|
# @option options [Hash] :datafile Datafile content
|
|
13
|
-
# @option options [
|
|
13
|
+
# @option options [DiagnosticReporter] :diagnostics Diagnostic reporter
|
|
14
14
|
def initialize(options)
|
|
15
15
|
datafile = options[:datafile]
|
|
16
|
-
@
|
|
16
|
+
@diagnostics = options[:diagnostics]
|
|
17
17
|
|
|
18
18
|
@schema_version = datafile[:schemaVersion]
|
|
19
19
|
@revision = datafile[:revision]
|
|
20
20
|
@featurevisor_version = datafile[:featurevisorVersion]
|
|
21
21
|
@segments = (datafile[:segments] || {}).transform_keys(&:to_sym)
|
|
22
22
|
@features = (datafile[:features] || {}).transform_keys(&:to_sym)
|
|
23
|
+
@variables = (datafile[:variables] || {}).transform_keys(&:to_sym)
|
|
23
24
|
|
|
24
25
|
# Transform nested structures to use symbol keys
|
|
25
26
|
@features.each do |_key, feature|
|
|
@@ -72,7 +73,8 @@ module Featurevisor
|
|
|
72
73
|
revision: @revision,
|
|
73
74
|
featurevisorVersion: @featurevisor_version,
|
|
74
75
|
segments: @segments,
|
|
75
|
-
features: @features
|
|
76
|
+
features: @features,
|
|
77
|
+
variables: @variables
|
|
76
78
|
}.compact
|
|
77
79
|
end
|
|
78
80
|
|
|
@@ -88,6 +90,10 @@ module Featurevisor
|
|
|
88
90
|
segment
|
|
89
91
|
end
|
|
90
92
|
|
|
93
|
+
def get_segment_keys
|
|
94
|
+
@segments.keys
|
|
95
|
+
end
|
|
96
|
+
|
|
91
97
|
# Get all feature keys
|
|
92
98
|
# @return [Array<Symbol>] Array of feature keys
|
|
93
99
|
def get_feature_keys
|
|
@@ -104,7 +110,9 @@ module Featurevisor
|
|
|
104
110
|
# Get variable keys for a feature
|
|
105
111
|
# @param feature_key [String] Feature key
|
|
106
112
|
# @return [Array<String>] Array of variable keys
|
|
107
|
-
def get_variable_keys(feature_key)
|
|
113
|
+
def get_variable_keys(feature_key = nil)
|
|
114
|
+
return @variables.keys if feature_key.nil?
|
|
115
|
+
|
|
108
116
|
feature = get_feature(feature_key)
|
|
109
117
|
|
|
110
118
|
return [] unless feature
|
|
@@ -112,6 +120,10 @@ module Featurevisor
|
|
|
112
120
|
(feature[:variablesSchema] || {}).keys
|
|
113
121
|
end
|
|
114
122
|
|
|
123
|
+
def get_global_variable(variable_key)
|
|
124
|
+
@variables[variable_key.to_sym] || @variables[variable_key]
|
|
125
|
+
end
|
|
126
|
+
|
|
115
127
|
# Check if a feature has variations
|
|
116
128
|
# @param feature_key [String] Feature key
|
|
117
129
|
# @return [Boolean] True if feature has variations
|
|
@@ -133,7 +145,15 @@ module Featurevisor
|
|
|
133
145
|
|
|
134
146
|
return @regex_cache[cache_key] if @regex_cache[cache_key]
|
|
135
147
|
|
|
136
|
-
|
|
148
|
+
ruby_flags = 0
|
|
149
|
+
ruby_flags |= Regexp::IGNORECASE if flags.include?("i")
|
|
150
|
+
# Ruby's MULTILINE option controls dot matching newlines, which is the
|
|
151
|
+
# JavaScript `s` behavior. Ruby anchors already operate per line.
|
|
152
|
+
ruby_flags |= Regexp::MULTILINE if flags.include?("s")
|
|
153
|
+
invalid_flags = flags.delete("gimsuy")
|
|
154
|
+
raise ArgumentError, "invalid regular expression flags: #{invalid_flags}" unless invalid_flags.empty?
|
|
155
|
+
|
|
156
|
+
regex = Regexp.new(regex_string, ruby_flags)
|
|
137
157
|
@regex_cache[cache_key] = regex
|
|
138
158
|
@regex_cache[cache_key]
|
|
139
159
|
end
|
|
@@ -155,12 +175,11 @@ module Featurevisor
|
|
|
155
175
|
result = Conditions.condition_is_matched(conditions, context, get_regex_proc)
|
|
156
176
|
return result
|
|
157
177
|
rescue => e
|
|
158
|
-
@
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
178
|
+
@diagnostics.warn(e.message, {
|
|
179
|
+
code: "condition_match_error",
|
|
180
|
+
error: e,
|
|
181
|
+
condition: conditions,
|
|
182
|
+
context: context
|
|
164
183
|
})
|
|
165
184
|
return false
|
|
166
185
|
end
|
|
@@ -183,10 +202,12 @@ module Featurevisor
|
|
|
183
202
|
end
|
|
184
203
|
|
|
185
204
|
if conditions.is_a?(Hash) && conditions[:not] && conditions[:not].is_a?(Array)
|
|
205
|
+
return false if conditions[:not].empty?
|
|
186
206
|
return !all_conditions_are_matched({ and: conditions[:not] }, context)
|
|
187
207
|
end
|
|
188
208
|
|
|
189
209
|
if conditions.is_a?(Hash) && conditions["not"] && conditions["not"].is_a?(Array)
|
|
210
|
+
return false if conditions["not"].empty?
|
|
190
211
|
return !all_conditions_are_matched({ "and" => conditions["not"] }, context)
|
|
191
212
|
end
|
|
192
213
|
|
|
@@ -244,10 +265,12 @@ module Featurevisor
|
|
|
244
265
|
end
|
|
245
266
|
|
|
246
267
|
if group_segments[:not] && group_segments[:not].is_a?(Array)
|
|
268
|
+
return false if group_segments[:not].empty?
|
|
247
269
|
return !all_segments_are_matched({ and: group_segments[:not] }, context)
|
|
248
270
|
end
|
|
249
271
|
|
|
250
272
|
if group_segments["not"] && group_segments["not"].is_a?(Array)
|
|
273
|
+
return false if group_segments["not"].empty?
|
|
251
274
|
return !all_segments_are_matched({ "and" => group_segments["not"] }, context)
|
|
252
275
|
end
|
|
253
276
|
|
|
@@ -333,7 +356,7 @@ module Featurevisor
|
|
|
333
356
|
begin
|
|
334
357
|
JSON.parse(conditions)
|
|
335
358
|
rescue => e
|
|
336
|
-
@
|
|
359
|
+
@diagnostics.error("Error parsing conditions", {
|
|
337
360
|
error: e,
|
|
338
361
|
details: {
|
|
339
362
|
conditions: conditions
|
|
@@ -355,5 +378,5 @@ module Featurevisor
|
|
|
355
378
|
end
|
|
356
379
|
end
|
|
357
380
|
|
|
358
|
-
private_constant :
|
|
381
|
+
private_constant :InstanceEvaluationDataProvider
|
|
359
382
|
end
|