featurevisor 0.1.1
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 +722 -0
- data/bin/cli.rb +142 -0
- data/bin/commands/assess_distribution.rb +236 -0
- data/bin/commands/benchmark.rb +274 -0
- data/bin/commands/test.rb +793 -0
- data/bin/commands.rb +10 -0
- data/bin/featurevisor +18 -0
- data/lib/featurevisor/bucketer.rb +95 -0
- data/lib/featurevisor/child_instance.rb +311 -0
- data/lib/featurevisor/compare_versions.rb +126 -0
- data/lib/featurevisor/conditions.rb +152 -0
- data/lib/featurevisor/datafile_reader.rb +350 -0
- data/lib/featurevisor/emitter.rb +60 -0
- data/lib/featurevisor/evaluate.rb +818 -0
- data/lib/featurevisor/events.rb +76 -0
- data/lib/featurevisor/hooks.rb +159 -0
- data/lib/featurevisor/instance.rb +463 -0
- data/lib/featurevisor/logger.rb +150 -0
- data/lib/featurevisor/murmurhash.rb +69 -0
- data/lib/featurevisor/version.rb +3 -0
- data/lib/featurevisor.rb +17 -0
- metadata +89 -0
data/README.md
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
# Featurevisor Ruby SDK <!-- omit in toc -->
|
|
2
|
+
|
|
3
|
+
This is a port of Featurevisor [JavaScript SDK](https://featurevisor.com/docs/sdks/javascript/) v2.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](https://featurevisor.com/) v2.0 projects and above.
|
|
6
|
+
|
|
7
|
+
## Table of contents <!-- omit in toc -->
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Initialization](#initialization)
|
|
11
|
+
- [Evaluation types](#evaluation-types)
|
|
12
|
+
- [Context](#context)
|
|
13
|
+
- [Setting initial context](#setting-initial-context)
|
|
14
|
+
- [Setting after initialization](#setting-after-initialization)
|
|
15
|
+
- [Replacing existing context](#replacing-existing-context)
|
|
16
|
+
- [Manually passing context](#manually-passing-context)
|
|
17
|
+
- [Check if enabled](#check-if-enabled)
|
|
18
|
+
- [Getting variation](#getting-variation)
|
|
19
|
+
- [Getting variables](#getting-variables)
|
|
20
|
+
- [Type specific methods](#type-specific-methods)
|
|
21
|
+
- [Getting all evaluations](#getting-all-evaluations)
|
|
22
|
+
- [Sticky](#sticky)
|
|
23
|
+
- [Initialize with sticky](#initialize-with-sticky)
|
|
24
|
+
- [Set sticky afterwards](#set-sticky-afterwards)
|
|
25
|
+
- [Setting datafile](#setting-datafile)
|
|
26
|
+
- [Updating datafile](#updating-datafile)
|
|
27
|
+
- [Interval-based update](#interval-based-update)
|
|
28
|
+
- [Logging](#logging)
|
|
29
|
+
- [Levels](#levels)
|
|
30
|
+
- [Customizing levels](#customizing-levels)
|
|
31
|
+
- [Handler](#handler)
|
|
32
|
+
- [Events](#events)
|
|
33
|
+
- [`datafile_set`](#datafile_set)
|
|
34
|
+
- [`context_set`](#context_set)
|
|
35
|
+
- [`sticky_set`](#sticky_set)
|
|
36
|
+
- [Evaluation details](#evaluation-details)
|
|
37
|
+
- [Hooks](#hooks)
|
|
38
|
+
- [Defining a hook](#defining-a-hook)
|
|
39
|
+
- [Registering hooks](#registering-hooks)
|
|
40
|
+
- [Child instance](#child-instance)
|
|
41
|
+
- [Close](#close)
|
|
42
|
+
- [CLI usage](#cli-usage)
|
|
43
|
+
- [Test](#test)
|
|
44
|
+
- [Benchmark](#benchmark)
|
|
45
|
+
- [Assess distribution](#assess-distribution)
|
|
46
|
+
- [Development](#development)
|
|
47
|
+
- [Setting up](#setting-up)
|
|
48
|
+
- [Running tests](#running-tests)
|
|
49
|
+
- [Releasing](#releasing)
|
|
50
|
+
- [License](#license)
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
Add this line to your application's Gemfile:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
gem 'featurevisor'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
And then execute:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
$ bundle install
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or install it yourself as:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
$ gem install featurevisor
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Initialization
|
|
73
|
+
|
|
74
|
+
The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
require 'featurevisor'
|
|
78
|
+
require 'net/http'
|
|
79
|
+
require 'json'
|
|
80
|
+
|
|
81
|
+
# Fetch datafile from URL
|
|
82
|
+
datafile_url = 'https://cdn.yoursite.com/datafile.json'
|
|
83
|
+
response = Net::HTTP.get_response(URI(datafile_url))
|
|
84
|
+
datafile_content = JSON.parse(response.body)
|
|
85
|
+
|
|
86
|
+
# Create SDK instance
|
|
87
|
+
f = Featurevisor.create_instance(
|
|
88
|
+
datafile: datafile_content
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Evaluation types
|
|
93
|
+
|
|
94
|
+
We can evaluate 3 types of values against a particular [feature](https://featurevisor.com/docs/features/):
|
|
95
|
+
|
|
96
|
+
- [**Flag**](#check-if-enabled) (`boolean`): whether the feature is enabled or not
|
|
97
|
+
- [**Variation**](#getting-variation) (`string`): the variation of the feature (if any)
|
|
98
|
+
- [**Variables**](#getting-variables): variable values of the feature (if any)
|
|
99
|
+
|
|
100
|
+
These evaluations are run against the provided context.
|
|
101
|
+
|
|
102
|
+
## Context
|
|
103
|
+
|
|
104
|
+
Contexts are [attribute](https://featurevisor.com/docs/attributes/) values that we pass to SDK for evaluating [features](https://featurevisor.com/docs/features/) against.
|
|
105
|
+
|
|
106
|
+
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).
|
|
107
|
+
|
|
108
|
+
They are plain hashes:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
context = {
|
|
112
|
+
userId: '123',
|
|
113
|
+
country: 'nl',
|
|
114
|
+
# ...other attributes
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Context can be passed to SDK instance in various different ways, depending on your needs:
|
|
119
|
+
|
|
120
|
+
### Setting initial context
|
|
121
|
+
|
|
122
|
+
You can set context at the time of initialization:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
require 'featurevisor'
|
|
126
|
+
|
|
127
|
+
f = Featurevisor.create_instance(
|
|
128
|
+
context: {
|
|
129
|
+
deviceId: '123',
|
|
130
|
+
country: 'nl'
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This is useful for values that don't change too frequently and available at the time of application startup.
|
|
136
|
+
|
|
137
|
+
### Setting after initialization
|
|
138
|
+
|
|
139
|
+
You can also set more context after the SDK has been initialized:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
f.set_context({
|
|
143
|
+
userId: '234'
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
This will merge the new context with the existing one (if already set).
|
|
148
|
+
|
|
149
|
+
### Replacing existing context
|
|
150
|
+
|
|
151
|
+
If you wish to fully replace the existing context, you can pass `true` in second argument:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
f.set_context({
|
|
155
|
+
deviceId: '123',
|
|
156
|
+
userId: '234',
|
|
157
|
+
country: 'nl',
|
|
158
|
+
browser: 'chrome'
|
|
159
|
+
}, true) # replace existing context
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Manually passing context
|
|
163
|
+
|
|
164
|
+
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:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
context = {
|
|
168
|
+
userId: '123',
|
|
169
|
+
country: 'nl'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
is_enabled = f.is_enabled('my_feature', context)
|
|
173
|
+
variation = f.get_variation('my_feature', context)
|
|
174
|
+
variable_value = f.get_variable('my_feature', 'my_variable', context)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value.
|
|
178
|
+
|
|
179
|
+
Further details for each evaluation types are described below.
|
|
180
|
+
|
|
181
|
+
## Check if enabled
|
|
182
|
+
|
|
183
|
+
Once the SDK is initialized, you can check if a feature is enabled or not:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
feature_key = 'my_feature'
|
|
187
|
+
|
|
188
|
+
is_enabled = f.is_enabled(feature_key)
|
|
189
|
+
|
|
190
|
+
if is_enabled
|
|
191
|
+
# do something
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
You can also pass additional context per evaluation:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
is_enabled = f.is_enabled(feature_key, {
|
|
199
|
+
# ...additional context
|
|
200
|
+
})
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Getting variation
|
|
204
|
+
|
|
205
|
+
If your feature has any [variations](https://featurevisor.com/docs/features/#variations) defined, you can evaluate them as follows:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
feature_key = 'my_feature'
|
|
209
|
+
|
|
210
|
+
variation = f.get_variation(feature_key)
|
|
211
|
+
|
|
212
|
+
if variation == 'treatment'
|
|
213
|
+
# do something for treatment variation
|
|
214
|
+
else
|
|
215
|
+
# handle default/control variation
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Additional context per evaluation can also be passed:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
variation = f.get_variation(feature_key, {
|
|
223
|
+
# ...additional context
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Getting variables
|
|
228
|
+
|
|
229
|
+
Your features may also include [variables](https://featurevisor.com/docs/features/#variables), which can be evaluated as follows:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
variable_key = 'bgColor'
|
|
233
|
+
|
|
234
|
+
bg_color_value = f.get_variable('my_feature', variable_key)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Additional context per evaluation can also be passed:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
bg_color_value = f.get_variable('my_feature', variable_key, {
|
|
241
|
+
# ...additional context
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Type specific methods
|
|
246
|
+
|
|
247
|
+
Next to generic `get_variable()` methods, there are also type specific methods available for convenience:
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
f.get_variable_boolean(feature_key, variable_key, context = {})
|
|
251
|
+
f.get_variable_string(feature_key, variable_key, context = {})
|
|
252
|
+
f.get_variable_integer(feature_key, variable_key, context = {})
|
|
253
|
+
f.get_variable_double(feature_key, variable_key, context = {})
|
|
254
|
+
f.get_variable_array(feature_key, variable_key, context = {})
|
|
255
|
+
f.get_variable_object(feature_key, variable_key, context = {})
|
|
256
|
+
f.get_variable_json(feature_key, variable_key, context = {})
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Getting all evaluations
|
|
260
|
+
|
|
261
|
+
You can get evaluations of all features available in the SDK instance:
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
all_evaluations = f.get_all_evaluations({})
|
|
265
|
+
|
|
266
|
+
puts all_evaluations
|
|
267
|
+
# {
|
|
268
|
+
# myFeature: {
|
|
269
|
+
# enabled: true,
|
|
270
|
+
# variation: "control",
|
|
271
|
+
# variables: {
|
|
272
|
+
# myVariableKey: "myVariableValue",
|
|
273
|
+
# },
|
|
274
|
+
# },
|
|
275
|
+
#
|
|
276
|
+
# anotherFeature: {
|
|
277
|
+
# enabled: true,
|
|
278
|
+
# variation: "treatment",
|
|
279
|
+
# }
|
|
280
|
+
# }
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
This is handy especially when you want to pass all evaluations from a backend application to the frontend.
|
|
284
|
+
|
|
285
|
+
## Sticky
|
|
286
|
+
|
|
287
|
+
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/):
|
|
288
|
+
|
|
289
|
+
### Initialize with sticky
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
require 'featurevisor'
|
|
293
|
+
|
|
294
|
+
f = Featurevisor.create_instance(
|
|
295
|
+
sticky: {
|
|
296
|
+
myFeatureKey: {
|
|
297
|
+
enabled: true,
|
|
298
|
+
# optional
|
|
299
|
+
variation: 'treatment',
|
|
300
|
+
variables: {
|
|
301
|
+
myVariableKey: 'myVariableValue'
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
anotherFeatureKey: {
|
|
305
|
+
enabled: false
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process.
|
|
312
|
+
|
|
313
|
+
### Set sticky afterwards
|
|
314
|
+
|
|
315
|
+
You can also set sticky features after the SDK is initialized:
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
f.set_sticky({
|
|
319
|
+
myFeatureKey: {
|
|
320
|
+
enabled: true,
|
|
321
|
+
variation: 'treatment',
|
|
322
|
+
variables: {
|
|
323
|
+
myVariableKey: 'myVariableValue'
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
anotherFeatureKey: {
|
|
327
|
+
enabled: false
|
|
328
|
+
}
|
|
329
|
+
}, true) # replace existing sticky features (false by default)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Setting datafile
|
|
333
|
+
|
|
334
|
+
You may also initialize the SDK without passing `datafile`, and set it later on:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
f.set_datafile(datafile_content)
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Updating datafile
|
|
341
|
+
|
|
342
|
+
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.
|
|
343
|
+
|
|
344
|
+
The triggers for setting the datafile again can be:
|
|
345
|
+
|
|
346
|
+
- periodic updates based on an interval (like every 5 minutes), or
|
|
347
|
+
- reacting to:
|
|
348
|
+
- a specific event in your application (like a user action), or
|
|
349
|
+
- an event served via websocket or server-sent events (SSE)
|
|
350
|
+
|
|
351
|
+
### Interval-based update
|
|
352
|
+
|
|
353
|
+
Here's an example of using interval-based update:
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
require 'net/http'
|
|
357
|
+
require 'json'
|
|
358
|
+
|
|
359
|
+
def update_datafile(f, datafile_url)
|
|
360
|
+
loop do
|
|
361
|
+
sleep(5 * 60) # 5 minutes
|
|
362
|
+
|
|
363
|
+
begin
|
|
364
|
+
response = Net::HTTP.get_response(URI(datafile_url))
|
|
365
|
+
datafile_content = JSON.parse(response.body)
|
|
366
|
+
f.set_datafile(datafile_content)
|
|
367
|
+
rescue => e
|
|
368
|
+
# handle error
|
|
369
|
+
puts "Failed to update datafile: #{e.message}"
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Start the update thread
|
|
375
|
+
Thread.new { update_datafile(f, datafile_url) }
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Logging
|
|
379
|
+
|
|
380
|
+
By default, Featurevisor SDKs will print out logs to the console for `info` level and above.
|
|
381
|
+
|
|
382
|
+
### Levels
|
|
383
|
+
|
|
384
|
+
These are all the available log levels:
|
|
385
|
+
|
|
386
|
+
- `error`
|
|
387
|
+
- `warn`
|
|
388
|
+
- `info`
|
|
389
|
+
- `debug`
|
|
390
|
+
|
|
391
|
+
### Customizing levels
|
|
392
|
+
|
|
393
|
+
If you choose `debug` level to make the logs more verbose, you can set it at the time of SDK initialization.
|
|
394
|
+
|
|
395
|
+
Setting `debug` level will print out all logs, including `info`, `warn`, and `error` levels.
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
require 'featurevisor'
|
|
399
|
+
|
|
400
|
+
f = Featurevisor.create_instance(
|
|
401
|
+
logger: Featurevisor.create_logger(level: 'debug')
|
|
402
|
+
)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Alternatively, you can also set `log_level` directly:
|
|
406
|
+
|
|
407
|
+
```ruby
|
|
408
|
+
f = Featurevisor.create_instance(
|
|
409
|
+
log_level: 'debug'
|
|
410
|
+
)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
You can also set log level from SDK instance afterwards:
|
|
414
|
+
|
|
415
|
+
```ruby
|
|
416
|
+
f.set_log_level('debug')
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Handler
|
|
420
|
+
|
|
421
|
+
You can also pass your own log handler, if you do not wish to print the logs to the console:
|
|
422
|
+
|
|
423
|
+
```ruby
|
|
424
|
+
require 'featurevisor'
|
|
425
|
+
|
|
426
|
+
f = Featurevisor.create_instance(
|
|
427
|
+
logger: Featurevisor.create_logger(
|
|
428
|
+
level: 'info',
|
|
429
|
+
handler: ->(level, message, details) {
|
|
430
|
+
# do something with the log
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Further log levels like `info` and `debug` will help you understand how the feature variations and variables are evaluated in the runtime against given context.
|
|
437
|
+
|
|
438
|
+
## Events
|
|
439
|
+
|
|
440
|
+
Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime.
|
|
441
|
+
|
|
442
|
+
You can listen to these events that can occur at various stages in your application:
|
|
443
|
+
|
|
444
|
+
### `datafile_set`
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
unsubscribe = f.on('datafile_set') do |event|
|
|
448
|
+
revision = event[:revision] # new revision
|
|
449
|
+
previous_revision = event[:previous_revision]
|
|
450
|
+
revision_changed = event[:revision_changed] # true if revision has changed
|
|
451
|
+
|
|
452
|
+
# list of feature keys that have new updates,
|
|
453
|
+
# and you should re-evaluate them
|
|
454
|
+
features = event[:features]
|
|
455
|
+
|
|
456
|
+
# handle here
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# stop listening to the event
|
|
460
|
+
unsubscribe.call
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
The `features` array will contain keys of features that have either been:
|
|
464
|
+
|
|
465
|
+
- added, or
|
|
466
|
+
- updated, or
|
|
467
|
+
- removed
|
|
468
|
+
|
|
469
|
+
compared to the previous datafile content that existed in the SDK instance.
|
|
470
|
+
|
|
471
|
+
### `context_set`
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
unsubscribe = f.on('context_set') do |event|
|
|
475
|
+
replaced = event[:replaced] # true if context was replaced
|
|
476
|
+
context = event[:context] # the new context
|
|
477
|
+
|
|
478
|
+
puts 'Context set'
|
|
479
|
+
end
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### `sticky_set`
|
|
483
|
+
|
|
484
|
+
```ruby
|
|
485
|
+
unsubscribe = f.on('sticky_set') do |event|
|
|
486
|
+
replaced = event[:replaced] # true if sticky features got replaced
|
|
487
|
+
features = event[:features] # list of all affected feature keys
|
|
488
|
+
|
|
489
|
+
puts 'Sticky features set'
|
|
490
|
+
end
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Evaluation details
|
|
494
|
+
|
|
495
|
+
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:
|
|
496
|
+
|
|
497
|
+
```ruby
|
|
498
|
+
# flag
|
|
499
|
+
evaluation = f.evaluate_flag(feature_key, context = {})
|
|
500
|
+
|
|
501
|
+
# variation
|
|
502
|
+
evaluation = f.evaluate_variation(feature_key, context = {})
|
|
503
|
+
|
|
504
|
+
# variable
|
|
505
|
+
evaluation = f.evaluate_variable(feature_key, variable_key, context = {})
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
The returned object will always contain the following properties:
|
|
509
|
+
|
|
510
|
+
- `feature_key`: the feature key
|
|
511
|
+
- `reason`: the reason how the value was evaluated
|
|
512
|
+
|
|
513
|
+
And optionally these properties depending on whether you are evaluating a feature variation or a variable:
|
|
514
|
+
|
|
515
|
+
- `bucket_value`: the bucket value between 0 and 100,000
|
|
516
|
+
- `rule_key`: the rule key
|
|
517
|
+
- `error`: the error object
|
|
518
|
+
- `enabled`: if feature itself is enabled or not
|
|
519
|
+
- `variation`: the variation object
|
|
520
|
+
- `variation_value`: the variation value
|
|
521
|
+
- `variable_key`: the variable key
|
|
522
|
+
- `variable_value`: the variable value
|
|
523
|
+
- `variable_schema`: the variable schema
|
|
524
|
+
|
|
525
|
+
## Hooks
|
|
526
|
+
|
|
527
|
+
Hooks allow you to intercept the evaluation process and customize it further as per your needs.
|
|
528
|
+
|
|
529
|
+
### Defining a hook
|
|
530
|
+
|
|
531
|
+
A hook is a simple hash with a unique required `name` and optional functions:
|
|
532
|
+
|
|
533
|
+
```ruby
|
|
534
|
+
require 'featurevisor'
|
|
535
|
+
|
|
536
|
+
my_custom_hook = {
|
|
537
|
+
# only required property
|
|
538
|
+
name: 'my-custom-hook',
|
|
539
|
+
|
|
540
|
+
# rest of the properties below are all optional per hook
|
|
541
|
+
|
|
542
|
+
# before evaluation
|
|
543
|
+
before: ->(options) {
|
|
544
|
+
# update context before evaluation
|
|
545
|
+
options[:context] = options[:context].merge({
|
|
546
|
+
someAdditionalAttribute: 'value'
|
|
547
|
+
})
|
|
548
|
+
options
|
|
549
|
+
},
|
|
550
|
+
|
|
551
|
+
# after evaluation
|
|
552
|
+
after: ->(evaluation, options) {
|
|
553
|
+
reason = evaluation[:reason]
|
|
554
|
+
if reason == 'error'
|
|
555
|
+
# log error
|
|
556
|
+
return
|
|
557
|
+
end
|
|
558
|
+
},
|
|
559
|
+
|
|
560
|
+
# configure bucket key
|
|
561
|
+
bucket_key: ->(options) {
|
|
562
|
+
# return custom bucket key
|
|
563
|
+
options[:bucket_key]
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
# configure bucket value (between 0 and 100,000)
|
|
567
|
+
bucket_value: ->(options) {
|
|
568
|
+
# return custom bucket value
|
|
569
|
+
options[:bucket_value]
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Registering hooks
|
|
575
|
+
|
|
576
|
+
You can register hooks at the time of SDK initialization:
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
require 'featurevisor'
|
|
580
|
+
|
|
581
|
+
f = Featurevisor.create_instance(
|
|
582
|
+
hooks: [my_custom_hook]
|
|
583
|
+
)
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Or after initialization:
|
|
587
|
+
|
|
588
|
+
```ruby
|
|
589
|
+
f.add_hook(my_custom_hook)
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
## Child instance
|
|
593
|
+
|
|
594
|
+
When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications.
|
|
595
|
+
|
|
596
|
+
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.
|
|
597
|
+
|
|
598
|
+
That's where child instances come in handy:
|
|
599
|
+
|
|
600
|
+
```ruby
|
|
601
|
+
child_f = f.spawn({
|
|
602
|
+
# user or request specific context
|
|
603
|
+
userId: '123'
|
|
604
|
+
})
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
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:
|
|
608
|
+
|
|
609
|
+
```ruby
|
|
610
|
+
is_enabled = child_f.is_enabled('my_feature')
|
|
611
|
+
variation = child_f.get_variation('my_feature')
|
|
612
|
+
variable_value = child_f.get_variable('my_feature', 'my_variable')
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Similar to parent SDK, child instances also support several additional methods:
|
|
616
|
+
|
|
617
|
+
- `set_context`
|
|
618
|
+
- `set_sticky`
|
|
619
|
+
- `is_enabled`
|
|
620
|
+
- `get_variation`
|
|
621
|
+
- `get_variable`
|
|
622
|
+
- `get_variable_boolean`
|
|
623
|
+
- `get_variable_string`
|
|
624
|
+
- `get_variable_integer`
|
|
625
|
+
- `get_variable_double`
|
|
626
|
+
- `get_variable_array`
|
|
627
|
+
- `get_variable_object`
|
|
628
|
+
- `get_variable_json`
|
|
629
|
+
- `get_all_evaluations`
|
|
630
|
+
- `on`
|
|
631
|
+
- `close`
|
|
632
|
+
|
|
633
|
+
## Close
|
|
634
|
+
|
|
635
|
+
Both primary and child instances support a `.close()` method, that removes forgotten event listeners (via `on` method) and cleans up any potential memory leaks.
|
|
636
|
+
|
|
637
|
+
```ruby
|
|
638
|
+
f.close()
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
## CLI usage
|
|
642
|
+
|
|
643
|
+
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.
|
|
644
|
+
|
|
645
|
+
- Global installation: you can access it as `featurevisor`
|
|
646
|
+
- Local installation: you can access it as `bundle exec featurevisor`
|
|
647
|
+
- From this repository: you can access it as `bin/featurevisor`
|
|
648
|
+
|
|
649
|
+
### Test
|
|
650
|
+
|
|
651
|
+
Learn more about testing [here](https://featurevisor.com/docs/testing/).
|
|
652
|
+
|
|
653
|
+
```bash
|
|
654
|
+
$ bundle exec featurevisor test --projectDirectoryPath="/absolute/path/to/your/featurevisor/project"
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Additional options that are available:
|
|
658
|
+
|
|
659
|
+
```bash
|
|
660
|
+
$ bundle exec featurevisor test \
|
|
661
|
+
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
|
|
662
|
+
--quiet|verbose \
|
|
663
|
+
--onlyFailures \
|
|
664
|
+
--keyPattern="myFeatureKey" \
|
|
665
|
+
--assertionPattern="#1"
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### Benchmark
|
|
669
|
+
|
|
670
|
+
Learn more about benchmarking [here](https://featurevisor.com/docs/cmd/#benchmarking).
|
|
671
|
+
|
|
672
|
+
```bash
|
|
673
|
+
$ bundle exec featurevisor benchmark \
|
|
674
|
+
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
|
|
675
|
+
--environment="production" \
|
|
676
|
+
--feature="myFeatureKey" \
|
|
677
|
+
--context='{"country": "nl"}' \
|
|
678
|
+
--n=1000
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Assess distribution
|
|
682
|
+
|
|
683
|
+
Learn more about assessing distribution [here](https://featurevisor.com/docs/cmd/#assess-distribution).
|
|
684
|
+
|
|
685
|
+
```bash
|
|
686
|
+
$ bundle exec featurevisor assess-distribution \
|
|
687
|
+
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
|
|
688
|
+
--environment=production \
|
|
689
|
+
--feature=foo \
|
|
690
|
+
--variation \
|
|
691
|
+
--context='{"country": "nl"}' \
|
|
692
|
+
--populateUuid=userId \
|
|
693
|
+
--populateUuid=deviceId \
|
|
694
|
+
--n=1000
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
## Development
|
|
698
|
+
|
|
699
|
+
### Setting up
|
|
700
|
+
|
|
701
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
702
|
+
|
|
703
|
+
### Running tests
|
|
704
|
+
|
|
705
|
+
```bash
|
|
706
|
+
$ bundle exec rspec
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
710
|
+
|
|
711
|
+
### Releasing
|
|
712
|
+
|
|
713
|
+
- Update version in `lib/featurevisor/version.rb`
|
|
714
|
+
- Run `bundle install`
|
|
715
|
+
- Push commit to `main` branch
|
|
716
|
+
- Wait for CI to complete
|
|
717
|
+
- Tag the release with the version number
|
|
718
|
+
- This will trigger a new release to RubyGems
|
|
719
|
+
|
|
720
|
+
## License
|
|
721
|
+
|
|
722
|
+
MIT © [Fahad Heylaal](https://fahad19.com)
|