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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: acf664663389bb7b3789a16f0e29c84aeb13e623f26bb38465e3bc8a8dcc984e
|
|
4
|
+
data.tar.gz: 0ab04bbea29092bcfd22361bda28ab6e36879b346d579b5a204780a3bbfb2eaa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e123cd15ff03b6230ca26de9cdee003414b59f5b075a116b946f7b90562123980f083a4c670748d71cefcdcee0c96197e13fceb6d8a8b6bd841d0cd419167ba6
|
|
7
|
+
data.tar.gz: ef355cc9d0f2e6660510545c9f78ccefd031c71a9a52646d36ec28fe4bac1c5d38e18c8b6f2b1d9768f54f695fa7360c5e69e2d89ff9ba2cf0fa877d91a96a56
|
data/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Fahad Heylaal (https://fahad19.com)
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
|
@@ -19,8 +19,9 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
|
|
|
19
19
|
- [Getting variation](#getting-variation)
|
|
20
20
|
- [Getting variables](#getting-variables)
|
|
21
21
|
- [Type specific methods](#type-specific-methods)
|
|
22
|
-
- [Getting
|
|
23
|
-
- [
|
|
22
|
+
- [Getting global variables](#getting-global-variables)
|
|
23
|
+
- [Getting aggregate evaluations](#getting-aggregate-evaluations)
|
|
24
|
+
- [Sticky features and variables](#sticky-features-and-variables)
|
|
24
25
|
- [Initialize with sticky](#initialize-with-sticky)
|
|
25
26
|
- [Set sticky afterwards](#set-sticky-afterwards)
|
|
26
27
|
- [Setting datafile](#setting-datafile)
|
|
@@ -36,7 +37,7 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
|
|
|
36
37
|
- [Events](#events)
|
|
37
38
|
- [`datafile_set`](#datafile_set)
|
|
38
39
|
- [`context_set`](#context_set)
|
|
39
|
-
- [`
|
|
40
|
+
- [`sticky_features_set` and `sticky_variables_set`](#sticky_features_set-and-sticky_variables_set)
|
|
40
41
|
- [`error`](#error)
|
|
41
42
|
- [Modules](#modules)
|
|
42
43
|
- [Defining a module](#defining-a-module)
|
|
@@ -89,6 +90,8 @@ f = Featurevisor.create_featurevisor(
|
|
|
89
90
|
|
|
90
91
|
Most applications only need this factory and the returned `Featurevisor::Instance`. Public extension and observability APIs include modules, diagnostics, events, and the datafile structures accepted by the factory.
|
|
91
92
|
|
|
93
|
+
Concurrent evaluations are safe after an instance is configured. Do not mutate or close the same instance concurrently with evaluations. Serialize calls to `set_datafile`, `set_context`, `set_sticky_features`, `set_sticky_variables`, `add_module`, `remove_module`, and `close`. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.
|
|
94
|
+
|
|
92
95
|
## Initialization
|
|
93
96
|
|
|
94
97
|
The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly:
|
|
@@ -293,12 +296,24 @@ f.get_variable_json(feature_key, variable_key, context = {})
|
|
|
293
296
|
|
|
294
297
|
Type specific methods do not coerce values. `get_variable_integer` returns `nil` for the string `"1"`, and boolean getters return `nil` for non-boolean values.
|
|
295
298
|
|
|
296
|
-
## Getting
|
|
299
|
+
## Getting global variables
|
|
300
|
+
|
|
301
|
+
Global variables use the same methods with a single variable key:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
message = f.get_variable_string('welcomeMessage', context)
|
|
305
|
+
value = f.get_variable('checkoutSettings', context)
|
|
306
|
+
evaluation = f.evaluate_variable('checkoutSettings', context)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Global variables resolve sticky values first, then required features, then the first matching override, and finally their default value.
|
|
310
|
+
|
|
311
|
+
## Getting aggregate evaluations
|
|
297
312
|
|
|
298
313
|
You can get evaluations of all features available in the SDK instance:
|
|
299
314
|
|
|
300
315
|
```ruby
|
|
301
|
-
all_evaluations = f.
|
|
316
|
+
all_evaluations = f.get_feature_evaluations({})
|
|
302
317
|
|
|
303
318
|
puts all_evaluations
|
|
304
319
|
# {
|
|
@@ -319,11 +334,17 @@ puts all_evaluations
|
|
|
319
334
|
|
|
320
335
|
This is handy especially when you want to pass all evaluations from a backend application to the frontend.
|
|
321
336
|
|
|
322
|
-
|
|
337
|
+
Global variable values are available separately:
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
global_variables = f.get_variable_evaluations({})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Sticky features and variables
|
|
323
344
|
|
|
324
345
|
For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](https://featurevisor.com/docs/building-datafiles/):
|
|
325
346
|
|
|
326
|
-
Sticky values belong to an SDK or child instance.
|
|
347
|
+
Sticky values belong to an SDK or child instance. Feature and global variable sticky values are independent.
|
|
327
348
|
|
|
328
349
|
### Initialize with sticky
|
|
329
350
|
|
|
@@ -331,7 +352,7 @@ Sticky values belong to an SDK or child instance. Evaluation options do not acce
|
|
|
331
352
|
require 'featurevisor'
|
|
332
353
|
|
|
333
354
|
f = Featurevisor.create_featurevisor(
|
|
334
|
-
|
|
355
|
+
sticky_features: {
|
|
335
356
|
myFeatureKey: {
|
|
336
357
|
enabled: true,
|
|
337
358
|
# optional
|
|
@@ -343,6 +364,9 @@ f = Featurevisor.create_featurevisor(
|
|
|
343
364
|
anotherFeatureKey: {
|
|
344
365
|
enabled: false
|
|
345
366
|
}
|
|
367
|
+
},
|
|
368
|
+
sticky_variables: {
|
|
369
|
+
welcomeMessage: 'Welcome back'
|
|
346
370
|
}
|
|
347
371
|
)
|
|
348
372
|
```
|
|
@@ -354,7 +378,7 @@ Once initialized with sticky features, the SDK will look for values there first
|
|
|
354
378
|
You can also set sticky features after the SDK is initialized:
|
|
355
379
|
|
|
356
380
|
```ruby
|
|
357
|
-
f.
|
|
381
|
+
f.set_sticky_features({
|
|
358
382
|
myFeatureKey: {
|
|
359
383
|
enabled: true,
|
|
360
384
|
variation: 'treatment',
|
|
@@ -366,6 +390,8 @@ f.set_sticky({
|
|
|
366
390
|
enabled: false
|
|
367
391
|
}
|
|
368
392
|
}, true) # replace existing sticky features (false by default)
|
|
393
|
+
|
|
394
|
+
f.set_sticky_variables({ welcomeMessage: 'Welcome back' }, true)
|
|
369
395
|
```
|
|
370
396
|
|
|
371
397
|
## Setting datafile
|
|
@@ -391,7 +417,7 @@ By default, `set_datafile(datafile)` merges the incoming datafile into the SDK's
|
|
|
391
417
|
- `segments` are merged, with incoming entries overriding existing ones
|
|
392
418
|
- `features` are merged, with incoming entries overriding existing ones
|
|
393
419
|
|
|
394
|
-
This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features and
|
|
420
|
+
This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features, segments, and global variables together.
|
|
395
421
|
|
|
396
422
|
### Replacing
|
|
397
423
|
|
|
@@ -543,15 +569,19 @@ unsubscribe = f.on('context_set') do |event|
|
|
|
543
569
|
end
|
|
544
570
|
```
|
|
545
571
|
|
|
546
|
-
### `
|
|
572
|
+
### `sticky_features_set` and `sticky_variables_set`
|
|
547
573
|
|
|
548
574
|
```ruby
|
|
549
|
-
|
|
575
|
+
feature_unsubscribe = f.on('sticky_features_set') do |event|
|
|
550
576
|
replaced = event[:replaced] # true if sticky features got replaced
|
|
551
577
|
features = event[:features] # list of all affected feature keys
|
|
552
578
|
|
|
553
579
|
puts 'Sticky features set'
|
|
554
580
|
end
|
|
581
|
+
|
|
582
|
+
variable_unsubscribe = f.on('sticky_variables_set') do |event|
|
|
583
|
+
variables = event[:variables]
|
|
584
|
+
end
|
|
555
585
|
```
|
|
556
586
|
|
|
557
587
|
### `error`
|
|
@@ -581,11 +611,14 @@ evaluation = f.evaluate_variation(feature_key, context = {})
|
|
|
581
611
|
|
|
582
612
|
# variable
|
|
583
613
|
evaluation = f.evaluate_variable(feature_key, variable_key, context = {})
|
|
614
|
+
|
|
615
|
+
# global variable
|
|
616
|
+
global_evaluation = f.evaluate_variable(variable_key, context = {})
|
|
584
617
|
```
|
|
585
618
|
|
|
586
619
|
The returned object will always contain the following properties:
|
|
587
620
|
|
|
588
|
-
- `feature_key`: the feature key
|
|
621
|
+
- `feature_key`: the feature key when evaluating a feature
|
|
589
622
|
- `reason`: the reason how the value was evaluated
|
|
590
623
|
|
|
591
624
|
And optionally these properties depending on whether you are evaluating a feature variation or a variable:
|
|
@@ -604,6 +637,8 @@ And optionally these properties depending on whether you are evaluating a featur
|
|
|
604
637
|
|
|
605
638
|
Modules allow you to intercept the evaluation process and customize SDK behavior.
|
|
606
639
|
|
|
640
|
+
For feature evaluations, all `before` callbacks run in registration order, followed by all `before_evaluation` callbacks. After evaluation and caller defaults, all `after_evaluation` callbacks run, followed by all `after` callbacks. Global variable evaluations use only `before_evaluation` and `after_evaluation`. Required feature checks run through the complete module pipeline, and transformed defaults are preserved.
|
|
641
|
+
|
|
607
642
|
### Defining a module
|
|
608
643
|
|
|
609
644
|
A module is a simple hash with a unique recommended `name` and optional lifecycle functions:
|
|
@@ -637,6 +672,9 @@ my_custom_module = {
|
|
|
637
672
|
options
|
|
638
673
|
},
|
|
639
674
|
|
|
675
|
+
# unified callback for feature and global variable evaluations
|
|
676
|
+
before_evaluation: ->(options) { options },
|
|
677
|
+
|
|
640
678
|
# after evaluation
|
|
641
679
|
after: ->(evaluation, options) {
|
|
642
680
|
reason = evaluation[:reason]
|
|
@@ -646,6 +684,9 @@ my_custom_module = {
|
|
|
646
684
|
end
|
|
647
685
|
},
|
|
648
686
|
|
|
687
|
+
# unified callback for feature and global variable evaluations
|
|
688
|
+
after_evaluation: ->(evaluation, options) { evaluation },
|
|
689
|
+
|
|
649
690
|
# configure bucket key
|
|
650
691
|
bucket_key: ->(options) {
|
|
651
692
|
# return custom bucket key
|
|
@@ -697,6 +738,8 @@ f.remove_module('my-custom-module')
|
|
|
697
738
|
|
|
698
739
|
## Child instance
|
|
699
740
|
|
|
741
|
+
A child snapshots the parent keys that exist when it is spawned. Child values win for those keys. Parent keys introduced later are still inherited. Calling `close` removes both child-owned listeners and subscriptions delegated to the parent.
|
|
742
|
+
|
|
700
743
|
When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications.
|
|
701
744
|
|
|
702
745
|
But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request.
|
|
@@ -716,14 +759,19 @@ Now you can pass the child instance where your individual request is being handl
|
|
|
716
759
|
is_enabled = child_f.is_enabled('my_feature')
|
|
717
760
|
variation = child_f.get_variation('my_feature')
|
|
718
761
|
variable_value = child_f.get_variable('my_feature', 'my_variable')
|
|
762
|
+
global_value = child_f.get_variable('welcomeMessage')
|
|
719
763
|
```
|
|
720
764
|
|
|
721
765
|
Similar to parent SDK, child instances also support several additional methods:
|
|
722
766
|
|
|
723
767
|
- `set_context`
|
|
724
|
-
- `
|
|
768
|
+
- `set_sticky_features`
|
|
769
|
+
- `set_sticky_variables`
|
|
770
|
+
- `evaluate_flag`
|
|
725
771
|
- `is_enabled`
|
|
772
|
+
- `evaluate_variation`
|
|
726
773
|
- `get_variation`
|
|
774
|
+
- `evaluate_variable`
|
|
727
775
|
- `get_variable`
|
|
728
776
|
- `get_variable_boolean`
|
|
729
777
|
- `get_variable_string`
|
|
@@ -732,7 +780,8 @@ Similar to parent SDK, child instances also support several additional methods:
|
|
|
732
780
|
- `get_variable_array`
|
|
733
781
|
- `get_variable_object`
|
|
734
782
|
- `get_variable_json`
|
|
735
|
-
- `
|
|
783
|
+
- `get_feature_evaluations`
|
|
784
|
+
- `get_variable_evaluations`
|
|
736
785
|
- `on`
|
|
737
786
|
- `close`
|
|
738
787
|
|
|
@@ -821,7 +870,7 @@ The provider currently requires Ruby 3.4 or newer because that is the minimum ve
|
|
|
821
870
|
Install the provider:
|
|
822
871
|
|
|
823
872
|
```ruby
|
|
824
|
-
gem "featurevisor-openfeature", "~>
|
|
873
|
+
gem "featurevisor-openfeature", "~> 3.0"
|
|
825
874
|
```
|
|
826
875
|
|
|
827
876
|
It installs the matching `featurevisor` gem and the official `openfeature-sdk` dependency. The provider and base SDK deliberately share the same version, and the provider requires that exact Featurevisor version.
|
|
@@ -845,9 +894,9 @@ enabled = client.fetch_boolean_value(
|
|
|
845
894
|
)
|
|
846
895
|
```
|
|
847
896
|
|
|
848
|
-
Use `checkout` for a flag, `checkout:variation` for its variation,
|
|
897
|
+
Use `checkout` for a flag, `checkout:variation` for its variation, `checkout:title` for its `title` variable, and `variable:welcomeMessage` for a global variable. Boolean variables use the boolean resolver. Arrays, hashes, and JSON variables use the object resolver.
|
|
849
898
|
|
|
850
|
-
OpenFeature's targeting key maps to `userId` by default. `targeting_key_field`, `key_separator`, and `
|
|
899
|
+
OpenFeature's targeting key maps to `userId` by default. `targeting_key_field`, `key_separator`, `variation_key`, and `global_variable_prefix` can customize the mapping. The global variable prefix defaults to `variable` and cannot contain the separator.
|
|
851
900
|
|
|
852
901
|
You can pass any Featurevisor initialization options directly to the provider. These options are used to create the Featurevisor instance owned by the provider:
|
|
853
902
|
|
|
@@ -901,7 +950,7 @@ The build produces `featurevisor-VERSION.gem` and `featurevisor-openfeature-VERS
|
|
|
901
950
|
- Run `bundle install`
|
|
902
951
|
- Push commit to `main` branch
|
|
903
952
|
- Wait for CI to complete
|
|
904
|
-
- Tag the release with the same version number, for example `
|
|
953
|
+
- Tag the release with the same version number, for example `v3.0.0`
|
|
905
954
|
- The workflow verifies that the tag matches the shared version
|
|
906
955
|
- The workflow publishes `featurevisor` first, followed by `featurevisor-openfeature`
|
|
907
956
|
|
data/bin/cli.rb
CHANGED
|
@@ -43,7 +43,7 @@ module FeaturevisorCLI
|
|
|
43
43
|
options.environment = v
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
opts.on("--feature=FEATURE", "Feature key
|
|
46
|
+
opts.on("--feature=FEATURE", "Feature key") do |v|
|
|
47
47
|
options.feature = v
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -150,6 +150,6 @@ module FeaturevisorCLI
|
|
|
150
150
|
puts " featurevisor benchmark --feature=myFeature --environment=dev --n=10000"
|
|
151
151
|
puts " featurevisor assess-distribution --feature=myFeature --n=10000"
|
|
152
152
|
puts ""
|
|
153
|
-
puts "Note: benchmark
|
|
153
|
+
puts "Note: benchmark requires --environment and either --feature or --variable"
|
|
154
154
|
end
|
|
155
155
|
end
|
data/bin/commands/benchmark.rb
CHANGED
|
@@ -22,8 +22,8 @@ module FeaturevisorCLI
|
|
|
22
22
|
exit 1
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
unless @options.feature
|
|
26
|
-
puts "Error: --feature is required for benchmark command"
|
|
25
|
+
unless @options.feature || @options.variable
|
|
26
|
+
puts "Error: --feature or --variable is required for benchmark command"
|
|
27
27
|
exit 1
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -41,7 +41,8 @@ module FeaturevisorCLI
|
|
|
41
41
|
datafile_build_duration_ms = (datafile_build_duration * 1000).round
|
|
42
42
|
|
|
43
43
|
puts "\nBenchmark Featurevisor feature"
|
|
44
|
-
puts " Feature: #{@options.feature}"
|
|
44
|
+
puts " Feature: #{@options.feature}" if @options.feature
|
|
45
|
+
puts " Global variable: #{@options.variable}" if @options.variable && !@options.feature
|
|
45
46
|
puts " Environment: #{@options.environment}"
|
|
46
47
|
puts " Target: #{target}" if target
|
|
47
48
|
puts " Iterations: #{@options.n}"
|
|
@@ -59,7 +60,10 @@ module FeaturevisorCLI
|
|
|
59
60
|
puts "Against context: #{context.to_json}"
|
|
60
61
|
|
|
61
62
|
# Run the appropriate benchmark
|
|
62
|
-
if @options.
|
|
63
|
+
if @options.variable && !@options.feature
|
|
64
|
+
puts "Evaluating global variable \"#{@options.variable}\" #{@options.n} times..."
|
|
65
|
+
output = benchmark_global_variable(instance, @options.variable, context, @options.n)
|
|
66
|
+
elsif @options.variation
|
|
63
67
|
puts "Evaluating variation #{@options.n} times..."
|
|
64
68
|
output = benchmark_feature_variation(instance, @options.feature, context, @options.n)
|
|
65
69
|
elsif @options.variable
|
|
@@ -223,6 +227,12 @@ module FeaturevisorCLI
|
|
|
223
227
|
end
|
|
224
228
|
end
|
|
225
229
|
|
|
230
|
+
def benchmark_global_variable(instance, variable_key, context, n)
|
|
231
|
+
benchmark_evaluation(n) do
|
|
232
|
+
instance.get_variable(variable_key, context)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
226
236
|
def format_value(value)
|
|
227
237
|
if value.nil?
|
|
228
238
|
"null"
|
data/bin/commands/test.rb
CHANGED
|
@@ -235,10 +235,12 @@ module FeaturevisorCLI
|
|
|
235
235
|
|
|
236
236
|
def create_tester_instance(datafile, level, assertion)
|
|
237
237
|
sticky = parse_sticky(assertion[:sticky])
|
|
238
|
+
sticky_variables = assertion[:stickyVariables].is_a?(Hash) ? assertion[:stickyVariables] : {}
|
|
238
239
|
|
|
239
240
|
Featurevisor.create_featurevisor(
|
|
240
241
|
datafile: datafile,
|
|
241
|
-
|
|
242
|
+
sticky_features: sticky,
|
|
243
|
+
sticky_variables: sticky_variables,
|
|
242
244
|
log_level: level,
|
|
243
245
|
modules: [
|
|
244
246
|
{
|
|
@@ -265,7 +267,7 @@ module FeaturevisorCLI
|
|
|
265
267
|
tests.each do |test|
|
|
266
268
|
test_key = test[:key]
|
|
267
269
|
assertions = test[:assertions] || []
|
|
268
|
-
if test[:feature] && !@options.targets.empty?
|
|
270
|
+
if (test[:feature] || test[:variable]) && !@options.targets.empty?
|
|
269
271
|
assertions = assertions.select do |assertion|
|
|
270
272
|
assertion[:target].nil? || @options.targets.include?(assertion[:target])
|
|
271
273
|
end
|
|
@@ -301,6 +303,14 @@ module FeaturevisorCLI
|
|
|
301
303
|
|
|
302
304
|
test_result = run_test_feature(assertion, test[:feature], instance, level)
|
|
303
305
|
end
|
|
306
|
+
elsif test[:variable]
|
|
307
|
+
datafile = resolve_datafile_for_assertion(assertion, datafiles_by_key)
|
|
308
|
+
if datafile
|
|
309
|
+
instance = create_tester_instance(datafile, level, assertion)
|
|
310
|
+
test_result = run_test_variable(assertion, test[:variable], instance)
|
|
311
|
+
else
|
|
312
|
+
test_result = { has_error: true, errors: " ✘ no datafile found for assertion target/environment combination\n", duration: 0 }
|
|
313
|
+
end
|
|
304
314
|
elsif test[:segment]
|
|
305
315
|
segment_key = test[:segment]
|
|
306
316
|
segment = segments_by_key[segment_key]
|
|
@@ -347,6 +357,31 @@ module FeaturevisorCLI
|
|
|
347
357
|
end
|
|
348
358
|
end
|
|
349
359
|
|
|
360
|
+
def run_test_variable(assertion, variable_key, instance)
|
|
361
|
+
context = parse_context(assertion[:context])
|
|
362
|
+
options = {}
|
|
363
|
+
options[:default_variable_value] = assertion[:defaultVariableValue] if assertion.key?(:defaultVariableValue)
|
|
364
|
+
started = Time.now
|
|
365
|
+
errors = ""
|
|
366
|
+
|
|
367
|
+
if assertion.key?(:expectedValue)
|
|
368
|
+
actual = instance.get_variable(variable_key, context, options)
|
|
369
|
+
unless compare_values(actual, assertion[:expectedValue])
|
|
370
|
+
errors += " ✘ expectedValue: expected #{assertion[:expectedValue].inspect} but received #{actual.inspect}\n"
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
if assertion[:expectedEvaluation].is_a?(Hash)
|
|
375
|
+
evaluation = instance.evaluate_variable(variable_key, context, options)
|
|
376
|
+
assertion[:expectedEvaluation].each do |key, expected|
|
|
377
|
+
actual = get_evaluation_value(evaluation, key)
|
|
378
|
+
errors += " ✘ expectedEvaluation.#{key}: expected #{expected.inspect} but received #{actual.inspect}\n" unless compare_values(actual, expected)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
{ has_error: !errors.empty?, errors: errors, duration: Time.now - started }
|
|
383
|
+
end
|
|
384
|
+
|
|
350
385
|
def run_test_feature(assertion, feature_key, instance, level)
|
|
351
386
|
context = parse_context(assertion[:context])
|
|
352
387
|
sticky = parse_sticky(assertion[:sticky])
|
|
@@ -354,7 +389,7 @@ module FeaturevisorCLI
|
|
|
354
389
|
# Set context and sticky for this assertion
|
|
355
390
|
instance.set_context(context, false)
|
|
356
391
|
if sticky && !sticky.empty?
|
|
357
|
-
instance.
|
|
392
|
+
instance.set_sticky_features(sticky, false)
|
|
358
393
|
end
|
|
359
394
|
|
|
360
395
|
# Create override options
|
|
@@ -492,7 +527,7 @@ module FeaturevisorCLI
|
|
|
492
527
|
# Create a local copy to ensure it's never nil
|
|
493
528
|
child_sticky = sticky || {}
|
|
494
529
|
if !child_sticky.empty?
|
|
495
|
-
child_instance.
|
|
530
|
+
child_instance.set_sticky_features(child_sticky, false)
|
|
496
531
|
end
|
|
497
532
|
|
|
498
533
|
child_result = run_test_feature_child(child, feature_key, child_instance, level)
|
|
@@ -671,7 +706,7 @@ module FeaturevisorCLI
|
|
|
671
706
|
|
|
672
707
|
if assertion.key?(:expectedToMatch)
|
|
673
708
|
expected_to_match = assertion[:expectedToMatch]
|
|
674
|
-
actual = instance.instance_variable_get(:@
|
|
709
|
+
actual = instance.instance_variable_get(:@datafile).all_conditions_are_matched(conditions, context)
|
|
675
710
|
|
|
676
711
|
if actual != expected_to_match
|
|
677
712
|
has_error = true
|
|
@@ -795,6 +830,8 @@ module FeaturevisorCLI
|
|
|
795
830
|
evaluation[:force]
|
|
796
831
|
when :required
|
|
797
832
|
evaluation[:required]
|
|
833
|
+
when :requiredFeatures
|
|
834
|
+
evaluation[:required_features]
|
|
798
835
|
when :sticky
|
|
799
836
|
evaluation[:sticky]
|
|
800
837
|
when :variation
|
|
@@ -809,6 +846,10 @@ module FeaturevisorCLI
|
|
|
809
846
|
evaluation[:variable_schema]
|
|
810
847
|
when :variableOverrideIndex
|
|
811
848
|
evaluation[:variable_override_index]
|
|
849
|
+
when :variableOverrideKey
|
|
850
|
+
evaluation[:variable_override_key]
|
|
851
|
+
when :variableOverridePath
|
|
852
|
+
evaluation[:variable_override_path]
|
|
812
853
|
else
|
|
813
854
|
nil
|
|
814
855
|
end
|
|
@@ -30,16 +30,16 @@ module Featurevisor
|
|
|
30
30
|
# - feature_key [String] The feature key
|
|
31
31
|
# - bucket_by [String, Array<String>, Hash] Bucketing strategy
|
|
32
32
|
# - context [Hash] User context
|
|
33
|
-
# -
|
|
33
|
+
# - diagnostics [DiagnosticReporter] Diagnostic reporter
|
|
34
34
|
# @return [String] The bucket key
|
|
35
35
|
# @raise [StandardError] If bucket_by is invalid
|
|
36
36
|
def self.get_bucket_key(options)
|
|
37
37
|
feature_key = options[:feature_key]
|
|
38
38
|
bucket_by = options[:bucket_by]
|
|
39
39
|
context = options[:context]
|
|
40
|
-
|
|
40
|
+
diagnostics = options[:diagnostics]
|
|
41
41
|
|
|
42
|
-
type, attribute_keys = parse_bucket_by(bucket_by,
|
|
42
|
+
type, attribute_keys = parse_bucket_by(bucket_by, diagnostics, feature_key)
|
|
43
43
|
|
|
44
44
|
bucket_key = build_bucket_key(attribute_keys, context, type, feature_key)
|
|
45
45
|
|
|
@@ -50,10 +50,10 @@ module Featurevisor
|
|
|
50
50
|
|
|
51
51
|
# Parse bucket_by configuration to determine type and attribute keys
|
|
52
52
|
# @param bucket_by [String, Array<String>, Hash] Bucketing strategy
|
|
53
|
-
# @param
|
|
53
|
+
# @param diagnostics [DiagnosticReporter] Diagnostic reporter
|
|
54
54
|
# @param feature_key [String] Feature key for error logging
|
|
55
55
|
# @return [Array] Tuple of [type, attribute_keys]
|
|
56
|
-
def self.parse_bucket_by(bucket_by,
|
|
56
|
+
def self.parse_bucket_by(bucket_by, diagnostics, feature_key)
|
|
57
57
|
if bucket_by.is_a?(String)
|
|
58
58
|
["plain", [bucket_by]]
|
|
59
59
|
elsif bucket_by.is_a?(Array)
|
|
@@ -61,7 +61,7 @@ module Featurevisor
|
|
|
61
61
|
elsif bucket_by.is_a?(Hash) && bucket_by[:or].is_a?(Array)
|
|
62
62
|
["or", bucket_by[:or]]
|
|
63
63
|
else
|
|
64
|
-
|
|
64
|
+
diagnostics.error("invalid bucketBy", { feature_key: feature_key, bucket_by: bucket_by })
|
|
65
65
|
raise StandardError, "invalid bucketBy"
|
|
66
66
|
end
|
|
67
67
|
end
|
|
@@ -78,7 +78,7 @@ module Featurevisor
|
|
|
78
78
|
attribute_keys.each do |attribute_key|
|
|
79
79
|
attribute_value = Featurevisor::Conditions.get_value_from_context(context, attribute_key)
|
|
80
80
|
|
|
81
|
-
next if attribute_value.nil?
|
|
81
|
+
next if attribute_value.nil? && !context_path_exists?(context, attribute_key)
|
|
82
82
|
|
|
83
83
|
if type == "plain" || type == "and"
|
|
84
84
|
bucket_key << attribute_value
|
|
@@ -89,7 +89,61 @@ module Featurevisor
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
bucket_key << feature_key
|
|
92
|
-
bucket_key
|
|
92
|
+
bucket_key.map { |value| javascript_string(value) }
|
|
93
93
|
end
|
|
94
|
+
|
|
95
|
+
def self.context_path_exists?(context, path)
|
|
96
|
+
path.split(".").reduce(context) do |current, key|
|
|
97
|
+
return false unless current.is_a?(Hash)
|
|
98
|
+
return false unless current.key?(key.to_sym) || current.key?(key)
|
|
99
|
+
|
|
100
|
+
current.key?(key.to_sym) ? current[key.to_sym] : current[key]
|
|
101
|
+
end
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.javascript_string(value)
|
|
106
|
+
return "" if value.nil?
|
|
107
|
+
return value ? "true" : "false" if value == true || value == false
|
|
108
|
+
if value.is_a?(Float)
|
|
109
|
+
return "NaN" if value.nan?
|
|
110
|
+
return value.positive? ? "Infinity" : "-Infinity" if value.infinite?
|
|
111
|
+
return "0" if value.zero?
|
|
112
|
+
return value.to_i.to_s if value == value.to_i && value.abs < 1e21
|
|
113
|
+
if value.abs >= 1e-6 && value.abs < 1e21
|
|
114
|
+
return scientific_to_fixed(value.to_s)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
coefficient, exponent = value.to_s.downcase.split("e")
|
|
118
|
+
return "#{coefficient.delete_suffix('.0')}e#{Integer(exponent) >= 0 ? '+' : ''}#{Integer(exponent)}"
|
|
119
|
+
end
|
|
120
|
+
return value.map { |item| javascript_string(item) }.join(",") if value.is_a?(Array)
|
|
121
|
+
return "[object Object]" if value.is_a?(Hash)
|
|
122
|
+
|
|
123
|
+
value.to_s
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.scientific_to_fixed(value)
|
|
127
|
+
return value unless value.downcase.include?("e")
|
|
128
|
+
|
|
129
|
+
coefficient, exponent_text = value.downcase.split("e")
|
|
130
|
+
exponent = Integer(exponent_text)
|
|
131
|
+
negative = coefficient.start_with?("-")
|
|
132
|
+
digits = coefficient.delete_prefix("-").delete(".")
|
|
133
|
+
decimal_position = coefficient.delete_prefix("-").index(".") || coefficient.delete_prefix("-").length
|
|
134
|
+
decimal_position += exponent
|
|
135
|
+
|
|
136
|
+
result = if decimal_position <= 0
|
|
137
|
+
"0.#{"0" * -decimal_position}#{digits}"
|
|
138
|
+
elsif decimal_position >= digits.length
|
|
139
|
+
"#{digits}#{"0" * (decimal_position - digits.length)}"
|
|
140
|
+
else
|
|
141
|
+
"#{digits[0...decimal_position]}.#{digits[decimal_position..]}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
result = result.sub(/(\.\d*?)0+\z/, '\1').delete_suffix(".")
|
|
145
|
+
negative ? "-#{result}" : result
|
|
146
|
+
end
|
|
147
|
+
private_class_method :scientific_to_fixed
|
|
94
148
|
end
|
|
95
149
|
end
|