billdogeng 1.0.0-beta.1

Official BilldogEng server SDK for Rust — Analytics, Feature Flags (remote + local eval), Surveys, Messaging, and LLM observability.
Documentation
# 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

| Option              | Default                     | Description |
| ------------------- | --------------------------- | ----------- |
| `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