use eval::Context;
use parking_lot::RwLock;
use std::io;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::runtime::Runtime;
use launchdarkly_server_sdk_evaluation::{self as eval, Detail, FlagValue, PrerequisiteEvent};
use serde::Serialize;
use thiserror::Error;
use tokio::sync::{broadcast, Semaphore};
use super::config::Config;
use super::data_source::{DataSource, EventReceived};
use super::data_source_builders::BuildError as DataSourceError;
use super::evaluation::{FlagDetail, FlagDetailConfig};
use super::stores::store::DataStore;
use super::stores::store_builders::BuildError as DataStoreError;
use crate::events::event::EventFactory;
use crate::events::event::InputEvent;
use crate::events::processor::EventProcessor;
use crate::events::processor_builders::BuildError as EventProcessorError;
struct EventsScope {
disabled: bool,
event_factory: EventFactory,
prerequisite_event_recorder: Box<dyn eval::PrerequisiteEventRecorder + Send + Sync>,
}
struct PrerequisiteEventRecorder {
event_factory: EventFactory,
event_processor: Arc<dyn EventProcessor>,
}
impl eval::PrerequisiteEventRecorder for PrerequisiteEventRecorder {
fn record(&self, event: PrerequisiteEvent) {
let evt = self.event_factory.new_eval_event(
&event.prerequisite_flag.key,
event.context.clone(),
&event.prerequisite_flag,
event.prerequisite_result,
FlagValue::Json(serde_json::Value::Null),
Some(event.target_flag_key),
);
self.event_processor.send(evt);
}
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum BuildError {
#[error("invalid client config: {0}")]
InvalidConfig(String),
}
impl From<DataSourceError> for BuildError {
fn from(error: DataSourceError) -> Self {
Self::InvalidConfig(error.to_string())
}
}
impl From<DataStoreError> for BuildError {
fn from(error: DataStoreError) -> Self {
Self::InvalidConfig(error.to_string())
}
}
impl From<EventProcessorError> for BuildError {
fn from(error: EventProcessorError) -> Self {
Self::InvalidConfig(error.to_string())
}
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum StartError {
#[error("couldn't spawn background thread for client: {0}")]
SpawnFailed(io::Error),
}
#[derive(PartialEq, Copy, Clone, Debug)]
enum ClientInitState {
Initializing = 0,
Initialized = 1,
InitializationFailed = 2,
}
impl PartialEq<usize> for ClientInitState {
fn eq(&self, other: &usize) -> bool {
*self as usize == *other
}
}
impl From<usize> for ClientInitState {
fn from(val: usize) -> Self {
match val {
0 => ClientInitState::Initializing,
1 => ClientInitState::Initialized,
2 => ClientInitState::InitializationFailed,
_ => unreachable!(),
}
}
}
pub struct Client {
event_processor: Arc<dyn EventProcessor>,
data_source: Arc<dyn DataSource>,
data_store: Arc<RwLock<dyn DataStore>>,
events_default: EventsScope,
events_with_reasons: EventsScope,
init_notify: Arc<Semaphore>,
init_state: Arc<AtomicUsize>,
started: AtomicBool,
offline: bool,
sdk_key: String,
shutdown_broadcast: broadcast::Sender<()>,
runtime: RwLock<Option<Runtime>>,
}
impl Client {
pub fn build(config: Config) -> Result<Self, BuildError> {
if config.offline() {
info!("Started LaunchDarkly Client in offline mode");
}
let tags = config.application_tag();
let endpoints = config.service_endpoints_builder().build()?;
let event_processor =
config
.event_processor_builder()
.build(&endpoints, config.sdk_key(), tags.clone())?;
let data_source =
config
.data_source_builder()
.build(&endpoints, config.sdk_key(), tags.clone())?;
let data_store = config.data_store_builder().build()?;
let events_default = EventsScope {
disabled: config.offline(),
event_factory: EventFactory::new(false),
prerequisite_event_recorder: Box::new(PrerequisiteEventRecorder {
event_factory: EventFactory::new(false),
event_processor: event_processor.clone(),
}),
};
let events_with_reasons = EventsScope {
disabled: config.offline(),
event_factory: EventFactory::new(true),
prerequisite_event_recorder: Box::new(PrerequisiteEventRecorder {
event_factory: EventFactory::new(true),
event_processor: event_processor.clone(),
}),
};
let (shutdown_tx, _) = broadcast::channel(1);
Ok(Client {
event_processor,
data_source,
data_store,
events_default,
events_with_reasons,
init_notify: Arc::new(Semaphore::new(0)),
init_state: Arc::new(AtomicUsize::new(ClientInitState::Initializing as usize)),
started: AtomicBool::new(false),
offline: config.offline(),
sdk_key: config.sdk_key().into(),
shutdown_broadcast: shutdown_tx,
runtime: RwLock::new(None),
})
}
pub fn start_with_default_executor_and_callback(&self, event_received: EventReceived) {
if self.started.load(Ordering::SeqCst) {
return;
}
self.started.store(true, Ordering::SeqCst);
self.start_with_default_executor_internal(event_received);
}
pub fn start_with_default_executor(&self) {
if self.started.load(Ordering::SeqCst) {
return;
}
self.started.store(true, Ordering::SeqCst);
self.start_with_default_executor_internal(Arc::new(move |_ev| {}));
}
fn start_with_default_executor_internal(&self, event_received: EventReceived) {
let notify = self.init_notify.clone();
let init_state = self.init_state.clone();
self.data_source.subscribe(
self.data_store.clone(),
Arc::new(move |success| {
init_state.store(
(if success {
ClientInitState::Initialized
} else {
ClientInitState::InitializationFailed
}) as usize,
Ordering::SeqCst,
);
notify.add_permits(1);
}),
event_received,
self.shutdown_broadcast.subscribe(),
);
}
pub fn start_with_runtime(&self) -> Result<bool, StartError> {
if self.started.load(Ordering::SeqCst) {
return Ok(true);
}
self.started.store(true, Ordering::SeqCst);
let runtime = Runtime::new().map_err(StartError::SpawnFailed)?;
let _guard = runtime.enter();
self.runtime.write().replace(runtime);
self.start_with_default_executor_internal(Arc::new(move |_ev| {}));
Ok(true)
}
pub async fn initialized_async(&self) -> bool {
if self.offline {
return true;
}
if ClientInitState::Initialized != self.init_state.load(Ordering::SeqCst) {
let _permit = self.init_notify.acquire().await;
}
ClientInitState::Initialized == self.init_state.load(Ordering::SeqCst)
}
pub fn initialized(&self) -> bool {
self.offline || ClientInitState::Initialized == self.init_state.load(Ordering::SeqCst)
}
pub fn close(&self) {
self.event_processor.close();
if !self.offline {
if let Err(e) = self.shutdown_broadcast.send(()) {
error!("Failed to shutdown client appropriately: {}", e);
}
}
self.runtime.write().take();
}
pub fn flush(&self) {
self.event_processor.flush();
}
pub fn identify(&self, context: Context) {
if self.events_default.disabled {
return;
}
self.send_internal(self.events_default.event_factory.new_identify(context));
}
pub fn bool_variation(&self, context: &Context, flag_key: &str, default: bool) -> bool {
let val = self.variation(context, flag_key, default);
if let Some(b) = val.as_bool() {
b
} else {
warn!(
"bool_variation called for a non-bool flag {:?} (got {:?})",
flag_key, val
);
default
}
}
pub fn str_variation(&self, context: &Context, flag_key: &str, default: String) -> String {
let val = self.variation(context, flag_key, default.clone());
if let Some(s) = val.as_string() {
s
} else {
warn!(
"str_variation called for a non-string flag {:?} (got {:?})",
flag_key, val
);
default
}
}
pub fn float_variation(&self, context: &Context, flag_key: &str, default: f64) -> f64 {
let val = self.variation(context, flag_key, default);
if let Some(f) = val.as_float() {
f
} else {
warn!(
"float_variation called for a non-float flag {:?} (got {:?})",
flag_key, val
);
default
}
}
pub fn int_variation(&self, context: &Context, flag_key: &str, default: i64) -> i64 {
let val = self.variation(context, flag_key, default);
if let Some(f) = val.as_int() {
f
} else {
warn!(
"int_variation called for a non-int flag {:?} (got {:?})",
flag_key, val
);
default
}
}
pub fn json_variation(
&self,
context: &Context,
flag_key: &str,
default: serde_json::Value,
) -> serde_json::Value {
self.variation(context, flag_key, default.clone())
.as_json()
.unwrap_or(default)
}
pub fn bool_variation_detail(
&self,
context: &Context,
flag_key: &str,
default: bool,
) -> Detail<bool> {
self.variation_detail(context, flag_key, default).try_map(
|val| val.as_bool(),
default,
eval::Error::WrongType,
)
}
pub fn str_variation_detail(
&self,
context: &Context,
flag_key: &str,
default: String,
) -> Detail<String> {
self.variation_detail(context, flag_key, default.clone())
.try_map(|val| val.as_string(), default, eval::Error::WrongType)
}
pub fn float_variation_detail(
&self,
context: &Context,
flag_key: &str,
default: f64,
) -> Detail<f64> {
self.variation_detail(context, flag_key, default).try_map(
|val| val.as_float(),
default,
eval::Error::WrongType,
)
}
pub fn int_variation_detail(
&self,
context: &Context,
flag_key: &str,
default: i64,
) -> Detail<i64> {
self.variation_detail(context, flag_key, default).try_map(
|val| val.as_int(),
default,
eval::Error::WrongType,
)
}
pub fn json_variation_detail(
&self,
context: &Context,
flag_key: &str,
default: serde_json::Value,
) -> Detail<serde_json::Value> {
self.variation_detail(context, flag_key, default.clone())
.try_map(|val| val.as_json(), default, eval::Error::WrongType)
}
pub fn secure_mode_hash(&self, context: &Context) -> String {
let key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, self.sdk_key.as_bytes());
let tag = ring::hmac::sign(&key, context.key().as_bytes());
data_encoding::HEXLOWER.encode(tag.as_ref())
}
pub fn all_flags_detail(
&self,
context: &Context,
flag_state_config: FlagDetailConfig,
) -> FlagDetail {
if self.offline {
warn!(
"all_flags_detail() called, but client is in offline mode. Returning empty state"
);
return FlagDetail::new(false);
}
if !self.initialized() {
warn!("all_flags_detail() called before client has finished initializing! Feature store unavailable - returning empty state");
return FlagDetail::new(false);
}
let data_store = self.data_store.read();
let mut flag_detail = FlagDetail::new(true);
flag_detail.populate(&*data_store, context, flag_state_config);
flag_detail
}
pub fn variation_detail<T: Into<FlagValue> + Clone>(
&self,
context: &Context,
flag_key: &str,
default: T,
) -> Detail<FlagValue> {
self.variation_internal(context, flag_key, default, &self.events_with_reasons)
}
pub fn variation<T: Into<FlagValue> + Clone>(
&self,
context: &Context,
flag_key: &str,
default: T,
) -> FlagValue {
self.variation_internal(context, flag_key, default, &self.events_default)
.value
.unwrap()
}
pub fn track_event(&self, context: Context, key: impl Into<String>) {
let _ = self.track(context, key, None, serde_json::Value::Null);
}
pub fn track_data(
&self,
context: Context,
key: impl Into<String>,
data: impl Serialize,
) -> serde_json::Result<()> {
self.track(context, key, None, data)
}
pub fn track_metric(
&self,
context: Context,
key: impl Into<String>,
value: f64,
data: impl Serialize,
) {
let _ = self.track(context, key, Some(value), data);
}
fn track(
&self,
context: Context,
key: impl Into<String>,
metric_value: Option<f64>,
data: impl Serialize,
) -> serde_json::Result<()> {
if !self.events_default.disabled {
let event =
self.events_default
.event_factory
.new_custom(context, key, metric_value, data)?;
self.send_internal(event);
}
Ok(())
}
fn variation_internal<T: Into<FlagValue> + Clone>(
&self,
context: &Context,
flag_key: &str,
default: T,
events_scope: &EventsScope,
) -> Detail<FlagValue> {
if self.offline {
return Detail::err_default(eval::Error::ClientNotReady, default.into());
}
let (flag, result) = match self.initialized() {
false => (
None,
Detail::err_default(eval::Error::ClientNotReady, default.clone().into()),
),
true => {
let data_store = self.data_store.read();
match data_store.flag(flag_key) {
Some(flag) => {
let result = eval::evaluate(
data_store.to_store(),
&flag,
context,
Some(&*events_scope.prerequisite_event_recorder),
)
.map(|v| v.clone())
.or(default.clone().into());
(Some(flag), result)
}
None => (
None,
Detail::err_default(eval::Error::FlagNotFound, default.clone().into()),
),
}
}
};
if !events_scope.disabled {
let event = match &flag {
Some(f) => events_scope.event_factory.new_eval_event(
flag_key,
context.clone(),
f,
result.clone(),
default.into(),
None,
),
None => events_scope.event_factory.new_unknown_flag_event(
flag_key,
context.clone(),
result.clone(),
default.into(),
),
};
self.send_internal(event);
}
result
}
fn send_internal(&self, event: InputEvent) {
self.event_processor.send(event);
}
}
#[cfg(test)]
mod tests {
use crossbeam_channel::Receiver;
use eval::ContextBuilder;
use launchdarkly_server_sdk_evaluation::Reason;
use std::collections::HashMap;
use tokio::time::Instant;
use crate::data_source::MockDataSource;
use crate::data_source_builders::MockDataSourceBuilder;
use crate::events::create_event_sender;
use crate::events::event::{OutputEvent, VariationKey};
use crate::events::processor_builders::EventProcessorBuilder;
use crate::stores::store_types::{PatchTarget, StorageItem};
use crate::test_common::{
self, basic_flag, basic_flag_with_prereq, basic_int_flag, basic_off_flag,
};
use crate::ConfigBuilder;
use test_case::test_case;
use super::*;
fn is_send_and_sync<T: Send + Sync>() {}
#[test]
fn ensure_client_is_send_and_sync() {
is_send_and_sync::<Client>()
}
#[tokio::test]
async fn client_asynchronously_initializes() {
let (client, _event_rx) = make_mocked_client_with_delay(1000, false);
client.start_with_default_executor();
let now = Instant::now();
let initialized = client.initialized_async().await;
let elapsed_time = now.elapsed();
assert!(initialized);
assert!(elapsed_time.as_millis() > 500)
}
#[tokio::test]
async fn client_initializes_immediately_in_offline_mode() {
let (client, _event_rx) = make_mocked_client_with_delay(1000, true);
client.start_with_default_executor();
assert!(client.initialized());
let now = Instant::now();
let initialized = client.initialized_async().await;
let elapsed_time = now.elapsed();
assert!(initialized);
assert!(elapsed_time.as_millis() < 500)
}
#[test_case(basic_flag("myFlag"), false.into(), true.into())]
#[test_case(basic_int_flag("myFlag"), 0.into(), test_common::FLOAT_TO_INT_MAX.into())]
fn client_updates_changes_evaluation_results(
flag: eval::Flag,
default: FlagValue,
expected: FlagValue,
) {
let context = ContextBuilder::new("foo")
.build()
.expect("Failed to create context");
let (client, _event_rx) = make_mocked_client();
let result = client.variation_detail(&context, "myFlag", default.clone());
assert_eq!(result.value.unwrap(), default);
client.start_with_default_executor();
client
.data_store
.write()
.upsert(
&flag.key,
PatchTarget::Flag(StorageItem::Item(flag.clone())),
)
.expect("patch should apply");
let result = client.variation_detail(&context, "myFlag", default);
assert_eq!(result.value.unwrap(), expected);
assert!(matches!(
result.reason,
Reason::Fallthrough {
in_experiment: false
}
));
}
#[test]
fn variation_tracks_events_correctly() {
let (client, event_rx) = make_mocked_client();
client.start_with_default_executor();
client
.data_store
.write()
.upsert(
"myFlag",
PatchTarget::Flag(StorageItem::Item(basic_flag("myFlag"))),
)
.expect("patch should apply");
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
let flag_value = client.variation(&context, "myFlag", FlagValue::Bool(false));
assert!(flag_value.as_bool().unwrap());
client.flush();
client.close();
let events = event_rx.iter().collect::<Vec<OutputEvent>>();
assert_eq!(events.len(), 2);
assert_eq!(events[0].kind(), "index");
assert_eq!(events[1].kind(), "summary");
if let OutputEvent::Summary(event_summary) = events[1].clone() {
let variation_key = VariationKey {
version: Some(42),
variation: Some(1),
};
let feature = event_summary.features.get("myFlag");
assert!(feature.is_some());
let feature = feature.unwrap();
assert!(feature.counters.contains_key(&variation_key));
} else {
panic!("Event should be a summary type");
}
}
#[test]
fn variation_handles_offline_mode() {
let (client, event_rx) = make_mocked_offline_client();
client.start_with_default_executor();
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
let flag_value = client.variation(&context, "myFlag", FlagValue::Bool(false));
assert!(!flag_value.as_bool().unwrap());
client.flush();
client.close();
assert_eq!(event_rx.iter().count(), 0);
}
#[test]
fn variation_handles_unknown_flags() {
let (client, event_rx) = make_mocked_client();
client.start_with_default_executor();
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
let flag_value = client.variation(&context, "non-existent-flag", FlagValue::Bool(false));
assert!(!flag_value.as_bool().unwrap());
client.flush();
client.close();
let events = event_rx.iter().collect::<Vec<OutputEvent>>();
assert_eq!(events.len(), 2);
assert_eq!(events[0].kind(), "index");
assert_eq!(events[1].kind(), "summary");
if let OutputEvent::Summary(event_summary) = events[1].clone() {
let variation_key = VariationKey {
version: None,
variation: None,
};
let feature = event_summary.features.get("non-existent-flag");
assert!(feature.is_some());
let feature = feature.unwrap();
assert!(feature.counters.contains_key(&variation_key));
} else {
panic!("Event should be a summary type");
}
}
#[test]
fn variation_detail_handles_debug_events_correctly() {
let (client, event_rx) = make_mocked_client();
client.start_with_default_executor();
let mut flag = basic_flag("myFlag");
flag.debug_events_until_date = Some(64_060_606_800_000); client
.data_store
.write()
.upsert(
&flag.key,
PatchTarget::Flag(StorageItem::Item(flag.clone())),
)
.expect("patch should apply");
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
let detail = client.variation_detail(&context, "myFlag", FlagValue::Bool(false));
assert!(detail.value.unwrap().as_bool().unwrap());
assert!(matches!(
detail.reason,
Reason::Fallthrough {
in_experiment: false
}
));
client.flush();
client.close();
let events = event_rx.try_iter().collect::<Vec<OutputEvent>>();
assert_eq!(events.len(), 3);
assert_eq!(events[0].kind(), "index");
assert_eq!(events[1].kind(), "debug");
assert_eq!(events[2].kind(), "summary");
if let OutputEvent::Summary(event_summary) = events[2].clone() {
let variation_key = VariationKey {
version: Some(42),
variation: Some(1),
};
let feature = event_summary.features.get("myFlag");
assert!(feature.is_some());
let feature = feature.unwrap();
assert!(feature.counters.contains_key(&variation_key));
} else {
panic!("Event should be a summary type");
}
}
#[test]
fn variation_detail_tracks_events_correctly() {
let (client, event_rx) = make_mocked_client();
client.start_with_default_executor();
client
.data_store
.write()
.upsert(
"myFlag",
PatchTarget::Flag(StorageItem::Item(basic_flag("myFlag"))),
)
.expect("patch should apply");
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
let detail = client.variation_detail(&context, "myFlag", FlagValue::Bool(false));
assert!(detail.value.unwrap().as_bool().unwrap());
assert!(matches!(
detail.reason,
Reason::Fallthrough {
in_experiment: false
}
));
client.flush();
client.close();
let events = event_rx.iter().collect::<Vec<OutputEvent>>();
assert_eq!(events.len(), 2);
assert_eq!(events[0].kind(), "index");
assert_eq!(events[1].kind(), "summary");
if let OutputEvent::Summary(event_summary) = events[1].clone() {
let variation_key = VariationKey {
version: Some(42),
variation: Some(1),
};
let feature = event_summary.features.get("myFlag");
assert!(feature.is_some());
let feature = feature.unwrap();
assert!(feature.counters.contains_key(&variation_key));
} else {
panic!("Event should be a summary type");
}
}
#[test]
fn variation_detail_handles_offline_mode() {
let (client, event_rx) = make_mocked_offline_client();
client.start_with_default_executor();
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
let detail = client.variation_detail(&context, "myFlag", FlagValue::Bool(false));
assert!(!detail.value.unwrap().as_bool().unwrap());
assert!(matches!(
detail.reason,
Reason::Error {
error: eval::Error::ClientNotReady
}
));
client.flush();
client.close();
assert_eq!(event_rx.iter().count(), 0);
}
#[test]
fn variation_handles_off_flag_without_variation() {
let (client, event_rx) = make_mocked_client();
client.start_with_default_executor();
client
.data_store
.write()
.upsert(
"myFlag",
PatchTarget::Flag(StorageItem::Item(basic_off_flag("myFlag"))),
)
.expect("patch should apply");
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
let result = client.variation(&context, "myFlag", FlagValue::Bool(false));
assert!(!result.as_bool().unwrap());
client.flush();
client.close();
let events = event_rx.iter().collect::<Vec<OutputEvent>>();
assert_eq!(events.len(), 2);
assert_eq!(events[0].kind(), "index");
assert_eq!(events[1].kind(), "summary");
if let OutputEvent::Summary(event_summary) = events[1].clone() {
let variation_key = VariationKey {
version: Some(42),
variation: None,
};
let feature = event_summary.features.get("myFlag");
assert!(feature.is_some());
let feature = feature.unwrap();
assert!(feature.counters.contains_key(&variation_key));
} else {
panic!("Event should be a summary type");
}
}
#[test]
fn variation_detail_tracks_prereq_events_correctly() {
let (client, event_rx) = make_mocked_client();
client.start_with_default_executor();
let mut basic_preqreq_flag = basic_flag("prereqFlag");
basic_preqreq_flag.track_events = true;
client
.data_store
.write()
.upsert(
"prereqFlag",
PatchTarget::Flag(StorageItem::Item(basic_preqreq_flag)),
)
.expect("patch should apply");
let mut basic_flag = basic_flag_with_prereq("myFlag", "prereqFlag");
basic_flag.track_events = true;
client
.data_store
.write()
.upsert("myFlag", PatchTarget::Flag(StorageItem::Item(basic_flag)))
.expect("patch should apply");
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
let detail = client.variation_detail(&context, "myFlag", FlagValue::Bool(false));
assert!(detail.value.unwrap().as_bool().unwrap());
assert!(matches!(
detail.reason,
Reason::Fallthrough {
in_experiment: false
}
));
client.flush();
client.close();
let events = event_rx.iter().collect::<Vec<OutputEvent>>();
assert_eq!(events.len(), 4);
assert_eq!(events[0].kind(), "index");
assert_eq!(events[1].kind(), "feature");
assert_eq!(events[2].kind(), "feature");
assert_eq!(events[3].kind(), "summary");
if let OutputEvent::Summary(event_summary) = events[3].clone() {
let variation_key = VariationKey {
version: Some(42),
variation: Some(1),
};
let feature = event_summary.features.get("myFlag");
assert!(feature.is_some());
let feature = feature.unwrap();
assert!(feature.counters.contains_key(&variation_key));
let variation_key = VariationKey {
version: Some(42),
variation: Some(1),
};
let feature = event_summary.features.get("prereqFlag");
assert!(feature.is_some());
let feature = feature.unwrap();
assert!(feature.counters.contains_key(&variation_key));
}
}
#[test]
fn variation_handles_failed_prereqs_correctly() {
let (client, event_rx) = make_mocked_client();
client.start_with_default_executor();
let mut basic_preqreq_flag = basic_off_flag("prereqFlag");
basic_preqreq_flag.track_events = true;
client
.data_store
.write()
.upsert(
"prereqFlag",
PatchTarget::Flag(StorageItem::Item(basic_preqreq_flag)),
)
.expect("patch should apply");
let mut basic_flag = basic_flag_with_prereq("myFlag", "prereqFlag");
basic_flag.track_events = true;
client
.data_store
.write()
.upsert("myFlag", PatchTarget::Flag(StorageItem::Item(basic_flag)))
.expect("patch should apply");
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
let detail = client.variation(&context, "myFlag", FlagValue::Bool(false));
assert!(!detail.as_bool().unwrap());
client.flush();
client.close();
let events = event_rx.iter().collect::<Vec<OutputEvent>>();
assert_eq!(events.len(), 4);
assert_eq!(events[0].kind(), "index");
assert_eq!(events[1].kind(), "feature");
assert_eq!(events[2].kind(), "feature");
assert_eq!(events[3].kind(), "summary");
if let OutputEvent::Summary(event_summary) = events[3].clone() {
let variation_key = VariationKey {
version: Some(42),
variation: Some(0),
};
let feature = event_summary.features.get("myFlag");
assert!(feature.is_some());
let feature = feature.unwrap();
assert!(feature.counters.contains_key(&variation_key));
let variation_key = VariationKey {
version: Some(42),
variation: None,
};
let feature = event_summary.features.get("prereqFlag");
assert!(feature.is_some());
let feature = feature.unwrap();
assert!(feature.counters.contains_key(&variation_key));
}
}
#[test]
fn variation_detail_handles_flag_not_found() {
let (client, event_rx) = make_mocked_client();
client.start_with_default_executor();
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
let detail = client.variation_detail(&context, "non-existent-flag", FlagValue::Bool(false));
assert!(!detail.value.unwrap().as_bool().unwrap());
assert!(matches!(
detail.reason,
Reason::Error {
error: eval::Error::FlagNotFound
}
));
client.flush();
client.close();
let events = event_rx.iter().collect::<Vec<OutputEvent>>();
assert_eq!(events.len(), 2);
assert_eq!(events[0].kind(), "index");
assert_eq!(events[1].kind(), "summary");
if let OutputEvent::Summary(event_summary) = events[1].clone() {
let variation_key = VariationKey {
version: None,
variation: None,
};
let feature = event_summary.features.get("non-existent-flag");
assert!(feature.is_some());
let feature = feature.unwrap();
assert!(feature.counters.contains_key(&variation_key));
} else {
panic!("Event should be a summary type");
}
}
#[tokio::test]
async fn variation_detail_handles_client_not_ready() {
let (client, event_rx) = make_mocked_client_with_delay(u64::MAX, false);
client.start_with_default_executor();
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
let detail = client.variation_detail(&context, "non-existent-flag", FlagValue::Bool(false));
assert!(!detail.value.unwrap().as_bool().unwrap());
assert!(matches!(
detail.reason,
Reason::Error {
error: eval::Error::ClientNotReady
}
));
client.flush();
client.close();
let events = event_rx.iter().collect::<Vec<OutputEvent>>();
assert_eq!(events.len(), 2);
assert_eq!(events[0].kind(), "index");
assert_eq!(events[1].kind(), "summary");
if let OutputEvent::Summary(event_summary) = events[1].clone() {
let variation_key = VariationKey {
version: None,
variation: None,
};
let feature = event_summary.features.get("non-existent-flag");
assert!(feature.is_some());
let feature = feature.unwrap();
assert!(feature.counters.contains_key(&variation_key));
} else {
panic!("Event should be a summary type");
}
}
#[test]
fn identify_sends_identify_event() {
let (client, event_rx) = make_mocked_client();
client.start_with_default_executor();
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
client.identify(context);
client.flush();
client.close();
let events = event_rx.iter().collect::<Vec<OutputEvent>>();
assert_eq!(events.len(), 1);
assert_eq!(events[0].kind(), "identify");
}
#[test]
fn identify_sends_sends_nothing_in_offline_mode() {
let (client, event_rx) = make_mocked_offline_client();
client.start_with_default_executor();
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
client.identify(context);
client.flush();
client.close();
assert_eq!(event_rx.iter().count(), 0);
}
#[test]
fn secure_mode_hash() {
let config = ConfigBuilder::new("secret").offline(true).build();
let client = Client::build(config).expect("Should be built.");
let context = ContextBuilder::new("Message")
.build()
.expect("Failed to create context");
assert_eq!(
client.secure_mode_hash(&context),
"aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597"
);
}
#[derive(Serialize)]
struct MyCustomData {
pub answer: u32,
}
#[test]
fn track_sends_track_and_index_events() -> serde_json::Result<()> {
let (client, event_rx) = make_mocked_client();
client.start_with_default_executor();
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
client.track_event(context.clone(), "event-with-null");
client.track_data(context.clone(), "event-with-string", "string-data")?;
client.track_data(context.clone(), "event-with-json", json!({"answer": 42}))?;
client.track_data(
context.clone(),
"event-with-struct",
MyCustomData { answer: 42 },
)?;
client.track_metric(context, "event-with-metric", 42.0, serde_json::Value::Null);
client.flush();
client.close();
let events = event_rx.iter().collect::<Vec<OutputEvent>>();
assert_eq!(events.len(), 6);
let mut events_by_type: HashMap<&str, usize> = HashMap::new();
for event in events {
if let Some(count) = events_by_type.get_mut(event.kind()) {
*count += 1;
} else {
events_by_type.insert(event.kind(), 1);
}
}
assert!(matches!(events_by_type.get("index"), Some(1)));
assert!(matches!(events_by_type.get("custom"), Some(5)));
Ok(())
}
#[test]
fn track_sends_nothing_in_offline_mode() -> serde_json::Result<()> {
let (client, event_rx) = make_mocked_offline_client();
client.start_with_default_executor();
let context = ContextBuilder::new("bob")
.build()
.expect("Failed to create context");
client.track_event(context.clone(), "event-with-null");
client.track_data(context.clone(), "event-with-string", "string-data")?;
client.track_data(context.clone(), "event-with-json", json!({"answer": 42}))?;
client.track_data(
context.clone(),
"event-with-struct",
MyCustomData { answer: 42 },
)?;
client.track_metric(context, "event-with-metric", 42.0, serde_json::Value::Null);
client.flush();
client.close();
assert_eq!(event_rx.iter().count(), 0);
Ok(())
}
fn make_mocked_client_with_delay(delay: u64, offline: bool) -> (Client, Receiver<OutputEvent>) {
let updates = Arc::new(MockDataSource::new_with_init_delay(delay));
let (event_sender, event_rx) = create_event_sender();
let config = ConfigBuilder::new("sdk-key")
.offline(offline)
.data_source(MockDataSourceBuilder::new().data_source(updates))
.event_processor(EventProcessorBuilder::new().event_sender(Arc::new(event_sender)))
.build();
let client = Client::build(config).expect("Should be built.");
(client, event_rx)
}
fn make_mocked_offline_client() -> (Client, Receiver<OutputEvent>) {
make_mocked_client_with_delay(0, true)
}
fn make_mocked_client() -> (Client, Receiver<OutputEvent>) {
make_mocked_client_with_delay(0, false)
}
}