Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Add `featurevisor` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:featurevisor, "~> 0.1"}
{:featurevisor, "~> 1.0"}
]
end
```
Expand Down Expand Up @@ -130,11 +130,12 @@ The supervisor owns shutdown and restart. Module close callbacks run during supe

## Evaluation types

Featurevisor evaluates three kinds of values:
Featurevisor evaluates four 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
- a feature variable returns remote configuration owned by a feature
- a global variable returns remote configuration independently of a feature

Every evaluation uses the active datafile and the effective context.

Expand Down Expand Up @@ -221,6 +222,13 @@ title = Featurevisor.get_variable(f, "checkout", "title", context)

JSON variables are decoded before they are returned.

Global variables use explicit function names so feature ownership remains clear:

```elixir
email = Featurevisor.get_global_variable(f, "supportEmail", context)
evaluation = Featurevisor.evaluate_global_variable(f, "supportEmail", context)
```

### Type specific getters

Use a typed getter when your application wants runtime type validation:
Expand All @@ -240,13 +248,15 @@ Typed getters return `nil` for a mismatched value and do not coerce strings, boo
## Getting all evaluations

```elixir
evaluations = Featurevisor.get_all_evaluations(f, context)
features = Featurevisor.get_feature_evaluations(f, context)
variables = Featurevisor.get_global_variable_evaluations(f, context)
```

Pass feature keys to evaluate a selected set:

```elixir
evaluations = Featurevisor.get_all_evaluations(f, context, ["checkout", "pricing"])
features = Featurevisor.get_feature_evaluations(f, context, ["checkout", "pricing"])
variables = Featurevisor.get_global_variable_evaluations(f, context, ["supportEmail"])
```

## Sticky
Expand All @@ -258,21 +268,23 @@ Sticky values keep selected evaluations stable for the lifetime of an instance o
```elixir
f = Featurevisor.create_featurevisor(%{
datafile: datafile,
sticky: %{
sticky_features: %{
"checkout" => %{
"enabled" => true,
"variation" => "treatment",
"variables" => %{"title" => "Welcome back"}
}
}
},
sticky_variables: %{"supportEmail" => "sticky@example.com"}
})
```

### Updating sticky values

```elixir
Featurevisor.set_sticky(f, sticky)
Featurevisor.set_sticky(f, replacement, true)
Featurevisor.set_sticky_features(f, sticky_features)
Featurevisor.set_sticky_variables(f, sticky_variables)
Featurevisor.set_sticky_features(f, replacement, true)
```

Sticky values are instance state. They are not accepted as public per evaluation options.
Expand All @@ -283,7 +295,7 @@ Sticky values are instance state. They are not accepted as public per evaluation

### Merging by default

Incoming features and segments are merged into the stored datafile. Incoming entries replace entries with the same key.
Incoming features, global variables, and segments are merged into the stored datafile. Incoming entries replace entries with the same key.

```elixir
Featurevisor.set_datafile(f, next_datafile)
Expand Down Expand Up @@ -345,9 +357,12 @@ Supported events are:

- `:datafile_set`
- `:context_set`
- `:sticky_set`
- `:sticky_features_set`
- `:sticky_variables_set`
- `:error`

The `:datafile_set` details include changed `features` and `variables`, including dependants affected by changed required features or segments.

## Evaluation details

Use detailed methods when you need reasons, rule keys, bucket values, or matched definitions:
Expand All @@ -356,6 +371,7 @@ Use detailed methods when you need reasons, rule keys, bucket values, or matched
flag = Featurevisor.evaluate_flag(f, "checkout", context)
variation = Featurevisor.evaluate_variation(f, "checkout", context)
variable = Featurevisor.evaluate_variable(f, "checkout", "title", context)
global_variable = Featurevisor.evaluate_global_variable(f, "supportEmail", context)
```

Each method returns a `Featurevisor.Evaluation` struct.
Expand All @@ -370,13 +386,13 @@ module = %Featurevisor.Module{
setup: fn api ->
IO.puts("Revision: #{api.get_revision.()}")
end,
before: fn options ->
before_evaluation: fn options ->
%{options | context: Map.put_new(options.context, "service", "checkout")}
end,
bucket_value: fn options ->
options.bucket_value
end,
after: fn evaluation, _options ->
after_evaluation: fn evaluation, _options ->
evaluation
end,
close: fn ->
Expand All @@ -388,7 +404,7 @@ 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`.
Module callbacks are `setup`, `before`, `before_evaluation`, `bucket_key`, `bucket_value`, `after_evaluation`, `after`, and `close`. For feature evaluations, all `before` callbacks run in registration order, followed by all `before_evaluation` callbacks. After evaluation and caller defaults, all `after_evaluation` callbacks run, followed by all `after` callbacks. Global variable evaluations use only `before_evaluation` and `after_evaluation`. Required feature checks run through the complete module pipeline, and transformed defaults are preserved. 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.

Expand All @@ -400,14 +416,16 @@ A child has isolated context, sticky state, and local listeners while sharing it
child = Featurevisor.spawn(
f,
%{"accountId" => "account-123"},
%{sticky: sticky}
%{sticky_features: sticky_features, sticky_variables: sticky_variables}
)

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.get_global_variable(child, "supportEmail")
Featurevisor.Child.get_feature_evaluations(child)
Featurevisor.Child.get_global_variable_evaluations(child)

Featurevisor.Child.close(child)
```
Expand Down Expand Up @@ -467,6 +485,8 @@ Use one or more Targets when required:

Benchmark output reports total duration and the minimum, average, and maximum duration of individual evaluations.

Pass `--variable=supportEmail` without `--feature` to benchmark a global variable.

### Assess distribution

```sh
Expand Down
Loading
Loading