Featurevisor Ruby SDK
This is a port of Featurevisor JavaScript SDK v3.x to Ruby, providing a way to evaluate feature flags, variations, and variables in your Ruby applications.
This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
Table of contents
- Installation
- Public API
- Initialization
- Evaluation types
- Context
- Check if enabled
- Getting variation
- Getting variables
- Getting all evaluations
- Sticky
- Setting datafile
- Evaluation details
- Diagnostics
- Events
- Modules
- Child instance
- Close
- CLI usage
- Development
- License
Installation
Add this line to your application's Gemfile:
gem 'featurevisor'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install featurevisor
Public API
The main runtime API is Featurevisor.create_featurevisor:
f = Featurevisor.create_featurevisor(
datafile: datafile_content
)
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.
Initialization
The SDK can be initialized by passing datafile content directly:
require 'featurevisor'
require 'net/http'
require 'json'
# Fetch datafile from URL
datafile_url = 'https://cdn.yoursite.com/datafile.json'
response = Net::HTTP.get_response(URI(datafile_url))
# Parse JSON with symbolized keys (required)
datafile_content = JSON.parse(response.body, symbolize_names: true)
# Create SDK instance
f = Featurevisor.create_featurevisor(
datafile: datafile_content
)
Important: When parsing JSON datafiles, you must use symbolize_names: true to ensure proper key handling by the SDK.
Alternatively, you can pass a JSON string directly and the SDK will parse it automatically:
# Option 1: Parse JSON yourself (recommended)
datafile_content = JSON.parse(json_string, symbolize_names: true)
f = Featurevisor.create_featurevisor(datafile: datafile_content)
# Option 2: Pass JSON string directly (automatic parsing)
f = Featurevisor.create_featurevisor(datafile: json_string)
Evaluation types
We can evaluate 3 types of values against a particular feature:
- Flag (
boolean): whether the feature is enabled or not - Variation (
string): the variation of the feature (if any) - Variables: variable values of the feature (if any)
These evaluations are run against the provided context.
Context
Contexts are attribute values that we pass to SDK for evaluating features against.
Think of the conditions that you define in your segments, which are used in your feature's rules.
They are plain hashes:
context = {
userId: '123',
country: 'nl',
# ...other attributes
}
Context can be passed to SDK instance in various different ways, depending on your needs:
Setting initial context
You can set context at the time of initialization:
require 'featurevisor'
f = Featurevisor.create_featurevisor(
context: {
deviceId: '123',
country: 'nl'
}
)
This is useful for values that don't change too frequently and available at the time of application startup.
Setting after initialization
You can also set more context after the SDK has been initialized:
f.set_context({
userId: '234'
})
This will merge the new context with the existing one (if already set).
Replacing existing context
If you wish to fully replace the existing context, you can pass true in second argument:
f.set_context({
deviceId: '123',
userId: '234',
country: 'nl',
browser: 'chrome'
}, true) # replace existing context
Manually passing context
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:
context = {
userId: '123',
country: 'nl'
}
is_enabled = f.is_enabled('my_feature', context)
variation = f.get_variation('my_feature', context)
variable_value = f.get_variable('my_feature', 'my_variable', context)
When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value.
Further details for each evaluation types are described below.
Check if enabled
Once the SDK is initialized, you can check if a feature is enabled or not:
feature_key = 'my_feature'
is_enabled = f.is_enabled(feature_key)
if is_enabled
# do something
end
You can also pass additional context per evaluation:
is_enabled = f.is_enabled(feature_key, {
# ...additional context
})
Getting variation
If your feature has any variations defined, you can evaluate them as follows:
feature_key = 'my_feature'
variation = f.get_variation(feature_key)
if variation == 'treatment'
# do something for treatment variation
else
# handle default/control variation
end
Additional context per evaluation can also be passed:
variation = f.get_variation(feature_key, {
# ...additional context
})
Getting variables
Your features may also include variables, which can be evaluated as follows:
variable_key = 'bgColor'
bg_color_value = f.get_variable('my_feature', variable_key)
Additional context per evaluation can also be passed:
bg_color_value = f.get_variable('my_feature', variable_key, {
# ...additional context
})
Type specific methods
Next to generic get_variable() methods, there are also type specific methods available for convenience:
f.get_variable_boolean(feature_key, variable_key, context = {})
f.get_variable_string(feature_key, variable_key, context = {})
f.get_variable_integer(feature_key, variable_key, context = {})
f.get_variable_double(feature_key, variable_key, context = {})
f.get_variable_array(feature_key, variable_key, context = {})
f.get_variable_object(feature_key, variable_key, context = {})
f.get_variable_json(feature_key, variable_key, context = {})
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.
Getting all evaluations
You can get evaluations of all features available in the SDK instance:
all_evaluations = f.get_all_evaluations({})
puts all_evaluations
# {
# myFeature: {
# enabled: true,
# variation: "control",
# variables: {
# myVariableKey: "myVariableValue",
# },
# },
#
# anotherFeature: {
# enabled: true,
# variation: "treatment",
# }
# }
This is handy especially when you want to pass all evaluations from a backend application to the frontend.
Sticky
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:
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.
Initialize with sticky
require 'featurevisor'
f = Featurevisor.create_featurevisor(
sticky: {
myFeatureKey: {
enabled: true,
# optional
variation: 'treatment',
variables: {
myVariableKey: 'myVariableValue'
}
},
anotherFeatureKey: {
enabled: false
}
}
)
Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process.
Set sticky afterwards
You can also set sticky features after the SDK is initialized:
f.set_sticky({
myFeatureKey: {
enabled: true,
variation: 'treatment',
variables: {
myVariableKey: 'myVariableValue'
}
},
anotherFeatureKey: {
enabled: false
}
}, true) # replace existing sticky features (false by default)
Setting datafile
You may also initialize the SDK without passing datafile, and set it later on:
# Parse with symbolized keys before setting
datafile_content = JSON.parse(json_string, symbolize_names: true)
f.set_datafile(datafile_content)
# Or pass JSON string directly for automatic parsing
f.set_datafile(json_string)
Important: When calling set_datafile(), ensure JSON is parsed with symbolize_names: true if you're parsing it yourself.
Merging by default
By default, set_datafile(datafile) merges the incoming datafile into the SDK's current datafile:
- top-level metadata such as
schemaVersion,revision, andfeaturevisorVersioncomes from the incoming datafile segmentsare merged, with incoming entries overriding existing onesfeaturesare merged, with incoming entries overriding existing ones
This means you can call set_datafile more than once with different datafiles, and the SDK instance accumulates their features and segments together.
Replacing
To fully replace the stored datafile, pass true as the second argument:
f.set_datafile(datafile_content, true)
Loading datafiles on demand
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.
This pairs well with targets, where each target produces a smaller datafile for a specific part of your application:
require "open-uri"
f = Featurevisor.create_featurevisor({})
def load_datafile(f, target)
url = "https://cdn.yoursite.com/production/featurevisor-#{target}.json"
datafile = JSON.parse(URI.open(url).read, symbolize_names: true)
# merges into whatever was loaded before
f.set_datafile(datafile)
end
load_datafile(f, "products")
# later, when the user reaches checkout
load_datafile(f, "checkout")
Updating datafile
You can set the datafile as many times as you want in your application, which will result in emitting a datafile_set event that you can listen and react to accordingly.
The triggers for setting the datafile again can be:
- periodic updates based on an interval (like every 5 minutes), or
- reacting to:
- a specific event in your application (like a user action), or
- an event served via websocket or server-sent events (SSE)
Interval-based update
Here's an example of using interval-based update:
require 'net/http'
require 'json'
def update_datafile(f, datafile_url)
loop do
sleep(5 * 60) # 5 minutes
begin
response = Net::HTTP.get_response(URI(datafile_url))
datafile_content = JSON.parse(response.body)
f.set_datafile(datafile_content)
rescue => e
# handle error
puts "Failed to update datafile: #{e.}"
end
end
end
# Start the update thread
Thread.new { update_datafile(f, datafile_url) }
Diagnostics
By default, Featurevisor reports diagnostics to the console for info level and above with a [Featurevisor] prefix.
Levels
Available diagnostic levels are fatal, error, warn, info, and debug.
Set the level during initialization or update it afterwards:
f = Featurevisor.create_featurevisor(log_level: "debug")
f.set_log_level("info")
Handler
Use on_diagnostic to send structured diagnostics to your observability system:
f = Featurevisor.create_featurevisor(
log_level: "info",
on_diagnostic: ->(diagnostic) {
puts "#{diagnostic[:level]} #{diagnostic[:code]} #{diagnostic[:message]}"
}
)
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.
Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations.
Events
Featurevisor SDK implements a simple event emitter that allows you to listen to events that happen in the runtime.
You can listen to these events that can occur at various stages in your application:
datafile_set
unsubscribe = f.on('datafile_set') do |event|
revision = event[:revision] # new revision
previous_revision = event[:previousRevision]
revision_changed = event[:revisionChanged] # true if revision has changed
# list of feature keys that have new updates,
# and you should re-evaluate them
features = event[:features]
# handle here
end
# stop listening to the event
unsubscribe.call
The features array will contain keys of features that have either been:
- added, or
- updated, or
- removed
compared to the previous datafile content that existed in the SDK instance.
The event also includes replaced, which is true when the datafile replaced the previous content instead of merging into it.
context_set
unsubscribe = f.on('context_set') do |event|
replaced = event[:replaced] # true if context was replaced
context = event[:context] # the new context
puts 'Context set'
end
sticky_set
unsubscribe = f.on('sticky_set') do |event|
replaced = event[:replaced] # true if sticky features got replaced
features = event[:features] # list of all affected feature keys
puts 'Sticky features set'
end
error
unsubscribe = f.on('error') do |event|
diagnostic = event[:diagnostic]
code = diagnostic[:code]
= diagnostic[:message]
puts "Featurevisor error: #{code} #{}"
end
The error event is emitted for diagnostics reported with level: "error".
Evaluation details
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:
# flag
evaluation = f.evaluate_flag(feature_key, context = {})
# variation
evaluation = f.evaluate_variation(feature_key, context = {})
# variable
evaluation = f.evaluate_variable(feature_key, variable_key, context = {})
The returned object will always contain the following properties:
feature_key: the feature keyreason: the reason how the value was evaluated
And optionally these properties depending on whether you are evaluating a feature variation or a variable:
bucket_value: the bucket value between 0 and 100,000rule_key: the rule keyerror: the error objectenabled: if feature itself is enabled or notvariation: the variation objectvariation_value: the variation valuevariable_key: the variable keyvariable_value: the variable valuevariable_schema: the variable schema
Modules
Modules allow you to intercept the evaluation process and customize SDK behavior.
Defining a module
A module is a simple hash with a unique recommended name and optional lifecycle functions:
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.
require 'featurevisor'
my_custom_module = {
# recommended, and used for duplicate detection/removal
name: 'my-custom-module',
# rest of the properties below are all optional per module
# setup once, when the module is registered
setup: ->(api) {
revision = api[:get_revision].call
api[:on_diagnostic].call(->(diagnostic) {
puts diagnostic[:message]
})
},
# before evaluation
before: ->() {
# update context before evaluation
[:context] = [:context].merge({
someAdditionalAttribute: 'value'
})
},
# after evaluation
after: ->(evaluation, ) {
reason = evaluation[:reason]
if reason == 'error'
# log error
return
end
},
# configure bucket key
bucket_key: ->() {
# return custom bucket key
[:bucket_key]
},
# configure bucket value (between 0 and 100,000)
bucket_value: ->() {
# return custom bucket value
[:bucket_value]
},
# cleanup when module is removed or SDK is closed
close: -> {
# cleanup here
}
}
The module API passed to setup exposes:
get_revisionon_diagnosticreport_diagnostic
Registering modules
You can register modules at the time of SDK initialization:
require 'featurevisor'
f = Featurevisor.create_featurevisor(
modules: [my_custom_module]
)
Or after initialization:
remove_module = f.add_module(my_custom_module)
# remove later by calling the returned function
remove_module.call
# or remove by name
f.remove_module('my-custom-module')
Child instance
When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications.
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.
That's where child instances come in handy:
child_f = f.spawn({
# user or request specific context
userId: '123'
})
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:
is_enabled = child_f.is_enabled('my_feature')
variation = child_f.get_variation('my_feature')
variable_value = child_f.get_variable('my_feature', 'my_variable')
Similar to parent SDK, child instances also support several additional methods:
set_contextset_stickyis_enabledget_variationget_variableget_variable_booleanget_variable_stringget_variable_integerget_variable_doubleget_variable_arrayget_variable_objectget_variable_jsonget_all_evaluationsonclose
Close
Both primary and child instances support a .close() method, that removes forgotten event listeners (via on method) and cleans up any potential memory leaks.
f.close()
CLI usage
This package also provides a CLI tool for running your Featurevisor project's test specs and benchmarking against this Ruby SDK.
- Global installation: you can access it as
featurevisor - Local installation: you can access it as
bundle exec featurevisor - From this repository: you can access it as
bin/featurevisor
Test
Learn more about testing here.
$ bundle exec featurevisor test --projectDirectoryPath="/absolute/path/to/your/featurevisor/project"
Additional options that are available:
$ bundle exec featurevisor test \
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
--quiet|--verbose \
--onlyFailures \
--keyPattern="myFeatureKey" \
--assertionPattern="#1"
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.
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.
Test against local monorepo's example-1
$ cd /absolute/path/to/featurevisor-ruby
$ bundle exec ruby bin/featurevisor test --projectDirectoryPath=/Users/fahad/Projects/featurevisor/featurevisor/examples/example-1 --onlyFailures
$ make test-example-1
Benchmark
Learn more about benchmarking here.
$ bundle exec featurevisor benchmark \
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
--environment="production" \
--feature="myFeatureKey" \
--context='{"country": "nl"}' \
--n=1000
Assess distribution
Learn more about assessing distribution here.
$ bundle exec featurevisor assess-distribution \
--projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
--environment=production \
--feature=foo \
--variation \
--context='{"country": "nl"}' \
--populateUuid=userId \
--populateUuid=deviceId \
--n=1000
Development
Setting up
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.
Running tests
$ bundle exec rspec
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.
Releasing
- Update version in
lib/featurevisor/version.rb - Run
bundle install - Push commit to
mainbranch - Wait for CI to complete
- Tag the release with the version number
- This will trigger a new release to RubyGems
License
MIT © Fahad Heylaal