billdogeng (Elixir)

Copy Markdown View Source

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):

OptionDefaultDescription
:host"https://api.billdog.io/v1"Base URL for all API requests
:flush_at20Batch size that triggers a flush
:flush_interval10_000Background flush cadence (ms)
:max_queue_size1000Max queued events before oldest dropped
:gziptrueGzip large request bodies (≥ 1 KiB)
:local_evaluationfalseEnable local feature-flag evaluation
:request_timeout10_000Per-request timeout (ms)
:max_retries3Retry attempts for 5xx / network errors
:group_type_indexMap of group-type → positional index 0..4
:enable_loggingfalseVerbose 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 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:

  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