Skip to content

Commit e89cf98

Browse files
authored
feat: global variables in Ruby (#6)
1 parent 4e0a716 commit e89cf98

25 files changed

Lines changed: 1457 additions & 470 deletions

‎Gemfile.lock‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
featurevisor (2.0.0)
4+
featurevisor (3.0.0)
55
benchmark (>= 0, < 1)
66

77
GEM

‎README.md‎

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
1919
- [Getting variation](#getting-variation)
2020
- [Getting variables](#getting-variables)
2121
- [Type specific methods](#type-specific-methods)
22-
- [Getting all evaluations](#getting-all-evaluations)
23-
- [Sticky](#sticky)
22+
- [Getting global variables](#getting-global-variables)
23+
- [Getting aggregate evaluations](#getting-aggregate-evaluations)
24+
- [Sticky features and variables](#sticky-features-and-variables)
2425
- [Initialize with sticky](#initialize-with-sticky)
2526
- [Set sticky afterwards](#set-sticky-afterwards)
2627
- [Setting datafile](#setting-datafile)
@@ -36,7 +37,7 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
3637
- [Events](#events)
3738
- [`datafile_set`](#datafile_set)
3839
- [`context_set`](#context_set)
39-
- [`sticky_set`](#sticky_set)
40+
- [`sticky_features_set` and `sticky_variables_set`](#sticky_features_set-and-sticky_variables_set)
4041
- [`error`](#error)
4142
- [Modules](#modules)
4243
- [Defining a module](#defining-a-module)
@@ -89,7 +90,7 @@ f = Featurevisor.create_featurevisor(
8990

9091
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.
9192

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`, `set_sticky`, `add_module`, `remove_module`, and `close`. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.
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.
9394

9495
## Initialization
9596

@@ -295,12 +296,24 @@ f.get_variable_json(feature_key, variable_key, context = {})
295296

296297
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.
297298

298-
## Getting all evaluations
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
299312

300313
You can get evaluations of all features available in the SDK instance:
301314

302315
```ruby
303-
all_evaluations = f.get_all_evaluations({})
316+
all_evaluations = f.get_feature_evaluations({})
304317

305318
puts all_evaluations
306319
# {
@@ -321,19 +334,25 @@ puts all_evaluations
321334

322335
This is handy especially when you want to pass all evaluations from a backend application to the frontend.
323336

324-
## Sticky
337+
Global variable values are available separately:
338+
339+
```ruby
340+
global_variables = f.get_variable_evaluations({})
341+
```
342+
343+
## Sticky features and variables
325344

326345
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/):
327346

328-
Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use `spawn(context, sticky: ...)` when a child needs its own sticky state.
347+
Sticky values belong to an SDK or child instance. Feature and global variable sticky values are independent.
329348

330349
### Initialize with sticky
331350

332351
```ruby
333352
require 'featurevisor'
334353

335354
f = Featurevisor.create_featurevisor(
336-
sticky: {
355+
sticky_features: {
337356
myFeatureKey: {
338357
enabled: true,
339358
# optional
@@ -345,6 +364,9 @@ f = Featurevisor.create_featurevisor(
345364
anotherFeatureKey: {
346365
enabled: false
347366
}
367+
},
368+
sticky_variables: {
369+
welcomeMessage: 'Welcome back'
348370
}
349371
)
350372
```
@@ -356,7 +378,7 @@ Once initialized with sticky features, the SDK will look for values there first
356378
You can also set sticky features after the SDK is initialized:
357379

358380
```ruby
359-
f.set_sticky({
381+
f.set_sticky_features({
360382
myFeatureKey: {
361383
enabled: true,
362384
variation: 'treatment',
@@ -368,6 +390,8 @@ f.set_sticky({
368390
enabled: false
369391
}
370392
}, true) # replace existing sticky features (false by default)
393+
394+
f.set_sticky_variables({ welcomeMessage: 'Welcome back' }, true)
371395
```
372396

373397
## Setting datafile
@@ -393,7 +417,7 @@ By default, `set_datafile(datafile)` merges the incoming datafile into the SDK's
393417
- `segments` are merged, with incoming entries overriding existing ones
394418
- `features` are merged, with incoming entries overriding existing ones
395419

396-
This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together.
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.
397421

398422
### Replacing
399423

@@ -545,15 +569,19 @@ unsubscribe = f.on('context_set') do |event|
545569
end
546570
```
547571

548-
### `sticky_set`
572+
### `sticky_features_set` and `sticky_variables_set`
549573

550574
```ruby
551-
unsubscribe = f.on('sticky_set') do |event|
575+
feature_unsubscribe = f.on('sticky_features_set') do |event|
552576
replaced = event[:replaced] # true if sticky features got replaced
553577
features = event[:features] # list of all affected feature keys
554578

555579
puts 'Sticky features set'
556580
end
581+
582+
variable_unsubscribe = f.on('sticky_variables_set') do |event|
583+
variables = event[:variables]
584+
end
557585
```
558586

559587
### `error`
@@ -583,11 +611,14 @@ evaluation = f.evaluate_variation(feature_key, context = {})
583611

584612
# variable
585613
evaluation = f.evaluate_variable(feature_key, variable_key, context = {})
614+
615+
# global variable
616+
global_evaluation = f.evaluate_variable(variable_key, context = {})
586617
```
587618

588619
The returned object will always contain the following properties:
589620

590-
- `feature_key`: the feature key
621+
- `feature_key`: the feature key when evaluating a feature
591622
- `reason`: the reason how the value was evaluated
592623

593624
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
606637

607638
Modules allow you to intercept the evaluation process and customize SDK behavior.
608639

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+
609642
### Defining a module
610643

611644
A module is a simple hash with a unique recommended `name` and optional lifecycle functions:
@@ -639,6 +672,9 @@ my_custom_module = {
639672
options
640673
},
641674

675+
# unified callback for feature and global variable evaluations
676+
before_evaluation: ->(options) { options },
677+
642678
# after evaluation
643679
after: ->(evaluation, options) {
644680
reason = evaluation[:reason]
@@ -648,6 +684,9 @@ my_custom_module = {
648684
end
649685
},
650686

687+
# unified callback for feature and global variable evaluations
688+
after_evaluation: ->(evaluation, options) { evaluation },
689+
651690
# configure bucket key
652691
bucket_key: ->(options) {
653692
# return custom bucket key
@@ -720,12 +759,14 @@ Now you can pass the child instance where your individual request is being handl
720759
is_enabled = child_f.is_enabled('my_feature')
721760
variation = child_f.get_variation('my_feature')
722761
variable_value = child_f.get_variable('my_feature', 'my_variable')
762+
global_value = child_f.get_variable('welcomeMessage')
723763
```
724764

725765
Similar to parent SDK, child instances also support several additional methods:
726766

727767
- `set_context`
728-
- `set_sticky`
768+
- `set_sticky_features`
769+
- `set_sticky_variables`
729770
- `evaluate_flag`
730771
- `is_enabled`
731772
- `evaluate_variation`
@@ -739,7 +780,8 @@ Similar to parent SDK, child instances also support several additional methods:
739780
- `get_variable_array`
740781
- `get_variable_object`
741782
- `get_variable_json`
742-
- `get_all_evaluations`
783+
- `get_feature_evaluations`
784+
- `get_variable_evaluations`
743785
- `on`
744786
- `close`
745787

@@ -828,7 +870,7 @@ The provider currently requires Ruby 3.4 or newer because that is the minimum ve
828870
Install the provider:
829871

830872
```ruby
831-
gem "featurevisor-openfeature", "~> 2.0"
873+
gem "featurevisor-openfeature", "~> 3.0"
832874
```
833875

834876
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(
852894
)
853895
```
854896

855-
Use `checkout` for a flag, `checkout:variation` for its variation, and `checkout:title` for its `title` variable. Boolean variables use the boolean resolver. Arrays, hashes, and JSON variables use the object resolver.
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.
856898

857-
OpenFeature's targeting key maps to `userId` by default. `targeting_key_field`, `key_separator`, and `variation_key` can customize the mapping.
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.
858900

859901
You can pass any Featurevisor initialization options directly to the provider. These options are used to create the Featurevisor instance owned by the provider:
860902

@@ -908,7 +950,7 @@ The build produces `featurevisor-VERSION.gem` and `featurevisor-openfeature-VERS
908950
- Run `bundle install`
909951
- Push commit to `main` branch
910952
- Wait for CI to complete
911-
- Tag the release with the same version number, for example `v2.0.0`
953+
- Tag the release with the same version number, for example `v3.0.0`
912954
- The workflow verifies that the tag matches the shared version
913955
- The workflow publishes `featurevisor` first, followed by `featurevisor-openfeature`
914956

‎bin/cli.rb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def self.parse(args)
4343
options.environment = v
4444
end
4545

46-
opts.on("--feature=FEATURE", "Feature key (required for benchmark)") do |v|
46+
opts.on("--feature=FEATURE", "Feature key") do |v|
4747
options.feature = v
4848
end
4949

@@ -150,6 +150,6 @@ def self.show_help
150150
puts " featurevisor benchmark --feature=myFeature --environment=dev --n=10000"
151151
puts " featurevisor assess-distribution --feature=myFeature --n=10000"
152152
puts ""
153-
puts "Note: benchmark command requires --environment and --feature options"
153+
puts "Note: benchmark requires --environment and either --feature or --variable"
154154
end
155155
end

‎bin/commands/benchmark.rb‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ def run
2222
exit 1
2323
end
2424

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"
2727
exit 1
2828
end
2929

@@ -41,7 +41,8 @@ def run_for_target(context, target)
4141
datafile_build_duration_ms = (datafile_build_duration * 1000).round
4242

4343
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
4546
puts " Environment: #{@options.environment}"
4647
puts " Target: #{target}" if target
4748
puts " Iterations: #{@options.n}"
@@ -59,7 +60,10 @@ def run_for_target(context, target)
5960
puts "Against context: #{context.to_json}"
6061

6162
# Run the appropriate benchmark
62-
if @options.variation
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
6367
puts "Evaluating variation #{@options.n} times..."
6468
output = benchmark_feature_variation(instance, @options.feature, context, @options.n)
6569
elsif @options.variable
@@ -223,6 +227,12 @@ def benchmark_feature_variable(instance, feature_key, variable_key, context, n)
223227
end
224228
end
225229

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+
226236
def format_value(value)
227237
if value.nil?
228238
"null"

0 commit comments

Comments
 (0)