# billdogeng (Rust)
Official **BilldogEng** server SDK for Rust — the engagement suite for
server-side use: **Analytics**, **Feature Flags** (remote + local evaluation),
**Surveys** (data API), **Messaging** dispatch, and **LLM** observability.
This crate mirrors the canonical Node reference implementation
(`billdogeng-node`) idiomatically in Rust: a synchronous, thread-safe API with a
background flush thread for analytics batching.
## Install
```toml
[dependencies]
billdogeng = "1"
```
## Quickstart
```rust
use std::collections::HashMap;
use billdogeng::{BilldogEng, BilldogEngOptions, DispatchParams, FlagEvalOptions, LlmTraceParams};
use serde_json::json;
let bd = BilldogEng::new("bd_test_xxx", BilldogEngOptions {
// host: "https://api.billdog.io/v1".into(), // default
flush_at: 20, // batch size that triggers a flush
flush_interval_ms: 10_000, // background flush cadence
local_evaluation: true, // server-only: evaluate flags locally
..Default::default()
});
// ─ Analytics ─ (batched + flushed automatically)
bd.capture("user-123", "order_completed",
json!({ "revenue": 49.99 }).as_object().unwrap().clone(),
HashMap::from([("company".into(), "acme".into())]));
bd.identify("user-123", json!({ "email": "a@b.com", "plan": "pro" }).as_object().unwrap().clone());
bd.group_identify("company", "acme", json!({ "seats": 50 }).as_object().unwrap().clone());
bd.alias("user-123", "anon-abc");
// ─ Feature flags ─
let on = bd.is_feature_enabled("new_checkout", "user-123", &FlagEvalOptions::default())?;
let variant = bd.get_feature_flag("paywall_test", "user-123", &FlagEvalOptions::default())?; // Bool | Variant | None
let payload = bd.get_feature_flag_payload("paywall_test", "user-123", &FlagEvalOptions::default())?;
let all = bd.get_all_flags("user-123", &FlagEvalOptions::default())?;
// ─ Surveys (data API) ─
let surveys = bd.surveys.list(Some("user-123"), None)?;
let config = bd.surveys.fetch(&surveys[0].id, Some("user-123"), None)?;
// ─ Messaging dispatch ─ (Bearer JWT auth, not the API key)
bd.messaging.dispatch(&DispatchParams {
project_id: "project-uuid".into(),
channel: "push".into(),
content: json!({ "title": "Hi", "body": "There" }).as_object().unwrap().clone(),
targeting: None,
scheduling: None,
template_id: None,
access_token: "<supabase-session-jwt>".into(),
})?;
// ─ LLM observability ─
bd.capture_trace(&LlmTraceParams {
trace_id: "t-1".into(), span_id: "s-1".into(), model: "gpt-4o".into(),
input_text: "prompt".into(), output_text: "completion".into(),
prompt_tokens: Some(100), completion_tokens: Some(50),
duration_ms: Some(820), cost_usd: Some(0.003),
..Default::default()
})?;
// Flush remaining events + stop the background thread before exit.
bd.shutdown()?;
# Ok::<(), billdogeng::BilldogEngError>(())
```
## Configuration
| `host` | `https://api.billdog.io/v1` | API base URL |
| `flush_at` | `20` | Batch size that triggers a flush |
| `flush_interval_ms` | `10000` | Background flush cadence (ms) |
| `max_queue_size` | `1000` | Drop oldest events past this many queued |
| `gzip` | `true` | Gzip large request bodies |
| `local_evaluation` | `false` | Evaluate feature flags locally (server-only) |
| `request_timeout_ms`| `10000` | Per-request timeout (ms) |
| `max_retries` | `3` | Retry attempts for 5xx / 429 / network errors |
| `group_type_index` | — | Stable group-type → `$group_0..4` index map |
| `enable_logging` | `false` | Verbose diagnostics to stderr |
Authentication uses the `x-api-key: <apiKey>` header (`bd_test_*` sandbox /
`bd_live_*` live) on every request, except **messaging dispatch**, which
authenticates with a Supabase session Bearer JWT + project membership.
## Analytics batching & delivery
- Events accumulate in an in-memory queue and ship as a **single batched POST**
to `/ingest-events`.
- A flush happens on `flush_at`, every `flush_interval_ms`, or on `flush()` /
`shutdown()`.
- Failed flushes **re-queue** their batch (never lose events on a transient
failure); the transport also retries with exponential backoff (1s, 2s, 4s).
- Bodies are gzip-compressed when large enough to benefit.
## Feature flags — local evaluation
With `local_evaluation: true`, the SDK fetches flag **definitions** once
(`POST /feature-flag-definitions`), caches them for 5 minutes, and evaluates
deterministically on-process:
1. missing/inactive → `false`
2. ALL `targeting_rules` must match `person_properties`, else `false`
3. `bucket = murmurhash3("{key}.{distinctId}") % 100`; ON iff `bucket < rollout_percentage`
4. multivariate: walk `variants` by cumulative rollout within the ON bucket → variant key
The murmurhash3 (32-bit, seed 0) implementation is byte-identical across web /
iOS / Android / all server SDKs, so a user buckets the same everywhere.
## Build & test
```bash
cargo build
cargo test
```
## License
MIT