Packages

Official BilldogEng server SDK for Elixir — Analytics, Feature Flags (remote + local evaluation), Surveys, Messaging, and LLM observability.

Current section

Files

Jump to
billdogeng README.md
Raw

README.md

# 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 |
| `: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:

1. Missing/inactive → `false`.
2. The flag's `targeting_rule` must match, else `false`. This is the CANONICAL
   audience DSL — the same rule object the backend evaluates — resolved by
   `BilldogEng.Targeting`: `AND`/`OR` combinators, rule status, date windows, the
   full operator set, and `group` membership 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).
3. `bucket = murmurhash3("flag:{key}:{distinct_id}") % 100`; ON iff `bucket < rollout_percentage`.
4. Multivariate: walk `variants` by 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 `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.
3b. **Targeting conformance** — `test/targeting_conformance_test.exs` asserts the
   audience evaluator against `tests/fixtures/flag-targeting.json`, generated FROM
   the backend evaluator (never from a sibling SDK).
4. **Retry** — 503 then 200 succeeds after backoff (against a local stub).
5. **Survey round-trip + messaging dispatch + LLM trace** payload shapes.

## BillDog Auth

Session-token verification is **not available in this SDK yet**.
[`billdogeng-node`](https://www.npmjs.com/package/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