ExLaunchDark

Elixir Launch Darkly integration library

Installation

If available in Hex, the package can be installed by adding ex_launch_dark to your list of dependencies in mix.exs:

def deps do
[
{:ex_launch_dark, "~> 0.1.0"},
# or using git source
{:ex_launch_dark, git: "url_to_repo", tag: "1.1.0"}
]
end

Configuration

The client requires a Launch Darkly SDK key to connect to the service. You can get this key from your Launch Darkly account.

Then in the host application configuration file, typically config/config.exs or config/runtime.exs, add:

# List all project keys to be used
config :ex_launch_dark, :projects, [:project_key_1, :project_key_n]
# Defined shared url among all different project clients
config :ex_launch_dark, :base_uri, "https://app.launchdarkly.com"
# For each project, add its own SDK key
config :ex_launch_dark, :project_key_1, "sdk-xxxxx-yyyyy-zzzzzzz-11111"
config :ex_launch_dark, :project_key_n, "sdk-xxxxx-yyyyy-zzzzzzz-22222"

Usage

To start using the library main functions you can start playing with the ExLaunchDark.LDAdapter module. Which exposes some of the most common flag operations, like:

# Retrieve the current value of any given feature flag
ld_ctx = %ExLaunchDark.LDContextStruct{key: "ctx_key_123", kind: "user"}
case ExLaunchDark.LDAdapter.get_feature_flag_value(:project_key_1, "flag_foo", ld_ctx, false) do
{:ok, value, _reason} ->
# All good, use the value
{:error, _default, reason} ->
# Something went wrong, handle the error using given reason
end
# Retrieve the current value of a feature flag using a multi-context
# (e.g. application + user in a single evaluation)
app_ctx =
%ExLaunchDark.LDContextStruct{
key: "my_app_backend",
kind: "service",
attributes: %{}
}
user_ctx =
%ExLaunchDark.LDContextStruct{
key: "user_123",
kind: "user",
attributes: %{
"country" => "uk",
"roles" => ["admin"]
}
}
multi_ctx =
%ExLaunchDark.LDMultiContextStruct{
contexts: [app_ctx, user_ctx]
}
case ExLaunchDark.LDAdapter.get_feature_flag_value(
:project_key_1,
"flag_foo",
multi_ctx,
false
) do
{:ok, value, _reason} ->
# All good, use the value
{:error, _default, reason} ->
# Something went wrong, handle the error using given reason
end

NOTE: Elixir generally prefers underscores rather than hypens (e.g. "flag_foo" rather than "flag-foo") but Launchdarkly idioms prefer hyphens. ExLaunchDark.LDAdapter.get_feature_flag_value makes no assumptions nor enforcement of this. If you want to use the Launchdarkly style for your flag_key then use ExLaunchDark.LDAdapter.normalise first.

Development

In order run this project isolated, you need to ensure you have first installed manually the asdf tool manager in your host machine, then run:

asdf install
# or use the util local command
mise install

which will install the required Erlang and Elixir versions as specified in the .tool-versions file.

Then you can fetch the dependencies with:

mix deps.get
mix deps.compile

In-memory adapter (testing & local development)

For testing or local development, the library provides an in-memory adapter based on ETS:

ExLaunchDark.InMemoryAdapter

This adapter implements the same interface as ExLaunchDark.LDAdapter, but stores feature flag overrides in memory.

⚠️ Do not use this adapter in production. Data stored in ETS is lost on application restart and is local to a single node.

Default startup

The in-memory adapter's ETS table is managed by ExLaunchDark.InMemoryAdapter.TableKeeper, a supervised GenServer that starts automatically with the application and owns the table for its full lifetime. No caller process can accidentally become the ETS owner, so the table is never deleted by a short-lived test or request process.

To disable the keeper (e.g. in services that never use the in-memory adapter):

config :ex_launch_dark, :start_in_memory_adapter, false

When disabled, any call to InMemoryAdapter raises a clear RuntimeError rather than silently creating an unowned table.

Scope configuration

By default, flag overrides are shared globally across all processes:

config :ex_launch_dark, :in_memory_adapter_scope, :global # default

For concurrent test suites, use :process scope to store each override under the calling process's PID, giving every test process an independent set of overrides:

config :ex_launch_dark, :in_memory_adapter_scope, :process

Under :process scope, child or spawned processes do not inherit the parent's overrides because lookups use self() as part of the ETS key.

Cleanup behavior

ScopeAPI
:globalInMemoryAdapter.clear_flags/0
:processInMemoryAdapter.clear_flags_for(self())

clear_flags/0 intentionally raises ArgumentError in :process scope to prevent one process from wiping every other process's overrides. Use clear_flags_for/1 instead.

Custom table name

The table name is read by TableKeeper once at startup. Configure it before the keeper starts (typically in config/test.exs):

config :ex_launch_dark, :in_memory_adapter_table, :my_custom_table

Changing this setting after the keeper has started has no effect on the running table.

Example usage

# Point your application at the in-memory adapter (compile time)
config :my_app, :feature_flags_adapter, ExLaunchDark.InMemoryAdapter
# Override a flag value
ExLaunchDark.InMemoryAdapter.enable("example-feature-flag")
# Disable a flag
ExLaunchDark.InMemoryAdapter.disable("example-feature-flag")
# Clear all overrides (global scope only)
ExLaunchDark.InMemoryAdapter.clear_flags()
# Clear overrides for a specific process (process scope)
ExLaunchDark.InMemoryAdapter.clear_flags_for(self())

Externally defined adapters must implement the ExLaunchDark.Adapter behaviour, which defines the get_feature_flag_value/4 callback used for flag evaluation.

Real local tests

To experiment against the real LaunchDarkly service, you need to define the necessary configuration for any real LD project so the library can connect to it, and perform the operations predefined.

e.g:

# Would get you the value of "flag_foo" using a random generated context
ExLaunchDark.LDSandbox.get_flag_value_random_context(:project_key_1, "flag_foo", false)
# Would get you the value of "flag_foo" using a predefined context kind and random key
ExLaunchDark.LDSandbox.get_flag_value_context_kind(:project_key_1, "flag_foo", "user")
# Would get you the value of "flag_foo" using a fully predefined context
ExLaunchDark.LDSandbox.get_flag_value_fixed_context(:project_key_1, "flag_foo", "user", "user_key_123", %{"foo" => "bar"})

Application CLI utils

In order to ease some of the common development tasks, you can use any of the "commands/tasks" defined in the mise.toml file, like:

mise start
mise test
mise code:check
mise code:format

Release

Bump the new version and push to GH

To bump the version in the mix.exs file run one of the following:

mix bump patch
mix bump minor
mix bump major

This can be done on the current branch/PR or, if the PR is particularly complex or there are multiple PRs, create a new branch for a new release.

Perform the actual release

This project isn't integrated with suite-py yet so the release must be created manually:

  1. Go to https://github.com/primait/ex-launch-dark/releases
  2. Click "Draft a new release"
  3. Create a new tag that is the new version (e.g. if the new version is 1.1.3 then the tag is called "1.1.3")
  4. Create the release

This will then kick off the build-and-release-to-hex workflow.

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/ex_launch_dark.