featurevisor 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +172 -75
- data/bin/cli.rb +15 -2
- data/bin/commands/assess_distribution.rb +34 -15
- data/bin/commands/benchmark.rb +65 -63
- data/bin/commands/test.rb +250 -100
- data/lib/featurevisor/child_instance.rb +22 -24
- data/lib/featurevisor/datafile_reader.rb +18 -13
- data/lib/featurevisor/emitter.rb +2 -2
- data/lib/featurevisor/evaluate.rb +106 -100
- data/lib/featurevisor/events.rb +5 -4
- data/lib/featurevisor/instance.rb +225 -39
- data/lib/featurevisor/logger.rb +0 -7
- data/lib/featurevisor/modules.rb +184 -0
- data/lib/featurevisor/version.rb +1 -1
- data/lib/featurevisor.rb +4 -1
- metadata +16 -2
- data/lib/featurevisor/hooks.rb +0 -159
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5951351cf7893e83e9f3994999f48389e1998bb8dec84092f3e7627f986bfd02
|
|
4
|
+
data.tar.gz: 3d24e02b8fd0708084b7716863396119728bcd83cbd0ac209b7a1c62fb982f68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 368e45cb26cb6a98d13bc14b4fe9116bd36af47acadad3fbc99fc1926c8ce768edf5c9ade31aa36dd3c85ff95fc6dab2d3433d4d607bbeee99076a21838e0dae
|
|
7
|
+
data.tar.gz: 160b9eb960915113e3f9dfa3507b9f06664224ac3517d7a84432ec64d2d26d26c2ba379a09aeba64efe78a310972b5f497ad5d5ef1bbef1cc41ffa1408ffa70f
|
data/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# Featurevisor Ruby SDK <!-- omit in toc -->
|
|
2
2
|
|
|
3
|
-
This is a port of Featurevisor [JavaScript SDK](https://featurevisor.com/docs/sdks/javascript/)
|
|
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
4
|
|
|
5
|
-
This SDK is compatible with
|
|
5
|
+
This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
|
|
6
6
|
|
|
7
7
|
## Table of contents <!-- omit in toc -->
|
|
8
8
|
|
|
9
9
|
- [Installation](#installation)
|
|
10
|
+
- [Public API](#public-api)
|
|
10
11
|
- [Initialization](#initialization)
|
|
11
12
|
- [Evaluation types](#evaluation-types)
|
|
12
13
|
- [Context](#context)
|
|
@@ -23,24 +24,28 @@ This SDK is compatible with [Featurevisor](https://featurevisor.com/) v2.0 proje
|
|
|
23
24
|
- [Initialize with sticky](#initialize-with-sticky)
|
|
24
25
|
- [Set sticky afterwards](#set-sticky-afterwards)
|
|
25
26
|
- [Setting datafile](#setting-datafile)
|
|
27
|
+
- [Merging by default](#merging-by-default)
|
|
28
|
+
- [Replacing](#replacing)
|
|
29
|
+
- [Loading datafiles on demand](#loading-datafiles-on-demand)
|
|
26
30
|
- [Updating datafile](#updating-datafile)
|
|
27
31
|
- [Interval-based update](#interval-based-update)
|
|
28
|
-
- [
|
|
32
|
+
- [Evaluation details](#evaluation-details)
|
|
33
|
+
- [Diagnostics](#diagnostics)
|
|
29
34
|
- [Levels](#levels)
|
|
30
|
-
- [Customizing levels](#customizing-levels)
|
|
31
35
|
- [Handler](#handler)
|
|
32
36
|
- [Events](#events)
|
|
33
37
|
- [`datafile_set`](#datafile_set)
|
|
34
38
|
- [`context_set`](#context_set)
|
|
35
39
|
- [`sticky_set`](#sticky_set)
|
|
36
|
-
- [
|
|
37
|
-
- [
|
|
38
|
-
- [Defining a
|
|
39
|
-
- [Registering
|
|
40
|
+
- [`error`](#error)
|
|
41
|
+
- [Modules](#modules)
|
|
42
|
+
- [Defining a module](#defining-a-module)
|
|
43
|
+
- [Registering modules](#registering-modules)
|
|
40
44
|
- [Child instance](#child-instance)
|
|
41
45
|
- [Close](#close)
|
|
42
46
|
- [CLI usage](#cli-usage)
|
|
43
47
|
- [Test](#test)
|
|
48
|
+
- [Test against local monorepo's example-1](#test-against-local-monorepos-example-1)
|
|
44
49
|
- [Benchmark](#benchmark)
|
|
45
50
|
- [Assess distribution](#assess-distribution)
|
|
46
51
|
- [Development](#development)
|
|
@@ -49,6 +54,8 @@ This SDK is compatible with [Featurevisor](https://featurevisor.com/) v2.0 proje
|
|
|
49
54
|
- [Releasing](#releasing)
|
|
50
55
|
- [License](#license)
|
|
51
56
|
|
|
57
|
+
<!-- FEATUREVISOR_DOCS_BEGIN -->
|
|
58
|
+
|
|
52
59
|
## Installation
|
|
53
60
|
|
|
54
61
|
Add this line to your application's Gemfile:
|
|
@@ -69,6 +76,18 @@ Or install it yourself as:
|
|
|
69
76
|
$ gem install featurevisor
|
|
70
77
|
```
|
|
71
78
|
|
|
79
|
+
## Public API
|
|
80
|
+
|
|
81
|
+
The main runtime API is `Featurevisor.create_featurevisor`:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
f = Featurevisor.create_featurevisor(
|
|
85
|
+
datafile: datafile_content
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
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.
|
|
90
|
+
|
|
72
91
|
## Initialization
|
|
73
92
|
|
|
74
93
|
The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly:
|
|
@@ -86,7 +105,7 @@ response = Net::HTTP.get_response(URI(datafile_url))
|
|
|
86
105
|
datafile_content = JSON.parse(response.body, symbolize_names: true)
|
|
87
106
|
|
|
88
107
|
# Create SDK instance
|
|
89
|
-
f = Featurevisor.
|
|
108
|
+
f = Featurevisor.create_featurevisor(
|
|
90
109
|
datafile: datafile_content
|
|
91
110
|
)
|
|
92
111
|
```
|
|
@@ -98,10 +117,10 @@ Alternatively, you can pass a JSON string directly and the SDK will parse it aut
|
|
|
98
117
|
```ruby
|
|
99
118
|
# Option 1: Parse JSON yourself (recommended)
|
|
100
119
|
datafile_content = JSON.parse(json_string, symbolize_names: true)
|
|
101
|
-
f = Featurevisor.
|
|
120
|
+
f = Featurevisor.create_featurevisor(datafile: datafile_content)
|
|
102
121
|
|
|
103
122
|
# Option 2: Pass JSON string directly (automatic parsing)
|
|
104
|
-
f = Featurevisor.
|
|
123
|
+
f = Featurevisor.create_featurevisor(datafile: json_string)
|
|
105
124
|
```
|
|
106
125
|
|
|
107
126
|
## Evaluation types
|
|
@@ -139,7 +158,7 @@ You can set context at the time of initialization:
|
|
|
139
158
|
```ruby
|
|
140
159
|
require 'featurevisor'
|
|
141
160
|
|
|
142
|
-
f = Featurevisor.
|
|
161
|
+
f = Featurevisor.create_featurevisor(
|
|
143
162
|
context: {
|
|
144
163
|
deviceId: '123',
|
|
145
164
|
country: 'nl'
|
|
@@ -271,6 +290,8 @@ f.get_variable_object(feature_key, variable_key, context = {})
|
|
|
271
290
|
f.get_variable_json(feature_key, variable_key, context = {})
|
|
272
291
|
```
|
|
273
292
|
|
|
293
|
+
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.
|
|
294
|
+
|
|
274
295
|
## Getting all evaluations
|
|
275
296
|
|
|
276
297
|
You can get evaluations of all features available in the SDK instance:
|
|
@@ -301,12 +322,14 @@ This is handy especially when you want to pass all evaluations from a backend ap
|
|
|
301
322
|
|
|
302
323
|
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/):
|
|
303
324
|
|
|
325
|
+
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.
|
|
326
|
+
|
|
304
327
|
### Initialize with sticky
|
|
305
328
|
|
|
306
329
|
```ruby
|
|
307
330
|
require 'featurevisor'
|
|
308
331
|
|
|
309
|
-
f = Featurevisor.
|
|
332
|
+
f = Featurevisor.create_featurevisor(
|
|
310
333
|
sticky: {
|
|
311
334
|
myFeatureKey: {
|
|
312
335
|
enabled: true,
|
|
@@ -359,6 +382,49 @@ f.set_datafile(json_string)
|
|
|
359
382
|
|
|
360
383
|
**Important**: When calling `set_datafile()`, ensure JSON is parsed with `symbolize_names: true` if you're parsing it yourself.
|
|
361
384
|
|
|
385
|
+
### Merging by default
|
|
386
|
+
|
|
387
|
+
By default, `set_datafile(datafile)` merges the incoming datafile into the SDK's current datafile:
|
|
388
|
+
|
|
389
|
+
- top-level metadata such as `schemaVersion`, `revision`, and `featurevisorVersion` comes from the incoming datafile
|
|
390
|
+
- `segments` are merged, with incoming entries overriding existing ones
|
|
391
|
+
- `features` are merged, with incoming entries overriding existing ones
|
|
392
|
+
|
|
393
|
+
This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together.
|
|
394
|
+
|
|
395
|
+
### Replacing
|
|
396
|
+
|
|
397
|
+
To fully replace the stored datafile, pass `true` as the second argument:
|
|
398
|
+
|
|
399
|
+
```ruby
|
|
400
|
+
f.set_datafile(datafile_content, true)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Loading datafiles on demand
|
|
404
|
+
|
|
405
|
+
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.
|
|
406
|
+
|
|
407
|
+
This pairs well with [targets](https://featurevisor.com/docs/targets/), where each target produces a smaller datafile for a specific part of your application:
|
|
408
|
+
|
|
409
|
+
```ruby
|
|
410
|
+
require "open-uri"
|
|
411
|
+
|
|
412
|
+
f = Featurevisor.create_featurevisor({})
|
|
413
|
+
|
|
414
|
+
def load_datafile(f, target)
|
|
415
|
+
url = "https://cdn.yoursite.com/production/featurevisor-#{target}.json"
|
|
416
|
+
datafile = JSON.parse(URI.open(url).read, symbolize_names: true)
|
|
417
|
+
|
|
418
|
+
# merges into whatever was loaded before
|
|
419
|
+
f.set_datafile(datafile)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
load_datafile(f, "products")
|
|
423
|
+
|
|
424
|
+
# later, when the user reaches checkout
|
|
425
|
+
load_datafile(f, "checkout")
|
|
426
|
+
```
|
|
427
|
+
|
|
362
428
|
### Updating datafile
|
|
363
429
|
|
|
364
430
|
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.
|
|
@@ -381,7 +447,7 @@ require 'json'
|
|
|
381
447
|
def update_datafile(f, datafile_url)
|
|
382
448
|
loop do
|
|
383
449
|
sleep(5 * 60) # 5 minutes
|
|
384
|
-
|
|
450
|
+
|
|
385
451
|
begin
|
|
386
452
|
response = Net::HTTP.get_response(URI(datafile_url))
|
|
387
453
|
datafile_content = JSON.parse(response.body)
|
|
@@ -397,65 +463,38 @@ end
|
|
|
397
463
|
Thread.new { update_datafile(f, datafile_url) }
|
|
398
464
|
```
|
|
399
465
|
|
|
400
|
-
##
|
|
466
|
+
## Diagnostics
|
|
401
467
|
|
|
402
|
-
By default, Featurevisor
|
|
468
|
+
By default, Featurevisor reports diagnostics to the console for `info` level and above with a `[Featurevisor]` prefix.
|
|
403
469
|
|
|
404
470
|
### Levels
|
|
405
471
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
- `error`
|
|
409
|
-
- `warn`
|
|
410
|
-
- `info`
|
|
411
|
-
- `debug`
|
|
412
|
-
|
|
413
|
-
### Customizing levels
|
|
414
|
-
|
|
415
|
-
If you choose `debug` level to make the logs more verbose, you can set it at the time of SDK initialization.
|
|
416
|
-
|
|
417
|
-
Setting `debug` level will print out all logs, including `info`, `warn`, and `error` levels.
|
|
418
|
-
|
|
419
|
-
```ruby
|
|
420
|
-
require 'featurevisor'
|
|
421
|
-
|
|
422
|
-
f = Featurevisor.create_instance(
|
|
423
|
-
logger: Featurevisor.create_logger(level: 'debug')
|
|
424
|
-
)
|
|
425
|
-
```
|
|
472
|
+
Available diagnostic levels are `fatal`, `error`, `warn`, `info`, and `debug`.
|
|
426
473
|
|
|
427
|
-
|
|
474
|
+
Set the level during initialization or update it afterwards:
|
|
428
475
|
|
|
429
476
|
```ruby
|
|
430
|
-
f = Featurevisor.
|
|
431
|
-
|
|
432
|
-
)
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
You can also set log level from SDK instance afterwards:
|
|
436
|
-
|
|
437
|
-
```ruby
|
|
438
|
-
f.set_log_level('debug')
|
|
477
|
+
f = Featurevisor.create_featurevisor(log_level: "debug")
|
|
478
|
+
f.set_log_level("info")
|
|
439
479
|
```
|
|
440
480
|
|
|
441
481
|
### Handler
|
|
442
482
|
|
|
443
|
-
|
|
483
|
+
Use `on_diagnostic` to send structured diagnostics to your observability system:
|
|
444
484
|
|
|
445
485
|
```ruby
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
handler: ->(level, message, details) {
|
|
452
|
-
# do something with the log
|
|
453
|
-
}
|
|
454
|
-
)
|
|
486
|
+
f = Featurevisor.create_featurevisor(
|
|
487
|
+
log_level: "info",
|
|
488
|
+
on_diagnostic: ->(diagnostic) {
|
|
489
|
+
puts "#{diagnostic[:level]} #{diagnostic[:code]} #{diagnostic[:message]}"
|
|
490
|
+
}
|
|
455
491
|
)
|
|
456
492
|
```
|
|
457
493
|
|
|
458
|
-
|
|
494
|
+
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`.
|
|
495
|
+
|
|
496
|
+
Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations.
|
|
497
|
+
|
|
459
498
|
|
|
460
499
|
## Events
|
|
461
500
|
|
|
@@ -468,8 +507,8 @@ You can listen to these events that can occur at various stages in your applicat
|
|
|
468
507
|
```ruby
|
|
469
508
|
unsubscribe = f.on('datafile_set') do |event|
|
|
470
509
|
revision = event[:revision] # new revision
|
|
471
|
-
previous_revision = event[:
|
|
472
|
-
revision_changed = event[:
|
|
510
|
+
previous_revision = event[:previousRevision]
|
|
511
|
+
revision_changed = event[:revisionChanged] # true if revision has changed
|
|
473
512
|
|
|
474
513
|
# list of feature keys that have new updates,
|
|
475
514
|
# and you should re-evaluate them
|
|
@@ -490,6 +529,8 @@ The `features` array will contain keys of features that have either been:
|
|
|
490
529
|
|
|
491
530
|
compared to the previous datafile content that existed in the SDK instance.
|
|
492
531
|
|
|
532
|
+
The event also includes `replaced`, which is `true` when the datafile replaced the previous content instead of merging into it.
|
|
533
|
+
|
|
493
534
|
### `context_set`
|
|
494
535
|
|
|
495
536
|
```ruby
|
|
@@ -512,6 +553,20 @@ unsubscribe = f.on('sticky_set') do |event|
|
|
|
512
553
|
end
|
|
513
554
|
```
|
|
514
555
|
|
|
556
|
+
### `error`
|
|
557
|
+
|
|
558
|
+
```ruby
|
|
559
|
+
unsubscribe = f.on('error') do |event|
|
|
560
|
+
diagnostic = event[:diagnostic]
|
|
561
|
+
code = diagnostic[:code]
|
|
562
|
+
message = diagnostic[:message]
|
|
563
|
+
|
|
564
|
+
puts "Featurevisor error: #{code} #{message}"
|
|
565
|
+
end
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
The `error` event is emitted for diagnostics reported with `level: "error"`.
|
|
569
|
+
|
|
515
570
|
## Evaluation details
|
|
516
571
|
|
|
517
572
|
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:
|
|
@@ -544,22 +599,33 @@ And optionally these properties depending on whether you are evaluating a featur
|
|
|
544
599
|
- `variable_value`: the variable value
|
|
545
600
|
- `variable_schema`: the variable schema
|
|
546
601
|
|
|
547
|
-
##
|
|
602
|
+
## Modules
|
|
603
|
+
|
|
604
|
+
Modules allow you to intercept the evaluation process and customize SDK behavior.
|
|
548
605
|
|
|
549
|
-
|
|
606
|
+
### Defining a module
|
|
550
607
|
|
|
551
|
-
|
|
608
|
+
A module is a simple hash with a unique recommended `name` and optional lifecycle functions:
|
|
552
609
|
|
|
553
|
-
|
|
610
|
+
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.
|
|
554
611
|
|
|
555
612
|
```ruby
|
|
556
613
|
require 'featurevisor'
|
|
557
614
|
|
|
558
|
-
|
|
559
|
-
#
|
|
560
|
-
name: 'my-custom-
|
|
615
|
+
my_custom_module = {
|
|
616
|
+
# recommended, and used for duplicate detection/removal
|
|
617
|
+
name: 'my-custom-module',
|
|
618
|
+
|
|
619
|
+
# rest of the properties below are all optional per module
|
|
620
|
+
|
|
621
|
+
# setup once, when the module is registered
|
|
622
|
+
setup: ->(api) {
|
|
623
|
+
revision = api[:get_revision].call
|
|
561
624
|
|
|
562
|
-
|
|
625
|
+
api[:on_diagnostic].call(->(diagnostic) {
|
|
626
|
+
puts diagnostic[:message]
|
|
627
|
+
})
|
|
628
|
+
},
|
|
563
629
|
|
|
564
630
|
# before evaluation
|
|
565
631
|
before: ->(options) {
|
|
@@ -589,26 +655,43 @@ my_custom_hook = {
|
|
|
589
655
|
bucket_value: ->(options) {
|
|
590
656
|
# return custom bucket value
|
|
591
657
|
options[:bucket_value]
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
# cleanup when module is removed or SDK is closed
|
|
661
|
+
close: -> {
|
|
662
|
+
# cleanup here
|
|
592
663
|
}
|
|
593
664
|
}
|
|
594
665
|
```
|
|
595
666
|
|
|
596
|
-
|
|
667
|
+
The module API passed to `setup` exposes:
|
|
668
|
+
|
|
669
|
+
- `get_revision`
|
|
670
|
+
- `on_diagnostic`
|
|
671
|
+
- `report_diagnostic`
|
|
597
672
|
|
|
598
|
-
|
|
673
|
+
### Registering modules
|
|
674
|
+
|
|
675
|
+
You can register modules at the time of SDK initialization:
|
|
599
676
|
|
|
600
677
|
```ruby
|
|
601
678
|
require 'featurevisor'
|
|
602
679
|
|
|
603
|
-
f = Featurevisor.
|
|
604
|
-
|
|
680
|
+
f = Featurevisor.create_featurevisor(
|
|
681
|
+
modules: [my_custom_module]
|
|
605
682
|
)
|
|
606
683
|
```
|
|
607
684
|
|
|
608
685
|
Or after initialization:
|
|
609
686
|
|
|
610
687
|
```ruby
|
|
611
|
-
f.
|
|
688
|
+
remove_module = f.add_module(my_custom_module)
|
|
689
|
+
|
|
690
|
+
# remove later by calling the returned function
|
|
691
|
+
remove_module.call
|
|
692
|
+
|
|
693
|
+
# or remove by name
|
|
694
|
+
f.remove_module('my-custom-module')
|
|
612
695
|
```
|
|
613
696
|
|
|
614
697
|
## Child instance
|
|
@@ -681,15 +764,27 @@ Additional options that are available:
|
|
|
681
764
|
```bash
|
|
682
765
|
$ bundle exec featurevisor test \
|
|
683
766
|
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
|
|
684
|
-
--quiet
|
|
767
|
+
--quiet|--verbose \
|
|
685
768
|
--onlyFailures \
|
|
686
769
|
--keyPattern="myFeatureKey" \
|
|
687
770
|
--assertionPattern="#1"
|
|
688
771
|
```
|
|
689
772
|
|
|
773
|
+
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.
|
|
774
|
+
|
|
775
|
+
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.
|
|
776
|
+
|
|
777
|
+
### Test against local monorepo's example-1
|
|
778
|
+
|
|
779
|
+
```bash
|
|
780
|
+
$ cd /absolute/path/to/featurevisor-ruby
|
|
781
|
+
$ bundle exec ruby bin/featurevisor test --projectDirectoryPath=/Users/fahad/Projects/featurevisor/featurevisor/examples/example-1 --onlyFailures
|
|
782
|
+
$ make test-example-1
|
|
783
|
+
```
|
|
784
|
+
|
|
690
785
|
### Benchmark
|
|
691
786
|
|
|
692
|
-
Learn more about benchmarking [here](https://featurevisor.com/docs/
|
|
787
|
+
Learn more about benchmarking [here](https://featurevisor.com/docs/cli/#benchmarking).
|
|
693
788
|
|
|
694
789
|
```bash
|
|
695
790
|
$ bundle exec featurevisor benchmark \
|
|
@@ -702,7 +797,7 @@ $ bundle exec featurevisor benchmark \
|
|
|
702
797
|
|
|
703
798
|
### Assess distribution
|
|
704
799
|
|
|
705
|
-
Learn more about assessing distribution [here](https://featurevisor.com/docs/
|
|
800
|
+
Learn more about assessing distribution [here](https://featurevisor.com/docs/cli/#assess-distribution).
|
|
706
801
|
|
|
707
802
|
```bash
|
|
708
803
|
$ bundle exec featurevisor assess-distribution \
|
|
@@ -716,6 +811,8 @@ $ bundle exec featurevisor assess-distribution \
|
|
|
716
811
|
--n=1000
|
|
717
812
|
```
|
|
718
813
|
|
|
814
|
+
<!-- FEATUREVISOR_DOCS_END -->
|
|
815
|
+
|
|
719
816
|
## Development
|
|
720
817
|
|
|
721
818
|
### Setting up
|
data/bin/cli.rb
CHANGED
|
@@ -7,12 +7,13 @@ module FeaturevisorCLI
|
|
|
7
7
|
attr_accessor :command, :assertion_pattern, :context, :environment, :feature,
|
|
8
8
|
:key_pattern, :n, :only_failures, :quiet, :variable, :variation,
|
|
9
9
|
:verbose, :inflate, :show_datafile, :schema_version, :project_directory_path,
|
|
10
|
-
:populate_uuid
|
|
10
|
+
:populate_uuid, :with_scopes, :with_tags, :targets
|
|
11
11
|
|
|
12
12
|
def initialize
|
|
13
13
|
@n = 1000
|
|
14
14
|
@project_directory_path = Dir.pwd
|
|
15
15
|
@populate_uuid = []
|
|
16
|
+
@targets = []
|
|
16
17
|
end
|
|
17
18
|
end
|
|
18
19
|
|
|
@@ -82,10 +83,18 @@ module FeaturevisorCLI
|
|
|
82
83
|
options.show_datafile = true
|
|
83
84
|
end
|
|
84
85
|
|
|
85
|
-
opts.on("--schemaVersion=VERSION", "
|
|
86
|
+
opts.on("--schemaVersion=VERSION", "--schema-version=VERSION", "Legacy schema version option accepted and ignored") do |v|
|
|
86
87
|
options.schema_version = v
|
|
87
88
|
end
|
|
88
89
|
|
|
90
|
+
opts.on("--with-scopes", "--withScopes", "Legacy scope option accepted and ignored") do
|
|
91
|
+
options.with_scopes = true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
opts.on("--with-tags", "--withTags", "Legacy tag option accepted and ignored") do
|
|
95
|
+
options.with_tags = true
|
|
96
|
+
end
|
|
97
|
+
|
|
89
98
|
opts.on("--projectDirectoryPath=PATH", "Project directory path") do |v|
|
|
90
99
|
options.project_directory_path = v
|
|
91
100
|
end
|
|
@@ -94,6 +103,10 @@ module FeaturevisorCLI
|
|
|
94
103
|
options.populate_uuid << v
|
|
95
104
|
end
|
|
96
105
|
|
|
106
|
+
opts.on("--target=TARGET", "Target datafile; repeat for multiple targets") do |v|
|
|
107
|
+
options.targets << v unless options.targets.include?(v)
|
|
108
|
+
end
|
|
109
|
+
|
|
97
110
|
opts.on("-h", "--help", "Show this help message") do
|
|
98
111
|
puts opts
|
|
99
112
|
exit
|
|
@@ -29,13 +29,21 @@ module FeaturevisorCLI
|
|
|
29
29
|
exit 1
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
puts ""
|
|
33
|
-
puts "Assessing distribution for feature: \"#{@options.feature}\"..."
|
|
34
|
-
puts ""
|
|
35
|
-
|
|
36
|
-
# Parse context if provided
|
|
37
32
|
context = parse_context
|
|
38
33
|
|
|
34
|
+
targets = resolve_targets
|
|
35
|
+
(targets.empty? ? [nil] : targets).each { |target| run_for_target(context, target) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def run_for_target(context, target)
|
|
41
|
+
puts "\nAssess Featurevisor distribution"
|
|
42
|
+
puts " Feature: #{@options.feature}"
|
|
43
|
+
puts " Environment: #{@options.environment}"
|
|
44
|
+
puts " Target: #{target}" if target
|
|
45
|
+
puts " Iterations: #{@options.n}"
|
|
46
|
+
|
|
39
47
|
# Print context information
|
|
40
48
|
if @options.context
|
|
41
49
|
puts "Against context: #{@options.context}"
|
|
@@ -47,10 +55,10 @@ module FeaturevisorCLI
|
|
|
47
55
|
puts ""
|
|
48
56
|
|
|
49
57
|
# Build datafile
|
|
50
|
-
datafile = build_datafile(@options.environment)
|
|
58
|
+
datafile = build_datafile(@options.environment, target)
|
|
51
59
|
|
|
52
60
|
# Create SDK instance
|
|
53
|
-
instance =
|
|
61
|
+
instance = create_featurevisor(datafile)
|
|
54
62
|
|
|
55
63
|
# Check if feature has variations
|
|
56
64
|
feature = instance.get_feature(@options.feature)
|
|
@@ -104,7 +112,21 @@ module FeaturevisorCLI
|
|
|
104
112
|
end
|
|
105
113
|
end
|
|
106
114
|
|
|
107
|
-
|
|
115
|
+
def resolve_targets
|
|
116
|
+
return [] if @options.targets.empty?
|
|
117
|
+
stdout, stderr, status = Open3.capture3("npx", "featurevisor", "list", "--targets", "--json", chdir: @project_path)
|
|
118
|
+
unless status.success?
|
|
119
|
+
puts stderr
|
|
120
|
+
exit 1
|
|
121
|
+
end
|
|
122
|
+
available = JSON.parse(stdout).map { |item| item.is_a?(Hash) ? (item["key"] || item["name"]) : item }
|
|
123
|
+
unknown = @options.targets.find { |target| !available.include?(target) }
|
|
124
|
+
if unknown
|
|
125
|
+
puts "Unknown target \"#{unknown}\". Available targets: #{available.empty? ? "none" : available.join(", ")}."
|
|
126
|
+
exit 1
|
|
127
|
+
end
|
|
128
|
+
@options.targets
|
|
129
|
+
end
|
|
108
130
|
|
|
109
131
|
def parse_context
|
|
110
132
|
if @options.context
|
|
@@ -121,15 +143,12 @@ module FeaturevisorCLI
|
|
|
121
143
|
end
|
|
122
144
|
end
|
|
123
145
|
|
|
124
|
-
def build_datafile(environment)
|
|
146
|
+
def build_datafile(environment, target = nil)
|
|
125
147
|
puts "Building datafile for environment: #{environment}..."
|
|
126
148
|
|
|
127
149
|
# Build the command similar to Go implementation
|
|
128
150
|
command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--json"]
|
|
129
|
-
|
|
130
|
-
if @options.schema_version
|
|
131
|
-
command_parts << "--schemaVersion=#{@options.schema_version}"
|
|
132
|
-
end
|
|
151
|
+
command_parts << "--target=#{target}" if target
|
|
133
152
|
|
|
134
153
|
if @options.inflate
|
|
135
154
|
command_parts << "--inflate=#{@options.inflate}"
|
|
@@ -159,9 +178,9 @@ module FeaturevisorCLI
|
|
|
159
178
|
[stdout, stderr, exit_status.exitstatus]
|
|
160
179
|
end
|
|
161
180
|
|
|
162
|
-
def
|
|
181
|
+
def create_featurevisor(datafile)
|
|
163
182
|
# Create SDK instance
|
|
164
|
-
Featurevisor.
|
|
183
|
+
Featurevisor.create_featurevisor(
|
|
165
184
|
datafile: datafile,
|
|
166
185
|
log_level: get_logger_level
|
|
167
186
|
)
|