sigstat 0.0.4

Statsig Rust SDK for usage in multi-user server environments.
Documentation
mod utils;

use crate::utils::helpers::{enforce_array, enforce_object, enforce_string};
use crate::utils::mock_event_logging_adapter::MockEventLoggingAdapter;
use crate::utils::mock_specs_adapter::MockSpecsAdapter;
use sigstat::{Statsig, StatsigOptions, StatsigUser};
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;

#[tokio::test]
async fn test_gate_exposures_initialized() {
    let logging_adapter = Arc::new(MockEventLoggingAdapter::new());
    let specs_adapter = create_bootrapped_specs_adapter();
    let user = StatsigUser::with_user_id("a_user_id".into());

    let statsig = create_statsig(&specs_adapter, &logging_adapter);
    statsig.initialize().await.unwrap();

    let _ = statsig.check_gate(&user, "test_public");
    sleep(Duration::from_millis(1)).await;
    statsig.flush_events().await;

    let reason = get_reason_from_adapter(&logging_adapter).await;
    assert_eq!(reason, "Bootstrap:Recognized");
}

#[tokio::test]
async fn test_gate_exposures_formatting() {
    let logging_adapter = Arc::new(MockEventLoggingAdapter::new());
    let specs_adapter = create_bootrapped_specs_adapter();
    let user = StatsigUser::with_user_id("a_user_id".into());

    let statsig = create_statsig(&specs_adapter, &logging_adapter);
    statsig.initialize().await.unwrap();

    let _ = statsig.check_gate(&user, "test_50_50");
    sleep(Duration::from_millis(1)).await;
    statsig.flush_events().await;

    let received = logging_adapter.force_get_received_payloads().await;

    let statsig_meta = enforce_object(&received.statsig_metadata);
    assert_eq!(statsig_meta["sdkType"], "statsig-server-core");
    assert!(statsig_meta["sdkVersion"].as_str().is_some());

    let exposure = logging_adapter.force_get_first_event().await;
    assert_eq!(exposure["eventName"], "statsig::gate_exposure");

    let sec_expos = enforce_array(&exposure["secondaryExposures"]);
    let holdout_exposure = enforce_object(&sec_expos[0]);
    assert_eq!(holdout_exposure["gate"], "global_holdout");
    assert_eq!(holdout_exposure["gateValue"], "false");
    assert_eq!(holdout_exposure["ruleID"], "3QoA4ncNdVGBaMt3N1KYjz:0.50:1");
}

#[tokio::test]
async fn test_gate_exposures_uninitialized() {
    let logging_adapter = Arc::new(MockEventLoggingAdapter::new());
    let specs_adapter = create_bootrapped_specs_adapter();
    let user = StatsigUser::with_user_id("a_user_id".into());

    let statsig = create_statsig(&specs_adapter, &logging_adapter);

    let _ = statsig.check_gate(&user, "test_public");
    sleep(Duration::from_millis(1)).await;
    statsig.flush_events().await;

    let reason = get_reason_from_adapter(&logging_adapter).await;
    assert_eq!(reason, "Uninitialized");
}

#[tokio::test]
async fn test_gate_exposures_unrecognized() {
    let logging_adapter = Arc::new(MockEventLoggingAdapter::new());
    let specs_adapter = create_bootrapped_specs_adapter();
    let user = StatsigUser::with_user_id("a_user_id".into());

    let statsig = create_statsig(&specs_adapter, &logging_adapter);
    statsig.initialize().await.unwrap();

    let _ = statsig.check_gate(&user, "not_a_gate");
    sleep(Duration::from_millis(1)).await;
    statsig.flush_events().await;

    let reason = get_reason_from_adapter(&logging_adapter).await;
    assert_eq!(reason, "Bootstrap:Unrecognized");
}

#[tokio::test]
async fn test_gate_exposures_bad_network() {
    let logging_adapter = Arc::new(MockEventLoggingAdapter::new());
    let specs_adapter = create_trowing_specs_adapter();
    let user = StatsigUser::with_user_id("a_user_id".into());

    let statsig = create_statsig(&specs_adapter, &logging_adapter);
    let _ = statsig.initialize().await;

    let _ = statsig.check_gate(&user, "not_a_gate");
    sleep(Duration::from_millis(1)).await;
    statsig.flush_events().await;

    let reason = get_reason_from_adapter(&logging_adapter).await;
    assert_eq!(reason, "NoValues");
}

#[tokio::test]
async fn test_gate_exposures_not_awaited() {
    let logging_adapter = Arc::new(MockEventLoggingAdapter::new());
    let specs_adapter = create_delayed_specs_adapter();
    let user = StatsigUser::with_user_id("a_user_id".into());

    let statsig = Arc::new(create_statsig(&specs_adapter, &logging_adapter));

    let shared_statsig = statsig.clone();
    tokio::task::spawn(async move {
        shared_statsig.initialize().await.unwrap();
    });

    sleep(Duration::from_millis(1)).await;

    let _ = statsig.check_gate(&user, "not_a_gate");
    sleep(Duration::from_millis(1)).await;
    statsig.flush_events().await;

    let reason = get_reason_from_adapter(&logging_adapter).await;
    assert_eq!(reason, "Loading:Unrecognized");
}

#[tokio::test]
async fn test_check_gate_exposure_with_secondary_exposures() {
    let logging_adapter = Arc::new(MockEventLoggingAdapter::new());
    let specs_adapter = create_bootrapped_specs_adapter();
    let user = StatsigUser::with_user_id("a_user_id".into());

    let statsig = create_statsig(&specs_adapter, &logging_adapter);
    statsig.initialize().await.unwrap();

    let _ = statsig.check_gate(&user, "test_nested_gate_condition");
    sleep(Duration::from_millis(1)).await;
    statsig.flush_events().await;

    let event = logging_adapter.force_get_first_event().await;
    let secondary_expo = enforce_array(&event["secondaryExposures"]);

    let one = enforce_object(&secondary_expo[0]);
    assert_eq!(one["gate"], "test_email");
    assert_eq!(one["ruleID"], "default");
    assert_eq!(one["gateValue"], "false");

    let two = enforce_object(&secondary_expo[1]);
    assert_eq!(two["gate"], "test_environment_tier");
    assert_eq!(two["ruleID"], "default");
    assert_eq!(two["gateValue"], "false");
}

#[tokio::test]
async fn test_get_feature_gate_exposure_with_secondary_exposures() {
    let logging_adapter = Arc::new(MockEventLoggingAdapter::new());
    let specs_adapter = create_bootrapped_specs_adapter();
    let user = StatsigUser::with_user_id("a_user_id".into());

    let statsig = create_statsig(&specs_adapter, &logging_adapter);
    statsig.initialize().await.unwrap();

    let _ = statsig.get_feature_gate(&user, "test_nested_gate_condition");
    sleep(Duration::from_millis(1)).await;
    statsig.flush_events().await;

    let event = logging_adapter.force_get_first_event().await;
    let secondary_expo = enforce_array(&event["secondaryExposures"]);

    let one = enforce_object(&secondary_expo[0]);
    assert_eq!(one["gate"], "test_email");
    assert_eq!(one["ruleID"], "default");
    assert_eq!(one["gateValue"], "false");

    let two = enforce_object(&secondary_expo[1]);
    assert_eq!(two["gate"], "test_environment_tier");
    assert_eq!(two["ruleID"], "default");
    assert_eq!(two["gateValue"], "false");
}

#[tokio::test]
async fn test_get_layer_copies_undelegated_exposures() {
    let logging_adapter = Arc::new(MockEventLoggingAdapter::new());
    let specs_adapter = create_bootrapped_specs_adapter();
    let user = StatsigUser::with_user_id("a_user_id".into());

    let statsig = create_statsig(&specs_adapter, &logging_adapter);
    statsig.initialize().await.unwrap();

    let layer = statsig.get_layer(&user, "layer_in_global_holdout");
    layer.get_string("shared_param", "".to_string());

    sleep(Duration::from_millis(1)).await;
    statsig.flush_events().await;

    let event = logging_adapter.force_get_first_event().await;
    let secondary_expo = enforce_array(&event["secondaryExposures"]);

    let one = enforce_object(&secondary_expo[0]);
    assert_eq!(one["gate"], "global_holdout");
    assert_eq!(one["ruleID"], "3QoA4ncNdVGBaMt3N1KYjz:0.50:1");
    assert_eq!(one["gateValue"], "false");
}

#[tokio::test]
async fn test_get_layer_with_holdouts() {
    let logging_adapter = Arc::new(MockEventLoggingAdapter::new());
    let specs_adapter = create_bootrapped_specs_adapter();
    let user = StatsigUser::with_user_id("user-in-layer-holdout-4".into());

    let statsig = create_statsig(&specs_adapter, &logging_adapter);
    statsig.initialize().await.unwrap();

    let layer = statsig.get_layer(&user, "test_layer_in_holdout");
    layer.get_string("layer_val", "".to_string());

    sleep(Duration::from_millis(1)).await;
    statsig.flush_events().await;

    let event = logging_adapter.force_get_first_event().await;
    let secondary_expo = enforce_array(&event["secondaryExposures"]);

    let one = enforce_object(&secondary_expo[0]);
    assert_eq!(one["gate"], "global_holdout");
    assert_eq!(one["ruleID"], "3QoA4ncNdVGBaMt3N1KYjz:0.50:1");
    assert_eq!(one["gateValue"], "false");

    let two = enforce_object(&secondary_expo[1]);
    assert_eq!(two["gate"], "layer_holdout");
    assert_eq!(two["ruleID"], "2bAVp6R3C85vCYrR6be36n:10.00:5");
    assert_eq!(two["gateValue"], "true");
}

fn create_bootrapped_specs_adapter() -> Arc<MockSpecsAdapter> {
    Arc::new(MockSpecsAdapter::with_data("tests/data/eval_proj_dcs.json"))
}

fn create_trowing_specs_adapter() -> Arc<MockSpecsAdapter> {
    Arc::new(MockSpecsAdapter::throwing())
}

fn create_delayed_specs_adapter() -> Arc<MockSpecsAdapter> {
    Arc::new(MockSpecsAdapter::delayed(
        "tests/data/eval_proj_dcs.json",
        100,
    ))
}

fn create_statsig(
    specs_adapter: &Arc<MockSpecsAdapter>,
    logging_adapter: &Arc<MockEventLoggingAdapter>,
) -> Statsig {
    Statsig::new(
        "secret-shhh",
        Some(Arc::new(StatsigOptions {
            specs_adapter: Some(specs_adapter.clone()),
            event_logging_adapter: Some(logging_adapter.clone()),
            ..StatsigOptions::new()
        })),
    )
}

async fn get_reason_from_adapter(logging_adapter: &MockEventLoggingAdapter) -> String {
    let event = logging_adapter.force_get_first_event().await;
    let metadata = enforce_object(&event["metadata"]);

    enforce_string(&metadata["reason"])
}