Featurevisor SDK for Elixir
This is the official Featurevisor SDK for Elixir. It evaluates feature flags, variations, and variables in Elixir applications using Featurevisor schema version 2 datafiles.
The SDK supports concurrent evaluations, structured diagnostics, lifecycle modules, child instances, and the Featurevisor project test runner.
Table of contents
- Installation
- Public API
- Initialization
- Supervision
- Evaluation types
- Context
- Check if enabled
- Getting variation
- Getting variables
- Getting all evaluations
- Sticky
- Setting datafile
- Diagnostics
- Events
- Evaluation details
- Modules
- Child instance
- Close
- CLI usage
- Development
- Publishing
- License
Installation
Add featurevisor to your dependencies in mix.exs:
def deps do
[
{:featurevisor, "~> 0.1"}
]
end
Then install dependencies:
mix deps.get
Public API
Create instances with Featurevisor.create_featurevisor/1. The returned Featurevisor handle is the main SDK instance.
Most applications use:
Featurevisor.create_featurevisor/1Featurevisor.start_link/1andFeaturevisor.instance/1for supervisionFeaturevisor.enabled?/2Featurevisor.get_variation/2Featurevisor.get_variable/3Featurevisor.close/1Featurevisor.Modulefor extensionsFeaturevisor.Diagnosticfor observabilityFeaturevisor.Evaluationfor detailed results
The datafile and context use ordinary Elixir maps with string keys. This preserves the JSON wire format and avoids a second public reader API.
Initialization
Initialize with a decoded datafile:
datafile =
"datafile.json"
|> File.read!()
|> Jason.decode!()
f = Featurevisor.create_featurevisor(%{datafile: datafile})
Create one long lived instance for your application and share it between processes. Evaluation reads immutable ETS snapshots and does not queue through the instance process. The convenience constructor is not linked to the calling process, so its owner must call Featurevisor.close/1 during application shutdown.
You may pass the JSON string directly:
f = Featurevisor.create_featurevisor(%{datafile: File.read!("datafile.json")})
Invalid datafiles do not replace the active datafile. They report an invalid_datafile diagnostic with the stable message Could not parse datafile.
Supervision
Use a supervised instance in long running OTP applications:
children = [
{Featurevisor,
name: MyApp.Featurevisor,
datafile: datafile,
log_level: :warn}
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
Resolve its Featurevisor handle wherever it is needed:
f = Featurevisor.instance(MyApp.Featurevisor)
Featurevisor.enabled?(f, "my_feature", %{"userId" => "123"})
The supervisor owns shutdown and restart. Module close callbacks run during supervised shutdown. Resolve the handle again after a restart because the new owner process creates new ETS tables.
Evaluation types
Featurevisor evaluates three kinds of values:
- a flag answers whether a feature is enabled
- a variation returns a variation value
- a variable returns remote configuration for a feature
Every evaluation uses the active datafile and the effective context.
Context
Contexts are maps of attributes used by conditions and bucketing. Use string keys so nested paths and datafile attribute names match exactly.
Date conditions accept ISO 8601 strings with an explicit time zone and native DateTime values.
Setting initial context
f = Featurevisor.create_featurevisor(%{
datafile: datafile,
context: %{
"userId" => "123",
"country" => "nl"
}
})
Setting after initialization
Context is merged by default:
Featurevisor.set_context(f, %{"device" => "mobile"})
Pass true to replace all stored context:
Featurevisor.set_context(f, %{"userId" => "456"}, true)
Passing context for one evaluation
context = %{"country" => "de"}
Featurevisor.enabled?(f, "my_feature", context)
Featurevisor.get_variation(f, "my_feature", context)
Featurevisor.get_variable(f, "my_feature", "title", context)
Evaluation context wins over stored context for matching keys.
Check if enabled
if Featurevisor.enabled?(f, "my_feature", %{"userId" => "123"}) do
# show the enabled experience
end
The idiomatic Elixir enabled?/4 function corresponds to isEnabled in the JavaScript SDK and similarly named methods in other Featurevisor SDKs.
Getting variation
case Featurevisor.get_variation(f, "checkout_experiment", context) do
"control" -> show_control()
"treatment" -> show_treatment()
nil -> show_fallback()
end
Provide an explicit fallback when no variation is selected:
Featurevisor.get_variation(
f,
"checkout_experiment",
context,
%{default_variation_value: "control"}
)
Getting variables
title = Featurevisor.get_variable(f, "checkout", "title", context)
JSON variables are decoded before they are returned.
Type specific getters
Use a typed getter when your application wants runtime type validation:
enabled = Featurevisor.get_variable_boolean(f, "checkout", "enabled", context)
title = Featurevisor.get_variable_string(f, "checkout", "title", context)
count = Featurevisor.get_variable_integer(f, "checkout", "count", context)
ratio = Featurevisor.get_variable_double(f, "checkout", "ratio", context)
items = Featurevisor.get_variable_array(f, "checkout", "items", context)
config = Featurevisor.get_variable_object(f, "checkout", "config", context)
json = Featurevisor.get_variable_json(f, "checkout", "json", context)
Typed getters return nil for a mismatched value and do not coerce strings, booleans, or collections.
Getting all evaluations
evaluations = Featurevisor.get_all_evaluations(f, context)
Pass feature keys to evaluate a selected set:
evaluations = Featurevisor.get_all_evaluations(f, context, ["checkout", "pricing"])
Sticky
Sticky values keep selected evaluations stable for the lifetime of an instance or child instance.
Setting initial sticky values
f = Featurevisor.create_featurevisor(%{
datafile: datafile,
sticky: %{
"checkout" => %{
"enabled" => true,
"variation" => "treatment",
"variables" => %{"title" => "Welcome back"}
}
}
})
Updating sticky values
Featurevisor.set_sticky(f, sticky)
Featurevisor.set_sticky(f, replacement, true)
Sticky values are instance state. They are not accepted as public per evaluation options.
Setting datafile
set_datafile/3 accepts a decoded map or JSON string.
Merging by default
Incoming features and segments are merged into the stored datafile. Incoming entries replace entries with the same key.
Featurevisor.set_datafile(f, next_datafile)
Replacing
Pass true to replace the complete datafile:
Featurevisor.set_datafile(f, next_datafile, true)
Loading datafiles
Use the HTTP client and scheduling tools already present in your application. Pass each successful response body to set_datafile/3. The SDK does not start a background fetch process or choose an HTTP client for you.
Diagnostics
Diagnostics are the only SDK observability API. There is no separate logger handler.
Levels
The levels are :fatal, :error, :warn, :info, and :debug.
Featurevisor.set_log_level(f, :warn)
Handler
f = Featurevisor.create_featurevisor(%{
datafile: datafile,
log_level: :warn,
on_diagnostic: fn diagnostic ->
Logger.warning("#{diagnostic.code}: #{diagnostic.message}")
end
})
Every Featurevisor.Diagnostic contains level, code, message, and an always present details map. Error diagnostics also emit the :error event.
Featurevisor project linting enforces the portable regular expression subset shared by all SDKs. The Elixir runtime treats the g flag as a compatibility no op. Invalid patterns or flags produce a condition_match_error diagnostic and do not match.
Events
Register event callbacks with Featurevisor.on/3. The returned unsubscribe function is idempotent.
unsubscribe = Featurevisor.on(f, :datafile_set, fn details ->
IO.inspect(details, label: "datafile changed")
end)
unsubscribe.()
Supported events are:
:datafile_set:context_set:sticky_set:error
Evaluation details
Use detailed methods when you need reasons, rule keys, bucket values, or matched definitions:
flag = Featurevisor.evaluate_flag(f, "checkout", context)
variation = Featurevisor.evaluate_variation(f, "checkout", context)
variable = Featurevisor.evaluate_variable(f, "checkout", "title", context)
Each method returns a Featurevisor.Evaluation struct.
Modules
Modules extend evaluation and lifecycle behaviour without changing evaluation methods.
module = %Featurevisor.Module{
name: "audit",
setup: fn api ->
IO.puts("Revision: #{api.get_revision.()}")
end,
before: fn options ->
%{options | context: Map.put_new(options.context, "service", "checkout")}
end,
bucket_value: fn options ->
options.bucket_value
end,
after: fn evaluation, _options ->
evaluation
end,
close: fn ->
:ok
end
}
remove = Featurevisor.add_module(f, module)
remove.()
Module callbacks are setup, before, bucket_key, bucket_value, after, and close. Callback option maps use idiomatic snake case keys such as bucket_key and bucket_value.
Named duplicates are rejected with a duplicate_module diagnostic. A failed setup is removed, its diagnostic subscriptions are cleared, and its close callback is invoked.
Child instance
A child has isolated context, sticky state, and local listeners while sharing its parent's datafile, modules, and diagnostics.
child = Featurevisor.spawn(
f,
%{"accountId" => "account-123"},
%{sticky: sticky}
)
Featurevisor.Child.enabled?(child, "checkout", %{"userId" => "user-456"})
Featurevisor.Child.get_variation(child, "checkout")
Featurevisor.Child.get_variable(child, "checkout", "title")
Featurevisor.Child.get_variable_string(child, "checkout", "title")
Featurevisor.Child.get_all_evaluations(child)
Featurevisor.Child.close(child)
Existing parent context keys are snapshotted when the child is created. Parent keys added later are inherited. Child context wins over parent context, and per evaluation context wins over child context.
Close
Close an instance when its owner stops:
Featurevisor.close(f)
Close invokes module close callbacks and removes listeners, diagnostic subscriptions, and caches. Calling close more than once is safe.
Do not call close/1 directly for a supervised instance. Stop its supervisor or remove its child instead. A closed handle evaluates against an empty datafile, so flags return false, variations and variables return nil, and the revision returns "unknown".
CLI usage
Build the escript from this repository:
mix escript.build
The Elixir runner delegates project parsing, Target discovery, matrix expansion, and datafile generation to npx featurevisor. Evaluations and assertions run through this Elixir SDK.
Test
./featurevisor test \
--projectDirectoryPath=../featurevisor/examples/example-1 \
--onlyFailures
Use one or more Targets when required:
./featurevisor test \
--projectDirectoryPath=../featurevisor/examples/example-1 \
--target=all \
--target=checkout
Benchmark
./featurevisor benchmark \
--projectDirectoryPath=../featurevisor/examples/example-1 \
--environment=production \
--feature=allowSignup \
--variation \
--context='{"country":"nl"}' \
--n=1000000
Benchmark output reports total duration and the minimum, average, and maximum duration of individual evaluations.
Assess distribution
./featurevisor assess-distribution \
--projectDirectoryPath=../featurevisor/examples/example-1 \
--environment=production \
--feature=allowSignup \
--context='{"country":"nl"}' \
--populateUuid=userId \
--n=10000
Repeat --target and --populateUuid where needed.
Development
Install dependencies and run the complete local gate:
mix deps.get
make check
Run the SDK against the sibling Featurevisor example project:
make test-example-1
The integration target executes all expanded example-1 assertions, including Target datafiles, through the Elixir evaluator.
make check remains the fast package gate. The separate make test-example-1 target requires the sibling Featurevisor monorepo, so checks and publishing workflows run that integration explicitly after the package gate.
Publishing
Before tagging a release:
make check
mix hex.publish --dry-run
The release workflow verifies the tag, package contents, documentation, and example-1 integration before publishing to Hex.pm.
The published Hex package intentionally includes runtime source, mix.exs, the README, and the licence. Tests and the conformance fixture remain repository verification assets and are not shipped to consumers.
License
MIT. See LICENSE.