SDKs
Elixir SDK
Featurevisor's Elixir SDK is designed for concurrent evaluation in Elixir and Erlang applications.
Installation#
Add featurevisor to your dependencies in mix.exs:
def deps do [ {:featurevisor, "~> 0.1"} ]endThen install dependencies:
mix deps.getPublic 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 experienceendThe 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()endProvide 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.buildThe 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 \ --onlyFailuresUse one or more Targets when required:
./featurevisor test \ --projectDirectoryPath=../featurevisor/examples/example-1 \ --target=all \ --target=checkoutBenchmark#
./featurevisor benchmark \ --projectDirectoryPath=../featurevisor/examples/example-1 \ --environment=production \ --feature=allowSignup \ --variation \ --context='{"country":"nl"}' \ --n=1000000Benchmark 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=10000Repeat --target and --populateUuid where needed.
GitHub repositories#
- See SDK repository here: featurevisor/featurevisor-elixir
- See example application repository here: featurevisor/featurevisor-example-elixir

