billdogeng 1.0.0-beta.1

Official BilldogEng server SDK for Rust — Analytics, Feature Flags (remote + local eval), Surveys, Messaging, and LLM observability.
Documentation
//! Spec test #1 (batching) + #2 (identify / groupIdentify / alias shapes).

mod common;

use std::collections::HashMap;

use billdogeng::{BilldogEng, BilldogEngOptions};
use common::{start_stub, StubResponse};
use serde_json::json;

fn opts(host: &str, flush_at: usize) -> BilldogEngOptions {
    BilldogEngOptions {
        host: host.to_string(),
        flush_at,
        ..Default::default()
    }
}

#[test]
fn ten_captures_produce_one_batched_post_of_ten() {
    let stub = start_stub(|_req, _hit| {
        StubResponse::envelope(json!({ "processed": 10, "duplicates": 0, "invalid": 0 }))
    });

    let bd = BilldogEng::new("bd_test_x", opts(&stub.url, 100));
    for i in 0..10 {
        let mut props = serde_json::Map::new();
        props.insert("n".to_string(), json!(i));
        bd.capture("user-123", "order_completed", props, HashMap::new());
    }
    assert_eq!(bd.queue_length(), 10);

    bd.flush().unwrap();
    bd.shutdown().unwrap();

    let ingest = stub.requests_for("/ingest-events");
    assert_eq!(ingest.len(), 1, "exactly one POST to /ingest-events");

    let body = ingest[0].body.as_ref().unwrap();
    assert_eq!(body["api_key"], json!("bd_test_x"));
    let events = body["events"].as_array().unwrap();
    assert_eq!(events.len(), 10);

    let ev = &events[0];
    assert_eq!(ev["event_name"], json!("order_completed"));
    assert_eq!(ev["user_id"], json!("user-123"));
    assert!(ev["event_timestamp"].is_number());
    assert_eq!(ev["properties"]["n"], json!(0));

    assert_eq!(ingest[0].header("x-api-key"), Some("bd_test_x"));
}

#[test]
fn auto_flushes_when_queue_reaches_flush_at() {
    let stub = start_stub(|_req, _hit| StubResponse::envelope(json!({ "processed": 5 })));
    let bd = BilldogEng::new("bd_test_x", opts(&stub.url, 5));

    for i in 0..5 {
        let mut props = serde_json::Map::new();
        props.insert("i".to_string(), json!(i));
        bd.capture("u", "e", props, HashMap::new());
    }
    bd.flush().unwrap();
    bd.shutdown().unwrap();

    let ingest = stub.requests_for("/ingest-events");
    assert!(!ingest.is_empty());
    let total: usize = ingest
        .iter()
        .map(|r| r.body.as_ref().unwrap()["events"].as_array().unwrap().len())
        .sum();
    assert_eq!(total, 5);
}

#[test]
fn identify_emits_set() {
    let stub = start_stub(|_req, _hit| StubResponse::envelope(json!({ "processed": 1 })));
    let bd = BilldogEng::new("bd_test_x", opts(&stub.url, 100));

    let props = json!({ "email": "a@b.com", "plan": "pro" })
        .as_object()
        .unwrap()
        .clone();
    bd.identify("user-123", props);
    bd.flush().unwrap();
    bd.shutdown().unwrap();

    let reqs = stub.requests();
    let ev = &reqs[0].body.as_ref().unwrap()["events"][0];
    assert_eq!(ev["event_name"], json!("$identify"));
    assert_eq!(ev["user_id"], json!("user-123"));
    assert_eq!(ev["properties"]["$set"], json!({ "email": "a@b.com", "plan": "pro" }));
}

#[test]
fn group_identify_emits_type_key_set() {
    let stub = start_stub(|_req, _hit| StubResponse::envelope(json!({ "processed": 1 })));
    let bd = BilldogEng::new("bd_test_x", opts(&stub.url, 100));

    let props = json!({ "seats": 50 }).as_object().unwrap().clone();
    bd.group_identify("company", "acme", props);
    bd.flush().unwrap();
    bd.shutdown().unwrap();

    let reqs = stub.requests();
    let ev = &reqs[0].body.as_ref().unwrap()["events"][0];
    assert_eq!(ev["event_name"], json!("$groupidentify"));
    assert_eq!(ev["user_id"], json!("acme"));
    assert_eq!(ev["properties"]["$group_type"], json!("company"));
    assert_eq!(ev["properties"]["$group_key"], json!("acme"));
    assert_eq!(ev["properties"]["$group_set"], json!({ "seats": 50 }));
}

#[test]
fn alias_emits_create_alias() {
    let stub = start_stub(|_req, _hit| StubResponse::envelope(json!({ "processed": 1 })));
    let bd = BilldogEng::new("bd_test_x", opts(&stub.url, 100));

    bd.alias("user-123", "anon-abc");
    bd.flush().unwrap();
    bd.shutdown().unwrap();

    let reqs = stub.requests();
    let ev = &reqs[0].body.as_ref().unwrap()["events"][0];
    assert_eq!(ev["event_name"], json!("$create_alias"));
    assert_eq!(ev["properties"]["alias"], json!("anon-abc"));
}

#[test]
fn capture_mirrors_groups_into_groups_and_positional() {
    let stub = start_stub(|_req, _hit| StubResponse::envelope(json!({ "processed": 1 })));
    let mut index = HashMap::new();
    index.insert("company".to_string(), 0u8);
    index.insert("team".to_string(), 1u8);
    let bd = BilldogEng::new(
        "bd_test_x",
        BilldogEngOptions {
            host: stub.url.clone(),
            flush_at: 100,
            group_type_index: Some(index),
            ..Default::default()
        },
    );

    let mut props = serde_json::Map::new();
    props.insert("x".to_string(), json!(1));
    let mut groups = HashMap::new();
    groups.insert("company".to_string(), "acme".to_string());
    groups.insert("team".to_string(), "core".to_string());
    bd.capture("user-123", "event", props, groups);
    bd.flush().unwrap();
    bd.shutdown().unwrap();

    let reqs = stub.requests();
    let ev = &reqs[0].body.as_ref().unwrap()["events"][0];
    let p = &ev["properties"];
    assert_eq!(p["$groups"], json!({ "company": "acme", "team": "core" }));
    assert_eq!(p["$group_0"], json!("acme"));
    assert_eq!(p["$group_1"], json!("core"));
}