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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82c9c9135ecbcd5961523a67d92b068f019eaff3db7d8685f050c074766ef9de
4
- data.tar.gz: ac09d5e0b55134b4184c599b430d4db35c1740aa70a5afdf59673aed98cd7620
3
+ metadata.gz: 3a8495a304439dace1b52ba45bd2a7e9b4947cf482a179af80b9a3c8bb1f8e2c
4
+ data.tar.gz: 3473b28bac62de80fd0950c895d1a1a912919752cb99007cf04f922702bd6bb1
5
5
  SHA512:
6
- metadata.gz: 30b5381e7d8dcd6e5d789bda4923690b69fcc4902dbeee29b81537383d79d583e6ea3138f2600e475e79ffaeb11732c65c9ddc88befcf08ad07cf185cc82f178
7
- data.tar.gz: 4dbbdd99168585f1f714cef6a476122e263f354d1cb379594ecdd7440ae9155a2f02ed6f659e08cb9c4206bcf7c6c97ea503a8968e55fd0f6eefcd11772599d9
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 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)
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
- - [`sticky_set`](#sticky_set)
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`, `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.
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 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
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.get_all_evaluations({})
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
- ## 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
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. 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.
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
- sticky: {
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.set_sticky({
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 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.
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
- ### `sticky_set`
572
+ ### `sticky_features_set` and `sticky_variables_set`
549
573
 
550
574
  ```ruby
551
- unsubscribe = f.on('sticky_set') do |event|
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
- - `set_sticky`
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
- - `get_all_evaluations`
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", "~> 2.0"
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, 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.
856
898
 
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.
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 `v2.0.0`
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.nil? || selector.empty?
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
- if evaluation.dig(:variable_schema, :type) == "json" && value.is_a?(String)
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
- return %(Variable "#{evaluation[:variable_key]}" was not found for feature "#{evaluation[:feature_key]}") if evaluation[:reason] == "variable_not_found"
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: 2.0.0
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: 2.0.0
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: 2.0.0
25
+ version: 3.0.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: openfeature-sdk
28
28
  requirement: !ruby/object:Gem::Requirement