featurevisor-openfeature 3.1.0 → 4.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: 9b95462003a714b5ba431826e55cfbdacaa79353bd3a2ef94b0362783563ff9e
4
- data.tar.gz: 1f369ffa08695ab9eeef87253ef78f8f0891712d597c831d689543d9e16b8f9a
3
+ metadata.gz: f89eaf90729c4f9df207452f0b7231306b2fe742eee5d234fa850b0764e82538
4
+ data.tar.gz: 899b0752508c9a783a389c3d5fe1e873e9bfe824cb80be21fec9ee6fede31477
5
5
  SHA512:
6
- metadata.gz: dadb82981756de2034d404cff611a78971e89f42cf5a1e50d43af13686d46bb2384a4fab9feb6b08a9fd8e6ca9f38d00e4a78839b36fba97844bdb23e56a592b
7
- data.tar.gz: 562954bcdf0b0173c947c87f0f6c47aa4ada512f2c0213f8c536e049b8f43086d93c8fb306f923f1830977a9659598b69ef9dd290f5190d5fa63ed5e26b2ba11
6
+ metadata.gz: 22aacb6eef143c68bb8ba454c9e193a7981a4fa50f8ba93845225202c8ea3ec4e5920878e24c5b50d9e1bb9b23555d0f2374ffe7bc11cb75339fc384eab9a1e4
7
+ data.tar.gz: 96be39005361a93aaa285e79f6a7b90c9e4eee544772f7666995a97842717f68868351cc8bdfae0f4764427ecff8179bc3bc33c041ea86cea183ae32a556beb0
data/README.md CHANGED
@@ -1,15 +1,13 @@
1
1
  # Featurevisor Ruby SDK <!-- omit in toc -->
2
2
 
3
- This is a port of Featurevisor [JavaScript SDK](https://featurevisor.com/docs/sdks/javascript/) v3.x to Ruby, providing a way to evaluate feature flags, variations, and variables in your Ruby applications.
4
-
5
- This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
3
+ Use the Featurevisor Ruby SDK to evaluate flags, variations and variables in Ruby applications.
6
4
 
7
5
  ## Table of contents <!-- omit in toc -->
8
6
 
9
7
  - [Installation](#installation)
10
- - [Public API](#public-api)
11
8
  - [Initialization](#initialization)
12
- - [Evaluation types](#evaluation-types)
9
+ - [Public API](#public-api)
10
+ - [Evaluation types](#evaluation-types)
13
11
  - [Context](#context)
14
12
  - [Setting initial context](#setting-initial-context)
15
13
  - [Setting after initialization](#setting-after-initialization)
@@ -18,10 +16,13 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
18
16
  - [Check if enabled](#check-if-enabled)
19
17
  - [Getting variation](#getting-variation)
20
18
  - [Getting variables](#getting-variables)
19
+ - [Getting feature variables](#getting-feature-variables)
21
20
  - [Type specific methods](#type-specific-methods)
22
- - [Getting global variables](#getting-global-variables)
23
- - [Getting aggregate evaluations](#getting-aggregate-evaluations)
24
- - [Sticky features and variables](#sticky-features-and-variables)
21
+ - [Getting global variables](#getting-global-variables)
22
+ - [Conditions](#conditions)
23
+ - [Composition](#composition)
24
+ - [Getting evaluations](#getting-evaluations)
25
+ - [Sticky](#sticky)
25
26
  - [Initialize with sticky](#initialize-with-sticky)
26
27
  - [Set sticky afterwards](#set-sticky-afterwards)
27
28
  - [Setting datafile](#setting-datafile)
@@ -30,7 +31,6 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
30
31
  - [Loading datafiles on demand](#loading-datafiles-on-demand)
31
32
  - [Updating datafile](#updating-datafile)
32
33
  - [Interval-based update](#interval-based-update)
33
- - [Evaluation details](#evaluation-details)
34
34
  - [Diagnostics](#diagnostics)
35
35
  - [Levels](#levels)
36
36
  - [Handler](#handler)
@@ -39,6 +39,7 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
39
39
  - [`context_set`](#context_set)
40
40
  - [`sticky_features_set` and `sticky_variables_set`](#sticky_features_set-and-sticky_variables_set)
41
41
  - [`error`](#error)
42
+ - [Evaluation details](#evaluation-details)
42
43
  - [Modules](#modules)
43
44
  - [Defining a module](#defining-a-module)
44
45
  - [Registering modules](#registering-modules)
@@ -50,6 +51,7 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
50
51
  - [Test against local monorepo's example-1](#test-against-local-monorepos-example-1)
51
52
  - [Benchmark](#benchmark)
52
53
  - [Assess distribution](#assess-distribution)
54
+ - [GitHub repositories](#github-repositories)
53
55
  - [Development](#development)
54
56
  - [Setting up](#setting-up)
55
57
  - [Running tests](#running-tests)
@@ -60,38 +62,26 @@ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
60
62
 
61
63
  ## Installation
62
64
 
65
+ Upgrading an existing application? Follow the [Ruby migration guide](https://featurevisor.com/docs/migrations/v4/ruby/).
66
+
63
67
  Add this line to your application's Gemfile:
64
68
 
65
69
  ```ruby
66
- gem 'featurevisor'
70
+ gem 'featurevisor', '~> 4.0'
67
71
  ```
68
72
 
69
73
  And then execute:
70
74
 
71
75
  ```bash
72
- $ bundle install
76
+ bundle install
73
77
  ```
74
78
 
75
79
  Or install it yourself as:
76
80
 
77
81
  ```bash
78
- $ gem install featurevisor
82
+ gem install featurevisor -v "~> 4.0"
79
83
  ```
80
84
 
81
- ## Public API
82
-
83
- The main runtime API is `Featurevisor.create_featurevisor`:
84
-
85
- ```ruby
86
- f = Featurevisor.create_featurevisor(
87
- datafile: datafile_content
88
- )
89
- ```
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.
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
-
95
85
  ## Initialization
96
86
 
97
87
  The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly:
@@ -102,8 +92,11 @@ require 'net/http'
102
92
  require 'json'
103
93
 
104
94
  # Fetch datafile from URL
105
- datafile_url = 'https://cdn.yoursite.com/datafile.json'
95
+ datafile_url = 'https://cdn.yoursite.com/featurevisor-all.json'
106
96
  response = Net::HTTP.get_response(URI(datafile_url))
97
+ unless response.is_a?(Net::HTTPSuccess)
98
+ raise "Datafile request failed: HTTP #{response.code}"
99
+ end
107
100
 
108
101
  # Parse JSON with symbolized keys (required)
109
102
  datafile_content = JSON.parse(response.body, symbolize_names: true)
@@ -112,11 +105,18 @@ datafile_content = JSON.parse(response.body, symbolize_names: true)
112
105
  f = Featurevisor.create_featurevisor(
113
106
  datafile: datafile_content
114
107
  )
108
+ begin
109
+ puts f.is_enabled('checkout', { userId: '123' })
110
+ ensure
111
+ f.close
112
+ end
115
113
  ```
116
114
 
115
+ Network errors, unsuccessful HTTP responses and invalid JSON stop initialization. Handle these exceptions at your application's startup boundary.
116
+
117
117
  **Important**: When parsing JSON datafiles, you must use `symbolize_names: true` to ensure proper key handling by the SDK.
118
118
 
119
- Alternatively, you can pass a JSON string directly and the SDK will parse it automatically:
119
+ Pass a JSON string for the SDK to parse:
120
120
 
121
121
  ```ruby
122
122
  # Option 1: Parse JSON yourself (recommended)
@@ -127,21 +127,38 @@ f = Featurevisor.create_featurevisor(datafile: datafile_content)
127
127
  f = Featurevisor.create_featurevisor(datafile: json_string)
128
128
  ```
129
129
 
130
- ## Evaluation types
130
+ ### Public API
131
+
132
+ The main runtime API is `Featurevisor.create_featurevisor`:
133
+
134
+ ```ruby
135
+ f = Featurevisor.create_featurevisor(
136
+ datafile: datafile_content
137
+ )
138
+ ```
139
+
140
+ 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.
141
+
142
+ Concurrent evaluations and a single background datafile refresh writer are supported on MRI. Warm entity reads rely on MRI's execution model. Thread safety is not guaranteed on other Ruby implementations. CI covers MRI 3.0 and 3.4. Serialize writer operations. Do not change context, sticky state or modules, or close the instance, concurrently with evaluations on other threads. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.
131
143
 
132
- We can evaluate 3 types of values against a particular [feature](https://featurevisor.com/docs/features/):
144
+ ### Evaluation types
133
145
 
134
- - [**Flag**](#check-if-enabled) (`boolean`): whether the feature is enabled or not
135
- - [**Variation**](#getting-variation) (`string`): the variation of the feature (if any)
136
- - [**Variables**](#getting-variables): variable values of the feature (if any)
146
+ Featurevisor evaluates four kinds of values:
147
+
148
+ - [**Flag**](#check-if-enabled) (`true` or `false`): whether the feature is enabled or not
149
+ - [**Variation**](#getting-variation) (`String`): the variation of the feature (if any)
150
+ - [**Feature variable**](#getting-feature-variables): a configuration value owned by a feature
151
+ - [**Global variable**](#getting-global-variables): a configuration value evaluated independently of a feature
137
152
 
138
153
  These evaluations are run against the provided context.
139
154
 
140
155
  ## Context
141
156
 
142
- Contexts are [attribute](https://featurevisor.com/docs/attributes/) values that we pass to SDK for evaluating [features](https://featurevisor.com/docs/features/) against.
157
+ Contexts are [attribute](https://featurevisor.com/docs/attributes/) values you pass to the SDK for evaluating [features](https://featurevisor.com/docs/features/) against.
158
+
159
+ Conditions compare these attributes using the [operators](https://featurevisor.com/docs/operators/), including `empty` and `notEmpty`.
143
160
 
144
- Think of the conditions that you define in your [segments](https://featurevisor.com/docs/segments/), which are used in your feature's [rules](https://featurevisor.com/docs/features/#rules).
161
+ Date comparisons with `before` and `after` accept portable timestamp strings, `Time` and `DateTime`. Native values are compared by their instant and truncated to milliseconds without rounding. Both the native written year and the UTC year must be within 1 to 9999. Native offsets must have a magnitude of at most 14 hours, including any seconds or fractions supported by the type. Offsets within that limit retain their full precision. A plain `Date` is not accepted because it has no time or offset. Pass `Time.utc(2026, 1, 1)` or `'2026-01-01T00:00:00Z'` instead of `Date.new(2026, 1, 1)`. Invalid values make both operators return `false`.
145
162
 
146
163
  They are plain hashes:
147
164
 
@@ -153,11 +170,9 @@ context = {
153
170
  }
154
171
  ```
155
172
 
156
- Context can be passed to SDK instance in various different ways, depending on your needs:
157
-
158
173
  ### Setting initial context
159
174
 
160
- You can set context at the time of initialization:
175
+ Set context when you create the instance:
161
176
 
162
177
  ```ruby
163
178
  require 'featurevisor'
@@ -174,7 +189,7 @@ This is useful for values that don't change too frequently and available at the
174
189
 
175
190
  ### Setting after initialization
176
191
 
177
- You can also set more context after the SDK has been initialized:
192
+ Add context later:
178
193
 
179
194
  ```ruby
180
195
  f.set_context({
@@ -186,7 +201,7 @@ This will merge the new context with the existing one (if already set).
186
201
 
187
202
  ### Replacing existing context
188
203
 
189
- If you wish to fully replace the existing context, you can pass `true` in second argument:
204
+ Replace the context by passing `true` as the second argument:
190
205
 
191
206
  ```ruby
192
207
  f.set_context({
@@ -199,7 +214,7 @@ f.set_context({
199
214
 
200
215
  ### Manually passing context
201
216
 
202
- You can optionally pass additional context manually for each and every evaluation separately, without needing to set it to the SDK instance affecting all evaluations:
217
+ Pass context to one evaluation without changing the instance context:
203
218
 
204
219
  ```ruby
205
220
  context = {
@@ -207,21 +222,19 @@ context = {
207
222
  country: 'nl'
208
223
  }
209
224
 
210
- is_enabled = f.is_enabled('my_feature', context)
211
- variation = f.get_variation('my_feature', context)
212
- variable_value = f.get_variable('my_feature', 'my_variable', context)
225
+ is_enabled = f.is_enabled('checkout', context)
226
+ variation = f.get_variation('checkout', context)
227
+ variable_value = f.get_variable('checkout', 'bgColor', context)
213
228
  ```
214
229
 
215
230
  When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value.
216
231
 
217
- Further details for each evaluation types are described below.
218
-
219
232
  ## Check if enabled
220
233
 
221
- Once the SDK is initialized, you can check if a feature is enabled or not:
234
+ Check whether a feature is enabled:
222
235
 
223
236
  ```ruby
224
- feature_key = 'my_feature'
237
+ feature_key = 'checkout'
225
238
 
226
239
  is_enabled = f.is_enabled(feature_key)
227
240
 
@@ -230,7 +243,7 @@ if is_enabled
230
243
  end
231
244
  ```
232
245
 
233
- You can also pass additional context per evaluation:
246
+ Pass additional context to one evaluation:
234
247
 
235
248
  ```ruby
236
249
  is_enabled = f.is_enabled(feature_key, {
@@ -240,10 +253,10 @@ is_enabled = f.is_enabled(feature_key, {
240
253
 
241
254
  ## Getting variation
242
255
 
243
- If your feature has any [variations](https://featurevisor.com/docs/features/#variations) defined, you can evaluate them as follows:
256
+ Evaluate a feature's [variations](https://featurevisor.com/docs/features/#variations):
244
257
 
245
258
  ```ruby
246
- feature_key = 'my_feature'
259
+ feature_key = 'checkout'
247
260
 
248
261
  variation = f.get_variation(feature_key)
249
262
 
@@ -254,7 +267,7 @@ else
254
267
  end
255
268
  ```
256
269
 
257
- Additional context per evaluation can also be passed:
270
+ Pass additional context to one evaluation:
258
271
 
259
272
  ```ruby
260
273
  variation = f.get_variation(feature_key, {
@@ -264,18 +277,20 @@ variation = f.get_variation(feature_key, {
264
277
 
265
278
  ## Getting variables
266
279
 
267
- Your features may also include [variables](https://featurevisor.com/docs/features/#variables), which can be evaluated as follows:
280
+ ### Getting feature variables
281
+
282
+ Evaluate a feature's [variables](https://featurevisor.com/docs/features/#variables):
268
283
 
269
284
  ```ruby
270
285
  variable_key = 'bgColor'
271
286
 
272
- bg_color_value = f.get_variable('my_feature', variable_key)
287
+ bg_color_value = f.get_variable('checkout', variable_key)
273
288
  ```
274
289
 
275
- Additional context per evaluation can also be passed:
290
+ Pass additional context to one evaluation:
276
291
 
277
292
  ```ruby
278
- bg_color_value = f.get_variable('my_feature', variable_key, {
293
+ bg_color_value = f.get_variable('checkout', variable_key, {
279
294
  # ...additional context
280
295
  })
281
296
  ```
@@ -288,7 +303,7 @@ Next to generic `get_variable()` methods, there are also type specific methods a
288
303
  f.get_variable_boolean(feature_key, variable_key, context = {})
289
304
  f.get_variable_string(feature_key, variable_key, context = {})
290
305
  f.get_variable_integer(feature_key, variable_key, context = {})
291
- f.get_variable_double(feature_key, variable_key, context = {})
306
+ f.get_variable_number(feature_key, variable_key, context = {})
292
307
  f.get_variable_array(feature_key, variable_key, context = {})
293
308
  f.get_variable_object(feature_key, variable_key, context = {})
294
309
  f.get_variable_json(feature_key, variable_key, context = {})
@@ -296,7 +311,9 @@ f.get_variable_json(feature_key, variable_key, context = {})
296
311
 
297
312
  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.
298
313
 
299
- ## Getting global variables
314
+ `get_variable_number` reads a variable of type `number` as a `Float`, whether it holds `3` or `4.5`. Datafiles built before v4 call that type `double`, and it reads the same way. The v3 name `get_variable_double` was removed in v4.
315
+
316
+ ### Getting global variables
300
317
 
301
318
  Global variables use the same methods with a single variable key:
302
319
 
@@ -306,18 +323,89 @@ value = f.get_variable('checkoutSettings', context)
306
323
  evaluation = f.evaluate_variable('checkoutSettings', context)
307
324
  ```
308
325
 
309
- Global variables resolve sticky values first, then required features, then the first matching override, and finally their default value.
326
+ Overrides and conditions are applied for you. See [Variables](https://featurevisor.com/docs/variables/#global-variables) and [Conditions](https://featurevisor.com/docs/conditions/) for how to define them.
327
+
328
+ To see which overrides changed a value, inspect `variable_override_paths` in the detailed evaluation:
329
+
330
+ ```ruby
331
+ evaluation = f.evaluate_variable('runtimePolicy', country: 'nl')
332
+ value = evaluation[:variable_value]
333
+ matched_paths = evaluation[:variable_override_paths]
334
+ ```
335
+
336
+ Treat sticky feature and variable maps, including nested values passed to the SDK or returned by modules, as read only during evaluation. Use the SDK setters to change owned sticky state.
337
+
338
+ Modules should return the same result for the same input during one evaluation. Pass values that change through context.
339
+
340
+ ## Conditions
341
+
342
+ [Conditions](https://featurevisor.com/docs/conditions/) are shared by segments, feature and global variable root gates, force entries, traffic rules and variable overrides. Generated v4 datafiles use `schemaVersion: "4"`. Arrays imply AND. Logical expressions use `and`, `or` and `not`. NOT negates the AND of its direct children. Empty expressions are false.
343
+
344
+ Leaves compare context attributes, reference a segment by its literal string key, check a feature's enabled state and optional variation, or compare an evaluated variable. Feature enabled checks default to true. Variable leaves may name a feature owner and use already compiled path arrays. Dots and slashes in variable keys remain literal.
345
+
346
+ For example, a compiled root gate can combine all four leaf kinds:
347
+
348
+ ```json
349
+ {
350
+ "conditions": [
351
+ {"attribute": "country", "operator": "equals", "value": "nl"},
352
+ "eligibleAudience",
353
+ {"feature": "checkout", "variation": "treatment"},
354
+ {"variable": "policy", "path": ["items", 0, "enabled"], "operator": "equals", "value": true}
355
+ ]
356
+ }
357
+ ```
358
+
359
+ Continue using ordinary getters:
360
+
361
+ ```ruby
362
+ enabled = f.is_enabled('checkout', country: 'nl')
363
+ policy = f.get_variable('policy')
364
+ ```
365
+
366
+ Feature evaluation checks sticky state, force, its root gate and then traffic rules. Force bypasses the gate. Global variables check sticky state and their gate before reading sources or applying overrides. A missed root gate reports `conditions_unmet` and `evaluation[:conditions]`. Variation and feature variable evaluations retain their ordinary disabled reasons and fallbacks.
367
+
368
+ Conditions preserve written order and short circuit. Segments and getter dependencies share the current request session and a combined depth limit of 256 internal handoffs. Cycles and excessive depth fail the whole evaluation. Later independent calls start fresh sessions. An ordinary completed false or nil getter result remains a value.
369
+
370
+ ## Composition
371
+
372
+ [Composed variables](https://featurevisor.com/docs/variables/#compose-values) read sources through ordinary getters with the effective context, modules and sticky state. A compiled list assembles one fresh outer hash with literal property names:
373
+
374
+ ```json
375
+ {
376
+ "type": "object",
377
+ "compose": [
378
+ {"key": "enabled", "feature": "checkout"},
379
+ {"key": "assignment", "feature": "checkout", "variation": true},
380
+ {"key": "title", "feature": "checkout", "variable": "title"},
381
+ {"key": "policy", "variable": "policy"}
382
+ ]
383
+ }
384
+ ```
385
+
386
+ The string form, `"compose": "policy"`, is an alias of that literal global variable key. It returns the evaluated source directly, including false, zero, empty strings, arrays and nil. JSON sources are decoded exactly once, including when their decoded strings resemble JSON.
387
+
388
+ ```ruby
389
+ screen = f.get_variable('screen', country: 'nl')
390
+ details = f.evaluate_variable('screen', country: 'nl')
391
+ ```
392
+
393
+ Compositions do not declare `defaultValue` or `useDefaultWhenDisabled`. They may have root `conditions` and `disabledValue`. An unmet gate skips every source and override and returns the disabled value or nil. Whole variable sticky state bypasses both assembly and overrides. Explicit defaults supplied to the outer getter are never forwarded to sources.
394
+
395
+ All members are read before runtime overrides. Overrides use the existing mutation operations and provenance fields. The reason is `variable_composed`, or `variable_override_rule` when an override matches. Mutations copy changed containers without modifying source values, sibling properties or sticky values.
310
396
 
311
- ## Getting aggregate evaluations
397
+ Treat returned hashes and arrays as read only. Composition properties and aliases keep ordinary getter ownership, so descendants may be shared. Only the map composition's outer hash is guaranteed to be fresh. Ordinary source getter failures retain their normal fallbacks. Dependency failures invalidate the whole composition.
312
398
 
313
- You can get evaluations of all features available in the SDK instance:
399
+ ## Getting evaluations
400
+
401
+ Evaluate all features available in the instance:
314
402
 
315
403
  ```ruby
316
404
  all_evaluations = f.get_feature_evaluations({})
317
405
 
318
406
  puts all_evaluations
319
407
  # {
320
- # myFeature: {
408
+ # checkout: {
321
409
  # enabled: true,
322
410
  # variation: "control",
323
411
  # variables: {
@@ -340,9 +428,9 @@ Global variable values are available separately:
340
428
  global_variables = f.get_variable_evaluations({})
341
429
  ```
342
430
 
343
- ## Sticky features and variables
431
+ ## Sticky
344
432
 
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/):
433
+ Set sticky values to take precedence over feature evaluation against the [datafile](https://featurevisor.com/docs/building-datafiles/):
346
434
 
347
435
  Sticky values belong to an SDK or child instance. Feature and global variable sticky values are independent.
348
436
 
@@ -353,7 +441,7 @@ require 'featurevisor'
353
441
 
354
442
  f = Featurevisor.create_featurevisor(
355
443
  sticky_features: {
356
- myFeatureKey: {
444
+ checkout: {
357
445
  enabled: true,
358
446
  # optional
359
447
  variation: 'treatment',
@@ -375,11 +463,11 @@ Once initialized with sticky features, the SDK will look for values there first
375
463
 
376
464
  ### Set sticky afterwards
377
465
 
378
- You can also set sticky features after the SDK is initialized:
466
+ Set sticky features after initialization:
379
467
 
380
468
  ```ruby
381
469
  f.set_sticky_features({
382
- myFeatureKey: {
470
+ checkout: {
383
471
  enabled: true,
384
472
  variation: 'treatment',
385
473
  variables: {
@@ -407,7 +495,9 @@ f.set_datafile(datafile_content)
407
495
  f.set_datafile(json_string)
408
496
  ```
409
497
 
410
- **Important**: When calling `set_datafile()`, ensure JSON is parsed with `symbolize_names: true` if you're parsing it yourself.
498
+ `set_datafile` returns `true` when the datafile is accepted, including unchanged content. It returns `false` when the datafile is rejected or the instance is closed. Rejected datafiles leave the last accepted datafile in place. Diagnostics and events keep their existing behaviour.
499
+
500
+ Schema version `"2"` selectors normalize eagerly before merging. Schema version `"4"` entities normalize lazily on first read. Informational schema metadata is retained. Partial updates invalidate only incoming entities. Replacement clears normalized caches. Caller definitions are never changed.
411
501
 
412
502
  ### Merging by default
413
503
 
@@ -416,45 +506,32 @@ By default, `set_datafile(datafile)` merges the incoming datafile into the SDK's
416
506
  - top-level metadata such as `schemaVersion`, `revision`, and `featurevisorVersion` comes from the incoming datafile
417
507
  - `segments` are merged, with incoming entries overriding existing ones
418
508
  - `features` are merged, with incoming entries overriding existing ones
509
+ - global `variables` are merged, with incoming entries overriding existing ones
419
510
 
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.
511
+ Call `set_datafile` with different datafiles to merge their features, segments, and global variables.
421
512
 
422
513
  ### Replacing
423
514
 
424
515
  To fully replace the stored datafile, pass `true` as the second argument:
425
516
 
426
517
  ```ruby
427
- f.set_datafile(datafile_content, true)
518
+ f.set_datafile(datafile_content, true) # replace
428
519
  ```
429
520
 
430
521
  ### Loading datafiles on demand
431
522
 
432
- Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront.
433
-
434
- This pairs well with [targets](https://featurevisor.com/docs/targets/), where each target produces a smaller datafile for a specific part of your application:
523
+ Merge datafiles for different [targets](https://featurevisor.com/docs/targets/) into the same instance. Here, the two datafile values have already been loaded and validated by your application from the same published release:
435
524
 
436
525
  ```ruby
437
- require "open-uri"
438
-
439
- f = Featurevisor.create_featurevisor({})
440
-
441
- def load_datafile(f, target)
442
- url = "https://cdn.yoursite.com/production/featurevisor-#{target}.json"
443
- datafile = JSON.parse(URI.open(url).read, symbolize_names: true)
444
-
445
- # merges into whatever was loaded before
446
- f.set_datafile(datafile)
447
- end
448
-
449
- load_datafile(f, "products")
450
-
451
- # later, when the user reaches checkout
452
- load_datafile(f, "checkout")
526
+ f.set_datafile(products_datafile)
527
+ f.set_datafile(checkout_datafile)
453
528
  ```
454
529
 
530
+ Use immutable release URLs, check HTTP responses and keep shared dependencies consistent. See [Loading datafiles on demand](https://featurevisor.com/docs/use-cases/on-demand-datafiles/) for the complete loading and refresh flow.
531
+
455
532
  ### Updating datafile
456
533
 
457
- You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile-set) event that you can listen and react to accordingly.
534
+ Each accepted datafile update emits a [`datafile_set`](#datafile-set) event. Subscribe to react to updates.
458
535
 
459
536
  The triggers for setting the datafile again can be:
460
537
 
@@ -465,7 +542,7 @@ The triggers for setting the datafile again can be:
465
542
 
466
543
  ### Interval-based update
467
544
 
468
- Here's an example of using interval-based update:
545
+ Replace the complete datafile on each refresh so definitions removed upstream are removed from the SDK too. Use merging only when loading partial datafiles intentionally.
469
546
 
470
547
  ```ruby
471
548
  require 'net/http'
@@ -478,7 +555,7 @@ def update_datafile(f, datafile_url)
478
555
  begin
479
556
  response = Net::HTTP.get_response(URI(datafile_url))
480
557
  datafile_content = JSON.parse(response.body)
481
- f.set_datafile(datafile_content)
558
+ f.set_datafile(datafile_content, true) # replace
482
559
  rescue => e
483
560
  # handle error
484
561
  puts "Failed to update datafile: #{e.message}"
@@ -522,12 +599,13 @@ Every diagnostic has `:level`, `:code`, `:message`, and an object-shaped `:detai
522
599
 
523
600
  Diagnostic handlers are isolated from SDK behaviour. An exception in a handler does not stop other handlers or evaluations.
524
601
 
602
+ Malformed stringified selectors report `conditions_parse_error` at error level once per selector per incoming entity load. Details contain exactly `conditions` with the original string, `entityType` and `entityKey`. Schema 2 diagnostics run after accepted datafile storage. V4 diagnostics run after caching the first entity read. Handlers may safely read the accepted revision again. A malformed selector fails locally, even through NOT or segment references, without rejecting the datafile or disabling unrelated entities.
525
603
 
526
604
  ## Events
527
605
 
528
- Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime.
606
+ Subscribe to SDK events to respond to context, sticky state and datafile changes.
529
607
 
530
- You can listen to these events that can occur at various stages in your application:
608
+ Subscribe to instance lifecycle events:
531
609
 
532
610
  ### `datafile_set`
533
611
 
@@ -548,7 +626,7 @@ end
548
626
  unsubscribe.call
549
627
  ```
550
628
 
551
- The `features` array will contain keys of features that have either been:
629
+ The `features` and `variables` arrays include direct changes and transitive dependencies through conditions, segments and composition sources. They contain entities that have been:
552
630
 
553
631
  - added, or
554
632
  - updated, or
@@ -600,7 +678,7 @@ The `error` event is emitted for diagnostics reported with `level: "error"`.
600
678
 
601
679
  ## Evaluation details
602
680
 
603
- Besides logging with debug level enabled, you can also get more details about how the feature variations and variables are evaluated in the runtime against given context:
681
+ Inspect how variations and variables are evaluated against a context:
604
682
 
605
683
  ```ruby
606
684
  # flag
@@ -632,14 +710,24 @@ And optionally these properties depending on whether you are evaluating a featur
632
710
  - `variable_key`: the variable key
633
711
  - `variable_value`: the variable value
634
712
  - `variable_schema`: the variable schema
713
+ - `variable_override_index`: index of the final matched global variable override
714
+ - `variable_override_key`: key of the final matched global variable override
715
+ - `variable_override_path`: authoring path of the final matched global variable override
716
+ - `variable_override_paths`: all matched authoring paths from an accumulating override
717
+ - `conditions`: the normalized root expression when its gate is unmet
718
+ - `variable`: the global variable definition when applicable
635
719
 
636
720
  ## Modules
637
721
 
638
- Modules allow you to intercept the evaluation process and customize SDK behaviour.
722
+ The before evaluation and after evaluation phases each use the module membership at the start of that phase. Where an evaluation uses bucketing, the bucket key and bucket value phases each use their own starting membership. Adding or removing a module during a phase does not change the iteration already in progress. The next phase uses the membership then installed. A module added before the after phase can receive after callbacks for evaluations already in progress, even if it did not receive their before callbacks. A module removed before the after phase receives no after callback.
723
+
724
+ Module options expose `dependency_of` and `dependency_purpose` on internal reads. The requester contains `type`, optional `feature_key` and optional `variable_key`. Purpose is `condition` for gating reads or `composition` for consumed source values. Application calls have neither marker. Tracking modules can skip condition reads while retaining composition exposures.
639
725
 
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.
726
+ Equivalent dependency reads are memoized within one public call, separating purpose, requester, effective context and current SDK state. Condition snapshots are owned. Composition values retain ordinary getter ownership. Callback options do not retain private sessions. Independent public getters called from callbacks start fresh sessions.
641
727
 
642
- `before` and `after` remain available as deprecated feature-only compatibility callbacks. Use `before_evaluation` and `after_evaluation` for new modules so the same callbacks can handle feature and global variable evaluations.
728
+ Modules can intercept evaluation and participate in SDK lifecycle. The canonical callbacks are `setup`, `bucket_key`, `bucket_value`, `before_evaluation`, `after_evaluation`, and `close`.
729
+
730
+ `before_evaluation` and `after_evaluation` run for feature and global variable evaluations. Condition and composition dependency getters use the same module pipeline, while an accumulating override publishes only its final result to `after_evaluation`.
643
731
 
644
732
  ### Defining a module
645
733
 
@@ -710,7 +798,7 @@ The module API passed to `setup` exposes:
710
798
 
711
799
  ### Registering modules
712
800
 
713
- You can register modules at the time of SDK initialization:
801
+ Register modules when you create the instance:
714
802
 
715
803
  ```ruby
716
804
  require 'featurevisor'
@@ -749,12 +837,12 @@ child_f = f.spawn({
749
837
  })
750
838
  ```
751
839
 
752
- Now you can pass the child instance where your individual request is being handled, and you can continue to evaluate features targeting that specific user alone:
840
+ Pass the child instance to the request handler to evaluate features for that user:
753
841
 
754
842
  ```ruby
755
- is_enabled = child_f.is_enabled('my_feature')
756
- variation = child_f.get_variation('my_feature')
757
- variable_value = child_f.get_variable('my_feature', 'my_variable')
843
+ is_enabled = child_f.is_enabled('checkout')
844
+ variation = child_f.get_variation('checkout')
845
+ variable_value = child_f.get_variable('checkout', 'bgColor')
758
846
  global_value = child_f.get_variable('welcomeMessage')
759
847
  ```
760
848
 
@@ -772,7 +860,7 @@ Similar to parent SDK, child instances also support several additional methods:
772
860
  - `get_variable_boolean`
773
861
  - `get_variable_string`
774
862
  - `get_variable_integer`
775
- - `get_variable_double`
863
+ - `get_variable_number`
776
864
  - `get_variable_array`
777
865
  - `get_variable_object`
778
866
  - `get_variable_json`
@@ -783,80 +871,14 @@ Similar to parent SDK, child instances also support several additional methods:
783
871
 
784
872
  ## Close
785
873
 
786
- Both primary and child instances support a `.close()` method, that removes forgotten event listeners (via `on` method) and cleans up any potential memory leaks.
874
+ Both primary and child instances support a `.close()` method. It removes event listeners and child subscriptions. The primary instance also closes modules, clears diagnostic subscriptions and parsed expression caches, and releases its diagnostic handler after reporting any module shutdown errors. Closing is idempotent.
875
+
876
+ Ruby can collect unreachable reference cycles, but objects still referenced by your application remain alive. Call unsubscribe functions when finished with individual subscriptions, and close instances and children when finished with them. Retained instance handles still own their datafiles and context. Closing does not promise that every allocation is returned to the operating system.
787
877
 
788
878
  ```ruby
789
879
  f.close()
790
880
  ```
791
881
 
792
- ## CLI usage
793
-
794
- This package also provides a CLI tool for running your Featurevisor [project](https://featurevisor.com/docs/projects/)'s test specs and benchmarking against this Ruby SDK.
795
-
796
- - Global installation: you can access it as `featurevisor`
797
- - Local installation: you can access it as `bundle exec featurevisor`
798
- - From this repository: you can access it as `bin/featurevisor`
799
-
800
- ### Test
801
-
802
- Learn more about testing [here](https://featurevisor.com/docs/testing/).
803
-
804
- ```bash
805
- $ bundle exec featurevisor test --projectDirectoryPath="/absolute/path/to/your/featurevisor/project"
806
- ```
807
-
808
- Additional options that are available:
809
-
810
- ```bash
811
- $ bundle exec featurevisor test \
812
- --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
813
- --quiet|--verbose \
814
- --onlyFailures \
815
- --keyPattern="myFeatureKey" \
816
- --assertionPattern="#1"
817
- ```
818
-
819
- The Ruby test runner builds base datafiles and Target datafiles in memory via `npx featurevisor build --json`. When an assertion contains `target`, it is evaluated against the matching Target datafile.
820
-
821
- All three commands accept repeatable `--target=<target>` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behaviour is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.
822
-
823
- ### Test against local monorepo's example-1
824
-
825
- ```bash
826
- $ cd /absolute/path/to/featurevisor-ruby
827
- $ bundle exec ruby bin/featurevisor test --projectDirectoryPath=../featurevisor/examples/example-1 --onlyFailures
828
- $ make test-example-1
829
- ```
830
-
831
- ### Benchmark
832
-
833
- Learn more about benchmarking [here](https://featurevisor.com/docs/cli/#benchmarking).
834
-
835
- ```bash
836
- $ bundle exec featurevisor benchmark \
837
- --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
838
- --environment="production" \
839
- --feature="myFeatureKey" \
840
- --context='{"country": "nl"}' \
841
- --n=1000
842
- ```
843
-
844
- ### Assess distribution
845
-
846
- Learn more about assessing distribution [here](https://featurevisor.com/docs/cli/#assess-distribution).
847
-
848
- ```bash
849
- $ bundle exec featurevisor assess-distribution \
850
- --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
851
- --environment=production \
852
- --feature=foo \
853
- --variation \
854
- --context='{"country": "nl"}' \
855
- --populateUuid=userId \
856
- --populateUuid=deviceId \
857
- --n=1000
858
- ```
859
-
860
882
  ## OpenFeature
861
883
 
862
884
  The OpenFeature provider is published as a separate gem. This keeps OpenFeature code and dependencies out of applications that only use the Featurevisor SDK.
@@ -866,10 +888,10 @@ The provider currently requires Ruby 3.4 or newer because that is the minimum ve
866
888
  Install the provider:
867
889
 
868
890
  ```ruby
869
- gem "featurevisor-openfeature", "~> 3.0"
891
+ gem "featurevisor-openfeature", "~> 4.0"
870
892
  ```
871
893
 
872
- 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.
894
+ It installs the matching `featurevisor` gem and the official `openfeature-sdk ~> 0.6.5` dependency. The provider requires Ruby 3.4 or later, while the separate base gem supports Ruby 3.0 or later without OpenFeature. The provider and base SDK deliberately share the same version, and the provider requires that exact Featurevisor version.
873
895
 
874
896
  ```ruby
875
897
  require "featurevisor/openfeature_provider"
@@ -894,7 +916,7 @@ Use `checkout` for a flag, `checkout:variation` for its variation, `checkout:tit
894
916
 
895
917
  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.
896
918
 
897
- You can pass any Featurevisor initialization options directly to the provider. These options are used to create the Featurevisor instance owned by the provider:
919
+ Pass Featurevisor initialization options to the provider to configure the instance it owns:
898
920
 
899
921
  ```ruby
900
922
  provider = Featurevisor::OpenFeatureProvider.new(
@@ -905,19 +927,111 @@ provider = Featurevisor::OpenFeatureProvider.new(
905
927
  )
906
928
  ```
907
929
 
908
- You can also reuse an existing Featurevisor instance:
930
+ Reuse an existing Featurevisor instance:
909
931
 
910
932
  ```ruby
911
- featurevisor = Featurevisor.create_featurevisor(datafile: datafile_content)
912
- provider = Featurevisor::OpenFeatureProvider.new(featurevisor: featurevisor)
933
+ f = Featurevisor.create_featurevisor(datafile: datafile_content)
934
+ provider = Featurevisor::OpenFeatureProvider.new(featurevisor: f)
913
935
  ```
914
936
 
915
- The caller owns an instance passed this way. Provider shutdown does not close it. Call `featurevisor.close` when every consumer is finished with it. When the provider creates the instance from options, the provider owns and closes it. If both are supplied, `featurevisor` takes precedence over the options hash.
937
+ The caller owns an instance passed this way. Provider shutdown does not close it. Call `f.close` when every consumer is finished with it. When the provider creates the instance from options, the provider owns and closes it. If both are supplied, `featurevisor` takes precedence over the options hash.
938
+
939
+ An invalid initial datafile in an owned instance fails `set_provider_and_wait` with a provider initialization error carrying `PARSE_ERROR`, independently of `log_level`. A valid datafile update clears the error and emits readiness. Shutdown removes provider subscriptions and closes only an owned instance.
940
+
941
+ Root `conditions_unmet` outcomes map to OpenFeature `DISABLED`. Composed values and matched overrides map to `TARGETING_MATCH`. The provider does not decode composed JSON values again.
916
942
 
917
943
  See the [OpenFeature provider guide](https://featurevisor.com/docs/sdks/openfeature/) for resolution reasons, errors, metadata, tracking, lifecycle, and providers for other languages.
918
944
 
945
+ ## CLI usage
946
+
947
+ After adding the gem to your Gemfile as shown in [Installation](#installation), install it with:
948
+
949
+ ```bash
950
+ bundle install
951
+ ```
952
+
953
+ Use `bundle exec featurevisor` for a bundled installation, `featurevisor` for a global gem installation, or `bin/featurevisor` from this SDK repository.
954
+
955
+ The native test runner requires `@featurevisor/cli` 4.0.0 or later and Node.js 24 or later. Install the Node.js CLI in your Featurevisor project:
956
+
957
+ ```bash
958
+ npm install --save-dev @featurevisor/cli@^4.0.0
959
+ ```
960
+
961
+ The native CLI provides `test`, `benchmark` and `assess-distribution`. Use the Node.js CLI for authoring commands such as `init`, `lint` and `build`. Native commands delegate project parsing, test listing, matrix expansion and datafile generation to `npx featurevisor`, then evaluate with this Ruby SDK. The SDK runtime has no Node.js dependency.
962
+
963
+ Options use kebab case, such as `--project-directory-path`. The camelCase forms from earlier releases still work, and `--root-directory-path` is an alias for `--project-directory-path`. Pass values as `--name=value` or `--name value`. Repeat `--target` to select several targets in order. Run `<command> --help` to list every option.
964
+
965
+ Duplicate targets are evaluated once, at their first position. `--n` and `-n` accept a positive integer.
966
+
967
+ ### Test
968
+
969
+ Segment owners and checks run through ordinary SDK gates, including feature and variable dependencies, fixed `at`, context, sticky state and child state. Selected Target segment specializations remain authoritative. Compiled execution definitions restore only missing segments. Attribute only chains may run without an environment. Dependent segments require their selected datafile. Transitive references are preflighted before getters run. Synthetic gates are excluded from coverage. Testing does not automatically run lint.
970
+
971
+ ```bash
972
+ bundle exec featurevisor test \
973
+ --project-directory-path=/path/to/featurevisor-project \
974
+ --only-failures
975
+ ```
976
+
977
+ See [Testing](https://featurevisor.com/docs/testing/) for authoring specs and [Checks](https://featurevisor.com/docs/testing/#checks) for check syntax and operators.
978
+
979
+ Useful test options:
980
+
981
+ - `--key-pattern=<pattern>` and `--assertion-pattern=<pattern>` filter tests.
982
+ - `--entity-type=scenario` selects scenarios. `--set=<set>` selects one project set. Omitting `--set` runs all sets.
983
+ - `--case=<id>` reruns one expanded case. Copy the opaque ID or rerun command from a failure report.
984
+ - `--json --pretty` produces an indented JSON report.
985
+ - `--coverage` reports observed rule and override selections. `--uncovered` lists features and global variables without associated specs.
986
+ - `--only-failures`, `--quiet`, `--verbose` and `--show-datafile` control diagnostic output.
987
+
988
+ Printed rerun commands use POSIX shell quoting. JSON reports also include raw argument arrays.
989
+
990
+ ### Test against local monorepo's example-1
991
+
992
+ Contributor instructions are in the [repository development guide](https://github.com/featurevisor/featurevisor-ruby#development).
993
+
994
+ ### Benchmark
995
+
996
+ The examples below use a project without environments. Add `--environment=<name>` if your project configures environments. In a sets project, omit `--set` to run every set or pass `--set=<name>`. Repeat `--target=<key>` to evaluate specific target datafiles. The Node.js CLI validates these options and builds the datafiles. Ruby evaluates them. Replace `checkout` with a feature defined in your project.
997
+
998
+ ```bash
999
+ bundle exec featurevisor benchmark \
1000
+ --project-directory-path=/path/to/featurevisor-project \
1001
+ --feature=checkout \
1002
+ --context='{"country":"nl"}' \
1003
+ --n=1000
1004
+ ```
1005
+
1006
+ Use `--variation` for a variation or `--variable=<key>` for a feature variable. Omit `--feature` to benchmark a global variable. Do not combine `--variation` with `--variable`. See [Benchmarking](https://featurevisor.com/docs/cli/#benchmarking).
1007
+
1008
+ ### Assess distribution
1009
+
1010
+ ```bash
1011
+ bundle exec featurevisor assess-distribution \
1012
+ --project-directory-path=/path/to/featurevisor-project \
1013
+ --feature=checkout \
1014
+ --populate-uuid=userId \
1015
+ --n=1000
1016
+ ```
1017
+
1018
+ Repeat `--populate-uuid=<key>` to generate UUID values for multiple context attributes. Supply one key per option. See [Assess distribution](https://featurevisor.com/docs/cli/#assess-distribution).
1019
+
1020
+ ## GitHub repositories
1021
+
1022
+ - SDK: [featurevisor/featurevisor-ruby](https://github.com/featurevisor/featurevisor-ruby)
1023
+ - Example application: [featurevisor/featurevisor-example-ruby](https://github.com/featurevisor/featurevisor-example-ruby)
1024
+
919
1025
  <!-- FEATUREVISOR_DOCS_END -->
920
1026
 
1027
+ <a id="getting-aggregate-evaluations"></a>
1028
+
1029
+ The aggregate evaluation reference has moved to [Getting evaluations](#getting-evaluations).
1030
+
1031
+ <a id="sticky-features-and-variables"></a>
1032
+
1033
+ The sticky state reference has moved to [Sticky](#sticky).
1034
+
921
1035
  ## Development
922
1036
 
923
1037
  ### Setting up
@@ -932,6 +1046,8 @@ $ make test-base
932
1046
  $ make test-example-1
933
1047
  ```
934
1048
 
1049
+ Run `bundle exec rspec spec/readme_initialization_spec.rb` to execute the actual initialization snippet with simulated HTTP responses and transport errors. This check also runs in the ordinary SDK suite and CI.
1050
+
935
1051
  Build and verify both gems with:
936
1052
 
937
1053
  ```bash
@@ -942,11 +1058,13 @@ The build produces `featurevisor-VERSION.gem` and `featurevisor-openfeature-VERS
942
1058
 
943
1059
  ### Releasing
944
1060
 
1061
+ The next breaking release is `4.0.0` for both gems. Keep both Gemfile lockfiles aligned with the shared version. Run `make test`, `make test-base` and `make build` before tagging. The artefact check verifies the provider's exact dependency on the base gem and their separate contents.
1062
+
945
1063
  - Update the shared version in `lib/featurevisor/version.rb`
946
1064
  - Run `bundle install`
947
1065
  - Push commit to `main` branch
948
1066
  - Wait for CI to complete
949
- - Tag the release with the same version number, for example `v3.1.0`
1067
+ - Tag the release with the same version number, for example `vX.Y.Z`
950
1068
  - The workflow verifies that the tag matches the shared version
951
1069
  - The workflow publishes `featurevisor` first, followed by `featurevisor-openfeature`
952
1070
 
@@ -3,11 +3,12 @@
3
3
  require "json"
4
4
  require "time"
5
5
  require "open_feature/sdk"
6
- require_relative "../featurevisor"
6
+ require "featurevisor"
7
7
 
8
8
  module Featurevisor
9
9
  class OpenFeatureProvider
10
10
  Provider = OpenFeature::SDK::Provider
11
+ include Provider::EventEmitter
11
12
 
12
13
  attr_reader :metadata, :featurevisor
13
14
 
@@ -33,13 +34,26 @@ module Featurevisor
33
34
  end
34
35
  @error_unsubscribe = @featurevisor.on("error", lambda do |event|
35
36
  diagnostic = event[:diagnostic]
36
- @datafile_error = diagnostic[:message] if diagnostic&.dig(:code) == "invalid_datafile"
37
+ if diagnostic&.dig(:code) == "invalid_datafile"
38
+ @datafile_error = diagnostic[:message]
39
+ emit_event(OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR,
40
+ error_code: Provider::ErrorCode::PARSE_ERROR, message: @datafile_error)
41
+ end
42
+ end)
43
+ @datafile_unsubscribe = @featurevisor.on("datafile_set", lambda do |*|
44
+ recovering = !@datafile_error.nil?
45
+ @datafile_error = nil
46
+ emit_event(OpenFeature::SDK::ProviderEvent::PROVIDER_READY) if recovering
37
47
  end)
38
- @datafile_unsubscribe = @featurevisor.on("datafile_set", ->(*) { @datafile_error = nil })
39
- @featurevisor.set_datafile(initial_datafile, true) if @owns_featurevisor && initial_datafile
48
+ @featurevisor.set_datafile(initial_datafile, true) if @owns_featurevisor && !initial_datafile.nil?
40
49
  end
41
50
 
42
- def init(_evaluation_context = nil); end
51
+ def init(_evaluation_context = nil)
52
+ if @datafile_error
53
+ raise OpenFeature::SDK::ProviderInitializationError.new(@datafile_error,
54
+ provider: self, error_code: Provider::ErrorCode::PARSE_ERROR)
55
+ end
56
+ end
43
57
  def shutdown
44
58
  return if @shutdown
45
59
 
@@ -50,7 +64,7 @@ module Featurevisor
50
64
  end
51
65
  def hooks = []
52
66
 
53
- def track(tracking_event_name:, evaluation_context: nil, tracking_event_details: nil)
67
+ def track(name = nil, tracking_event_name: name, evaluation_context: nil, tracking_event_details: nil)
54
68
  @on_track&.call(tracking_event_name, evaluation_context, tracking_event_details)
55
69
  end
56
70
 
@@ -86,7 +100,7 @@ module Featurevisor
86
100
  feature_key, selector = split_key(flag_key)
87
101
  context = normalize(evaluation_context&.fields || {})
88
102
  targeting_key = evaluation_context&.targeting_key
89
- context[@targeting_key_field] = targeting_key if targeting_key && !targeting_key.empty?
103
+ context[@targeting_key_field] = targeting_key unless targeting_key.nil?
90
104
 
91
105
  if feature_key == @global_variable_prefix && selector && !selector.empty?
92
106
  evaluation = featurevisor.evaluate_variable(selector, context)
@@ -101,8 +115,10 @@ module Featurevisor
101
115
  else
102
116
  evaluation = featurevisor.evaluate_variable(feature_key, selector, context)
103
117
  value = evaluation[:variable_value]
118
+ end
119
+ if evaluation[:type] == :variable || evaluation[:type] == "variable"
104
120
  variable_type = evaluation.dig(:variable_schema, :type) || evaluation.dig(:variable, :type)
105
- if variable_type == "json" && value.is_a?(String)
121
+ if variable_type == "json" && !evaluation.dig(:variable, :compose) && value.is_a?(String)
106
122
  begin
107
123
  value = JSON.parse(value)
108
124
  rescue JSON::ParserError
@@ -155,9 +171,9 @@ module Featurevisor
155
171
 
156
172
  def reason(value)
157
173
  return Provider::Reason::ERROR if %w[feature_not_found variable_not_found no_variations error].include?(value)
158
- return Provider::Reason::TARGETING_MATCH if %w[required forced sticky rule variable_override_variation variable_override_rule].include?(value)
174
+ return Provider::Reason::TARGETING_MATCH if %w[forced sticky rule variable_override_variation variable_override_rule variable_composed].include?(value)
159
175
  return Provider::Reason::SPLIT if value == "allocated"
160
- return Provider::Reason::DISABLED if %w[disabled variation_disabled variable_disabled required_features_unmet].include?(value)
176
+ return Provider::Reason::DISABLED if %w[disabled variation_disabled variable_disabled conditions_unmet].include?(value)
161
177
  Provider::Reason::DEFAULT
162
178
  end
163
179
 
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: 3.1.0
4
+ version: 4.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: 3.1.0
18
+ version: 4.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: 3.1.0
25
+ version: 4.0.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: openfeature-sdk
28
28
  requirement: !ruby/object:Gem::Requirement