featurevisor-openfeature 1.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +912 -0
- data/lib/featurevisor/openfeature_provider.rb +205 -0
- data/lib/featurevisor-openfeature.rb +3 -0
- metadata +75 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ac21a86ef0432191f6a12f262dd00b524f520a0e5a1cd88bf2029b0428842c46
|
|
4
|
+
data.tar.gz: 9add287e8f085ee1b446db5ccf5c577a2f4758a78be61ca36dc0af4793b9f8ab
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f0ea87c62a2cfbe51bd00c5d3d75b2fb1f26f2989d5df8cfe44f52fba840374673c439960855441783074c61e531185d11decd393838cc7da485740467e53e3e
|
|
7
|
+
data.tar.gz: d93238543cd2e2bef46ed2d2bc6c56ce4ca6bf8fc35211a837f1523d1e14c7bcbb016a7aea95d2b1057e2d09632d949c928d098495d0315a38fc868fdb640ae4
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Fahad Heylaal (https://fahad19.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
# Featurevisor Ruby SDK <!-- omit in toc -->
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Table of contents <!-- omit in toc -->
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Public API](#public-api)
|
|
11
|
+
- [Initialization](#initialization)
|
|
12
|
+
- [Evaluation types](#evaluation-types)
|
|
13
|
+
- [Context](#context)
|
|
14
|
+
- [Setting initial context](#setting-initial-context)
|
|
15
|
+
- [Setting after initialization](#setting-after-initialization)
|
|
16
|
+
- [Replacing existing context](#replacing-existing-context)
|
|
17
|
+
- [Manually passing context](#manually-passing-context)
|
|
18
|
+
- [Check if enabled](#check-if-enabled)
|
|
19
|
+
- [Getting variation](#getting-variation)
|
|
20
|
+
- [Getting variables](#getting-variables)
|
|
21
|
+
- [Type specific methods](#type-specific-methods)
|
|
22
|
+
- [Getting all evaluations](#getting-all-evaluations)
|
|
23
|
+
- [Sticky](#sticky)
|
|
24
|
+
- [Initialize with sticky](#initialize-with-sticky)
|
|
25
|
+
- [Set sticky afterwards](#set-sticky-afterwards)
|
|
26
|
+
- [Setting datafile](#setting-datafile)
|
|
27
|
+
- [Merging by default](#merging-by-default)
|
|
28
|
+
- [Replacing](#replacing)
|
|
29
|
+
- [Loading datafiles on demand](#loading-datafiles-on-demand)
|
|
30
|
+
- [Updating datafile](#updating-datafile)
|
|
31
|
+
- [Interval-based update](#interval-based-update)
|
|
32
|
+
- [Evaluation details](#evaluation-details)
|
|
33
|
+
- [Diagnostics](#diagnostics)
|
|
34
|
+
- [Levels](#levels)
|
|
35
|
+
- [Handler](#handler)
|
|
36
|
+
- [Events](#events)
|
|
37
|
+
- [`datafile_set`](#datafile_set)
|
|
38
|
+
- [`context_set`](#context_set)
|
|
39
|
+
- [`sticky_set`](#sticky_set)
|
|
40
|
+
- [`error`](#error)
|
|
41
|
+
- [Modules](#modules)
|
|
42
|
+
- [Defining a module](#defining-a-module)
|
|
43
|
+
- [Registering modules](#registering-modules)
|
|
44
|
+
- [Child instance](#child-instance)
|
|
45
|
+
- [Close](#close)
|
|
46
|
+
- [OpenFeature](#openfeature)
|
|
47
|
+
- [CLI usage](#cli-usage)
|
|
48
|
+
- [Test](#test)
|
|
49
|
+
- [Test against local monorepo's example-1](#test-against-local-monorepos-example-1)
|
|
50
|
+
- [Benchmark](#benchmark)
|
|
51
|
+
- [Assess distribution](#assess-distribution)
|
|
52
|
+
- [Development](#development)
|
|
53
|
+
- [Setting up](#setting-up)
|
|
54
|
+
- [Running tests](#running-tests)
|
|
55
|
+
- [Releasing](#releasing)
|
|
56
|
+
- [License](#license)
|
|
57
|
+
|
|
58
|
+
<!-- FEATUREVISOR_DOCS_BEGIN -->
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
Add this line to your application's Gemfile:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
gem 'featurevisor'
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
And then execute:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
$ bundle install
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Or install it yourself as:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
$ gem install featurevisor
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Public API
|
|
81
|
+
|
|
82
|
+
The main runtime API is `Featurevisor.create_featurevisor`:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
f = Featurevisor.create_featurevisor(
|
|
86
|
+
datafile: datafile_content
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Most applications only need this factory and the returned `Featurevisor::Instance`. Public extension and observability APIs include modules, diagnostics, events, and the datafile structures accepted by the factory.
|
|
91
|
+
|
|
92
|
+
## Initialization
|
|
93
|
+
|
|
94
|
+
The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
require 'featurevisor'
|
|
98
|
+
require 'net/http'
|
|
99
|
+
require 'json'
|
|
100
|
+
|
|
101
|
+
# Fetch datafile from URL
|
|
102
|
+
datafile_url = 'https://cdn.yoursite.com/datafile.json'
|
|
103
|
+
response = Net::HTTP.get_response(URI(datafile_url))
|
|
104
|
+
|
|
105
|
+
# Parse JSON with symbolized keys (required)
|
|
106
|
+
datafile_content = JSON.parse(response.body, symbolize_names: true)
|
|
107
|
+
|
|
108
|
+
# Create SDK instance
|
|
109
|
+
f = Featurevisor.create_featurevisor(
|
|
110
|
+
datafile: datafile_content
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Important**: When parsing JSON datafiles, you must use `symbolize_names: true` to ensure proper key handling by the SDK.
|
|
115
|
+
|
|
116
|
+
Alternatively, you can pass a JSON string directly and the SDK will parse it automatically:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# Option 1: Parse JSON yourself (recommended)
|
|
120
|
+
datafile_content = JSON.parse(json_string, symbolize_names: true)
|
|
121
|
+
f = Featurevisor.create_featurevisor(datafile: datafile_content)
|
|
122
|
+
|
|
123
|
+
# Option 2: Pass JSON string directly (automatic parsing)
|
|
124
|
+
f = Featurevisor.create_featurevisor(datafile: json_string)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Evaluation types
|
|
128
|
+
|
|
129
|
+
We can evaluate 3 types of values against a particular [feature](https://featurevisor.com/docs/features/):
|
|
130
|
+
|
|
131
|
+
- [**Flag**](#check-if-enabled) (`boolean`): whether the feature is enabled or not
|
|
132
|
+
- [**Variation**](#getting-variation) (`string`): the variation of the feature (if any)
|
|
133
|
+
- [**Variables**](#getting-variables): variable values of the feature (if any)
|
|
134
|
+
|
|
135
|
+
These evaluations are run against the provided context.
|
|
136
|
+
|
|
137
|
+
## Context
|
|
138
|
+
|
|
139
|
+
Contexts are [attribute](https://featurevisor.com/docs/attributes/) values that we pass to SDK for evaluating [features](https://featurevisor.com/docs/features/) against.
|
|
140
|
+
|
|
141
|
+
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).
|
|
142
|
+
|
|
143
|
+
They are plain hashes:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
context = {
|
|
147
|
+
userId: '123',
|
|
148
|
+
country: 'nl',
|
|
149
|
+
# ...other attributes
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Context can be passed to SDK instance in various different ways, depending on your needs:
|
|
154
|
+
|
|
155
|
+
### Setting initial context
|
|
156
|
+
|
|
157
|
+
You can set context at the time of initialization:
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
require 'featurevisor'
|
|
161
|
+
|
|
162
|
+
f = Featurevisor.create_featurevisor(
|
|
163
|
+
context: {
|
|
164
|
+
deviceId: '123',
|
|
165
|
+
country: 'nl'
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This is useful for values that don't change too frequently and available at the time of application startup.
|
|
171
|
+
|
|
172
|
+
### Setting after initialization
|
|
173
|
+
|
|
174
|
+
You can also set more context after the SDK has been initialized:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
f.set_context({
|
|
178
|
+
userId: '234'
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
This will merge the new context with the existing one (if already set).
|
|
183
|
+
|
|
184
|
+
### Replacing existing context
|
|
185
|
+
|
|
186
|
+
If you wish to fully replace the existing context, you can pass `true` in second argument:
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
f.set_context({
|
|
190
|
+
deviceId: '123',
|
|
191
|
+
userId: '234',
|
|
192
|
+
country: 'nl',
|
|
193
|
+
browser: 'chrome'
|
|
194
|
+
}, true) # replace existing context
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Manually passing context
|
|
198
|
+
|
|
199
|
+
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:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
context = {
|
|
203
|
+
userId: '123',
|
|
204
|
+
country: 'nl'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
is_enabled = f.is_enabled('my_feature', context)
|
|
208
|
+
variation = f.get_variation('my_feature', context)
|
|
209
|
+
variable_value = f.get_variable('my_feature', 'my_variable', context)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value.
|
|
213
|
+
|
|
214
|
+
Further details for each evaluation types are described below.
|
|
215
|
+
|
|
216
|
+
## Check if enabled
|
|
217
|
+
|
|
218
|
+
Once the SDK is initialized, you can check if a feature is enabled or not:
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
feature_key = 'my_feature'
|
|
222
|
+
|
|
223
|
+
is_enabled = f.is_enabled(feature_key)
|
|
224
|
+
|
|
225
|
+
if is_enabled
|
|
226
|
+
# do something
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
You can also pass additional context per evaluation:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
is_enabled = f.is_enabled(feature_key, {
|
|
234
|
+
# ...additional context
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Getting variation
|
|
239
|
+
|
|
240
|
+
If your feature has any [variations](https://featurevisor.com/docs/features/#variations) defined, you can evaluate them as follows:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
feature_key = 'my_feature'
|
|
244
|
+
|
|
245
|
+
variation = f.get_variation(feature_key)
|
|
246
|
+
|
|
247
|
+
if variation == 'treatment'
|
|
248
|
+
# do something for treatment variation
|
|
249
|
+
else
|
|
250
|
+
# handle default/control variation
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Additional context per evaluation can also be passed:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
variation = f.get_variation(feature_key, {
|
|
258
|
+
# ...additional context
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Getting variables
|
|
263
|
+
|
|
264
|
+
Your features may also include [variables](https://featurevisor.com/docs/features/#variables), which can be evaluated as follows:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
variable_key = 'bgColor'
|
|
268
|
+
|
|
269
|
+
bg_color_value = f.get_variable('my_feature', variable_key)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Additional context per evaluation can also be passed:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
bg_color_value = f.get_variable('my_feature', variable_key, {
|
|
276
|
+
# ...additional context
|
|
277
|
+
})
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Type specific methods
|
|
281
|
+
|
|
282
|
+
Next to generic `get_variable()` methods, there are also type specific methods available for convenience:
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
f.get_variable_boolean(feature_key, variable_key, context = {})
|
|
286
|
+
f.get_variable_string(feature_key, variable_key, context = {})
|
|
287
|
+
f.get_variable_integer(feature_key, variable_key, context = {})
|
|
288
|
+
f.get_variable_double(feature_key, variable_key, context = {})
|
|
289
|
+
f.get_variable_array(feature_key, variable_key, context = {})
|
|
290
|
+
f.get_variable_object(feature_key, variable_key, context = {})
|
|
291
|
+
f.get_variable_json(feature_key, variable_key, context = {})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
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.
|
|
295
|
+
|
|
296
|
+
## Getting all evaluations
|
|
297
|
+
|
|
298
|
+
You can get evaluations of all features available in the SDK instance:
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
all_evaluations = f.get_all_evaluations({})
|
|
302
|
+
|
|
303
|
+
puts all_evaluations
|
|
304
|
+
# {
|
|
305
|
+
# myFeature: {
|
|
306
|
+
# enabled: true,
|
|
307
|
+
# variation: "control",
|
|
308
|
+
# variables: {
|
|
309
|
+
# myVariableKey: "myVariableValue",
|
|
310
|
+
# },
|
|
311
|
+
# },
|
|
312
|
+
#
|
|
313
|
+
# anotherFeature: {
|
|
314
|
+
# enabled: true,
|
|
315
|
+
# variation: "treatment",
|
|
316
|
+
# }
|
|
317
|
+
# }
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
This is handy especially when you want to pass all evaluations from a backend application to the frontend.
|
|
321
|
+
|
|
322
|
+
## Sticky
|
|
323
|
+
|
|
324
|
+
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/):
|
|
325
|
+
|
|
326
|
+
Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use `spawn(context, sticky: ...)` when a child needs its own sticky state.
|
|
327
|
+
|
|
328
|
+
### Initialize with sticky
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
require 'featurevisor'
|
|
332
|
+
|
|
333
|
+
f = Featurevisor.create_featurevisor(
|
|
334
|
+
sticky: {
|
|
335
|
+
myFeatureKey: {
|
|
336
|
+
enabled: true,
|
|
337
|
+
# optional
|
|
338
|
+
variation: 'treatment',
|
|
339
|
+
variables: {
|
|
340
|
+
myVariableKey: 'myVariableValue'
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
anotherFeatureKey: {
|
|
344
|
+
enabled: false
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process.
|
|
351
|
+
|
|
352
|
+
### Set sticky afterwards
|
|
353
|
+
|
|
354
|
+
You can also set sticky features after the SDK is initialized:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
f.set_sticky({
|
|
358
|
+
myFeatureKey: {
|
|
359
|
+
enabled: true,
|
|
360
|
+
variation: 'treatment',
|
|
361
|
+
variables: {
|
|
362
|
+
myVariableKey: 'myVariableValue'
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
anotherFeatureKey: {
|
|
366
|
+
enabled: false
|
|
367
|
+
}
|
|
368
|
+
}, true) # replace existing sticky features (false by default)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Setting datafile
|
|
372
|
+
|
|
373
|
+
You may also initialize the SDK without passing `datafile`, and set it later on:
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
# Parse with symbolized keys before setting
|
|
377
|
+
datafile_content = JSON.parse(json_string, symbolize_names: true)
|
|
378
|
+
f.set_datafile(datafile_content)
|
|
379
|
+
|
|
380
|
+
# Or pass JSON string directly for automatic parsing
|
|
381
|
+
f.set_datafile(json_string)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Important**: When calling `set_datafile()`, ensure JSON is parsed with `symbolize_names: true` if you're parsing it yourself.
|
|
385
|
+
|
|
386
|
+
### Merging by default
|
|
387
|
+
|
|
388
|
+
By default, `set_datafile(datafile)` merges the incoming datafile into the SDK's current datafile:
|
|
389
|
+
|
|
390
|
+
- top-level metadata such as `schemaVersion`, `revision`, and `featurevisorVersion` comes from the incoming datafile
|
|
391
|
+
- `segments` are merged, with incoming entries overriding existing ones
|
|
392
|
+
- `features` are merged, with incoming entries overriding existing ones
|
|
393
|
+
|
|
394
|
+
This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together.
|
|
395
|
+
|
|
396
|
+
### Replacing
|
|
397
|
+
|
|
398
|
+
To fully replace the stored datafile, pass `true` as the second argument:
|
|
399
|
+
|
|
400
|
+
```ruby
|
|
401
|
+
f.set_datafile(datafile_content, true)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Loading datafiles on demand
|
|
405
|
+
|
|
406
|
+
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.
|
|
407
|
+
|
|
408
|
+
This pairs well with [targets](https://featurevisor.com/docs/targets/), where each target produces a smaller datafile for a specific part of your application:
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
require "open-uri"
|
|
412
|
+
|
|
413
|
+
f = Featurevisor.create_featurevisor({})
|
|
414
|
+
|
|
415
|
+
def load_datafile(f, target)
|
|
416
|
+
url = "https://cdn.yoursite.com/production/featurevisor-#{target}.json"
|
|
417
|
+
datafile = JSON.parse(URI.open(url).read, symbolize_names: true)
|
|
418
|
+
|
|
419
|
+
# merges into whatever was loaded before
|
|
420
|
+
f.set_datafile(datafile)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
load_datafile(f, "products")
|
|
424
|
+
|
|
425
|
+
# later, when the user reaches checkout
|
|
426
|
+
load_datafile(f, "checkout")
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Updating datafile
|
|
430
|
+
|
|
431
|
+
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.
|
|
432
|
+
|
|
433
|
+
The triggers for setting the datafile again can be:
|
|
434
|
+
|
|
435
|
+
- periodic updates based on an interval (like every 5 minutes), or
|
|
436
|
+
- reacting to:
|
|
437
|
+
- a specific event in your application (like a user action), or
|
|
438
|
+
- an event served via websocket or server-sent events (SSE)
|
|
439
|
+
|
|
440
|
+
### Interval-based update
|
|
441
|
+
|
|
442
|
+
Here's an example of using interval-based update:
|
|
443
|
+
|
|
444
|
+
```ruby
|
|
445
|
+
require 'net/http'
|
|
446
|
+
require 'json'
|
|
447
|
+
|
|
448
|
+
def update_datafile(f, datafile_url)
|
|
449
|
+
loop do
|
|
450
|
+
sleep(5 * 60) # 5 minutes
|
|
451
|
+
|
|
452
|
+
begin
|
|
453
|
+
response = Net::HTTP.get_response(URI(datafile_url))
|
|
454
|
+
datafile_content = JSON.parse(response.body)
|
|
455
|
+
f.set_datafile(datafile_content)
|
|
456
|
+
rescue => e
|
|
457
|
+
# handle error
|
|
458
|
+
puts "Failed to update datafile: #{e.message}"
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Start the update thread
|
|
464
|
+
Thread.new { update_datafile(f, datafile_url) }
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Diagnostics
|
|
468
|
+
|
|
469
|
+
By default, Featurevisor reports diagnostics to the console for `info` level and above with a `[Featurevisor]` prefix.
|
|
470
|
+
|
|
471
|
+
### Levels
|
|
472
|
+
|
|
473
|
+
Available diagnostic levels are `fatal`, `error`, `warn`, `info`, and `debug`.
|
|
474
|
+
|
|
475
|
+
Set the level during initialization or update it afterwards:
|
|
476
|
+
|
|
477
|
+
```ruby
|
|
478
|
+
f = Featurevisor.create_featurevisor(log_level: "debug")
|
|
479
|
+
f.set_log_level("info")
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Handler
|
|
483
|
+
|
|
484
|
+
Use `on_diagnostic` to send structured diagnostics to your observability system:
|
|
485
|
+
|
|
486
|
+
```ruby
|
|
487
|
+
f = Featurevisor.create_featurevisor(
|
|
488
|
+
log_level: "info",
|
|
489
|
+
on_diagnostic: ->(diagnostic) {
|
|
490
|
+
puts "#{diagnostic[:level]} #{diagnostic[:code]} #{diagnostic[:message]}"
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Every diagnostic has `:level`, `:code`, `:message`, and an object-shaped `:details` hash. Optional `:module`, `:moduleName`, and `:originalError` fields describe provenance. Evaluation metadata belongs in `:details`.
|
|
496
|
+
|
|
497
|
+
Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations.
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
## Events
|
|
501
|
+
|
|
502
|
+
Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime.
|
|
503
|
+
|
|
504
|
+
You can listen to these events that can occur at various stages in your application:
|
|
505
|
+
|
|
506
|
+
### `datafile_set`
|
|
507
|
+
|
|
508
|
+
```ruby
|
|
509
|
+
unsubscribe = f.on('datafile_set') do |event|
|
|
510
|
+
revision = event[:revision] # new revision
|
|
511
|
+
previous_revision = event[:previousRevision]
|
|
512
|
+
revision_changed = event[:revisionChanged] # true if revision has changed
|
|
513
|
+
|
|
514
|
+
# list of feature keys that have new updates,
|
|
515
|
+
# and you should re-evaluate them
|
|
516
|
+
features = event[:features]
|
|
517
|
+
|
|
518
|
+
# handle here
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# stop listening to the event
|
|
522
|
+
unsubscribe.call
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
The `features` array will contain keys of features that have either been:
|
|
526
|
+
|
|
527
|
+
- added, or
|
|
528
|
+
- updated, or
|
|
529
|
+
- removed
|
|
530
|
+
|
|
531
|
+
compared to the previous datafile content that existed in the SDK instance.
|
|
532
|
+
|
|
533
|
+
The event also includes `replaced`, which is `true` when the datafile replaced the previous content instead of merging into it.
|
|
534
|
+
|
|
535
|
+
### `context_set`
|
|
536
|
+
|
|
537
|
+
```ruby
|
|
538
|
+
unsubscribe = f.on('context_set') do |event|
|
|
539
|
+
replaced = event[:replaced] # true if context was replaced
|
|
540
|
+
context = event[:context] # the new context
|
|
541
|
+
|
|
542
|
+
puts 'Context set'
|
|
543
|
+
end
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### `sticky_set`
|
|
547
|
+
|
|
548
|
+
```ruby
|
|
549
|
+
unsubscribe = f.on('sticky_set') do |event|
|
|
550
|
+
replaced = event[:replaced] # true if sticky features got replaced
|
|
551
|
+
features = event[:features] # list of all affected feature keys
|
|
552
|
+
|
|
553
|
+
puts 'Sticky features set'
|
|
554
|
+
end
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### `error`
|
|
558
|
+
|
|
559
|
+
```ruby
|
|
560
|
+
unsubscribe = f.on('error') do |event|
|
|
561
|
+
diagnostic = event[:diagnostic]
|
|
562
|
+
code = diagnostic[:code]
|
|
563
|
+
message = diagnostic[:message]
|
|
564
|
+
|
|
565
|
+
puts "Featurevisor error: #{code} #{message}"
|
|
566
|
+
end
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
The `error` event is emitted for diagnostics reported with `level: "error"`.
|
|
570
|
+
|
|
571
|
+
## Evaluation details
|
|
572
|
+
|
|
573
|
+
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:
|
|
574
|
+
|
|
575
|
+
```ruby
|
|
576
|
+
# flag
|
|
577
|
+
evaluation = f.evaluate_flag(feature_key, context = {})
|
|
578
|
+
|
|
579
|
+
# variation
|
|
580
|
+
evaluation = f.evaluate_variation(feature_key, context = {})
|
|
581
|
+
|
|
582
|
+
# variable
|
|
583
|
+
evaluation = f.evaluate_variable(feature_key, variable_key, context = {})
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
The returned object will always contain the following properties:
|
|
587
|
+
|
|
588
|
+
- `feature_key`: the feature key
|
|
589
|
+
- `reason`: the reason how the value was evaluated
|
|
590
|
+
|
|
591
|
+
And optionally these properties depending on whether you are evaluating a feature variation or a variable:
|
|
592
|
+
|
|
593
|
+
- `bucket_value`: the bucket value between 0 and 100,000
|
|
594
|
+
- `rule_key`: the rule key
|
|
595
|
+
- `error`: the error object
|
|
596
|
+
- `enabled`: if feature itself is enabled or not
|
|
597
|
+
- `variation`: the variation object
|
|
598
|
+
- `variation_value`: the variation value
|
|
599
|
+
- `variable_key`: the variable key
|
|
600
|
+
- `variable_value`: the variable value
|
|
601
|
+
- `variable_schema`: the variable schema
|
|
602
|
+
|
|
603
|
+
## Modules
|
|
604
|
+
|
|
605
|
+
Modules allow you to intercept the evaluation process and customize SDK behavior.
|
|
606
|
+
|
|
607
|
+
### Defining a module
|
|
608
|
+
|
|
609
|
+
A module is a simple hash with a unique recommended `name` and optional lifecycle functions:
|
|
610
|
+
|
|
611
|
+
If `setup` raises an exception, the module is not registered. Featurevisor removes subscriptions created during setup, reports `module_setup_error`, and calls `close` when present.
|
|
612
|
+
|
|
613
|
+
```ruby
|
|
614
|
+
require 'featurevisor'
|
|
615
|
+
|
|
616
|
+
my_custom_module = {
|
|
617
|
+
# recommended, and used for duplicate detection/removal
|
|
618
|
+
name: 'my-custom-module',
|
|
619
|
+
|
|
620
|
+
# rest of the properties below are all optional per module
|
|
621
|
+
|
|
622
|
+
# setup once, when the module is registered
|
|
623
|
+
setup: ->(api) {
|
|
624
|
+
revision = api[:get_revision].call
|
|
625
|
+
|
|
626
|
+
api[:on_diagnostic].call(->(diagnostic) {
|
|
627
|
+
puts diagnostic[:message]
|
|
628
|
+
})
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
# before evaluation
|
|
632
|
+
before: ->(options) {
|
|
633
|
+
# update context before evaluation
|
|
634
|
+
options[:context] = options[:context].merge({
|
|
635
|
+
someAdditionalAttribute: 'value'
|
|
636
|
+
})
|
|
637
|
+
options
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
# after evaluation
|
|
641
|
+
after: ->(evaluation, options) {
|
|
642
|
+
reason = evaluation[:reason]
|
|
643
|
+
if reason == 'error'
|
|
644
|
+
# log error
|
|
645
|
+
return
|
|
646
|
+
end
|
|
647
|
+
},
|
|
648
|
+
|
|
649
|
+
# configure bucket key
|
|
650
|
+
bucket_key: ->(options) {
|
|
651
|
+
# return custom bucket key
|
|
652
|
+
options[:bucket_key]
|
|
653
|
+
},
|
|
654
|
+
|
|
655
|
+
# configure bucket value (between 0 and 100,000)
|
|
656
|
+
bucket_value: ->(options) {
|
|
657
|
+
# return custom bucket value
|
|
658
|
+
options[:bucket_value]
|
|
659
|
+
},
|
|
660
|
+
|
|
661
|
+
# cleanup when module is removed or SDK is closed
|
|
662
|
+
close: -> {
|
|
663
|
+
# cleanup here
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
The module API passed to `setup` exposes:
|
|
669
|
+
|
|
670
|
+
- `get_revision`
|
|
671
|
+
- `on_diagnostic`
|
|
672
|
+
- `report_diagnostic`
|
|
673
|
+
|
|
674
|
+
### Registering modules
|
|
675
|
+
|
|
676
|
+
You can register modules at the time of SDK initialization:
|
|
677
|
+
|
|
678
|
+
```ruby
|
|
679
|
+
require 'featurevisor'
|
|
680
|
+
|
|
681
|
+
f = Featurevisor.create_featurevisor(
|
|
682
|
+
modules: [my_custom_module]
|
|
683
|
+
)
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
Or after initialization:
|
|
687
|
+
|
|
688
|
+
```ruby
|
|
689
|
+
remove_module = f.add_module(my_custom_module)
|
|
690
|
+
|
|
691
|
+
# remove later by calling the returned function
|
|
692
|
+
remove_module.call
|
|
693
|
+
|
|
694
|
+
# or remove by name
|
|
695
|
+
f.remove_module('my-custom-module')
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
## Child instance
|
|
699
|
+
|
|
700
|
+
When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications.
|
|
701
|
+
|
|
702
|
+
But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request.
|
|
703
|
+
|
|
704
|
+
That's where child instances come in handy:
|
|
705
|
+
|
|
706
|
+
```ruby
|
|
707
|
+
child_f = f.spawn({
|
|
708
|
+
# user or request specific context
|
|
709
|
+
userId: '123'
|
|
710
|
+
})
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
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:
|
|
714
|
+
|
|
715
|
+
```ruby
|
|
716
|
+
is_enabled = child_f.is_enabled('my_feature')
|
|
717
|
+
variation = child_f.get_variation('my_feature')
|
|
718
|
+
variable_value = child_f.get_variable('my_feature', 'my_variable')
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
Similar to parent SDK, child instances also support several additional methods:
|
|
722
|
+
|
|
723
|
+
- `set_context`
|
|
724
|
+
- `set_sticky`
|
|
725
|
+
- `is_enabled`
|
|
726
|
+
- `get_variation`
|
|
727
|
+
- `get_variable`
|
|
728
|
+
- `get_variable_boolean`
|
|
729
|
+
- `get_variable_string`
|
|
730
|
+
- `get_variable_integer`
|
|
731
|
+
- `get_variable_double`
|
|
732
|
+
- `get_variable_array`
|
|
733
|
+
- `get_variable_object`
|
|
734
|
+
- `get_variable_json`
|
|
735
|
+
- `get_all_evaluations`
|
|
736
|
+
- `on`
|
|
737
|
+
- `close`
|
|
738
|
+
|
|
739
|
+
## Close
|
|
740
|
+
|
|
741
|
+
Both primary and child instances support a `.close()` method, that removes forgotten event listeners (via `on` method) and cleans up any potential memory leaks.
|
|
742
|
+
|
|
743
|
+
```ruby
|
|
744
|
+
f.close()
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
## CLI usage
|
|
748
|
+
|
|
749
|
+
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.
|
|
750
|
+
|
|
751
|
+
- Global installation: you can access it as `featurevisor`
|
|
752
|
+
- Local installation: you can access it as `bundle exec featurevisor`
|
|
753
|
+
- From this repository: you can access it as `bin/featurevisor`
|
|
754
|
+
|
|
755
|
+
### Test
|
|
756
|
+
|
|
757
|
+
Learn more about testing [here](https://featurevisor.com/docs/testing/).
|
|
758
|
+
|
|
759
|
+
```bash
|
|
760
|
+
$ bundle exec featurevisor test --projectDirectoryPath="/absolute/path/to/your/featurevisor/project"
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
Additional options that are available:
|
|
764
|
+
|
|
765
|
+
```bash
|
|
766
|
+
$ bundle exec featurevisor test \
|
|
767
|
+
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
|
|
768
|
+
--quiet|--verbose \
|
|
769
|
+
--onlyFailures \
|
|
770
|
+
--keyPattern="myFeatureKey" \
|
|
771
|
+
--assertionPattern="#1"
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
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.
|
|
775
|
+
|
|
776
|
+
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 behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.
|
|
777
|
+
|
|
778
|
+
### Test against local monorepo's example-1
|
|
779
|
+
|
|
780
|
+
```bash
|
|
781
|
+
$ cd /absolute/path/to/featurevisor-ruby
|
|
782
|
+
$ bundle exec ruby bin/featurevisor test --projectDirectoryPath=../featurevisor/examples/example-1 --onlyFailures
|
|
783
|
+
$ make test-example-1
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### Benchmark
|
|
787
|
+
|
|
788
|
+
Learn more about benchmarking [here](https://featurevisor.com/docs/cli/#benchmarking).
|
|
789
|
+
|
|
790
|
+
```bash
|
|
791
|
+
$ bundle exec featurevisor benchmark \
|
|
792
|
+
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
|
|
793
|
+
--environment="production" \
|
|
794
|
+
--feature="myFeatureKey" \
|
|
795
|
+
--context='{"country": "nl"}' \
|
|
796
|
+
--n=1000
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
### Assess distribution
|
|
800
|
+
|
|
801
|
+
Learn more about assessing distribution [here](https://featurevisor.com/docs/cli/#assess-distribution).
|
|
802
|
+
|
|
803
|
+
```bash
|
|
804
|
+
$ bundle exec featurevisor assess-distribution \
|
|
805
|
+
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
|
|
806
|
+
--environment=production \
|
|
807
|
+
--feature=foo \
|
|
808
|
+
--variation \
|
|
809
|
+
--context='{"country": "nl"}' \
|
|
810
|
+
--populateUuid=userId \
|
|
811
|
+
--populateUuid=deviceId \
|
|
812
|
+
--n=1000
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
## OpenFeature
|
|
816
|
+
|
|
817
|
+
The OpenFeature provider is published as a separate gem. This keeps OpenFeature code and dependencies out of applications that only use the Featurevisor SDK.
|
|
818
|
+
|
|
819
|
+
The provider currently requires Ruby 3.4 or newer because that is the minimum version supported by the official OpenFeature Ruby SDK.
|
|
820
|
+
|
|
821
|
+
Install the provider:
|
|
822
|
+
|
|
823
|
+
```ruby
|
|
824
|
+
gem "featurevisor-openfeature", "~> 1.1"
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
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.
|
|
828
|
+
|
|
829
|
+
```ruby
|
|
830
|
+
require "featurevisor/openfeature_provider"
|
|
831
|
+
|
|
832
|
+
provider = Featurevisor::OpenFeatureProvider.new(
|
|
833
|
+
datafile: datafile_content,
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
OpenFeature::SDK.configure do |config|
|
|
837
|
+
config.set_provider_and_wait(provider)
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
client = OpenFeature::SDK.build_client
|
|
841
|
+
enabled = client.fetch_boolean_value(
|
|
842
|
+
flag_key: "checkout",
|
|
843
|
+
default_value: false,
|
|
844
|
+
evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "user-123"),
|
|
845
|
+
)
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
Use `checkout` for a flag, `checkout:variation` for its variation, and `checkout:title` for its `title` variable. Boolean variables use the boolean resolver. Arrays, hashes, and JSON variables use the object resolver.
|
|
849
|
+
|
|
850
|
+
OpenFeature's targeting key maps to `userId` by default. `targeting_key_field`, `key_separator`, and `variation_key` can customize the mapping.
|
|
851
|
+
|
|
852
|
+
You can pass any Featurevisor initialization options directly to the provider. These options are used to create the Featurevisor instance owned by the provider:
|
|
853
|
+
|
|
854
|
+
```ruby
|
|
855
|
+
provider = Featurevisor::OpenFeatureProvider.new(
|
|
856
|
+
datafile: datafile_content,
|
|
857
|
+
targeting_key_field: "accountId",
|
|
858
|
+
key_separator: "/",
|
|
859
|
+
variation_key: "$variation",
|
|
860
|
+
)
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
You can also reuse an existing Featurevisor instance:
|
|
864
|
+
|
|
865
|
+
```ruby
|
|
866
|
+
featurevisor = Featurevisor.create_featurevisor(datafile: datafile_content)
|
|
867
|
+
provider = Featurevisor::OpenFeatureProvider.new(featurevisor: featurevisor)
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
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.
|
|
871
|
+
|
|
872
|
+
See the [OpenFeature provider guide](https://featurevisor.com/docs/sdks/openfeature/) for resolution reasons, errors, metadata, tracking, lifecycle, and providers for other languages.
|
|
873
|
+
|
|
874
|
+
<!-- FEATUREVISOR_DOCS_END -->
|
|
875
|
+
|
|
876
|
+
## Development
|
|
877
|
+
|
|
878
|
+
### Setting up
|
|
879
|
+
|
|
880
|
+
After checking out the repository, run `make install` to install dependencies.
|
|
881
|
+
|
|
882
|
+
### Running tests
|
|
883
|
+
|
|
884
|
+
```bash
|
|
885
|
+
$ bundle exec rspec
|
|
886
|
+
$ make test-base
|
|
887
|
+
$ make test-example-1
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
Build and verify both gems with:
|
|
891
|
+
|
|
892
|
+
```bash
|
|
893
|
+
$ make build
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
The build produces `featurevisor-VERSION.gem` and `featurevisor-openfeature-VERSION.gem`. The artifact verifier confirms that OpenFeature code and dependencies are absent from the base gem and present only in the provider gem.
|
|
897
|
+
|
|
898
|
+
### Releasing
|
|
899
|
+
|
|
900
|
+
- Update the shared version in `lib/featurevisor/version.rb`
|
|
901
|
+
- Run `bundle install`
|
|
902
|
+
- Push commit to `main` branch
|
|
903
|
+
- Wait for CI to complete
|
|
904
|
+
- Tag the release with the same version number, for example `v1.1.0`
|
|
905
|
+
- The workflow verifies that the tag matches the shared version
|
|
906
|
+
- The workflow publishes `featurevisor` first, followed by `featurevisor-openfeature`
|
|
907
|
+
|
|
908
|
+
The gems are separate RubyGems packages built from the same repository. Publishing the base gem first ensures the provider's exact Featurevisor dependency is available when the provider is published. If the second push fails, rerun or retry the provider publication without republishing the existing base version.
|
|
909
|
+
|
|
910
|
+
## License
|
|
911
|
+
|
|
912
|
+
MIT © [Fahad Heylaal](https://fahad19.com)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require "open_feature/sdk"
|
|
6
|
+
require_relative "../featurevisor"
|
|
7
|
+
|
|
8
|
+
module Featurevisor
|
|
9
|
+
class OpenFeatureProvider
|
|
10
|
+
Provider = OpenFeature::SDK::Provider
|
|
11
|
+
|
|
12
|
+
attr_reader :metadata, :featurevisor
|
|
13
|
+
|
|
14
|
+
def initialize(options = {}, featurevisor: nil, targeting_key_field: "userId", key_separator: ":", variation_key: "variation", on_track: nil, **featurevisor_options)
|
|
15
|
+
raise ArgumentError, "options must be a Hash" unless options.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
@metadata = Provider::ProviderMetadata.new(name: "Featurevisor").freeze
|
|
18
|
+
@targeting_key_field = targeting_key_field.empty? ? "userId" : targeting_key_field
|
|
19
|
+
@key_separator = key_separator.empty? ? ":" : key_separator
|
|
20
|
+
@variation_key = variation_key.empty? ? "variation" : variation_key
|
|
21
|
+
@on_track = on_track
|
|
22
|
+
@datafile_error = nil
|
|
23
|
+
@shutdown = false
|
|
24
|
+
@owns_featurevisor = featurevisor.nil?
|
|
25
|
+
if featurevisor
|
|
26
|
+
@featurevisor = featurevisor
|
|
27
|
+
else
|
|
28
|
+
resolved_options = options.merge(featurevisor_options)
|
|
29
|
+
initial_datafile = resolved_options.delete(:datafile)
|
|
30
|
+
@featurevisor = Featurevisor.create_featurevisor(resolved_options)
|
|
31
|
+
end
|
|
32
|
+
@error_unsubscribe = @featurevisor.on("error", lambda do |event|
|
|
33
|
+
diagnostic = event[:diagnostic]
|
|
34
|
+
@datafile_error = diagnostic[:message] if diagnostic&.dig(:code) == "invalid_datafile"
|
|
35
|
+
end)
|
|
36
|
+
@datafile_unsubscribe = @featurevisor.on("datafile_set", ->(*) { @datafile_error = nil })
|
|
37
|
+
@featurevisor.set_datafile(initial_datafile, true) if @owns_featurevisor && initial_datafile
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def init(_evaluation_context = nil); end
|
|
41
|
+
def shutdown
|
|
42
|
+
return if @shutdown
|
|
43
|
+
|
|
44
|
+
@shutdown = true
|
|
45
|
+
@error_unsubscribe&.call
|
|
46
|
+
@datafile_unsubscribe&.call
|
|
47
|
+
featurevisor.close if @owns_featurevisor
|
|
48
|
+
end
|
|
49
|
+
def hooks = []
|
|
50
|
+
|
|
51
|
+
def track(tracking_event_name:, evaluation_context: nil, tracking_event_details: nil)
|
|
52
|
+
@on_track&.call(tracking_event_name, evaluation_context, tracking_event_details)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
|
|
56
|
+
resolve(flag_key, default_value, evaluation_context, :boolean)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
|
|
60
|
+
resolve(flag_key, default_value, evaluation_context, :string)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
|
|
64
|
+
resolve(flag_key, default_value, evaluation_context, :number)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
|
|
68
|
+
resolve(flag_key, default_value, evaluation_context, :integer)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
|
|
72
|
+
resolve(flag_key, default_value, evaluation_context, :float)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
|
|
76
|
+
resolve(flag_key, default_value, evaluation_context, :object)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def resolve(flag_key, default_value, evaluation_context, expected_type)
|
|
82
|
+
return error(default_value, Provider::ErrorCode::PARSE_ERROR, @datafile_error) if @datafile_error
|
|
83
|
+
|
|
84
|
+
feature_key, selector = split_key(flag_key)
|
|
85
|
+
context = normalize(evaluation_context&.fields || {})
|
|
86
|
+
targeting_key = evaluation_context&.targeting_key
|
|
87
|
+
context[@targeting_key_field] = targeting_key if targeting_key && !targeting_key.empty?
|
|
88
|
+
|
|
89
|
+
if selector.nil? || selector.empty?
|
|
90
|
+
return type_mismatch(flag_key, default_value, expected_type) unless expected_type == :boolean
|
|
91
|
+
evaluation = featurevisor.evaluate_flag(feature_key, context)
|
|
92
|
+
value = evaluation[:enabled]
|
|
93
|
+
elsif selector == @variation_key
|
|
94
|
+
evaluation = featurevisor.evaluate_variation(feature_key, context)
|
|
95
|
+
value = evaluation[:variation_value] || evaluation.dig(:variation, :value)
|
|
96
|
+
else
|
|
97
|
+
evaluation = featurevisor.evaluate_variable(feature_key, selector, context)
|
|
98
|
+
value = evaluation[:variable_value]
|
|
99
|
+
if evaluation.dig(:variable_schema, :type) == "json" && value.is_a?(String)
|
|
100
|
+
begin
|
|
101
|
+
value = JSON.parse(value)
|
|
102
|
+
rescue JSON::ParserError
|
|
103
|
+
# Type validation below returns TYPE_MISMATCH when object was requested.
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
metadata = metadata_for(evaluation)
|
|
109
|
+
code = error_code(evaluation[:reason])
|
|
110
|
+
return error(default_value, code, error_message(evaluation), metadata) if code
|
|
111
|
+
value = default_value if value.nil?
|
|
112
|
+
value = normalize(value) if expected_type == :object
|
|
113
|
+
return type_mismatch(flag_key, default_value, expected_type, metadata) unless matches?(value, expected_type)
|
|
114
|
+
|
|
115
|
+
Provider::ResolutionDetails.new(
|
|
116
|
+
value: value,
|
|
117
|
+
reason: reason(evaluation[:reason]),
|
|
118
|
+
variant: variant(evaluation),
|
|
119
|
+
flag_metadata: metadata
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def split_key(key)
|
|
124
|
+
index = key.index(@key_separator)
|
|
125
|
+
index ? [key[0...index], key[(index + @key_separator.length)..]] : [key, nil]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def metadata_for(evaluation)
|
|
129
|
+
metadata = {
|
|
130
|
+
"featureKey" => evaluation[:feature_key],
|
|
131
|
+
"featurevisorReason" => evaluation[:reason],
|
|
132
|
+
"schemaVersion" => featurevisor.get_schema_version
|
|
133
|
+
}
|
|
134
|
+
metadata["revision"] = featurevisor.get_revision if featurevisor.get_revision
|
|
135
|
+
{
|
|
136
|
+
variable_key: "variableKey",
|
|
137
|
+
rule_key: "ruleKey",
|
|
138
|
+
bucket_key: "bucketKey",
|
|
139
|
+
bucket_value: "bucketValue",
|
|
140
|
+
force_index: "forceIndex",
|
|
141
|
+
variable_override_index: "variableOverrideIndex"
|
|
142
|
+
}.each do |key, metadata_key|
|
|
143
|
+
metadata[metadata_key] = evaluation[key] unless evaluation[key].nil?
|
|
144
|
+
end
|
|
145
|
+
metadata
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def reason(value)
|
|
149
|
+
return Provider::Reason::ERROR if %w[feature_not_found variable_not_found no_variations error].include?(value)
|
|
150
|
+
return Provider::Reason::TARGETING_MATCH if %w[required forced sticky rule variable_override_variation variable_override_rule].include?(value)
|
|
151
|
+
return Provider::Reason::SPLIT if value == "allocated"
|
|
152
|
+
return Provider::Reason::DISABLED if %w[disabled variation_disabled variable_disabled].include?(value)
|
|
153
|
+
Provider::Reason::DEFAULT
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def error_code(value)
|
|
157
|
+
return Provider::ErrorCode::FLAG_NOT_FOUND if %w[feature_not_found variable_not_found no_variations].include?(value)
|
|
158
|
+
return Provider::ErrorCode::GENERAL if value == "error"
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def error_message(evaluation)
|
|
163
|
+
return evaluation[:error].message if evaluation[:error].respond_to?(:message)
|
|
164
|
+
return %(Feature "#{evaluation[:feature_key]}" was not found) if evaluation[:reason] == "feature_not_found"
|
|
165
|
+
return %(Variable "#{evaluation[:variable_key]}" was not found for feature "#{evaluation[:feature_key]}") if evaluation[:reason] == "variable_not_found"
|
|
166
|
+
return %(Feature "#{evaluation[:feature_key]}" has no variations) if evaluation[:reason] == "no_variations"
|
|
167
|
+
"Featurevisor evaluation failed"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def variant(evaluation) = evaluation[:variation_value] || evaluation.dig(:variation, :value)
|
|
171
|
+
|
|
172
|
+
def matches?(value, type)
|
|
173
|
+
case type
|
|
174
|
+
when :boolean then value == true || value == false
|
|
175
|
+
when :string then value.is_a?(String)
|
|
176
|
+
when :number then finite_number?(value)
|
|
177
|
+
when :integer then value.is_a?(Integer)
|
|
178
|
+
when :float then value.is_a?(Float) && value.finite?
|
|
179
|
+
when :object then value.is_a?(Hash) || value.is_a?(Array)
|
|
180
|
+
else false
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def finite_number?(value)
|
|
185
|
+
value.is_a?(Numeric) && !value.is_a?(Complex) && (!value.respond_to?(:finite?) || value.finite?)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def normalize(value)
|
|
189
|
+
case value
|
|
190
|
+
when Time, DateTime then value.iso8601
|
|
191
|
+
when Hash then value.to_h { |key, item| [key.to_s, normalize(item)] }
|
|
192
|
+
when Array then value.map { |item| normalize(item) }
|
|
193
|
+
else value
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def error(value, code, message, metadata = {})
|
|
198
|
+
Provider::ResolutionDetails.new(value: value, reason: Provider::Reason::ERROR, error_code: code, error_message: message, flag_metadata: metadata)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def type_mismatch(key, value, expected_type, metadata = {})
|
|
202
|
+
error(value, Provider::ErrorCode::TYPE_MISMATCH, %(Flag "#{key}" did not resolve to a #{expected_type} value), metadata)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: featurevisor-openfeature
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Fahad Heylaal
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: featurevisor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - '='
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 1.1.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - '='
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 1.1.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: openfeature-sdk
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.6.5
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.6.5
|
|
40
|
+
description: OpenFeature provider backed by the Featurevisor Ruby SDK
|
|
41
|
+
executables: []
|
|
42
|
+
extensions: []
|
|
43
|
+
extra_rdoc_files: []
|
|
44
|
+
files:
|
|
45
|
+
- LICENSE
|
|
46
|
+
- README.md
|
|
47
|
+
- lib/featurevisor-openfeature.rb
|
|
48
|
+
- lib/featurevisor/openfeature_provider.rb
|
|
49
|
+
homepage: https://featurevisor.com/docs/sdks/openfeature/
|
|
50
|
+
licenses:
|
|
51
|
+
- MIT
|
|
52
|
+
metadata:
|
|
53
|
+
source_code_uri: https://github.com/featurevisor/featurevisor-ruby
|
|
54
|
+
documentation_uri: https://featurevisor.com/docs/sdks/ruby/#openfeature
|
|
55
|
+
bug_tracker_uri: https://github.com/featurevisor/featurevisor-ruby/issues
|
|
56
|
+
allowed_push_host: https://rubygems.org
|
|
57
|
+
rubygems_mfa_required: 'true'
|
|
58
|
+
rdoc_options: []
|
|
59
|
+
require_paths:
|
|
60
|
+
- lib
|
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: 3.4.0
|
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0'
|
|
71
|
+
requirements: []
|
|
72
|
+
rubygems_version: 3.6.9
|
|
73
|
+
specification_version: 4
|
|
74
|
+
summary: OpenFeature provider for Featurevisor
|
|
75
|
+
test_files: []
|