featurevisor-openfeature 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +62 -20
- data/lib/featurevisor/openfeature_provider.rb +18 -7
- 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: 3a8495a304439dace1b52ba45bd2a7e9b4947cf482a179af80b9a3c8bb1f8e2c
|
|
4
|
+
data.tar.gz: 3473b28bac62de80fd0950c895d1a1a912919752cb99007cf04f922702bd6bb1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f325bd259bd59b4e8ae6a3ef7bb6b3b48d534bbfcaaf7e2212f1bacc9b23e334bcc1e54b7bd017c8adcd4ed09340e56345ad89798a7d4c7cbcf72f521169dd23
|
|
7
|
+
data.tar.gz: 101e2d9eab3c39afea5cb634e294107606cf500db85040916ecaf0c570b66ea7c47b09773a7dfa11c09604f17ad858c99c712b48854500483a044710d7a7feec
|
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,7 +90,7 @@ 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
|
|
|
92
|
-
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`, `
|
|
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.
|
|
93
94
|
|
|
94
95
|
## Initialization
|
|
95
96
|
|
|
@@ -295,12 +296,24 @@ f.get_variable_json(feature_key, variable_key, context = {})
|
|
|
295
296
|
|
|
296
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.
|
|
297
298
|
|
|
298
|
-
## 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
|
|
299
312
|
|
|
300
313
|
You can get evaluations of all features available in the SDK instance:
|
|
301
314
|
|
|
302
315
|
```ruby
|
|
303
|
-
all_evaluations = f.
|
|
316
|
+
all_evaluations = f.get_feature_evaluations({})
|
|
304
317
|
|
|
305
318
|
puts all_evaluations
|
|
306
319
|
# {
|
|
@@ -321,11 +334,17 @@ puts all_evaluations
|
|
|
321
334
|
|
|
322
335
|
This is handy especially when you want to pass all evaluations from a backend application to the frontend.
|
|
323
336
|
|
|
324
|
-
|
|
337
|
+
Global variable values are available separately:
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
global_variables = f.get_variable_evaluations({})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Sticky features and variables
|
|
325
344
|
|
|
326
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/):
|
|
327
346
|
|
|
328
|
-
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.
|
|
329
348
|
|
|
330
349
|
### Initialize with sticky
|
|
331
350
|
|
|
@@ -333,7 +352,7 @@ Sticky values belong to an SDK or child instance. Evaluation options do not acce
|
|
|
333
352
|
require 'featurevisor'
|
|
334
353
|
|
|
335
354
|
f = Featurevisor.create_featurevisor(
|
|
336
|
-
|
|
355
|
+
sticky_features: {
|
|
337
356
|
myFeatureKey: {
|
|
338
357
|
enabled: true,
|
|
339
358
|
# optional
|
|
@@ -345,6 +364,9 @@ f = Featurevisor.create_featurevisor(
|
|
|
345
364
|
anotherFeatureKey: {
|
|
346
365
|
enabled: false
|
|
347
366
|
}
|
|
367
|
+
},
|
|
368
|
+
sticky_variables: {
|
|
369
|
+
welcomeMessage: 'Welcome back'
|
|
348
370
|
}
|
|
349
371
|
)
|
|
350
372
|
```
|
|
@@ -356,7 +378,7 @@ Once initialized with sticky features, the SDK will look for values there first
|
|
|
356
378
|
You can also set sticky features after the SDK is initialized:
|
|
357
379
|
|
|
358
380
|
```ruby
|
|
359
|
-
f.
|
|
381
|
+
f.set_sticky_features({
|
|
360
382
|
myFeatureKey: {
|
|
361
383
|
enabled: true,
|
|
362
384
|
variation: 'treatment',
|
|
@@ -368,6 +390,8 @@ f.set_sticky({
|
|
|
368
390
|
enabled: false
|
|
369
391
|
}
|
|
370
392
|
}, true) # replace existing sticky features (false by default)
|
|
393
|
+
|
|
394
|
+
f.set_sticky_variables({ welcomeMessage: 'Welcome back' }, true)
|
|
371
395
|
```
|
|
372
396
|
|
|
373
397
|
## Setting datafile
|
|
@@ -393,7 +417,7 @@ By default, `set_datafile(datafile)` merges the incoming datafile into the SDK's
|
|
|
393
417
|
- `segments` are merged, with incoming entries overriding existing ones
|
|
394
418
|
- `features` are merged, with incoming entries overriding existing ones
|
|
395
419
|
|
|
396
|
-
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.
|
|
397
421
|
|
|
398
422
|
### Replacing
|
|
399
423
|
|
|
@@ -545,15 +569,19 @@ unsubscribe = f.on('context_set') do |event|
|
|
|
545
569
|
end
|
|
546
570
|
```
|
|
547
571
|
|
|
548
|
-
### `
|
|
572
|
+
### `sticky_features_set` and `sticky_variables_set`
|
|
549
573
|
|
|
550
574
|
```ruby
|
|
551
|
-
|
|
575
|
+
feature_unsubscribe = f.on('sticky_features_set') do |event|
|
|
552
576
|
replaced = event[:replaced] # true if sticky features got replaced
|
|
553
577
|
features = event[:features] # list of all affected feature keys
|
|
554
578
|
|
|
555
579
|
puts 'Sticky features set'
|
|
556
580
|
end
|
|
581
|
+
|
|
582
|
+
variable_unsubscribe = f.on('sticky_variables_set') do |event|
|
|
583
|
+
variables = event[:variables]
|
|
584
|
+
end
|
|
557
585
|
```
|
|
558
586
|
|
|
559
587
|
### `error`
|
|
@@ -583,11 +611,14 @@ evaluation = f.evaluate_variation(feature_key, context = {})
|
|
|
583
611
|
|
|
584
612
|
# variable
|
|
585
613
|
evaluation = f.evaluate_variable(feature_key, variable_key, context = {})
|
|
614
|
+
|
|
615
|
+
# global variable
|
|
616
|
+
global_evaluation = f.evaluate_variable(variable_key, context = {})
|
|
586
617
|
```
|
|
587
618
|
|
|
588
619
|
The returned object will always contain the following properties:
|
|
589
620
|
|
|
590
|
-
- `feature_key`: the feature key
|
|
621
|
+
- `feature_key`: the feature key when evaluating a feature
|
|
591
622
|
- `reason`: the reason how the value was evaluated
|
|
592
623
|
|
|
593
624
|
And optionally these properties depending on whether you are evaluating a feature variation or a variable:
|
|
@@ -606,6 +637,8 @@ And optionally these properties depending on whether you are evaluating a featur
|
|
|
606
637
|
|
|
607
638
|
Modules allow you to intercept the evaluation process and customize SDK behavior.
|
|
608
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
|
+
|
|
609
642
|
### Defining a module
|
|
610
643
|
|
|
611
644
|
A module is a simple hash with a unique recommended `name` and optional lifecycle functions:
|
|
@@ -639,6 +672,9 @@ my_custom_module = {
|
|
|
639
672
|
options
|
|
640
673
|
},
|
|
641
674
|
|
|
675
|
+
# unified callback for feature and global variable evaluations
|
|
676
|
+
before_evaluation: ->(options) { options },
|
|
677
|
+
|
|
642
678
|
# after evaluation
|
|
643
679
|
after: ->(evaluation, options) {
|
|
644
680
|
reason = evaluation[:reason]
|
|
@@ -648,6 +684,9 @@ my_custom_module = {
|
|
|
648
684
|
end
|
|
649
685
|
},
|
|
650
686
|
|
|
687
|
+
# unified callback for feature and global variable evaluations
|
|
688
|
+
after_evaluation: ->(evaluation, options) { evaluation },
|
|
689
|
+
|
|
651
690
|
# configure bucket key
|
|
652
691
|
bucket_key: ->(options) {
|
|
653
692
|
# return custom bucket key
|
|
@@ -720,12 +759,14 @@ Now you can pass the child instance where your individual request is being handl
|
|
|
720
759
|
is_enabled = child_f.is_enabled('my_feature')
|
|
721
760
|
variation = child_f.get_variation('my_feature')
|
|
722
761
|
variable_value = child_f.get_variable('my_feature', 'my_variable')
|
|
762
|
+
global_value = child_f.get_variable('welcomeMessage')
|
|
723
763
|
```
|
|
724
764
|
|
|
725
765
|
Similar to parent SDK, child instances also support several additional methods:
|
|
726
766
|
|
|
727
767
|
- `set_context`
|
|
728
|
-
- `
|
|
768
|
+
- `set_sticky_features`
|
|
769
|
+
- `set_sticky_variables`
|
|
729
770
|
- `evaluate_flag`
|
|
730
771
|
- `is_enabled`
|
|
731
772
|
- `evaluate_variation`
|
|
@@ -739,7 +780,8 @@ Similar to parent SDK, child instances also support several additional methods:
|
|
|
739
780
|
- `get_variable_array`
|
|
740
781
|
- `get_variable_object`
|
|
741
782
|
- `get_variable_json`
|
|
742
|
-
- `
|
|
783
|
+
- `get_feature_evaluations`
|
|
784
|
+
- `get_variable_evaluations`
|
|
743
785
|
- `on`
|
|
744
786
|
- `close`
|
|
745
787
|
|
|
@@ -828,7 +870,7 @@ The provider currently requires Ruby 3.4 or newer because that is the minimum ve
|
|
|
828
870
|
Install the provider:
|
|
829
871
|
|
|
830
872
|
```ruby
|
|
831
|
-
gem "featurevisor-openfeature", "~>
|
|
873
|
+
gem "featurevisor-openfeature", "~> 3.0"
|
|
832
874
|
```
|
|
833
875
|
|
|
834
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.
|
|
@@ -852,9 +894,9 @@ enabled = client.fetch_boolean_value(
|
|
|
852
894
|
)
|
|
853
895
|
```
|
|
854
896
|
|
|
855
|
-
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.
|
|
856
898
|
|
|
857
|
-
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.
|
|
858
900
|
|
|
859
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:
|
|
860
902
|
|
|
@@ -908,7 +950,7 @@ The build produces `featurevisor-VERSION.gem` and `featurevisor-openfeature-VERS
|
|
|
908
950
|
- Run `bundle install`
|
|
909
951
|
- Push commit to `main` branch
|
|
910
952
|
- Wait for CI to complete
|
|
911
|
-
- Tag the release with the same version number, for example `
|
|
953
|
+
- Tag the release with the same version number, for example `v3.0.0`
|
|
912
954
|
- The workflow verifies that the tag matches the shared version
|
|
913
955
|
- The workflow publishes `featurevisor` first, followed by `featurevisor-openfeature`
|
|
914
956
|
|
|
@@ -11,13 +11,15 @@ module Featurevisor
|
|
|
11
11
|
|
|
12
12
|
attr_reader :metadata, :featurevisor
|
|
13
13
|
|
|
14
|
-
def initialize(options = {}, featurevisor: nil, targeting_key_field: "userId", key_separator: ":", variation_key: "variation", on_track: nil, **featurevisor_options)
|
|
14
|
+
def initialize(options = {}, featurevisor: nil, targeting_key_field: "userId", key_separator: ":", variation_key: "variation", global_variable_prefix: "variable", on_track: nil, **featurevisor_options)
|
|
15
15
|
raise ArgumentError, "options must be a Hash" unless options.is_a?(Hash)
|
|
16
16
|
|
|
17
17
|
@metadata = Provider::ProviderMetadata.new(name: "Featurevisor").freeze
|
|
18
18
|
@targeting_key_field = targeting_key_field.empty? ? "userId" : targeting_key_field
|
|
19
19
|
@key_separator = key_separator.empty? ? ":" : key_separator
|
|
20
20
|
@variation_key = variation_key.empty? ? "variation" : variation_key
|
|
21
|
+
@global_variable_prefix = global_variable_prefix.empty? ? "variable" : global_variable_prefix
|
|
22
|
+
raise ArgumentError, "global_variable_prefix cannot contain key_separator" if @global_variable_prefix.include?(@key_separator)
|
|
21
23
|
@on_track = on_track
|
|
22
24
|
@datafile_error = nil
|
|
23
25
|
@shutdown = false
|
|
@@ -86,7 +88,10 @@ module Featurevisor
|
|
|
86
88
|
targeting_key = evaluation_context&.targeting_key
|
|
87
89
|
context[@targeting_key_field] = targeting_key if targeting_key && !targeting_key.empty?
|
|
88
90
|
|
|
89
|
-
if selector
|
|
91
|
+
if feature_key == @global_variable_prefix && selector && !selector.empty?
|
|
92
|
+
evaluation = featurevisor.evaluate_variable(selector, context)
|
|
93
|
+
value = evaluation[:variable_value]
|
|
94
|
+
elsif selector.nil? || selector.empty?
|
|
90
95
|
return type_mismatch(flag_key, default_value, expected_type) unless expected_type == :boolean
|
|
91
96
|
evaluation = featurevisor.evaluate_flag(feature_key, context)
|
|
92
97
|
value = evaluation[:enabled]
|
|
@@ -96,7 +101,8 @@ module Featurevisor
|
|
|
96
101
|
else
|
|
97
102
|
evaluation = featurevisor.evaluate_variable(feature_key, selector, context)
|
|
98
103
|
value = evaluation[:variable_value]
|
|
99
|
-
|
|
104
|
+
variable_type = evaluation.dig(:variable_schema, :type) || evaluation.dig(:variable, :type)
|
|
105
|
+
if variable_type == "json" && value.is_a?(String)
|
|
100
106
|
begin
|
|
101
107
|
value = JSON.parse(value)
|
|
102
108
|
rescue JSON::ParserError
|
|
@@ -127,10 +133,10 @@ module Featurevisor
|
|
|
127
133
|
|
|
128
134
|
def metadata_for(evaluation)
|
|
129
135
|
metadata = {
|
|
130
|
-
"featureKey" => evaluation[:feature_key],
|
|
131
136
|
"featurevisorReason" => evaluation[:reason],
|
|
132
137
|
"schemaVersion" => featurevisor.get_schema_version
|
|
133
138
|
}
|
|
139
|
+
metadata["featureKey"] = evaluation[:feature_key] unless evaluation[:feature_key].nil?
|
|
134
140
|
metadata["revision"] = featurevisor.get_revision if featurevisor.get_revision
|
|
135
141
|
{
|
|
136
142
|
variable_key: "variableKey",
|
|
@@ -138,7 +144,9 @@ module Featurevisor
|
|
|
138
144
|
bucket_key: "bucketKey",
|
|
139
145
|
bucket_value: "bucketValue",
|
|
140
146
|
force_index: "forceIndex",
|
|
141
|
-
variable_override_index: "variableOverrideIndex"
|
|
147
|
+
variable_override_index: "variableOverrideIndex",
|
|
148
|
+
variable_override_key: "variableOverrideKey",
|
|
149
|
+
variable_override_path: "variableOverridePath"
|
|
142
150
|
}.each do |key, metadata_key|
|
|
143
151
|
metadata[metadata_key] = evaluation[key] unless evaluation[key].nil?
|
|
144
152
|
end
|
|
@@ -149,7 +157,7 @@ module Featurevisor
|
|
|
149
157
|
return Provider::Reason::ERROR if %w[feature_not_found variable_not_found no_variations error].include?(value)
|
|
150
158
|
return Provider::Reason::TARGETING_MATCH if %w[required forced sticky rule variable_override_variation variable_override_rule].include?(value)
|
|
151
159
|
return Provider::Reason::SPLIT if value == "allocated"
|
|
152
|
-
return Provider::Reason::DISABLED if %w[disabled variation_disabled variable_disabled].include?(value)
|
|
160
|
+
return Provider::Reason::DISABLED if %w[disabled variation_disabled variable_disabled required_features_unmet].include?(value)
|
|
153
161
|
Provider::Reason::DEFAULT
|
|
154
162
|
end
|
|
155
163
|
|
|
@@ -162,7 +170,10 @@ module Featurevisor
|
|
|
162
170
|
def error_message(evaluation)
|
|
163
171
|
return evaluation[:error].message if evaluation[:error].respond_to?(:message)
|
|
164
172
|
return %(Feature "#{evaluation[:feature_key]}" was not found) if evaluation[:reason] == "feature_not_found"
|
|
165
|
-
|
|
173
|
+
if evaluation[:reason] == "variable_not_found"
|
|
174
|
+
return %(Variable "#{evaluation[:variable_key]}" was not found) unless evaluation[:feature_key]
|
|
175
|
+
return %(Variable "#{evaluation[:variable_key]}" was not found for feature "#{evaluation[:feature_key]}")
|
|
176
|
+
end
|
|
166
177
|
return %(Feature "#{evaluation[:feature_key]}" has no variations) if evaluation[:reason] == "no_variations"
|
|
167
178
|
"Featurevisor evaluation failed"
|
|
168
179
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: featurevisor-openfeature
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fahad Heylaal
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: 3.0.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
25
|
+
version: 3.0.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: openfeature-sdk
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|