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 +4 -4
- data/README.md +300 -182
- data/lib/featurevisor/openfeature_provider.rb +26 -10
- 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: f89eaf90729c4f9df207452f0b7231306b2fe742eee5d234fa850b0764e82538
|
|
4
|
+
data.tar.gz: 899b0752508c9a783a389c3d5fe1e873e9bfe824cb80be21fec9ee6fede31477
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
- [
|
|
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
|
-
- [
|
|
24
|
-
- [
|
|
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
|
-
|
|
76
|
+
bundle install
|
|
73
77
|
```
|
|
74
78
|
|
|
75
79
|
Or install it yourself as:
|
|
76
80
|
|
|
77
81
|
```bash
|
|
78
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
+
### Evaluation types
|
|
133
145
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
- [**
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
211
|
-
variation = f.get_variation('
|
|
212
|
-
variable_value = f.get_variable('
|
|
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
|
-
|
|
234
|
+
Check whether a feature is enabled:
|
|
222
235
|
|
|
223
236
|
```ruby
|
|
224
|
-
feature_key = '
|
|
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
|
-
|
|
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
|
-
|
|
256
|
+
Evaluate a feature's [variations](https://featurevisor.com/docs/features/#variations):
|
|
244
257
|
|
|
245
258
|
```ruby
|
|
246
|
-
feature_key = '
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
287
|
+
bg_color_value = f.get_variable('checkout', variable_key)
|
|
273
288
|
```
|
|
274
289
|
|
|
275
|
-
|
|
290
|
+
Pass additional context to one evaluation:
|
|
276
291
|
|
|
277
292
|
```ruby
|
|
278
|
-
bg_color_value = f.get_variable('
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
431
|
+
## Sticky
|
|
344
432
|
|
|
345
|
-
|
|
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
|
-
|
|
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
|
-
|
|
466
|
+
Set sticky features after initialization:
|
|
379
467
|
|
|
380
468
|
```ruby
|
|
381
469
|
f.set_sticky_features({
|
|
382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
606
|
+
Subscribe to SDK events to respond to context, sticky state and datafile changes.
|
|
529
607
|
|
|
530
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
756
|
-
variation = child_f.get_variation('
|
|
757
|
-
variable_value = child_f.get_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
|
-
- `
|
|
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
|
|
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", "~>
|
|
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
|
-
|
|
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
|
-
|
|
930
|
+
Reuse an existing Featurevisor instance:
|
|
909
931
|
|
|
910
932
|
```ruby
|
|
911
|
-
|
|
912
|
-
provider = Featurevisor::OpenFeatureProvider.new(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 `
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
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)
|
|
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
|
|
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
|
|
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[
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
25
|
+
version: 4.0.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: openfeature-sdk
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|