# billdogeng (Elixir)
Official **BilldogEng** server SDK for Elixir — the engagement suite for
server-side use: **Analytics**, **Feature Flags** (remote + local evaluation),
**Surveys** (data API), **Messaging** dispatch, and **LLM** observability.
This is the Elixir port of the canonical BilldogEng server SDK. The wire
contract and method surface match every other server SDK
(`billdogeng-{node,python,go,…}`) so the cross-language parity corpus passes —
in particular the murmurhash3 bucketing primitive reproduces the exact same
flag buckets on web / iOS / Android / every server.
## Installation
Add `billdogeng` to your `mix.exs` deps:
```elixir
def deps do
[
{:billdogeng, "~> 1.0"}
]
end
```
## Quickstart
```elixir
{:ok, bd} = BilldogEng.new("bd_test_xxx", local_evaluation: true)
# Analytics (batched + background flush + gzip + backoff retry)
BilldogEng.capture(bd, "user-123", "order_completed", %{"revenue" => 49.99})
BilldogEng.identify(bd, "user-123", %{"email" => "a@b.com", "plan" => "pro"})
BilldogEng.group_identify(bd, "company", "acme", %{"seats" => 50})
BilldogEng.alias(bd, "user-123", "anon-abc")
# Feature flags (local deterministic evaluation)
true = BilldogEng.is_feature_enabled(bd, "new_checkout", "user-123")
variant = BilldogEng.get_feature_flag(bd, "beta", "user-123",
person_properties: %{"plan" => "pro"})
# Surveys (data API — no UI rendering)
{:ok, surveys} = BilldogEng.list_surveys(bd, distinct_id: "user-123")
{:ok, config} = BilldogEng.fetch_survey(bd, survey_id, distinct_id: "user-123")
{:ok, %{"respondent_id" => rid}} = BilldogEng.start_survey(bd, survey_id, customer_id: "user-123")
{:ok, _} = BilldogEng.submit_survey(bd, survey_id,
[%{question_id: q_id, choice_id: c_id, answer_number: 9}],
respondent_id: rid)
# Messaging dispatch (Bearer JWT auth)
{:ok, %{"sent" => n}} = BilldogEng.dispatch_message(bd,
project_id: project_id,
channel: "push",
content: %{"title" => "Hi", "body" => "There"},
targeting: %{"type" => "all"},
access_token: jwt)
# LLM observability
{:ok, _} = BilldogEng.capture_trace(bd,
trace_id: "t-1", span_id: "s-1", model: "claude-opus-4-8",
input_text: "hello", output_text: "hi",
prompt_tokens: 10, completion_tokens: 5, duration_ms: 123, cost_usd: 0.002)
# Flush remaining events and stop the background timer.
BilldogEng.shutdown(bd)
```
## Configuration
`BilldogEng.new/2` options (all optional):
| Option | Default | Description |
| --- | --- | --- |
| `:host` | `"https://api.billdog.io/v1"` | Base URL for all API requests |
| `:flush_at` | `20` | Batch size that triggers a flush |
| `:flush_interval` | `10_000` | Background flush cadence (ms) |
| `:max_queue_size` | `1000` | Max queued events before oldest dropped |
| `:gzip` | `true` | Gzip large request bodies (≥ 1 KiB) |
| `:local_evaluation` | `false` | Enable local feature-flag evaluation |
| `:request_timeout` | `10_000` | Per-request timeout (ms) |
| `:max_retries` | `3` | Retry attempts for 5xx / network errors |
| `:group_type_index` | — | Map of group-type → positional index `0..4` |
| `:enable_logging` | `false` | Verbose diagnostics via `Logger` |
Auth uses the `x-api-key` header on every request (`bd_test_*` sandbox /
`bd_live_*` live). Messaging dispatch instead authenticates with a Supabase
session **Bearer JWT** (`:access_token`); LLM tracing uses `X-BillDog-API-Key`.
## Feature-flag evaluation
In `local_evaluation: true` mode, definitions are fetched once from
`POST /feature-flag-definitions`, cached with a 5-minute TTL, and evaluated
deterministically on this process:
1. Missing/inactive → `false`.
2. ALL `targeting_rules` must match `person_properties` (operators:
`equals`, `not_equals`, `contains`, `gt`, `lt`) else `false`.
3. `bucket = murmurhash3("{key}.{distinct_id}") % 100`; ON iff `bucket < rollout_percentage`.
4. Multivariate: walk `variants` by cumulative rollout within the ON bucket.
`BilldogEng.set_definitions/2` injects definitions directly (tests / hosts that
distribute definitions through their own channel).
## Architecture
The client holds two `GenServer`s: one for the analytics batch queue (with the
background flush timer), one for the feature-flag definition cache. `Surveys`,
`Messaging`, and `Llm` are stateless and called with the `%BilldogEng{}` struct.
HTTP runs on Erlang's built-in `:httpc` / `:zlib`, so the only runtime
dependency is `jason`.
## Tests
```sh
mix deps.get && mix test
```
Coverage mirrors the reference SDK:
1. **Batching** — 10 captures → ONE batched POST with 10 events.
2. **Identify/group/alias** — correct `event_name` + properties.
3. **Local flag bucketing** — the 12 canonical murmurhash3 vectors +
rollout / targeting / multivariate cases.
4. **Retry** — 503 then 200 succeeds after backoff (against a local stub).
5. **Survey round-trip + messaging dispatch + LLM trace** payload shapes.
## License
MIT