# FlagDash SDK for Elixir & Erlang

Feature flags, remote config, AI configs, translations and experiments for the
BEAM.

The client is a plain struct backed by a public ETS cache — no GenServer, no
supervision tree to wire up, and safe to share across processes.

## Installation

```elixir
def deps do
  [{:flagdash, "~> 0.1"}]
end
```

## Quick start

```elixir
{:ok, client} = FlagDash.Client.new(System.fetch_env!("FLAGDASH_SDK_KEY"))

if FlagDash.Client.flag(client, "checkout-v2", %{user_id: "alice"}, false) do
  # new checkout
end

FlagDash.Client.close(client)
```

Hold the client in your application state — `Application.put_env/3`, a
`:persistent_term`, or your own supervision tree — and reuse it. Each `new/2`
creates its own ETS table.

## API key tiers

The key decides which project and environment you read, and what you may reach.
There is no `environment` option anywhere in this SDK — the key carries it.

| Key | Prefix | Reaches |
|---|---|---|
| Client | `pk_` | Flag values and configs. |
| Server | `sk_` | The above, plus targeting rules, translations and experiments. |

## Configuration

```elixir
{:ok, client} =
  FlagDash.Client.new(sdk_key,
    base_url: "https://flagdash.io",   # self-hosted? point it here
    timeout: 5_000,                    # milliseconds
    cache_ttl: 60_000,                 # milliseconds
    region: "eu-west-1",               # omit to auto-detect
    req_options: []                    # passed through to Req
  )
```

`new/2` returns `{:error, :missing_sdk_key}` for an empty key rather than
raising. Region is detected from `FLY_REGION`, `AWS_REGION` and friends when
omitted, so region-scoped targeting works with no wiring.

## Feature flags

```elixir
alias FlagDash.Client

context = %{user_id: "alice", country: "GB"}

Client.flag(client, "checkout-v2", context, false)
Client.all_flags(client, context)

# Why did it resolve that way?
Client.flag_detail(client, "checkout-v2", context)
# %{"value" => true, "reason" => "rule_match", "variation_key" => "treatment"}

# Flag metadata without evaluating anything.
Client.list_flags(client)
```

**Include a `:user_id`** (or `:unit_id`) whenever you want a stable answer.
Percentage rollouts and A/B variations hash it, so a context without one
re-rolls on every call by design.

Any attribute you put in the context map can be targeted on:

```elixir
Client.flag(client, "beta-banner", %{
  user_id: "alice",
  country: "GB",
  plan: "premium"
}, false)
```

## Remote config

```elixir
Client.config(client, "rate_limit", 100)
Client.get_config(client, "rate_limit")   # the full record
Client.list_configs(client)
```

## AI configs

Prompts, agents, skills and rules, versioned per environment and editable
without a deploy.

```elixir
Client.ai_config(client, "support-agent.md")
Client.list_ai_configs(client, file_type: "agent")
```

## Translations

```elixir
Client.translation(client, "checkout.greeting", "fr",
  default: "Hello",
  variables: %{name: "Alice"}
)
```

The key is `namespace.message`. `{placeholders}` come from `:variables`, and
`:default` (falling back to the key itself) is returned whenever the catalogue,
namespace or message is missing — a lookup never raises.

## Experiments

```elixir
case Client.experiment(client, "checkout-redesign", %{user_id: "alice"}) do
  %{"variant" => "treatment"} -> # ...
  _ -> # control, or no assignment
end

Client.track_experiment_metric(client, %{
  experiment_key: "checkout-redesign",
  event_name: "purchase",
  user_id: "alice",
  value: 42.50,
  properties: %{currency: "GBP"}
})
```

`experiment/3` returns `nil` for a context with no identifier — an assignment
that cannot be stable is worse than none.

Unlike the other server SDKs, metrics here are **sent immediately** rather than
buffered: `track_experiment_metric/2` returns `:ok` or `{:error, reason}` and
there is no `flush`. Wrap it in a `Task` if you do not want the caller to wait.

## Caching

Reads are cached in ETS for `:cache_ttl` (60s by default), so a burst of `flag`
calls costs one request.

```elixir
Client.clear_cache(client)
Client.close(client)      # deletes the ETS table
```

`close/1` is what releases the table — a long-lived client never needs it, but a
short-lived one in a test should call it.

## From Erlang

The `flagdash_sdk` module wraps the common calls so you do not need Elixir
syntax:

```erlang
{ok, Client} = flagdash_sdk:new(<<"sk_...">>),
Enabled = flagdash_sdk:flag(Client, <<"checkout-v2">>, #{user_id => <<"alice">>}, false),
flagdash_sdk:close(Client).
```

`new/1,2`, `flag/3,4`, `all_flags/1,2`, `config/3`, `ai_config/2`,
`list_ai_configs/1`, `clear_cache/1` and `close/1` are exported. For anything
else, call `'Elixir.FlagDash.Client'` directly.

## Failure behaviour

Evaluation reads return the default you passed rather than raising, so an
outage degrades to your fallback values instead of taking a request path down.
Calls that fetch metadata return `{:ok, _}` / `{:error, _}` so an unreachable
host or a bad key stays visible.

## Security

Keep a server key on the server. A client key never receives targeting rules,
so an untrusted client cannot see who else you are targeting.

## License

MIT
