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:
def deps do
[
{:billdogeng, "~> 1.0"}
]
endQuickstart
{: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:
- Missing/inactive →
false. - ALL
targeting_rulesmust matchperson_properties(operators:equals,not_equals,contains,gt,lt) elsefalse. bucket = murmurhash3("{key}.{distinct_id}") % 100; ON iffbucket < rollout_percentage.- Multivariate: walk
variantsby 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 GenServers: 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
mix deps.get && mix test
Coverage mirrors the reference SDK:
- Batching — 10 captures → ONE batched POST with 10 events.
- Identify/group/alias — correct
event_name+ properties. - Local flag bucketing — the 12 canonical murmurhash3 vectors + rollout / targeting / multivariate cases.
- Retry — 503 then 200 succeeds after backoff (against a local stub).
- Survey round-trip + messaging dispatch + LLM trace payload shapes.
License
MIT