Elixir
This SDK follows the guidelines set out in GrowthBook's documentation, and the API is tested on conformance with the test cases from the JS SDK.
To ensure an Elixir-friendly API, the implementation deviates from the official SDK in the following ways:
- Instead of tuple-lists, this library uses actual tuples
- Comparisons with
undefinedare implemented by using:undefined - Function names are converted to
snake_case, andis_prefix is replaced with a?suffix - Instead of classes, a Context struct is used (similar to
%Plug.Conn{}inplug)
Installation
Add growthbook to your list of dependencies in mix.exs:
def deps do
[
{:growthbook, "~> 0.3"}
]
end
Quick Start
Get started with GrowthBook in just a few steps:
# 1. Initialize GrowthBook with your API key
GrowthBook.init(
client_key: "sdk-abc123",
api_host: "https://cdn.growthbook.io"
)
# 2. Create a context with user attributes
context = GrowthBook.build_context(%{
"id" => "user-123",
"country" => "US"
})
# 3. Evaluate features
if GrowthBook.feature(context, "new-dashboard").on? do
render_new_dashboard()
else
render_old_dashboard()
end
# Get feature values
button_color = GrowthBook.feature(context, "button-color").value
max_retries = GrowthBook.feature(context, "max-retries").value || 3
That's it! GrowthBook will automatically fetch and refresh your features in the background.
Automatic Features Refresh
The Elixir SDK provides a GenServer-based feature repository that automatically fetches and caches features from the GrowthBook API.
Initialization Options
GrowthBook.init(
# Required
client_key: "sdk-abc123",
# Optional configuration
api_host: "https://cdn.growthbook.io", # Default: "https://cdn.growthbook.io"
decryption_key: "key_abc123", # For encrypted features
swr_ttl_seconds: 60, # Cache TTL in seconds (default: 60)
refresh_strategy: :periodic, # :periodic (default) or :manual
# Optional callback
on_refresh: fn features ->
# Called whenever features are successfully refreshed
Logger.info("Features updated: #{map_size(features)}")
end
)
Refresh Strategies
The SDK supports two refresh strategies:
Periodic Refresh (Default)
Features are automatically refreshed in the background based on the TTL:
GrowthBook.init(
client_key: "sdk-abc123",
refresh_strategy: :periodic, # Auto-refresh enabled
swr_ttl_seconds: 60 # Refresh every 60 seconds
)
Manual Refresh
Features are only refreshed when explicitly requested:
GrowthBook.init(
client_key: "sdk-abc123",
refresh_strategy: :manual # No automatic refresh
)
# Later, manually trigger a refresh
GrowthBook.FeatureRepository.refresh()
Supervision Tree Integration
For production applications, add the FeatureRepository to your application's supervision tree:
# In your application.ex
defmodule MyApp.Application do
use Application
require Logger
def start(_type, _args) do
children = [
# Your other supervised processes
MyApp.Repo,
MyAppWeb.Endpoint,
# Add GrowthBook FeatureRepository
{GrowthBook.FeatureRepository,
client_key: System.get_env("GROWTHBOOK_CLIENT_KEY"),
api_host: "https://cdn.growthbook.io",
decryption_key: System.get_env("GROWTHBOOK_DECRYPTION_KEY"),
swr_ttl_seconds: 60,
refresh_strategy: :periodic,
on_refresh: fn features ->
Logger.info("GrowthBook features refreshed: #{map_size(features)}")
end
}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
case Supervisor.start_link(children, opts) do
{:ok, pid} ->
# Wait for features to load
case GrowthBook.FeatureRepository.await_initialization(
GrowthBook.FeatureRepository,
5000
) do
:ok ->
Logger.info("GrowthBook features initialized successfully")
{:error, reason} ->
Logger.error("Failed to initialize GrowthBook features: #{inspect(reason)}")
end
{:ok, pid}
error ->
error
end
end
end
Graceful Shutdown
When your application shuts down, stop the FeatureRepository GenServer:
# Stops the FeatureRepository and releases resources
GenServer.stop(GrowthBook.FeatureRepository)
Encryption & Security
The Elixir SDK supports encrypted feature payloads and secure attribute hashing to protect sensitive data.
Encrypted Features
Encrypted features ensure that sensitive feature configurations are never exposed in plain text:
# Enable encryption by providing a decryption key
GrowthBook.init(
client_key: "sdk-abc123",
api_host: "https://cdn.growthbook.io",
decryption_key: System.get_env("GROWTHBOOK_DECRYPTION_KEY")
)
Setup Steps:
- Enable "Encrypt SDK Payload" in your SDK Connection settings
- Copy the encryption key from your SDK Connection
- Store it securely in environment variables
- Pass it to
GrowthBook.init/1
Secure Attributes
When secure attribute hashing is enabled, you can safely target users based on sensitive attributes like email or phone numbers without exposing the actual values.
Setup
Enable secure attribute hashing in your SDK Connection, then hash sensitive attributes before passing them to GrowthBook:
defmodule MyApp.GrowthBook.SecureAttributes do
@salt System.get_env("GROWTHBOOK_SECURE_ATTRIBUTE_SALT")
def hash_attribute(value) when is_binary(value) do
:crypto.hash(:sha256, value <> @salt)
|> Base.encode16(case: :lower)
end
def build_secure_attributes(user) do
%{
"id" => user.id,
# Hash sensitive attributes
"email" => hash_attribute(user.email),
"phone" => hash_attribute(user.phone),
# Non-sensitive attributes remain plain
"country" => user.country,
"plan" => user.plan
}
end
end
Usage
# Build secure attributes for a user
secure_attrs = MyApp.GrowthBook.SecureAttributes.build_secure_attributes(current_user)
# Create context with hashed attributes
context = GrowthBook.build_context(secure_attrs)
# Features will use hashed attributes for targeting
if GrowthBook.feature(context, "premium-feature").on? do
show_premium_feature()
end
Security Best Practices
# Store keys in environment variables
config :my_app, :growthbook,
client_key: System.get_env("GROWTHBOOK_CLIENT_KEY"),
decryption_key: System.get_env("GROWTHBOOK_DECRYPTION_KEY"),
secure_attribute_salt: System.get_env("GROWTHBOOK_SECURE_ATTRIBUTE_SALT")
# Initialize with environment variables
GrowthBook.init(
client_key: Application.get_env(:my_app, :growthbook)[:client_key],
decryption_key: Application.get_env(:my_app, :growthbook)[:decryption_key]
)
Recommendations:
- Never commit encryption keys or salts to version control
- Use different keys for each environment (dev, staging, production)
- Rotate keys regularly and coordinate updates across all systems
- Monitor decryption failures which may indicate key rotation issues
- Use secrets management tools like Vault, AWS Secrets Manager, or similar
Using Features
Once GrowthBook is initialized, you can evaluate features and run experiments.
Feature Evaluation
# Create context with user attributes
context = GrowthBook.build_context(%{
"id" => "user-123",
"email" => "user@example.com",
"country" => "US",
"plan" => "premium"
})
# Boolean feature flag
if GrowthBook.feature(context, "new-dashboard").on? do
render_new_dashboard()
else
render_old_dashboard()
end
# Feature with value
max_retries = GrowthBook.feature(context, "max-retries").value || 3
# Complex feature values
config = GrowthBook.feature(context, "checkout-config").value
timeout = config["timeout"] || 5000
enabled_methods = config["payment_methods"] || []
Feature Result Properties
The GrowthBook.feature/2 function returns a result with these properties:
result = GrowthBook.feature(context, "my-feature")
# Check if feature is enabled
result.on? # true or false
# Get the feature value
result.value # Can be any JSON type
# Check the source of the value
result.source # :default_value, :force, :experiment, or :unknown_feature
# Access experiment information (if feature is from an experiment)
if result.source == :experiment do
experiment = result.experiment
experiment_result = result.experiment_result
Logger.info("User in experiment: #{experiment.key}")
end
Running Inline Experiments
You can run experiments directly without defining them as features:
# Define an experiment
experiment = %GrowthBook.Experiment{
key: "button-color-test",
active?: true,
coverage: 1.0,
variations: ["red", "blue", "green"],
weights: [0.5, 0.3, 0.2]
}
# Run the experiment
result = GrowthBook.run(context, experiment)
# Check if user is in the experiment
if result.in_experiment? do
color = result.value
Logger.info("Assigned color: #{color}")
set_button_color(color)
else
Logger.info("User not in experiment, using default")
set_button_color("blue")
end
Experiment Configuration
The Experiment struct supports these properties:
%GrowthBook.Experiment{
# Required
key: "my-experiment",
variations: ["control", "treatment"],
# Optional
active?: true, # Whether experiment is active
coverage: 1.0, # Percentage of users to include (0.0 to 1.0)
weights: [0.5, 0.5], # Traffic distribution
# Targeting
condition: %{"country" => "US"}, # Targeting conditions
hash_attribute: "id", # Attribute to use for hashing (default: "id")
# Namespaces for experiment isolation
namespace: {"pricing", 0, 0.5} # {name, start, end}
}
Experiment Result
The experiment result contains detailed information:
result = GrowthBook.run(context, experiment)
result.in_experiment? # Boolean: whether user is in experiment
result.variation_id # Integer: index of assigned variation
result.value # The actual variation value
result.hash_attribute # Attribute used for hashing
result.hash_value # Value of the hash attribute
Tracking Experiments
The SDK doesn't include built-in tracking callbacks, but you can implement tracking by checking experiment results:
# Run experiment
result = GrowthBook.run(context, experiment)
# Track if user is in experiment
if result.in_experiment? do
MyApp.Analytics.track_event("experiment_viewed", %{
experiment_id: experiment.key,
variation_id: result.variation_id,
user_id: context.attributes["id"]
})
end
# Use the variation
case result.value do
"control" -> render_control_version()
"treatment" -> render_treatment_version()
_ -> render_default_version()
end
User Attributes
Attributes are used for targeting and experiment assignment:
# Standard attributes
context = GrowthBook.build_context(%{
"id" => "user-123",
"email" => "user@example.com",
"country" => "US",
"browser" => "chrome"
})
# Custom business attributes
context = GrowthBook.build_context(%{
"id" => "user-123",
"subscription_tier" => "premium",
"lifetime_value" => 1500.00,
"account_age_days" => 365,
"is_high_value_customer" => true,
"purchased_categories" => ["electronics", "books"],
"feature_flags" => ["beta_access", "early_adopter"]
})
Best Practices:
- Use consistent naming conventions (snake_case or camelCase)
- Keep attribute values simple and serializable
- Document custom attributes for your team
- Consider attribute cardinality for targeting efficiency
Context Struct
The GrowthBook.Context struct is the core data structure:
%GrowthBook.Context{
enabled?: true, # Whether GrowthBook is enabled
features: %{}, # Map of feature definitions
attributes: %{}, # User attributes
forced_variations: %{}, # Forced experiment variations for QA
qa_mode?: false, # Disable randomization for testing
url: nil # Optional URL for URL-based targeting
}
You can create contexts manually or use the helper function:
# Using helper (recommended)
context = GrowthBook.build_context(%{"id" => "user-123"})
# Manual creation (for advanced use cases)
context = %GrowthBook.Context{
enabled?: true,
features: my_features,
attributes: %{"id" => "user-123"},
forced_variations: %{"experiment-1" => 1}, # Force variation for testing
qa_mode?: false
}
Error Handling
The SDK is designed to fail gracefully:
# If initialization fails, you can still use manual features
case GrowthBook.init(client_key: "invalid-key") do
{:ok, :initialized} ->
Logger.info("GrowthBook initialized")
{:error, reason} ->
Logger.warn("GrowthBook init failed: #{inspect(reason)}, using manual features")
# Fall back to manual feature configuration
fallback_features = load_fallback_features()
end
# Feature evaluation always returns a result
result = GrowthBook.feature(context, "nonexistent-feature")
# result.source will be :unknown_feature
# result.value will be nil
# result.on? will be false
Troubleshooting & Logging
Common Issues
Features Not Loading
If features aren't loading, check the following:
# 1. Verify initialization succeeded
case GrowthBook.init(client_key: "sdk-abc123") do
{:ok, :initialized} ->
Logger.info("GrowthBook initialized successfully")
{:error, reason} ->
Logger.error("Failed to initialize: #{inspect(reason)}")
end
# 2. Check if features are available
case GrowthBook.FeatureRepository.await_initialization(
GrowthBook.FeatureRepository,
5000
) do
:ok ->
Logger.info("Features loaded")
{:error, :timeout} ->
Logger.error("Features not loaded within timeout")
end
# 3. Manually check feature repository state
state = :sys.get_state(GrowthBook.FeatureRepository)
Logger.info("Feature count: #{map_size(state.features)}")
Decryption Failures
If encrypted features fail to decrypt:
# Ensure decryption key is set correctly
decryption_key = System.get_env("GROWTHBOOK_DECRYPTION_KEY")
if is_nil(decryption_key) or decryption_key == "" do
Logger.error("GROWTHBOOK_DECRYPTION_KEY not set")
end
# Test decryption manually
GrowthBook.init(
client_key: "sdk-abc123",
decryption_key: decryption_key,
on_refresh: fn features ->
Logger.info("Successfully decrypted #{map_size(features)} features")
end
)
Network Issues
For network connectivity problems:
# Test API connectivity
api_host = "https://cdn.growthbook.io"
client_key = System.get_env("GROWTHBOOK_CLIENT_KEY")
case HTTPoison.get("#{api_host}/api/features/#{client_key}") do
{:ok, %{status_code: 200}} ->
Logger.info("API is reachable")
{:ok, %{status_code: code}} ->
Logger.error("API returned status code: #{code}")
{:error, %{reason: reason}} ->
Logger.error("Network error: #{inspect(reason)}")
end
Logging Configuration
Enable detailed logging to debug issues:
# config/config.exs
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id, :module, :function]
# Set log level for GrowthBook
config :logger,
level: :debug
# In your application
require Logger
GrowthBook.init(
client_key: "sdk-abc123",
on_refresh: fn features ->
Logger.info("GrowthBook: Features refreshed",
feature_count: map_size(features),
timestamp: DateTime.utc_now()
)
end
)
Debug Helper Module
Create a helper module for debugging GrowthBook:
defmodule MyApp.GrowthBookDebug do
require Logger
def inspect_context(context) do
Logger.debug("""
GrowthBook Context:
Enabled: #{context.enabled?}
Features: #{map_size(context.features)}
Attributes: #{inspect(context.attributes)}
QA Mode: #{context.qa_mode?}
""")
end
def inspect_feature_result(key, result) do
Logger.debug("""
Feature: #{key}
Value: #{inspect(result.value)}
On: #{result.on?}
Source: #{result.source}
In Experiment: #{result.source == :experiment}
""")
end
def list_all_features do
state = :sys.get_state(GrowthBook.FeatureRepository)
state.features
|> Map.keys()
|> Enum.each(fn key ->
Logger.info("Feature: #{key}")
end)
end
end
# Usage
context = GrowthBook.build_context(%{"id" => "user-123"})
MyApp.GrowthBookDebug.inspect_context(context)
result = GrowthBook.feature(context, "my-feature")
MyApp.GrowthBookDebug.inspect_feature_result("my-feature", result)
Health Checks
Implement health checks for monitoring:
defmodule MyApp.HealthCheck do
def growthbook_status do
try do
case Process.whereis(GrowthBook.FeatureRepository) do
nil ->
{:error, "FeatureRepository not running"}
pid when is_pid(pid) ->
state = :sys.get_state(pid)
feature_count = map_size(state.features)
cond do
feature_count == 0 ->
{:warning, "No features loaded"}
true ->
{:ok, "#{feature_count} features loaded"}
end
end
rescue
e -> {:error, "Health check failed: #{inspect(e)}"}
end
end
end
# Use in a Phoenix health endpoint
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def show(conn, _params) do
growthbook_status = MyApp.HealthCheck.growthbook_status()
status = case growthbook_status do
{:ok, _} -> :ok
{:warning, _} -> :degraded
{:error, _} -> :error
end
json(conn, %{
status: status,
growthbook: growthbook_status
})
end
end
Integrations
The Elixir SDK integrates seamlessly with popular Elixir frameworks and libraries.
Phoenix Web Application
Integrate GrowthBook with Phoenix for feature flags in your web application:
# lib/my_app_web/plugs/growthbook_plug.ex
defmodule MyAppWeb.GrowthBookPlug do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
# Build user attributes from session/assigns
user = conn.assigns[:current_user]
attributes = %{
"id" => user_id(user),
"email" => user && user.email,
"country" => get_country_from_ip(conn.remote_ip),
"user_agent" => get_req_header(conn, "user-agent") |> List.first(),
"url" => conn.request_path,
"plan" => user && user.subscription_plan
}
# Create GrowthBook context
context = GrowthBook.build_context(attributes)
# Store context in conn.assigns for use in controllers/views
assign(conn, :growthbook, context)
end
defp user_id(nil), do: nil
defp user_id(user), do: to_string(user.id)
defp get_country_from_ip(_ip) do
# Implement IP geolocation
"US"
end
end
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {MyAppWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_user
plug MyAppWeb.GrowthBookPlug # Add GrowthBook plug
end
# Your routes...
end
# lib/my_app_web/controllers/dashboard_controller.ex
defmodule MyAppWeb.DashboardController do
use MyAppWeb, :controller
def index(conn, _params) do
# Access GrowthBook context from conn.assigns
gb = conn.assigns.growthbook
# Use feature flags to control UI
show_new_dashboard = GrowthBook.feature(gb, "new-dashboard").on?
max_items = GrowthBook.feature(gb, "dashboard-max-items").value || 10
# Track experiment if user is in one
color_result = GrowthBook.feature(gb, "dashboard-theme-color")
if color_result.source == :experiment do
track_experiment(conn, color_result.experiment, color_result.experiment_result)
end
render(conn, "index.html",
new_dashboard: show_new_dashboard,
max_items: max_items,
theme_color: color_result.value
)
end
defp track_experiment(conn, experiment, result) do
# Track to your analytics service
MyApp.Analytics.track(conn.assigns.current_user, "experiment_viewed", %{
experiment_id: experiment.key,
variation_id: result.variation_id
})
end
end
Phoenix LiveView
Use GrowthBook with Phoenix LiveView for real-time feature flag updates:
# lib/my_app_web/live/dashboard_live.ex
defmodule MyAppWeb.DashboardLive do
use MyAppWeb, :live_view
require Logger
@impl true
def mount(_params, session, socket) do
# Subscribe to feature updates if desired
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "growthbook:features")
end
# Get user from session
user = get_user_from_session(session)
# Build GrowthBook context
gb_context = GrowthBook.build_context(%{
"id" => user.id,
"email" => user.email,
"plan" => user.subscription_plan,
"country" => user.country
})
socket =
socket
|> assign(:user, user)
|> assign(:growthbook, gb_context)
|> assign_features()
{:ok, socket}
end
@impl true
def handle_info({:features_updated, _features}, socket) do
# Rebuild context with updated features
gb_context = GrowthBook.build_context(socket.assigns.user.attributes)
socket =
socket
|> assign(:growthbook, gb_context)
|> assign_features()
|> put_flash(:info, "Features updated")
{:noreply, socket}
end
defp assign_features(socket) do
gb = socket.assigns.growthbook
socket
|> assign(:new_dashboard, GrowthBook.feature(gb, "new-dashboard").on?)
|> assign(:max_items, GrowthBook.feature(gb, "max-items").value || 20)
|> assign(:theme, GrowthBook.feature(gb, "dashboard-theme").value || "light")
end
@impl true
def render(assigns) do
~H"""
<div class={"dashboard-container theme-#{@theme}"}>
<%= if @new_dashboard do %>
<.new_dashboard_view items={@max_items} />
<% else %>
<.legacy_dashboard_view items={@max_items} />
<% end %>
</div>
"""
end
defp get_user_from_session(session) do
# Get user from session
# Implementation depends on your auth system
end
end
# Set up PubSub notifications when features refresh
# In your GrowthBook initialization (application.ex)
GrowthBook.init(
client_key: System.get_env("GROWTHBOOK_CLIENT_KEY"),
on_refresh: fn features ->
Phoenix.PubSub.broadcast(
MyApp.PubSub,
"growthbook:features",
{:features_updated, features}
)
end
)
View Helpers
Create view helpers for easy feature flag usage in templates:
# lib/my_app_web/views/growthbook_helpers.ex
defmodule MyAppWeb.GrowthBookHelpers do
def feature_on?(conn, feature_key) do
conn.assigns.growthbook
|> GrowthBook.feature(feature_key)
|> Map.get(:on?)
end
def feature_value(conn, feature_key, default \\ nil) do
conn.assigns.growthbook
|> GrowthBook.feature(feature_key)
|> Map.get(:value)
|> case do
nil -> default
value -> value
end
end
end
# In your templates
<%= if feature_on?(@conn, "show-banner") do %>
<div class="banner">
<%= feature_value(@conn, "banner-text", "Default banner text") %>
</div>
<% end %>
Supported Features
FeaturesAll versions
ExperimentationAll versions
Encrypted Features≥ v0.3.0
Prerequisites≥ v0.2.0
SemVer Targeting≥ v0.2.0
v2 Hashing≥ v0.2.0