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.
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)