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:
def deps do
[
{:billdogeng, "~> 1.0"}
]
end
Quickstart
{: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 |
:enable_logging |
false |
Verbose diagnostics via Logger |
Auth uses the X-BillDog-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. - The flag's
targeting_rulemust match, elsefalse. This is the CANONICAL audience DSL — the same rule object the backend evaluates — resolved byBilldogEng.Targeting:AND/ORcombinators, rule status, date windows, the full operator set, andgroupmembership all evaluate locally, with no server round-trip. Context comes from the eval options —:person_properties,:groups,:country,:platform,:app_version,:sdk_version,:experiment_variants(the four scalar ones fall back to a same-named person property). bucket = murmurhash3("flag:{key}:{distinct_id}") % 100; ON iffbucket < rollout_percentage.- Multivariate: walk
variantsby cumulative rollout within the ON bucket.
Conditions that need the DATABASE (segment, cohort, survey_answer,
event_fired_in_window, group_property) are never guessed at: they go COLD and
the flag is resolved via POST /experiment-config instead. "We don't know" and
"not in the audience" are different answers.
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.
3b. Targeting conformance —
test/targeting_conformance_test.exsasserts the audience evaluator againsttests/fixtures/flag-targeting.json, generated FROM the backend evaluator (never from a sibling SDK). - Retry — 503 then 200 succeeds after backoff (against a local stub).
- Survey round-trip + messaging dispatch + LLM trace payload shapes.
BillDog Auth
Session-token verification is not available in this SDK yet.
billdogeng-node is the only
server SDK that can verify a BillDog Auth token offline today.
Until it lands here, either verify in a Node service, or call the
auth-introspect endpoint over HTTP — that costs a network round-trip per
check, and in exchange tells you whether the session is live right now rather
than only that the token was valid when it was issued.
License
MIT