diff --git a/.circleci/config.yml b/.circleci/config.yml index 9233c8c..c276dc7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -54,7 +54,7 @@ jobs: build-with-musl: docker: - - image: cimg/rust:1.59.0 + - image: cimg/rust:1.60.0 resource_class: medium+ steps: - checkout diff --git a/Makefile b/Makefile index be9c702..03adb38 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ start-contract-test-service-bg: run-contract-tests: @curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/main/downloader/run.sh \ - | VERSION=v1 PARAMS="-url http://localhost:8000 -debug -stop-service-at-end -skip-from ./contract-tests/testharness-suppressions.txt $(TEST_HARNESS_PARAMS)" sh + | VERSION=v2 PARAMS="-url http://localhost:8000 -debug -stop-service-at-end -skip-from ./contract-tests/testharness-suppressions.txt $(TEST_HARNESS_PARAMS)" sh contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests diff --git a/contract-tests/Cargo.toml b/contract-tests/Cargo.toml index baab36d..86df219 100644 --- a/contract-tests/Cargo.toml +++ b/contract-tests/Cargo.toml @@ -2,15 +2,16 @@ name = "contract-tests" version = "0.1.0" edition = "2021" +rust-version = "1.60.0" # MSRV license = "Apache-2.0" [dependencies] -actix = "0.12.0" -actix-web = "4.0.0-beta.10" -env_logger = "0.9.0" +actix = "0.13.0" +actix-web = "4.2.1" +env_logger = "0.10.0" log = "0.4.14" launchdarkly-server-sdk = { path = "../launchdarkly-server-sdk/" } serde = { version = "1.0.132", features = ["derive"] } serde_json = "1.0.73" futures = "0.3.12" -eventsource-client = "0.10.0" +eventsource-client = "0.11.0" diff --git a/contract-tests/src/client_entity.rs b/contract-tests/src/client_entity.rs index 516cd97..cd9f065 100644 --- a/contract-tests/src/client_entity.rs +++ b/contract-tests/src/client_entity.rs @@ -1,4 +1,5 @@ use eventsource_client::HttpsConnector; +use launchdarkly_server_sdk::{Context, ContextBuilder, MultiContextBuilder, Reference}; use std::time::Duration; const DEFAULT_POLLING_BASE_URL: &str = "https://sdk.launchdarkly.com"; @@ -11,6 +12,9 @@ use launchdarkly_server_sdk::{ ServiceEndpointsBuilder, StreamingDataSourceBuilder, }; +use crate::command_params::{ + ContextBuildParams, ContextConvertParams, ContextParam, ContextResponse, +}; use crate::{ command_params::{ CommandParams, CommandResponse, EvaluateAllFlagsParams, EvaluateAllFlagsResponse, @@ -95,16 +99,14 @@ impl ClientEntity { if let Some(capacity) = events.capacity { processor_builder.capacity(capacity); } - processor_builder - .inline_users_in_events(events.inline_users) - .all_attributes_private(events.all_attributes_private); + processor_builder.all_attributes_private(events.all_attributes_private); if let Some(interval) = events.flush_interval_ms { processor_builder.flush_interval(Duration::from_millis(interval)); } if let Some(attributes) = events.global_private_attributes { - processor_builder.private_attribute_names(attributes); + processor_builder.private_attributes(attributes); } config_builder.event_processor(&processor_builder) @@ -139,7 +141,7 @@ impl ClientEntity { match params.metric_value { Some(mv) => self.client.track_metric( - params.user, + params.context, params.event_key, mv, params @@ -148,14 +150,14 @@ impl ClientEntity { ), None if params.data.is_some() => { let _ = self.client.track_data( - params.user, + params.context, params.event_key, params .data .unwrap_or_else(|| serde_json::Value::Null.into()), ); } - None => self.client.track_event(params.user, params.event_key), + None => self.client.track_event(params.context, params.event_key), }; Ok(None) @@ -165,21 +167,89 @@ impl ClientEntity { command .identify_event .ok_or("Identify params should be set")? - .user, + .context, ); Ok(None) } - "aliasEvent" => { - let params = command.alias_event.ok_or("Alias params should be set")?; - self.client.alias(params.user, params.previous_user); - Ok(None) - } "flushEvents" => { self.client.flush(); Ok(None) } - command => return Err(format!("Invalid command requested: {}", command)), + "contextBuild" => { + let params = command + .context_build + .ok_or("ContextBuild params should be set")?; + Ok(Some(CommandResponse::ContextBuildOrConvert( + ContextResponse::from(Self::context_build(params)), + ))) + } + "contextConvert" => { + let params = command + .context_convert + .ok_or("ContextConvert params should be set")?; + Ok(Some(CommandResponse::ContextBuildOrConvert( + ContextResponse::from(Self::context_convert(params)), + ))) + } + command => Err(format!("Invalid command requested: {}", command)), + } + } + + fn context_build_single(single: ContextParam) -> Result { + let mut builder = ContextBuilder::new(single.key); + if let Some(kind) = single.kind { + builder.kind(kind); + } + if let Some(name) = single.name { + builder.name(name); + } + if let Some(anonymous) = single.anonymous { + builder.anonymous(anonymous); + } + if let Some(attribute_references) = single.private { + for attribute in attribute_references { + builder.add_private_attribute(Reference::new(attribute)); + } + } + if let Some(attributes) = single.custom { + for (k, v) in attributes { + builder.set_value(k.as_str(), v); + } } + builder.build() + } + + fn build_context_from_params(params: ContextBuildParams) -> Result { + if params.single.is_none() && params.multi.is_none() { + return Err("either 'single' or 'multi' required for contextBuild command".to_string()); + } + + if let Some(single) = params.single { + let context = Self::context_build_single(single)?; + return serde_json::to_string(&context).map_err(|e| e.to_string()); + } + + if let Some(multi) = params.multi { + let mut multi_builder = MultiContextBuilder::new(); + for single in multi { + let c = Self::context_build_single(single)?; + multi_builder.add_context(c); + } + let context = multi_builder.build()?; + return serde_json::to_string(&context).map_err(|e| e.to_string()); + } + + unreachable!() + } + + fn context_build(params: ContextBuildParams) -> Result { + Self::build_context_from_params(params) + } + + fn context_convert(params: ContextConvertParams) -> Result { + serde_json::from_str::(¶ms.input) + .map_err(|e| e.to_string()) + .and_then(|context| serde_json::to_string(&context).map_err(|e| e.to_string())) } fn evaluate(&self, params: EvaluateFlagParams) -> EvaluateFlagResponse { @@ -188,7 +258,7 @@ impl ClientEntity { "bool" => self .client .bool_variation_detail( - ¶ms.user, + ¶ms.context, ¶ms.flag_key, params .default_value @@ -199,7 +269,7 @@ impl ClientEntity { "int" => self .client .int_variation_detail( - ¶ms.user, + ¶ms.context, ¶ms.flag_key, params .default_value @@ -210,7 +280,7 @@ impl ClientEntity { "double" => self .client .float_variation_detail( - ¶ms.user, + ¶ms.context, ¶ms.flag_key, params .default_value @@ -221,7 +291,7 @@ impl ClientEntity { "string" => self .client .str_variation_detail( - ¶ms.user, + ¶ms.context, ¶ms.flag_key, params .default_value @@ -232,7 +302,7 @@ impl ClientEntity { _ => self .client .json_variation_detail( - ¶ms.user, + ¶ms.context, ¶ms.flag_key, params .default_value @@ -253,7 +323,7 @@ impl ClientEntity { "bool" => self .client .bool_variation( - ¶ms.user, + ¶ms.context, ¶ms.flag_key, params .default_value @@ -264,7 +334,7 @@ impl ClientEntity { "int" => self .client .int_variation( - ¶ms.user, + ¶ms.context, ¶ms.flag_key, params .default_value @@ -275,7 +345,7 @@ impl ClientEntity { "double" => self .client .float_variation( - ¶ms.user, + ¶ms.context, ¶ms.flag_key, params .default_value @@ -286,7 +356,7 @@ impl ClientEntity { "string" => self .client .str_variation( - ¶ms.user, + ¶ms.context, ¶ms.flag_key, params .default_value @@ -297,7 +367,7 @@ impl ClientEntity { _ => self .client .json_variation( - ¶ms.user, + ¶ms.context, ¶ms.flag_key, params .default_value @@ -329,7 +399,7 @@ impl ClientEntity { config.details_only_for_tracked_flags(); } - let all_flags = self.client.all_flags_detail(¶ms.user, config); + let all_flags = self.client.all_flags_detail(¶ms.context, config); EvaluateAllFlagsResponse { state: all_flags } } diff --git a/contract-tests/src/command_params.rs b/contract-tests/src/command_params.rs index 30c7175..4f68703 100644 --- a/contract-tests/src/command_params.rs +++ b/contract-tests/src/command_params.rs @@ -1,11 +1,13 @@ -use launchdarkly_server_sdk::{FlagDetail, FlagValue, Reason, User}; +use launchdarkly_server_sdk::{AttributeValue, Context, FlagDetail, FlagValue, Reason}; use serde::{self, Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Serialize, Clone, Debug)] #[serde(untagged)] pub enum CommandResponse { EvaluateFlag(EvaluateFlagResponse), EvaluateAll(EvaluateAllFlagsResponse), + ContextBuildOrConvert(ContextResponse), } #[derive(Deserialize, Debug)] @@ -16,14 +18,15 @@ pub struct CommandParams { pub evaluate_all: Option, pub custom_event: Option, pub identify_event: Option, - pub alias_event: Option, + pub context_build: Option, + pub context_convert: Option, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct EvaluateFlagParams { pub flag_key: String, - pub user: User, + pub context: Context, pub value_type: String, pub default_value: FlagValue, pub detail: bool, @@ -40,7 +43,7 @@ pub struct EvaluateFlagResponse { #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct EvaluateAllFlagsParams { - pub user: User, + pub context: Context, pub with_reasons: bool, pub client_side_only: bool, pub details_only_for_tracked_flags: bool, @@ -56,7 +59,7 @@ pub struct EvaluateAllFlagsResponse { #[serde(rename_all = "camelCase")] pub struct CustomEventParams { pub event_key: String, - pub user: User, + pub context: Context, pub data: Option, pub omit_null_data: bool, pub metric_value: Option, @@ -65,12 +68,51 @@ pub struct CustomEventParams { #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct IdentifyEventParams { - pub user: User, + pub context: Context, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct AliasEventParams { - pub user: User, - pub previous_user: User, +pub struct ContextParam { + pub kind: Option, + pub key: String, + pub name: Option, + pub anonymous: Option, + pub private: Option>, + pub custom: Option>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ContextBuildParams { + pub single: Option, + pub multi: Option>, +} + +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct ContextResponse { + pub output: Option, + pub error: Option, +} + +impl From> for ContextResponse { + fn from(r: Result) -> Self { + r.map_or_else( + |err| ContextResponse { + output: None, + error: Some(err), + }, + |json| ContextResponse { + output: Some(json), + error: None, + }, + ) + } +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ContextConvertParams { + pub input: String, } diff --git a/contract-tests/src/main.rs b/contract-tests/src/main.rs index b169135..37c04f4 100644 --- a/contract-tests/src/main.rs +++ b/contract-tests/src/main.rs @@ -7,6 +7,7 @@ use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Responder, Resu use client_entity::ClientEntity; use eventsource_client::HttpsConnector; use futures::executor; +use launchdarkly_server_sdk::Reference; use serde::{self, Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::sync::{mpsc, Mutex}; @@ -39,10 +40,8 @@ pub struct EventParameters { pub enable_diagnostics: bool, #[serde(default = "bool::default")] pub all_attributes_private: bool, - pub global_private_attributes: Option>, + pub global_private_attributes: Option>, pub flush_interval_ms: Option, - #[serde(default = "bool::default")] - pub inline_users: bool, } #[derive(Deserialize, Debug)] @@ -100,6 +99,7 @@ async fn status() -> impl Responder { "all-flags-details-only-for-tracked-flags".to_string(), "tags".to_string(), "service-endpoints".to_string(), + "context-type".to_string(), ], }) } diff --git a/contract-tests/testharness-suppressions.txt b/contract-tests/testharness-suppressions.txt index 5b9469b..4c426b1 100644 --- a/contract-tests/testharness-suppressions.txt +++ b/contract-tests/testharness-suppressions.txt @@ -1,12 +1,5 @@ evaluation/all flags state/client not ready evaluation/client not ready -streaming/retry behavior/do not retry after unrecoverable HTTP error on initial connect -streaming/retry behavior/retry after recoverable HTTP error on initial connect -streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 400 -streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 408 -streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 429 -streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 500 -streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 503 streaming/validation/drop and reconnect if stream event has malformed JSON/delete event streaming/validation/drop and reconnect if stream event has malformed JSON/patch event streaming/validation/drop and reconnect if stream event has malformed JSON/put event diff --git a/launchdarkly-server-sdk/Cargo.toml b/launchdarkly-server-sdk/Cargo.toml index 933cccf..e7d63bd 100644 --- a/launchdarkly-server-sdk/Cargo.toml +++ b/launchdarkly-server-sdk/Cargo.toml @@ -4,6 +4,7 @@ description = "LaunchDarkly Server-Side SDK" version = "1.0.0-beta.4" authors = ["LaunchDarkly"] edition = "2021" +rust-version = "1.60.0" # MSRV license = "Apache-2.0" homepage = "https://docs.launchdarkly.com/sdk/server-side/rust" repository = "https://github.com/launchdarkly/rust-server-sdk" @@ -20,27 +21,28 @@ eventsource-client = "0.11.0" futures = "0.3.12" lazy_static = "1.4.0" log = "0.4.14" -lru = { version = "0.7.2", default_features = false } -reqwest = { version = "0.9.11", default_features = false, features = ["rustls-tls"] } +lru = { version = "0.8.1", default_features = false } ring = "0.16.20" -launchdarkly-server-sdk-evaluation = "= 1.0.0-beta.5" +launchdarkly-server-sdk-evaluation = "1.0.0" serde = { version = "1.0.132", features = ["derive"] } serde_json = { version = "1.0.73", features = ["float_roundtrip"] } thiserror = "1.0" tokio = { version = "1.2.0", features = ["rt-multi-thread"] } -threadpool = "1.8.1" parking_lot = "0.12.0" tokio-stream = { version = "0.1.8", features = ["sync"] } -moka = "0.7.1" -uuid = {version = "1.0.0-alpha.1", features = ["v4"] } +moka = "0.9.6" +uuid = {version = "1.2.2", features = ["v4"] } +hyper = { version = "0.14.17", features = ["client", "http1", "http2", "tcp"] } +hyper-rustls = { version = "0.23.1" , features = ["http1", "http2"]} [dev-dependencies] maplit = "1.0.1" -env_logger = "0.9.0" +env_logger = "0.10.0" serde_json = { version = "1.0.73", features = ["preserve_order"] } # for deterministic JSON testing tokio = { version = "1.2.0", features = ["macros", "time"] } test-case = "2.0.0" mockito = "0.31.0" +assert-json-diff = "2.0.2" [[example]] name = "print_flags" diff --git a/launchdarkly-server-sdk/README.md b/launchdarkly-server-sdk/README.md index 0dc52a3..256f651 100644 --- a/launchdarkly-server-sdk/README.md +++ b/launchdarkly-server-sdk/README.md @@ -1,11 +1,9 @@ # LaunchDarkly Server-Side SDK for Rust -[![CircleCI](https://circleci.com/gh/launchdarkly/rust-server-sdk/tree/main.svg?style=svg)](https://circleci.com/gh/launchdarkly/rust-server-sdk/tree/master) +[![CircleCI](https://circleci.com/gh/launchdarkly/rust-server-sdk/tree/main.svg?style=svg)](https://circleci.com/gh/launchdarkly/rust-server-sdk/tree/main) The LaunchDarkly Server-Side SDK for Rust is designed primarily for use in multi-user systems such as web servers and applications. It follows the server-side LaunchDarkly model for multi-user contexts. It is not intended for use in desktop and embedded systems applications. -*This version of the SDK is a **beta** version and should not be considered ready for production use while this message is visible.* - ## LaunchDarkly overview [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! diff --git a/launchdarkly-server-sdk/examples/print_flags.rs b/launchdarkly-server-sdk/examples/print_flags.rs index 20fad74..3d977f5 100644 --- a/launchdarkly-server-sdk/examples/print_flags.rs +++ b/launchdarkly-server-sdk/examples/print_flags.rs @@ -1,13 +1,11 @@ #[macro_use] extern crate log; -#[macro_use] -extern crate maplit; use std::env; use std::process::exit; use std::time::Duration; -use launchdarkly_server_sdk::{Client, ConfigBuilder, ServiceEndpointsBuilder, User}; +use launchdarkly_server_sdk::{Client, ConfigBuilder, ContextBuilder, ServiceEndpointsBuilder}; use env_logger::Env; use tokio::time; @@ -34,7 +32,7 @@ async fn main() { } else if let [name] = bits { bool_flags.push(name.to_string()); } else { - assert!(false, "impossible"); + unreachable!(); } } if bool_flags.is_empty() && str_flags.is_empty() { @@ -47,10 +45,13 @@ async fn main() { let events_url_opt = env::var("LAUNCHDARKLY_EVENTS_URL"); let polling_url_opt = env::var("LAUNCHDARKLY_POLLING_URL"); - let alice = User::with_key("alice") - .custom(hashmap! { "team".into() => "Avengers".into() }) - .build(); - let bob = User::with_key("bob").build(); + let alice = ContextBuilder::new("alice") + .set_value("team", "Avengers".into()) + .build() + .expect("Failed to create context"); + let bob = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); let mut config_builder = ConfigBuilder::new(&sdk_key); match (stream_url_opt, events_url_opt, polling_url_opt) { @@ -87,22 +88,22 @@ async fn main() { loop { interval.tick().await; - for user in &[&alice, &bob] { + for context in &[&alice, &bob] { for flag_key in &bool_flags { - let flag_detail = client.bool_variation_detail(user, flag_key, false); + let flag_detail = client.bool_variation_detail(context, flag_key, false); info!( - "user {:?}, flag {}: {:?}", - user.key(), + "context {:?}, flag {}: {:?}", + context.key(), flag_key, flag_detail ); } for flag_key in &str_flags { let flag_detail = - client.str_variation_detail(user, flag_key, "default".to_string()); + client.str_variation_detail(context, flag_key, "default".to_string()); info!( - "user {:?}, flag {}: {:?}", - user.key(), + "context {:?}, flag {}: {:?}", + context.key(), flag_key, flag_detail ); diff --git a/launchdarkly-server-sdk/examples/progress.rs b/launchdarkly-server-sdk/examples/progress.rs index b02d1cd..bf19d05 100644 --- a/launchdarkly-server-sdk/examples/progress.rs +++ b/launchdarkly-server-sdk/examples/progress.rs @@ -9,8 +9,7 @@ use std::{ time::Duration, }; -use launchdarkly_server_sdk as ld; -use launchdarkly_server_sdk::{Client, ConfigBuilder, ServiceEndpointsBuilder}; +use launchdarkly_server_sdk::{Client, ConfigBuilder, ContextBuilder, ServiceEndpointsBuilder}; const MAX_PROGRESS: usize = 100; @@ -47,7 +46,9 @@ fn main() { eprintln!("Please enter a username on the command line."); exit(1); } - let mut user = ld::User::with_key(args[0].clone()).build(); + let context = ContextBuilder::new(args[0].clone()) + .build() + .expect("Failed to create context"); let mut config_builder = ConfigBuilder::new(&sdk_key); match (stream_url_opt, events_url_opt, polling_url_opt) { @@ -80,12 +81,10 @@ fn main() { counter.inc(); } while counter.count < 100 { - user.attribute("progress", counter.count as f64).unwrap(); - - let millis = client.int_variation(&user, "progress-delay", 100); + let millis = client.int_variation(&context, "progress-delay", 100); thread::sleep(Duration::from_millis(millis as u64)); - let increase = client.bool_variation(&user, "make-progress", false); + let increase = client.bool_variation(&context, "make-progress", false); if increase { counter.inc(); } diff --git a/launchdarkly-server-sdk/src/client.rs b/launchdarkly-server-sdk/src/client.rs index 55eb816..94a641a 100644 --- a/launchdarkly-server-sdk/src/client.rs +++ b/launchdarkly-server-sdk/src/client.rs @@ -1,12 +1,11 @@ +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, User, -}; +use launchdarkly_server_sdk_evaluation::{self as eval, Detail, FlagValue, PrerequisiteEvent}; use serde::Serialize; use thiserror::Error; use tokio::sync::{broadcast, Semaphore}; @@ -37,7 +36,7 @@ impl eval::PrerequisiteEventRecorder for PrerequisiteEventRecorder { fn record(&self, event: PrerequisiteEvent) { let evt = self.event_factory.new_eval_event( &event.prerequisite_flag.key, - event.user.clone(), + event.context.clone(), &event.prerequisite_flag, event.prerequisite_result, FlagValue::Json(serde_json::Value::Null), @@ -110,7 +109,7 @@ impl From for ClientInitState { /// A client for the LaunchDarkly API. /// -/// In order to create a client instance you must first create a [crate::Config]. +/// In order to create a client instance, first create a config using [crate::ConfigBuilder]. /// /// # Examples /// @@ -150,8 +149,6 @@ pub struct Client { sdk_key: String, shutdown_broadcast: broadcast::Sender<()>, runtime: RwLock>, - // TODO: Once we need the config for diagnostic events, then we should add this. - // config: Arc> } impl Client { @@ -254,7 +251,7 @@ impl Client { } self.started.store(true, Ordering::SeqCst); - let runtime = tokio::runtime::Runtime::new().map_err(StartError::SpawnFailed)?; + let runtime = Runtime::new().map_err(StartError::SpawnFailed)?; let _guard = runtime.enter(); self.runtime.write().replace(runtime); @@ -319,51 +316,27 @@ impl Client { self.event_processor.flush(); } - /// Identify reports details about a user. + /// Identify reports details about a context. /// /// For more information, see the Reference Guide: /// - pub fn identify(&self, user: User) { + pub fn identify(&self, context: Context) { if self.events_default.disabled { return; } - if user.key().is_empty() { - warn!("identify called with empty user key!"); - return; - } - - self.send_internal(self.events_default.event_factory.new_identify(user)); - } - - /// Alias associates two users for analytics purposes. - /// - /// This can be helpful in the situation where a person is represented by multiple LaunchDarkly - /// users. This may happen, for example, when a person initially logs into an application-- the - /// person might be represented by an anonymous user prior to logging in and a different user - /// after logging in, as denoted by a different user key. - /// - /// For more information, see the Reference Guide: - /// . - pub fn alias(&self, user: User, previous_user: User) { - if !self.events_default.disabled { - self.send_internal( - self.events_default - .event_factory - .new_alias(user, previous_user), - ); - } + self.send_internal(self.events_default.event_factory.new_identify(context)); } - /// Returns the value of a boolean feature flag for a given user. + /// Returns the value of a boolean feature flag for a given context. /// /// Returns `default` if there is an error, if the flag doesn't exist, or the feature is turned /// off and has no off variation. /// /// For more information, see the Reference Guide: /// . - pub fn bool_variation(&self, user: &User, flag_key: &str, default: bool) -> bool { - let val = self.variation(user, flag_key, default); + 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 { @@ -375,15 +348,15 @@ impl Client { } } - /// Returns the value of a string feature flag for a given user. + /// Returns the value of a string feature flag for a given context. /// /// Returns `default` if there is an error, if the flag doesn't exist, or the feature is turned /// off and has no off variation. /// /// For more information, see the Reference Guide: /// . - pub fn str_variation(&self, user: &User, flag_key: &str, default: String) -> String { - let val = self.variation(user, flag_key, default.clone()); + 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 { @@ -395,15 +368,15 @@ impl Client { } } - /// Returns the value of a float feature flag for a given user. + /// Returns the value of a float feature flag for a given context. /// /// Returns `default` if there is an error, if the flag doesn't exist, or the feature is turned /// off and has no off variation. /// /// For more information, see the Reference Guide: /// . - pub fn float_variation(&self, user: &User, flag_key: &str, default: f64) -> f64 { - let val = self.variation(user, flag_key, 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 { @@ -415,15 +388,15 @@ impl Client { } } - /// Returns the value of a integer feature flag for a given user. + /// Returns the value of a integer feature flag for a given context. /// /// Returns `default` if there is an error, if the flag doesn't exist, or the feature is turned /// off and has no off variation. /// /// For more information, see the Reference Guide: /// . - pub fn int_variation(&self, user: &User, flag_key: &str, default: i64) -> i64 { - let val = self.variation(user, flag_key, 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 { @@ -435,7 +408,7 @@ impl Client { } } - /// Returns the value of a feature flag for the given user, allowing the value to be + /// Returns the value of a feature flag for the given context, allowing the value to be /// of any JSON type. /// /// The value is returned as an [serde_json::Value]. @@ -446,11 +419,11 @@ impl Client { /// . pub fn json_variation( &self, - user: &User, + context: &Context, flag_key: &str, default: serde_json::Value, ) -> serde_json::Value { - self.variation(user, flag_key, default.clone()) + self.variation(context, flag_key, default.clone()) .as_json() .unwrap_or(default) } @@ -463,11 +436,11 @@ impl Client { /// . pub fn bool_variation_detail( &self, - user: &User, + context: &Context, flag_key: &str, default: bool, ) -> Detail { - self.variation_detail(user, flag_key, default).try_map( + self.variation_detail(context, flag_key, default).try_map( |val| val.as_bool(), default, eval::Error::WrongType, @@ -482,11 +455,11 @@ impl Client { /// . pub fn str_variation_detail( &self, - user: &User, + context: &Context, flag_key: &str, default: String, ) -> Detail { - self.variation_detail(user, flag_key, default.clone()) + self.variation_detail(context, flag_key, default.clone()) .try_map(|val| val.as_string(), default, eval::Error::WrongType) } @@ -496,8 +469,13 @@ impl Client { /// /// For more information, see the Reference Guide: /// . - pub fn float_variation_detail(&self, user: &User, flag_key: &str, default: f64) -> Detail { - self.variation_detail(user, flag_key, default).try_map( + pub fn float_variation_detail( + &self, + context: &Context, + flag_key: &str, + default: f64, + ) -> Detail { + self.variation_detail(context, flag_key, default).try_map( |val| val.as_float(), default, eval::Error::WrongType, @@ -510,8 +488,13 @@ impl Client { /// /// For more information, see the Reference Guide: /// . - pub fn int_variation_detail(&self, user: &User, flag_key: &str, default: i64) -> Detail { - self.variation_detail(user, flag_key, default).try_map( + pub fn int_variation_detail( + &self, + context: &Context, + flag_key: &str, + default: i64, + ) -> Detail { + self.variation_detail(context, flag_key, default).try_map( |val| val.as_int(), default, eval::Error::WrongType, @@ -526,26 +509,26 @@ impl Client { /// . pub fn json_variation_detail( &self, - user: &User, + context: &Context, flag_key: &str, default: serde_json::Value, ) -> Detail { - self.variation_detail(user, flag_key, default.clone()) + self.variation_detail(context, flag_key, default.clone()) .try_map(|val| val.as_json(), default, eval::Error::WrongType) } - /// Generates the secure mode hash value for a user. + /// Generates the secure mode hash value for a context. /// /// For more information, see the Reference Guide: /// . - pub fn secure_mode_hash(&self, user: &User) -> String { + 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, user.key().as_bytes()); + let tag = ring::hmac::sign(&key, context.key().as_bytes()); data_encoding::HEXLOWER.encode(tag.as_ref()) } - /// Returns an object that encapsulates the state of all feature flags for a given user. This + /// Returns an object that encapsulates the state of all feature flags for a given context. This /// includes the flag values, and also metadata that can be used on the front end. /// /// The most common use case for this method is to bootstrap a set of client-side feature flags @@ -555,7 +538,11 @@ impl Client { /// /// For more information, see the Reference Guide: /// - pub fn all_flags_detail(&self, user: &User, flag_state_config: FlagDetailConfig) -> FlagDetail { + 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" @@ -571,7 +558,7 @@ impl Client { let data_store = self.data_store.read(); let mut flag_detail = FlagDetail::new(true); - flag_detail.populate(&*data_store, user, flag_state_config); + flag_detail.populate(&*data_store, context, flag_state_config); flag_detail } @@ -583,14 +570,14 @@ impl Client { /// . pub fn variation_detail + Clone>( &self, - user: &User, + context: &Context, flag_key: &str, default: T, ) -> Detail { - self.variation_internal(user, flag_key, default, &self.events_with_reasons) + self.variation_internal(context, flag_key, default, &self.events_with_reasons) } - /// This is a generic function which returns the value of a feature flag for a given user. + /// This is a generic function which returns the value of a feature flag for a given context. /// /// This method is an alternatively to the type specified methods (e.g. /// [Client::bool_variation], [Client::int_variation], etc.). @@ -602,18 +589,16 @@ impl Client { /// . pub fn variation + Clone>( &self, - user: &User, + context: &Context, flag_key: &str, default: T, ) -> FlagValue { - // unwrap is safe here because value should have been replaced with default if it was None. - // TODO(ch108604) that is ugly, use the type system to fix it - self.variation_internal(user, flag_key, default, &self.events_default) + self.variation_internal(context, flag_key, default, &self.events_default) .value .unwrap() } - /// Reports that a user has performed an event. + /// Reports that a context has performed an event. /// /// The `key` parameter is defined by the application and will be shown in analytics reports; /// it normally corresponds to the event name of a metric that you have created through the @@ -622,11 +607,11 @@ impl Client { /// /// For more information, see the Reference Guide: /// . - pub fn track_event(&self, user: User, key: impl Into) { - let _ = self.track(user, key, None, serde_json::Value::Null); + pub fn track_event(&self, context: Context, key: impl Into) { + let _ = self.track(context, key, None, serde_json::Value::Null); } - /// Reports that a user has performed an event, and associates it with custom data. + /// Reports that a context has performed an event, and associates it with custom data. /// /// The `key` parameter is defined by the application and will be shown in analytics reports; /// it normally corresponds to the event name of a metric that you have created through the @@ -640,14 +625,14 @@ impl Client { /// . pub fn track_data( &self, - user: User, + context: Context, key: impl Into, data: impl Serialize, ) -> serde_json::Result<()> { - self.track(user, key, None, data) + self.track(context, key, None, data) } - /// Reports that a user has performed an event, and associates it with a numeric value. This + /// Reports that a context has performed an event, and associates it with a numeric value. This /// value is used by the LaunchDarkly experimentation feature in numeric custom metrics, and /// will also be returned as part of the custom event for Data Export. /// @@ -659,17 +644,17 @@ impl Client { /// . pub fn track_metric( &self, - user: User, + context: Context, key: impl Into, value: f64, data: impl Serialize, ) { - let _ = self.track(user, key, Some(value), data); + let _ = self.track(context, key, Some(value), data); } fn track( &self, - user: User, + context: Context, key: impl Into, metric_value: Option, data: impl Serialize, @@ -678,7 +663,7 @@ impl Client { let event = self.events_default .event_factory - .new_custom(user, key, metric_value, data)?; + .new_custom(context, key, metric_value, data)?; self.send_internal(event); } @@ -688,7 +673,7 @@ impl Client { fn variation_internal + Clone>( &self, - user: &User, + context: &Context, flag_key: &str, default: T, events_scope: &EventsScope, @@ -709,7 +694,7 @@ impl Client { let result = eval::evaluate( data_store.to_store(), &flag, - user, + context, Some(&*events_scope.prerequisite_event_recorder), ) .map(|v| v.clone()) @@ -729,7 +714,7 @@ impl Client { let event = match &flag { Some(f) => events_scope.event_factory.new_eval_event( flag_key, - user.clone(), + context.clone(), f, result.clone(), default.into(), @@ -737,7 +722,7 @@ impl Client { ), None => events_scope.event_factory.new_unknown_flag_event( flag_key, - user.clone(), + context.clone(), result.clone(), default.into(), ), @@ -756,7 +741,8 @@ impl Client { #[cfg(test)] mod tests { use crossbeam_channel::Receiver; - use launchdarkly_server_sdk_evaluation::{Reason, User}; + use eval::ContextBuilder; + use launchdarkly_server_sdk_evaluation::Reason; use std::collections::HashMap; use tokio::time::Instant; @@ -816,12 +802,14 @@ mod tests { default: FlagValue, expected: FlagValue, ) { - let user = User::with_key("foo".to_string()).build(); + let context = ContextBuilder::new("foo") + .build() + .expect("Failed to create context"); let (client, _event_rx) = make_mocked_client(); - let result = client.variation_detail(&user, "myFlag", default.clone()); - assert_eq!(result.value.unwrap(), default.clone()); + let result = client.variation_detail(&context, "myFlag", default.clone()); + assert_eq!(result.value.unwrap(), default); client.start_with_default_executor(); client @@ -833,7 +821,7 @@ mod tests { ) .expect("patch should apply"); - let result = client.variation_detail(&user, "myFlag", default); + let result = client.variation_detail(&context, "myFlag", default); assert_eq!(result.value.unwrap(), expected); assert!(matches!( result.reason, @@ -855,9 +843,11 @@ mod tests { PatchTarget::Flag(StorageItem::Item(basic_flag("myFlag"))), ) .expect("patch should apply"); - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - let flag_value = client.variation(&user, "myFlag", FlagValue::Bool(false)); + let flag_value = client.variation(&context, "myFlag", FlagValue::Bool(false)); assert!(flag_value.as_bool().unwrap()); client.flush(); @@ -870,11 +860,14 @@ mod tests { if let OutputEvent::Summary(event_summary) = events[1].clone() { let variation_key = VariationKey { - flag_key: "myFlag".into(), version: Some(42), variation: Some(1), }; - assert!(event_summary.features.contains_key(&variation_key)); + 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"); } @@ -885,24 +878,27 @@ mod tests { let (client, event_rx) = make_mocked_offline_client(); client.start_with_default_executor(); - let user = User::with_key("bob").build(); - let flag_value = client.variation(&user, "myFlag", FlagValue::Bool(false)); + 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::>(); - assert!(events.is_empty()); + 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 user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - let flag_value = client.variation(&user, "non-existent-flag", FlagValue::Bool(false)); + let flag_value = client.variation(&context, "non-existent-flag", FlagValue::Bool(false)); assert!(!flag_value.as_bool().unwrap()); client.flush(); @@ -915,11 +911,15 @@ mod tests { if let OutputEvent::Summary(event_summary) = events[1].clone() { let variation_key = VariationKey { - flag_key: "non-existent-flag".into(), version: None, variation: None, }; - assert!(event_summary.features.contains_key(&variation_key)); + + 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"); } @@ -941,9 +941,11 @@ mod tests { PatchTarget::Flag(StorageItem::Item(flag.clone())), ) .expect("patch should apply"); - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - let detail = client.variation_detail(&user, "myFlag", FlagValue::Bool(false)); + let detail = client.variation_detail(&context, "myFlag", FlagValue::Bool(false)); assert!(detail.value.unwrap().as_bool().unwrap()); assert!(matches!( @@ -963,11 +965,15 @@ mod tests { if let OutputEvent::Summary(event_summary) = events[2].clone() { let variation_key = VariationKey { - flag_key: "myFlag".into(), version: Some(42), variation: Some(1), }; - assert!(event_summary.features.contains_key(&variation_key)); + + 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"); } @@ -986,9 +992,11 @@ mod tests { PatchTarget::Flag(StorageItem::Item(basic_flag("myFlag"))), ) .expect("patch should apply"); - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - let detail = client.variation_detail(&user, "myFlag", FlagValue::Bool(false)); + let detail = client.variation_detail(&context, "myFlag", FlagValue::Bool(false)); assert!(detail.value.unwrap().as_bool().unwrap()); assert!(matches!( @@ -1007,11 +1015,15 @@ mod tests { if let OutputEvent::Summary(event_summary) = events[1].clone() { let variation_key = VariationKey { - flag_key: "myFlag".into(), version: Some(42), variation: Some(1), }; - assert!(event_summary.features.contains_key(&variation_key)); + + 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"); } @@ -1022,9 +1034,11 @@ mod tests { let (client, event_rx) = make_mocked_offline_client(); client.start_with_default_executor(); - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - let detail = client.variation_detail(&user, "myFlag", FlagValue::Bool(false)); + let detail = client.variation_detail(&context, "myFlag", FlagValue::Bool(false)); assert!(!detail.value.unwrap().as_bool().unwrap()); assert!(matches!( @@ -1036,8 +1050,7 @@ mod tests { client.flush(); client.close(); - let events = event_rx.iter().collect::>(); - assert!(events.is_empty()); + assert_eq!(event_rx.iter().count(), 0); } #[test] @@ -1053,9 +1066,11 @@ mod tests { PatchTarget::Flag(StorageItem::Item(basic_off_flag("myFlag"))), ) .expect("patch should apply"); - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - let result = client.variation(&user, "myFlag", FlagValue::Bool(false)); + let result = client.variation(&context, "myFlag", FlagValue::Bool(false)); assert!(!result.as_bool().unwrap()); client.flush(); @@ -1068,11 +1083,14 @@ mod tests { if let OutputEvent::Summary(event_summary) = events[1].clone() { let variation_key = VariationKey { - flag_key: "myFlag".into(), version: Some(42), variation: None, }; - assert!(event_summary.features.contains_key(&variation_key)); + 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"); } @@ -1102,9 +1120,11 @@ mod tests { .write() .upsert("myFlag", PatchTarget::Flag(StorageItem::Item(basic_flag))) .expect("patch should apply"); - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - let detail = client.variation_detail(&user, "myFlag", FlagValue::Bool(false)); + let detail = client.variation_detail(&context, "myFlag", FlagValue::Bool(false)); assert!(detail.value.unwrap().as_bool().unwrap()); assert!(matches!( @@ -1125,17 +1145,24 @@ mod tests { if let OutputEvent::Summary(event_summary) = events[3].clone() { let variation_key = VariationKey { - flag_key: "myFlag".into(), version: Some(42), variation: Some(1), }; - assert!(event_summary.features.contains_key(&variation_key)); + 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 { - flag_key: "prereqFlag".into(), version: Some(42), variation: Some(1), }; - assert!(event_summary.features.contains_key(&variation_key)); + let feature = event_summary.features.get("prereqFlag"); + assert!(feature.is_some()); + + let feature = feature.unwrap(); + assert!(feature.counters.contains_key(&variation_key)); } } @@ -1163,9 +1190,11 @@ mod tests { .write() .upsert("myFlag", PatchTarget::Flag(StorageItem::Item(basic_flag))) .expect("patch should apply"); - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - let detail = client.variation(&user, "myFlag", FlagValue::Bool(false)); + let detail = client.variation(&context, "myFlag", FlagValue::Bool(false)); assert!(!detail.as_bool().unwrap()); client.flush(); @@ -1180,17 +1209,24 @@ mod tests { if let OutputEvent::Summary(event_summary) = events[3].clone() { let variation_key = VariationKey { - flag_key: "myFlag".into(), version: Some(42), variation: Some(0), }; - assert!(event_summary.features.contains_key(&variation_key)); + 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 { - flag_key: "prereqFlag".into(), version: Some(42), variation: None, }; - assert!(event_summary.features.contains_key(&variation_key)); + let feature = event_summary.features.get("prereqFlag"); + assert!(feature.is_some()); + + let feature = feature.unwrap(); + assert!(feature.counters.contains_key(&variation_key)); } } @@ -1199,8 +1235,10 @@ mod tests { let (client, event_rx) = make_mocked_client(); client.start_with_default_executor(); - let user = User::with_key("bob").build(); - let detail = client.variation_detail(&user, "non-existent-flag", FlagValue::Bool(false)); + 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!( @@ -1219,11 +1257,14 @@ mod tests { if let OutputEvent::Summary(event_summary) = events[1].clone() { let variation_key = VariationKey { - flag_key: "non-existent-flag".into(), version: None, variation: None, }; - assert!(event_summary.features.contains_key(&variation_key)); + 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"); } @@ -1233,9 +1274,11 @@ mod tests { 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 user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - let detail = client.variation_detail(&user, "non-existent-flag", FlagValue::Bool(false)); + let detail = client.variation_detail(&context, "non-existent-flag", FlagValue::Bool(false)); assert!(!detail.value.unwrap().as_bool().unwrap()); assert!(matches!( @@ -1254,11 +1297,14 @@ mod tests { if let OutputEvent::Summary(event_summary) = events[1].clone() { let variation_key = VariationKey { - flag_key: "non-existent-flag".into(), version: None, variation: None, }; - assert!(event_summary.features.contains_key(&variation_key)); + 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"); } @@ -1269,9 +1315,11 @@ mod tests { let (client, event_rx) = make_mocked_client(); client.start_with_default_executor(); - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - client.identify(user); + client.identify(context); client.flush(); client.close(); @@ -1285,61 +1333,31 @@ mod tests { let (client, event_rx) = make_mocked_offline_client(); client.start_with_default_executor(); - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - client.identify(user); + client.identify(context); client.flush(); client.close(); - let events = event_rx.iter().collect::>(); - assert!(events.is_empty()); + 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 user = User::with_key("Message").build(); + let context = ContextBuilder::new("Message") + .build() + .expect("Failed to create context"); assert_eq!( - client.secure_mode_hash(&user), + client.secure_mode_hash(&context), "aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597" ); } - #[test] - fn alias_sends_alias_event() { - let (client, event_rx) = make_mocked_client(); - client.start_with_default_executor(); - - let user = User::with_key("bob").build(); - let previous_user = User::with_key("previous-bob").build(); - - client.alias(user, previous_user); - client.flush(); - client.close(); - - let events = event_rx.iter().collect::>(); - assert_eq!(events.len(), 1); - assert_eq!(events[0].kind(), "alias"); - } - - #[test] - fn alias_sends_nothing_in_offline_mode() { - let (client, event_rx) = make_mocked_offline_client(); - client.start_with_default_executor(); - - let user = User::with_key("bob").build(); - let previous_user = User::with_key("previous-bob").build(); - - client.alias(user, previous_user); - client.flush(); - client.close(); - - let events = event_rx.iter().collect::>(); - assert!(events.is_empty()); - } - #[derive(Serialize)] struct MyCustomData { pub answer: u32, @@ -1350,22 +1368,19 @@ mod tests { let (client, event_rx) = make_mocked_client(); client.start_with_default_executor(); - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - client.track_event(user.clone(), "event-with-null"); - client.track_data(user.clone(), "event-with-string", "string-data")?; - client.track_data(user.clone(), "event-with-json", json!({"answer": 42}))?; + 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( - user.clone(), + context.clone(), "event-with-struct", MyCustomData { answer: 42 }, )?; - client.track_metric( - user.clone(), - "event-with-metric", - 42.0, - serde_json::Value::Null, - ); + client.track_metric(context, "event-with-metric", 42.0, serde_json::Value::Null); client.flush(); client.close(); @@ -1392,28 +1407,24 @@ mod tests { let (client, event_rx) = make_mocked_offline_client(); client.start_with_default_executor(); - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); - client.track_event(user.clone(), "event-with-null"); - client.track_data(user.clone(), "event-with-string", "string-data")?; - client.track_data(user.clone(), "event-with-json", json!({"answer": 42}))?; + 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( - user.clone(), + context.clone(), "event-with-struct", MyCustomData { answer: 42 }, )?; - client.track_metric( - user.clone(), - "event-with-metric", - 42.0, - serde_json::Value::Null, - ); + client.track_metric(context, "event-with-metric", 42.0, serde_json::Value::Null); client.flush(); client.close(); - let events = event_rx.iter().collect::>(); - assert!(events.is_empty()); + assert_eq!(event_rx.iter().count(), 0); Ok(()) } @@ -1424,7 +1435,7 @@ mod tests { let config = ConfigBuilder::new("sdk-key") .offline(offline) - .data_source(MockDataSourceBuilder::new().data_source(updates.clone())) + .data_source(MockDataSourceBuilder::new().data_source(updates)) .event_processor(EventProcessorBuilder::new().event_sender(Arc::new(event_sender))) .build(); diff --git a/launchdarkly-server-sdk/src/config.rs b/launchdarkly-server-sdk/src/config.rs index 8c1af40..ee2fb80 100644 --- a/launchdarkly-server-sdk/src/config.rs +++ b/launchdarkly-server-sdk/src/config.rs @@ -167,10 +167,10 @@ impl Config { /// Used to create a [Config] struct for creating [crate::Client] instances. /// /// For usage examples see: -// TODO(doc) Include the data store builder example once we have something that can be customized -/// - [crate::ServiceEndpointsBuilder] -/// - [crate::StreamingDataSourceBuilder] -/// - [crate::EventProcessorBuilder] +/// - [Creating service endpoints](crate::ServiceEndpointsBuilder) +/// - [Configuring a persistent data store](crate::PersistentDataStoreBuilder) +/// - [Configuring the streaming data source](crate::StreamingDataSourceBuilder) +/// - [Configuring events sent to LaunchDarkly](crate::EventProcessorBuilder) pub struct ConfigBuilder { service_endpoints_builder: Option, data_store_builder: Option>, @@ -202,13 +202,16 @@ impl ConfigBuilder { } /// Set the data store to use for this client. + /// + /// By default, the SDK uses an in-memory data store. + /// For a persistent store, see [PersistentDataStoreBuilder](crate::stores::persistent_store_builders::PersistentDataStoreBuilder). pub fn data_store(mut self, builder: &dyn DataStoreFactory) -> Self { self.data_store_builder = Some(builder.to_owned()); self } /// Set the data source to use for this client. - /// For usage see [crate::data_source_builders::StreamingDataSourceBuilder] + /// For the streaming data source, see [StreamingDataSourceBuilder](crate::data_source_builders::StreamingDataSourceBuilder). /// /// If offline mode is enabled, this data source will be ignored. pub fn data_source(mut self, builder: &dyn DataSourceFactory) -> Self { @@ -217,7 +220,7 @@ impl ConfigBuilder { } /// Set the event processor to use for this client. - /// For usage see [crate::EventProcessorBuilder] + /// For usage see [EventProcessorBuilder](crate::EventProcessorBuilder). /// /// If offline mode is enabled, this event processor will be ignored. pub fn event_processor(mut self, builder: &dyn EventProcessorFactory) -> Self { diff --git a/launchdarkly-server-sdk/src/data_source.rs b/launchdarkly-server-sdk/src/data_source.rs index 5bcb3bb..70901d2 100644 --- a/launchdarkly-server-sdk/src/data_source.rs +++ b/launchdarkly-server-sdk/src/data_source.rs @@ -1,3 +1,9 @@ +use super::stores::store_types::{AllData, DataKind, PatchTarget, StorageItem}; +use crate::feature_requester::FeatureRequesterError; +use crate::feature_requester_builders::FeatureRequesterFactory; +use crate::reqwest::is_http_error_recoverable; +use crate::stores::store::{DataStore, UpdateError}; +use crate::LAUNCHDARKLY_TAGS_HEADER; use es::{Client, ClientBuilder, HttpsConnector, ReconnectOptionsBuilder}; use eventsource_client as es; use futures::StreamExt; @@ -10,12 +16,6 @@ use tokio::sync::broadcast; use tokio::time; use tokio_stream::wrappers::{BroadcastStream, IntervalStream}; -use super::stores::store_types::{AllData, DataKind, PatchTarget, StorageItem}; -use crate::feature_requester::FeatureRequesterError; -use crate::feature_requester_builders::FeatureRequesterFactory; -use crate::stores::store::{DataStore, UpdateError}; -use crate::LAUNCHDARKLY_TAGS_HEADER; - const FLAGS_PREFIX: &str = "/flags/"; const SEGMENTS_PREFIX: &str = "/segments/"; @@ -37,7 +37,6 @@ pub type Result = std::result::Result; pub(crate) struct PutData { #[serde(default = "String::default")] path: String, - data: AllData, } @@ -84,6 +83,7 @@ impl StreamingDataSource { ReconnectOptionsBuilder::new(true) .retry_initial(true) .delay(initial_reconnect_delay) + .delay_max(Duration::from_secs(30)) .build(), ) .header("Authorization", sdk_key)? @@ -153,6 +153,16 @@ impl DataSource for StreamingDataSource { }, es::SSE::Event(ev) => ev, }, + Some(Err(es::Error::UnexpectedResponse(status_code))) => { + match is_http_error_recoverable(status_code.as_u16()) { + true => continue, + _ => { + notify_init.call_once(|| (init_complete)(false)); + warn!("Returned unrecoverable failure. Unexpected response {:?}", status_code); + break + } + } + }, Some(Err(e)) => { error!("error on event stream: {:?}", e); @@ -188,12 +198,9 @@ impl DataSource for StreamingDataSource { error!("error processing update: {:?}", e); } - // Only want to notify for the first event. - // TODO: When error handling is added this should happen once we are successful, - // or if we have encountered an unrecoverable error. notify_init.call_once(|| (init_complete)(init_success)); }, - }; + } } }); } @@ -252,7 +259,7 @@ impl DataSource for PollingDataSource { loop { futures::select! { _ = interval.next() => { - match feature_requester.get_all() { + match feature_requester.get_all().await { Ok(all_data) => { let mut data_store = data_store.write(); data_store.init(all_data); @@ -269,7 +276,7 @@ impl DataSource for PollingDataSource { }; }, _ = shutdown_future => break - }; + } } }); } @@ -301,7 +308,7 @@ pub(crate) struct MockDataSource { #[cfg(test)] impl MockDataSource { pub fn new_with_init_delay(delay_init: u64) -> Self { - return MockDataSource { delay_init }; + MockDataSource { delay_init } } } @@ -389,10 +396,8 @@ mod tests { use tokio::sync::broadcast; use super::{DataSource, PollingDataSource, StreamingDataSource}; - use crate::{ - feature_requester_builders::ReqwestFeatureRequesterBuilder, - stores::store::InMemoryDataStore, LAUNCHDARKLY_TAGS_HEADER, - }; + use crate::feature_requester_builders::HyperFeatureRequesterBuilder; + use crate::{stores::store::InMemoryDataStore, LAUNCHDARKLY_TAGS_HEADER}; #[test_case(Some("application-id/abc:application-sha/xyz".into()), "application-id/abc:application-sha/xyz")] #[test_case(None, Matcher::Missing)] @@ -464,11 +469,10 @@ mod tests { let (shutdown_tx, _) = broadcast::channel::<()>(1); let initialized = Arc::new(AtomicBool::new(false)); - let reqwest_builder = - ReqwestFeatureRequesterBuilder::new(&mockito::server_url(), "sdk-key"); + let hyper_builder = HyperFeatureRequesterBuilder::new(&mockito::server_url(), "sdk-key"); let polling = PollingDataSource::new( - Arc::new(Mutex::new(Box::new(reqwest_builder))), + Arc::new(Mutex::new(Box::new(hyper_builder))), Duration::from_secs(10), tag, ); diff --git a/launchdarkly-server-sdk/src/data_source_builders.rs b/launchdarkly-server-sdk/src/data_source_builders.rs index f111283..c668b4d 100644 --- a/launchdarkly-server-sdk/src/data_source_builders.rs +++ b/launchdarkly-server-sdk/src/data_source_builders.rs @@ -1,6 +1,6 @@ use super::service_endpoints; use crate::data_source::{DataSource, NullDataSource, PollingDataSource, StreamingDataSource}; -use crate::feature_requester_builders::{FeatureRequesterFactory, ReqwestFeatureRequesterBuilder}; +use crate::feature_requester_builders::{FeatureRequesterFactory, HyperFeatureRequesterBuilder}; use eventsource_client::HttpsConnector; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -238,7 +238,7 @@ impl DataSourceFactory for PollingDataSourceBuilder { let feature_requester_factory: Arc>> = match &self.feature_requester_factory { Some(factory) => factory.clone(), - _ => Arc::new(Mutex::new(Box::new(ReqwestFeatureRequesterBuilder::new( + _ => Arc::new(Mutex::new(Box::new(HyperFeatureRequesterBuilder::new( endpoints.polling_base_url(), sdk_key, )))), @@ -284,7 +284,6 @@ impl MockDataSourceBuilder { #[cfg(test)] impl DataSourceFactory for MockDataSourceBuilder { - // TODO: Implement re-connect with jitter. fn build( &self, _endpoints: &service_endpoints::ServiceEndpoints, diff --git a/launchdarkly-server-sdk/src/evaluation.rs b/launchdarkly-server-sdk/src/evaluation.rs index b4685c1..033615d 100644 --- a/launchdarkly-server-sdk/src/evaluation.rs +++ b/launchdarkly-server-sdk/src/evaluation.rs @@ -1,7 +1,7 @@ use super::stores::store::DataStore; use serde::Serialize; -use launchdarkly_server_sdk_evaluation::{evaluate, FlagValue, Reason, User}; +use launchdarkly_server_sdk_evaluation::{evaluate, Context, FlagValue, Reason}; use std::collections::HashMap; use std::time::SystemTime; @@ -108,8 +108,8 @@ impl FlagDetail { } /// Populate the FlagDetail struct with the results of every flag found within the provided - /// store, evaluated for the specified user. - pub fn populate(&mut self, store: &dyn DataStore, user: &User, config: FlagDetailConfig) { + /// store, evaluated for the specified context. + pub fn populate(&mut self, store: &dyn DataStore, context: &Context, config: FlagDetailConfig) { let mut evaluations = HashMap::new(); let mut flag_state = HashMap::new(); @@ -118,7 +118,7 @@ impl FlagDetail { continue; } - let detail = evaluate(store.to_store(), &flag, user, None); + let detail = evaluate(store.to_store(), &flag, context, None); // Here we are applying the same logic used in EventFactory.new_feature_request_event // to determine whether the evaluation involved an experiment, in which case both @@ -188,11 +188,13 @@ mod tests { use crate::stores::store_types::{PatchTarget, StorageItem}; use crate::test_common::basic_flag; use crate::FlagDetailConfig; - use launchdarkly_server_sdk_evaluation::User; + use launchdarkly_server_sdk_evaluation::ContextBuilder; #[test] fn flag_detail_handles_default_configuration() { - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); let mut store = InMemoryDataStore::new(); store @@ -203,7 +205,7 @@ mod tests { .expect("patch should apply"); let mut flag_detail = FlagDetail::new(true); - flag_detail.populate(&store, &user, FlagDetailConfig::new()); + flag_detail.populate(&store, &context, FlagDetailConfig::new()); let expected = json!({ "myFlag": true, @@ -224,7 +226,9 @@ mod tests { #[test] fn flag_detail_handles_experimentation_reasons_correctly() { - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); let mut store = InMemoryDataStore::new(); let mut flag = basic_flag("myFlag"); @@ -236,7 +240,7 @@ mod tests { .expect("patch should apply"); let mut flag_detail = FlagDetail::new(true); - flag_detail.populate(&store, &user, FlagDetailConfig::new()); + flag_detail.populate(&store, &context, FlagDetailConfig::new()); let expected = json!({ "myFlag": true, @@ -262,7 +266,9 @@ mod tests { #[test] fn flag_detail_with_reasons_should_include_reason() { - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); let mut store = InMemoryDataStore::new(); store @@ -276,7 +282,7 @@ mod tests { config.with_reasons(); let mut flag_detail = FlagDetail::new(true); - flag_detail.populate(&store, &user, config); + flag_detail.populate(&store, &context, config); let expected = json!({ "myFlag": true, @@ -300,7 +306,9 @@ mod tests { #[test] fn flag_detail_details_only_should_exclude_reason() { - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); let mut store = InMemoryDataStore::new(); store @@ -314,7 +322,7 @@ mod tests { config.details_only_for_tracked_flags(); let mut flag_detail = FlagDetail::new(true); - flag_detail.populate(&store, &user, config); + flag_detail.populate(&store, &context, config); let expected = json!({ "myFlag": true, @@ -334,7 +342,9 @@ mod tests { #[test] fn flag_detail_details_only_with_tracked_events_includes_version() { - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); let mut store = InMemoryDataStore::new(); let mut flag = basic_flag("myFlag"); flag.track_events = true; @@ -347,7 +357,7 @@ mod tests { config.details_only_for_tracked_flags(); let mut flag_detail = FlagDetail::new(true); - flag_detail.populate(&store, &user, config); + flag_detail.populate(&store, &context, config); let expected = json!({ "myFlag": true, @@ -369,7 +379,9 @@ mod tests { #[test] fn flag_detail_with_default_config_but_tracked_event_should_include_version() { - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); let mut store = InMemoryDataStore::new(); let mut flag = basic_flag("myFlag"); flag.track_events = true; @@ -379,7 +391,7 @@ mod tests { .expect("patch should apply"); let mut flag_detail = FlagDetail::new(true); - flag_detail.populate(&store, &user, FlagDetailConfig::new()); + flag_detail.populate(&store, &context, FlagDetailConfig::new()); let expected = json!({ "myFlag": true, diff --git a/launchdarkly-server-sdk/src/events/dispatcher.rs b/launchdarkly-server-sdk/src/events/dispatcher.rs index 61d6b57..e4efd5d 100644 --- a/launchdarkly-server-sdk/src/events/dispatcher.rs +++ b/launchdarkly-server-sdk/src/events/dispatcher.rs @@ -2,9 +2,8 @@ use crossbeam_channel::{bounded, select, tick, Receiver, Sender}; use std::collections::HashSet; use std::iter::FromIterator; use std::time::SystemTime; -use threadpool::ThreadPool; -use launchdarkly_server_sdk_evaluation::User; +use launchdarkly_server_sdk_evaluation::Context; use lru::LruCache; use super::event::FeatureRequestEvent; @@ -71,31 +70,47 @@ impl Outbox { } pub(super) struct EventDispatcher { - flush_pool: ThreadPool, outbox: Outbox, - user_keys: LruCache, + context_keys: LruCache, events_configuration: EventsConfiguration, last_known_time: u128, disabled: bool, + thread_count: usize, } impl EventDispatcher { pub(super) fn new(events_configuration: EventsConfiguration) -> Self { Self { - flush_pool: ThreadPool::new(5), outbox: Outbox::new(events_configuration.capacity), - user_keys: LruCache::::new(events_configuration.user_keys_capacity), + context_keys: LruCache::::new(events_configuration.context_keys_capacity), events_configuration, last_known_time: 0, disabled: false, + thread_count: 5, } } pub(super) fn start(&mut self, inbox_rx: Receiver) { - let reset_user_cache_ticker = tick(self.events_configuration.user_keys_flush_interval); + let reset_context_cache_ticker = + tick(self.events_configuration.context_keys_flush_interval); let flush_ticker = tick(self.events_configuration.flush_interval); - let (event_result_tx, event_result_rx) = - bounded::(self.flush_pool.max_count()); + let (event_result_tx, event_result_rx) = bounded::(self.thread_count); + + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(self.thread_count) + .enable_io() + .enable_time() + .build(); + + let rt = match rt { + Ok(rt) => rt, + Err(e) => { + error!("Could not start runtime for event sending: {}", e); + return; + } + }; + + let (send, recv) = bounded::<()>(1); loop { debug!("waiting for a batch to send"); @@ -114,7 +129,7 @@ impl EventDispatcher { return; } }, - recv(reset_user_cache_ticker) -> _ => self.user_keys.clear(), + recv(reset_context_cache_ticker) -> _ => self.context_keys.clear(), recv(flush_ticker) -> _ => break, recv(inbox_rx) -> result => match result { Ok(EventDispatcherMessage::Flush) => break, @@ -124,7 +139,9 @@ impl EventDispatcher { } } Ok(EventDispatcherMessage::Close(sender)) => { - self.flush_pool.join(); + drop(send); + //Should unblock once all the senders are dropped. + let _ = recv.recv(); // We call drop here to make sure this receiver is completely // disconnected. This ensures the event processor cannot send another @@ -149,17 +166,17 @@ impl EventDispatcher { continue; } - if !self.outbox.is_empty() - && self.flush_pool.max_count() != self.flush_pool.active_count() - { + if !self.outbox.is_empty() { let payload = self.outbox.get_payload(); debug!("Sending batch of {} events", payload.len()); let sender = self.events_configuration.event_sender.clone(); let results = event_result_tx.clone(); - self.flush_pool.execute(move || { - let _ = sender.send_event_data(payload, results); + let send = send.clone(); + rt.spawn(async move { + sender.send_event_data(payload, results).await; + drop(send); }); } } @@ -170,10 +187,7 @@ impl EventDispatcher { InputEvent::FeatureRequest(fre) => { self.outbox.add_to_summary(&fre); - let first_time_seeing_user = self.notice_user(&fre.base.user); - let will_send_full_user_details_in_event = - fre.track_events && self.events_configuration.inline_users_in_events; - if !will_send_full_user_details_in_event && first_time_seeing_user { + if self.notice_context(&fre.base.context) { self.outbox.add_event(OutputEvent::Index(fre.to_index_event( self.events_configuration.all_attributes_private, self.events_configuration.private_attributes.clone(), @@ -197,19 +211,11 @@ impl EventDispatcher { } if fre.track_events { - let event = match self.events_configuration.inline_users_in_events { - true => fre.into_inline( - self.events_configuration.all_attributes_private, - self.events_configuration.private_attributes.clone(), - ), - false => fre, - }; - - self.outbox.add_event(OutputEvent::FeatureRequest(event)); + self.outbox.add_event(OutputEvent::FeatureRequest(fre)); } } InputEvent::Identify(identify) => { - self.notice_user(&identify.base.user); + self.notice_context(&identify.base.context); self.outbox .add_event(OutputEvent::Identify(identify.into_inline( self.events_configuration.all_attributes_private, @@ -218,13 +224,8 @@ impl EventDispatcher { ), ))); } - InputEvent::Alias(alias) => { - self.outbox.add_event(OutputEvent::Alias(alias)); - } InputEvent::Custom(custom) => { - if self.notice_user(&custom.base.user) - && !self.events_configuration.inline_users_in_events - { + if self.notice_context(&custom.base.context) { self.outbox .add_event(OutputEvent::Index(custom.to_index_event( self.events_configuration.all_attributes_private, @@ -232,30 +233,20 @@ impl EventDispatcher { ))); } - let event = match self.events_configuration.inline_users_in_events { - true => custom.into_inline( - self.events_configuration.all_attributes_private, - HashSet::from_iter( - self.events_configuration.private_attributes.iter().cloned(), - ), - ), - false => custom, - }; - - self.outbox.add_event(OutputEvent::Custom(event)); + self.outbox.add_event(OutputEvent::Custom(custom)); } } } - fn notice_user(&mut self, user: &User) -> bool { - let key = user.key().to_string(); + fn notice_context(&mut self, context: &Context) -> bool { + let key = context.canonical_key(); - if self.user_keys.get(&key).is_none() { - trace!("noticing new user {:?}", key); - self.user_keys.put(key, ()); + if self.context_keys.get(key).is_none() { + trace!("noticing new context {:?}", key); + self.context_keys.put(key.to_owned(), ()); true } else { - trace!("ignoring already-seen user {:?}", key); + trace!("ignoring already-seen context {:?}", key); false } } @@ -277,19 +268,21 @@ mod tests { use crate::events::event::{EventFactory, OutputEvent}; use crate::events::{create_event_sender, create_events_configuration}; use crate::test_common::basic_flag; - use launchdarkly_server_sdk_evaluation::{Detail, FlagValue, Reason}; + use launchdarkly_server_sdk_evaluation::{ContextBuilder, Detail, FlagValue, Reason}; use test_case::test_case; #[test] fn get_payload_from_outbox_empties_outbox() { let (event_sender, _) = create_event_sender(); let events_configuration = - create_events_configuration(event_sender, false, Duration::from_secs(100)); + create_events_configuration(event_sender, Duration::from_secs(100)); let mut dispatcher = create_dispatcher(events_configuration); - let user = User::with_key("user".to_string()).build(); + let context = ContextBuilder::new("context") + .build() + .expect("Failed to create context"); let event_factory = EventFactory::new(true); - dispatcher.process_event(event_factory.new_identify(user.clone())); + dispatcher.process_event(event_factory.new_identify(context)); let _ = dispatcher.outbox.get_payload(); assert!(dispatcher.outbox.is_empty()); @@ -299,14 +292,16 @@ mod tests { fn dispatcher_ignores_events_over_capacity() { let (event_sender, _) = create_event_sender(); let events_configuration = - create_events_configuration(event_sender, false, Duration::from_secs(100)); + create_events_configuration(event_sender, Duration::from_secs(100)); let mut dispatcher = create_dispatcher(events_configuration); - let user = User::with_key("user".to_string()).build(); + let context = ContextBuilder::new("context") + .build() + .expect("Failed to create context"); let event_factory = EventFactory::new(true); for _ in 0..10 { - dispatcher.process_event(event_factory.new_identify(user.clone())); + dispatcher.process_event(event_factory.new_identify(context.clone())); } assert_eq!(5, dispatcher.outbox.events.len()); @@ -315,17 +310,19 @@ mod tests { .events .iter() .all(|event| event.kind() == "identify")); - assert_eq!(1, dispatcher.user_keys.len()); + assert_eq!(1, dispatcher.context_keys.len()); } #[test] fn dispatcher_handles_feature_request_events_correctly() { let (event_sender, _) = create_event_sender(); let events_configuration = - create_events_configuration(event_sender, false, Duration::from_secs(100)); + create_events_configuration(event_sender, Duration::from_secs(100)); let mut dispatcher = create_dispatcher(events_configuration); - let user = User::with_key("user".to_string()).build(); + let context = ContextBuilder::new("context") + .build() + .expect("Failed to create context"); let mut flag = basic_flag("flag"); flag.debug_events_until_date = Some(64_060_606_800_000); flag.track_events = true; @@ -341,7 +338,7 @@ mod tests { let event_factory = EventFactory::new(true); let feature_request_event = event_factory.new_eval_event( &flag.key, - user, + context, &flag, detail, FlagValue::from(false), @@ -353,7 +350,7 @@ mod tests { assert_eq!("index", dispatcher.outbox.events[0].kind()); assert_eq!("debug", dispatcher.outbox.events[1].kind()); assert_eq!("feature", dispatcher.outbox.events[2].kind()); - assert_eq!(1, dispatcher.user_keys.len()); + assert_eq!(1, dispatcher.context_keys.len()); assert_eq!(1, dispatcher.outbox.summary.features.len()); } @@ -367,7 +364,9 @@ mod tests { ) { let mut flag = basic_flag("flag"); flag.debug_events_until_date = Some(debug_events_until_date); - let user = User::with_key("foo".to_string()).build(); + let context = ContextBuilder::new("foo") + .build() + .expect("Failed to create context"); let detail = Detail { value: Some(FlagValue::from(false)), variation_index: Some(1), @@ -379,16 +378,16 @@ mod tests { let event_factory = EventFactory::new(true); let feature_request = event_factory.new_eval_event( &flag.key, - user, + context, &flag, - detail.clone(), + detail, FlagValue::from(false), None, ); let (event_sender, event_rx) = create_event_sender(); let events_configuration = - create_events_configuration(event_sender, true, Duration::from_secs(100)); + create_events_configuration(event_sender, Duration::from_secs(100)); let (inbox_tx, inbox_rx) = bounded(events_configuration.capacity); let mut dispatcher = create_dispatcher(events_configuration); @@ -422,66 +421,53 @@ mod tests { } } - #[test] - fn dispatcher_does_not_notice_user_from_alias_event() { - let (event_sender, _) = create_event_sender(); - let events_configuration = - create_events_configuration(event_sender, false, Duration::from_secs(100)); - let mut dispatcher = create_dispatcher(events_configuration); - - let user = User::with_key("user".to_string()).build(); - let previous = User::with_key("previous".to_string()).build(); - let event_factory = EventFactory::new(true); - - dispatcher.process_event(event_factory.new_alias(user.clone(), previous)); - assert_eq!(1, dispatcher.outbox.events.len()); - assert_eq!("alias", dispatcher.outbox.events[0].kind()); - assert_eq!(0, dispatcher.user_keys.len()); - } - #[test] fn dispatcher_only_notices_identity_event_once() { let (event_sender, _) = create_event_sender(); let events_configuration = - create_events_configuration(event_sender, false, Duration::from_secs(100)); + create_events_configuration(event_sender, Duration::from_secs(100)); let mut dispatcher = create_dispatcher(events_configuration); - let user = User::with_key("user".to_string()).build(); + let context = ContextBuilder::new("context") + .build() + .expect("Failed to create context"); let event_factory = EventFactory::new(true); - dispatcher.process_event(event_factory.new_identify(user.clone())); - dispatcher.process_event(event_factory.new_identify(user)); + dispatcher.process_event(event_factory.new_identify(context.clone())); + dispatcher.process_event(event_factory.new_identify(context)); assert_eq!(2, dispatcher.outbox.events.len()); assert_eq!("identify", dispatcher.outbox.events[0].kind()); assert_eq!("identify", dispatcher.outbox.events[1].kind()); - assert_eq!(1, dispatcher.user_keys.len()); + assert_eq!(1, dispatcher.context_keys.len()); } #[test] fn dispatcher_adds_index_on_custom_event() { let (event_sender, _) = create_event_sender(); let events_configuration = - create_events_configuration(event_sender, false, Duration::from_secs(100)); + create_events_configuration(event_sender, Duration::from_secs(100)); let mut dispatcher = create_dispatcher(events_configuration); - let user = User::with_key("user".to_string()).build(); + let context = ContextBuilder::new("context") + .build() + .expect("Failed to create context"); let event_factory = EventFactory::new(true); let custom_event = event_factory - .new_custom(user, "user", None, "") + .new_custom(context, "context", None, "") .expect("failed to make new custom event"); dispatcher.process_event(custom_event); assert_eq!(2, dispatcher.outbox.events.len()); assert_eq!("index", dispatcher.outbox.events[0].kind()); assert_eq!("custom", dispatcher.outbox.events[1].kind()); - assert_eq!(1, dispatcher.user_keys.len()); + assert_eq!(1, dispatcher.context_keys.len()); } #[test] fn can_process_events_successfully() { let (event_sender, event_rx) = create_event_sender(); let events_configuration = - create_events_configuration(event_sender, false, Duration::from_secs(100)); + create_events_configuration(event_sender, Duration::from_secs(100)); let (inbox_tx, inbox_rx) = bounded(events_configuration.capacity); let mut dispatcher = create_dispatcher(events_configuration); @@ -489,12 +475,14 @@ mod tests { .spawn(move || dispatcher.start(inbox_rx)) .unwrap(); - let user = User::with_key("user".to_string()).build(); + let context = ContextBuilder::new("context") + .build() + .expect("Failed to create context"); let event_factory = EventFactory::new(true); inbox_tx .send(EventDispatcherMessage::EventMessage( - event_factory.new_identify(user), + event_factory.new_identify(context), )) .expect("event send failed"); inbox_tx @@ -508,15 +496,14 @@ mod tests { rx.recv().expect("failed to notify on close"); dispatcher_handle.join().unwrap(); - let events = event_rx.iter().collect::>(); - assert_eq!(1, events.len()); + assert_eq!(event_rx.iter().count(), 1); } #[test] fn dispatcher_flushes_periodically() { let (event_sender, event_rx) = create_event_sender(); let events_configuration = - create_events_configuration(event_sender, false, Duration::from_millis(200)); + create_events_configuration(event_sender, Duration::from_millis(200)); let (inbox_tx, inbox_rx) = bounded(events_configuration.capacity); let mut dispatcher = create_dispatcher(events_configuration); @@ -524,18 +511,19 @@ mod tests { .spawn(move || dispatcher.start(inbox_rx)) .unwrap(); - let user = User::with_key("user".to_string()).build(); + let context = ContextBuilder::new("context") + .build() + .expect("Failed to create context"); let event_factory = EventFactory::new(true); inbox_tx .send(EventDispatcherMessage::EventMessage( - event_factory.new_identify(user), + event_factory.new_identify(context), )) .expect("event send failed"); - std::thread::sleep(Duration::from_millis(300)); - let events = event_rx.try_iter().collect::>(); - assert_eq!(1, events.len()); + thread::sleep(Duration::from_millis(300)); + assert_eq!(event_rx.try_iter().count(), 1); } fn create_dispatcher(events_configuration: EventsConfiguration) -> EventDispatcher { diff --git a/launchdarkly-server-sdk/src/events/event.rs b/launchdarkly-server-sdk/src/events/event.rs index eef9ab2..ea505d0 100644 --- a/launchdarkly-server-sdk/src/events/event.rs +++ b/launchdarkly-server-sdk/src/events/event.rs @@ -3,43 +3,21 @@ use std::collections::{HashMap, HashSet}; use std::fmt::{self, Display, Formatter}; use launchdarkly_server_sdk_evaluation::{ - Detail, Flag, FlagValue, Reason, User, UserAttributes, VariationIndex, + Context, ContextAttributes, Detail, Flag, FlagValue, Kind, Reason, Reference, VariationIndex, }; use serde::ser::SerializeStruct; use serde::{Serialize, Serializer}; -#[derive(Clone, Debug, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum ContextKind { - AnonymousUser, - User, -} - -impl ContextKind { - fn is_user(&self) -> bool { - *self == ContextKind::User - } -} - -impl From for ContextKind { - fn from(user: User) -> Self { - match user.anonymous() { - Some(true) => Self::AnonymousUser, - Some(false) | None => Self::User, - } - } -} - #[derive(Clone, Debug, PartialEq)] pub struct BaseEvent { pub creation_date: u64, - pub user: User, + pub context: Context, // These attributes will not be serialized. They exist only to help serialize base event into // the right structure inline: bool, all_attribute_private: bool, - global_private_attributes: HashSet, + global_private_attributes: HashSet, } impl Serialize for BaseEvent { @@ -51,14 +29,14 @@ impl Serialize for BaseEvent { state.serialize_field("creationDate", &self.creation_date)?; if self.inline { - let user_attribute = UserAttributes::from_user( - self.user.clone(), + let context_attribute = ContextAttributes::from_context( + self.context.clone(), self.all_attribute_private, - &self.global_private_attributes, + self.global_private_attributes.clone(), ); - state.serialize_field("user", &user_attribute)?; + state.serialize_field("context", &context_attribute)?; } else { - state.serialize_field("userKey", &self.user.key())?; + state.serialize_field("contextKeys", &self.context.context_keys())?; } state.end() @@ -66,10 +44,10 @@ impl Serialize for BaseEvent { } impl BaseEvent { - pub fn new(creation_date: u64, user: User) -> Self { + pub fn new(creation_date: u64, context: Context) -> Self { Self { creation_date, - user, + context, inline: false, all_attribute_private: false, global_private_attributes: HashSet::new(), @@ -79,7 +57,7 @@ impl BaseEvent { pub(crate) fn into_inline( self, all_attribute_private: bool, - global_private_attributes: HashSet, + global_private_attributes: HashSet, ) -> Self { Self { inline: true, @@ -104,8 +82,6 @@ pub struct FeatureRequestEvent { version: Option, #[serde(skip_serializing_if = "Option::is_none")] prereq_of: Option, - #[serde(skip_serializing_if = "ContextKind::is_user")] - context_kind: ContextKind, #[serde(skip)] pub(crate) track_events: bool, @@ -118,7 +94,7 @@ impl FeatureRequestEvent { pub fn to_index_event( &self, all_attribute_private: bool, - global_private_attributes: HashSet, + global_private_attributes: HashSet, ) -> IndexEvent { self.base .clone() @@ -129,7 +105,7 @@ impl FeatureRequestEvent { pub(crate) fn into_inline( self, all_attribute_private: bool, - global_private_attributes: HashSet, + global_private_attributes: HashSet, ) -> Self { Self { base: self @@ -168,7 +144,7 @@ impl IdentifyEvent { pub(crate) fn into_inline( self, all_attribute_private: bool, - global_private_attributes: HashSet, + global_private_attributes: HashSet, ) -> Self { Self { base: self @@ -179,16 +155,6 @@ impl IdentifyEvent { } } -#[derive(Clone, Debug, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct AliasEvent { - pub creation_date: u64, - key: String, - context_kind: ContextKind, - previous_key: String, - previous_context_kind: ContextKind, -} - #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] pub struct CustomEvent { @@ -199,34 +165,19 @@ pub struct CustomEvent { metric_value: Option, #[serde(skip_serializing_if = "serde_json::Value::is_null")] data: serde_json::Value, - #[serde(skip_serializing_if = "ContextKind::is_user")] - context_kind: ContextKind, } impl CustomEvent { pub fn to_index_event( &self, all_attribute_private: bool, - global_private_attributes: HashSet, + global_private_attributes: HashSet, ) -> IndexEvent { self.base .clone() .into_inline(all_attribute_private, global_private_attributes) .into() } - - pub(crate) fn into_inline( - self, - all_attribute_private: bool, - global_private_attributes: HashSet, - ) -> Self { - Self { - base: self - .base - .into_inline(all_attribute_private, global_private_attributes), - ..self - } - } } #[derive(Clone, Debug, Serialize)] @@ -245,9 +196,6 @@ pub enum OutputEvent { #[serde(rename = "identify")] Identify(IdentifyEvent), - #[serde(rename = "alias")] - Alias(AliasEvent), - #[serde(rename = "custom")] Custom(CustomEvent), @@ -263,7 +211,6 @@ impl OutputEvent { OutputEvent::Debug { .. } => "debug", OutputEvent::FeatureRequest { .. } => "feature", OutputEvent::Identify { .. } => "identify", - OutputEvent::Alias { .. } => "alias", OutputEvent::Custom { .. } => "custom", OutputEvent::Summary { .. } => "summary", } @@ -274,7 +221,6 @@ impl OutputEvent { pub enum InputEvent { FeatureRequest(FeatureRequestEvent), Identify(IdentifyEvent), - Alias(AliasEvent), Custom(CustomEvent), } @@ -284,7 +230,6 @@ impl InputEvent { match self { InputEvent::FeatureRequest(FeatureRequestEvent { base, .. }) => Some(base), InputEvent::Identify(IdentifyEvent { base, .. }) => Some(base), - InputEvent::Alias(_) => None, InputEvent::Custom(CustomEvent { base, .. }) => Some(base), } } @@ -317,37 +262,34 @@ impl EventFactory { pub fn new_unknown_flag_event( &self, flag_key: &str, - user: User, + context: Context, detail: Detail, default: FlagValue, ) -> InputEvent { - self.new_feature_request_event(flag_key, user, None, detail, default, None) + self.new_feature_request_event(flag_key, context, None, detail, default, None) } pub fn new_eval_event( &self, flag_key: &str, - user: User, + context: Context, flag: &Flag, detail: Detail, default: FlagValue, prereq_of: Option, ) -> InputEvent { - self.new_feature_request_event(flag_key, user, Some(flag), detail, default, prereq_of) + self.new_feature_request_event(flag_key, context, Some(flag), detail, default, prereq_of) } fn new_feature_request_event( &self, flag_key: &str, - user: User, + context: Context, flag: Option<&Flag>, detail: Detail, default: FlagValue, prereq_of: Option, ) -> InputEvent { - // TODO(ch108604) Events created during prereq evaluation might not have a value set (e.g. - // flag is off and the off variation is None). In those situations, we are going to default - // to a JSON null until we can better sort out the Detail struct. let value = detail .value .unwrap_or(FlagValue::Json(serde_json::Value::Null)); @@ -373,7 +315,7 @@ impl EventFactory { }; InputEvent::FeatureRequest(FeatureRequestEvent { - base: BaseEvent::new(Self::now(), user.clone()), + base: BaseEvent::new(Self::now(), context), key: flag_key.to_owned(), default, reason, @@ -381,32 +323,21 @@ impl EventFactory { variation: detail.variation_index, version: flag.map(|f| f.version), prereq_of, - context_kind: user.into(), track_events: flag_track_events || require_experiment_data, debug_events_until_date, }) } - pub fn new_identify(&self, user: User) -> InputEvent { + pub fn new_identify(&self, context: Context) -> InputEvent { InputEvent::Identify(IdentifyEvent { - key: user.key().to_string(), - base: BaseEvent::new(Self::now(), user), - }) - } - - pub fn new_alias(&self, user: User, previous_user: User) -> InputEvent { - InputEvent::Alias(AliasEvent { - creation_date: Self::now(), - key: user.key().to_string(), - context_kind: user.into(), - previous_key: previous_user.key().to_string(), - previous_context_kind: previous_user.into(), + key: context.key().to_owned(), + base: BaseEvent::new(Self::now(), context), }) } pub fn new_custom( &self, - user: User, + context: Context, key: impl Into, metric_value: Option, data: impl Serialize, @@ -414,21 +345,20 @@ impl EventFactory { let data = serde_json::to_value(data)?; Ok(InputEvent::Custom(CustomEvent { - base: BaseEvent::new(Self::now(), user.clone()), + base: BaseEvent::new(Self::now(), context), key: key.into(), metric_value, data, - context_kind: user.into(), })) } } -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, Serialize)] #[serde(into = "EventSummaryOutput")] pub struct EventSummary { - pub start_date: u64, - pub end_date: u64, - pub features: HashMap, + pub(crate) start_date: u64, + pub(crate) end_date: u64, + pub(crate) features: HashMap, } impl Default for EventSummary { @@ -440,7 +370,7 @@ impl Default for EventSummary { impl EventSummary { pub fn new() -> Self { EventSummary { - start_date: std::u64::MAX, + start_date: u64::MAX, end_date: 0, features: HashMap::new(), } @@ -452,7 +382,12 @@ impl EventSummary { pub fn add(&mut self, event: &FeatureRequestEvent) { let FeatureRequestEvent { - base: BaseEvent { creation_date, .. }, + base: + BaseEvent { + creation_date, + context, + .. + }, key, value, version, @@ -465,31 +400,64 @@ impl EventSummary { self.end_date = max(self.end_date, *creation_date); let variation_key = VariationKey { - flag_key: key.clone(), version: *version, variation: *variation, }; - match self.features.get_mut(&variation_key) { - Some(summary) => summary.count_request(value, default), - None => { - self.features.insert( - variation_key, - VariationSummary::new(value.clone(), default.clone()), - ); - } - } + + let feature = self + .features + .entry(key.clone()) + .or_insert_with(|| FlagSummary::new(default.clone())); + + feature.track(variation_key, value, context); } pub fn reset(&mut self) { self.features.clear(); - self.start_date = std::u64::MAX; + self.start_date = u64::MAX; self.end_date = 0; } } +#[derive(Clone, Debug)] +pub struct FlagSummary { + pub(crate) counters: HashMap, + pub(crate) default: FlagValue, + pub(crate) context_kinds: HashSet, +} + +impl FlagSummary { + pub fn new(default: FlagValue) -> Self { + Self { + counters: HashMap::new(), + default, + context_kinds: HashSet::new(), + } + } + + pub fn track( + &mut self, + variation_key: VariationKey, + value: &FlagValue, + context: &Context, + ) -> &mut Self { + if let Some(summary) = self.counters.get_mut(&variation_key) { + summary.count_request(); + } else { + self.counters + .insert(variation_key, VariationSummary::new(value.clone())); + } + + for kind in context.kinds() { + self.context_kinds.insert(kind.clone()); + } + + self + } +} + #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct VariationKey { - pub flag_key: String, pub version: Option, pub variation: Option, } @@ -498,27 +466,15 @@ pub struct VariationKey { pub struct VariationSummary { pub count: u64, pub value: FlagValue, - pub default: FlagValue, } impl VariationSummary { - fn new(value: FlagValue, default: FlagValue) -> Self { - VariationSummary { - count: 1, - value, - default, - } + fn new(value: FlagValue) -> Self { + VariationSummary { count: 1, value } } - fn count_request(&mut self, value: &FlagValue, default: &FlagValue) { + fn count_request(&mut self) { self.count += 1; - - if &self.value != value { - self.value = value.clone(); - } - if &self.default != default { - self.default = default.clone(); - } } } @@ -537,18 +493,11 @@ struct EventSummaryOutput { impl From for EventSummaryOutput { fn from(summary: EventSummary) -> Self { - let mut features = HashMap::new(); - - for (variation_key, variation_summary) in summary.features { - match features.get_mut(&variation_key.flag_key) { - None => { - let feature_summary = - FeatureSummaryOutput::from((&variation_key, variation_summary)); - features.insert(variation_key.flag_key, feature_summary); - } - Some(feature_summary) => feature_summary.add(&variation_key, variation_summary), - } - } + let features = summary + .features + .into_iter() + .map(|(key, value)| (key, value.into())) + .collect(); EventSummaryOutput { start_date: summary.start_date, @@ -559,40 +508,29 @@ impl From for EventSummaryOutput { } #[derive(Serialize)] +#[serde(rename_all = "camelCase")] struct FeatureSummaryOutput { default: FlagValue, + context_kinds: HashSet, counters: Vec, } -impl From<(&VariationKey, VariationSummary)> for FeatureSummaryOutput { - fn from((variation_key, variation_summary): (&VariationKey, VariationSummary)) -> Self { - let counters = vec![VariationCounterOutput::from(( - variation_key, - variation_summary.value, - variation_summary.count, - ))]; +impl From for FeatureSummaryOutput { + fn from(flag_summary: FlagSummary) -> Self { + let counters = flag_summary + .counters + .into_iter() + .map(|(variation_key, variation_summary)| (variation_key, variation_summary).into()) + .collect::>(); - FeatureSummaryOutput { - default: variation_summary.default, + Self { + default: flag_summary.default, + context_kinds: flag_summary.context_kinds, counters, } } } -impl FeatureSummaryOutput { - fn add(&mut self, variation_key: &VariationKey, variation_summary: VariationSummary) { - if self.default != variation_summary.default { - self.default = variation_summary.default; - } - - self.counters.push(VariationCounterOutput::from(( - variation_key, - variation_summary.value, - variation_summary.count, - ))); - } -} - #[derive(Serialize)] struct VariationCounterOutput { pub value: FlagValue, @@ -605,13 +543,13 @@ struct VariationCounterOutput { pub variation: Option, } -impl From<(&VariationKey, FlagValue, u64)> for VariationCounterOutput { - fn from((variation_key, flag_value, count): (&VariationKey, FlagValue, u64)) -> Self { +impl From<(VariationKey, VariationSummary)> for VariationCounterOutput { + fn from((variation_key, variation_summary): (VariationKey, VariationSummary)) -> Self { VariationCounterOutput { - value: flag_value, + value: variation_summary.value, unknown: variation_key.version.map_or(Some(true), |_| None), version: variation_key.version, - count, + count: variation_summary.count, variation: variation_key.variation, } } @@ -619,27 +557,25 @@ impl From<(&VariationKey, FlagValue, u64)> for VariationCounterOutput { #[cfg(test)] mod tests { + use launchdarkly_server_sdk_evaluation::{ + AttributeValue, ContextBuilder, Kind, MultiContextBuilder, + }; use maplit::{hashmap, hashset}; use super::*; use crate::test_common::basic_flag; + use assert_json_diff::assert_json_eq; + use serde_json::json; use test_case::test_case; - #[test_case(None, ContextKind::User; "default users are user")] - #[test_case(Some(false), ContextKind::User; "non-anonymous users are user")] - #[test_case(Some(true), ContextKind::AnonymousUser; "anonymous users are anonymousUser")] - fn eval_event_context_kind_is_set_appropriately( - is_anonymous: Option, - context_kind: ContextKind, - ) { + #[test] + fn serializes_feature_request_event() { let flag = basic_flag("flag"); let default = FlagValue::from(false); - let mut user = User::with_key("alice".to_string()); - - if let Some(b) = is_anonymous { - user.anonymous(b); - } - + let context = ContextBuilder::new("alice") + .anonymous(true) + .build() + .expect("Failed to create context"); let fallthrough = Detail { value: Some(FlagValue::from(false)), variation_index: Some(1), @@ -649,81 +585,46 @@ mod tests { }; let event_factory = EventFactory::new(true); - let eval_event = event_factory.new_eval_event( - &flag.key.clone(), - user.build(), - &flag, - fallthrough.clone(), - default.clone(), - None, - ); - - if let InputEvent::FeatureRequest(event) = eval_event { - assert_eq!(event.context_kind, context_kind); - } else { - panic!("new_eval_event did not create a FeatureRequestEvent"); - } - } - - #[test_case(false, false, ContextKind::User, ContextKind::User; "neither are anonymous")] - #[test_case(true, true, ContextKind::AnonymousUser, ContextKind::AnonymousUser; "both are anonymous")] - #[test_case(false, true, ContextKind::User, ContextKind::AnonymousUser; "previous is anonymous")] - #[test_case(true, false, ContextKind::AnonymousUser, ContextKind::User; "user is anonymous")] - fn alias_event_contains_correct_information( - is_anonymous: bool, - previous_is_anonymous: bool, - context_kind: ContextKind, - previous_context_kind: ContextKind, - ) { - let user = User::with_key("alice".to_string()) - .anonymous(is_anonymous) - .build(); - let previous_user = User::with_key("previous-alice".to_string()) - .anonymous(previous_is_anonymous) - .build(); - - let event_factory = EventFactory::new(true); - let event = event_factory.new_alias(user.clone(), previous_user.clone()); - - if let InputEvent::Alias(alias) = event { - assert_eq!(alias.key, user.key()); - assert_eq!(alias.context_kind, context_kind); - assert_eq!(alias.previous_key, previous_user.key()); - assert_eq!(alias.previous_context_kind, previous_context_kind); - } else { - panic!("new_alias did not create an AliasEvent"); - } - } - - #[test_case(None, ContextKind::User; "default users are user")] - #[test_case(Some(false), ContextKind::User; "non-anonymous users are user")] - #[test_case(Some(true), ContextKind::AnonymousUser; "anonymous users are anonymousUser")] - fn custom_event_context_kind_is_set_appropriately( - is_anonymous: Option, - context_kind: ContextKind, - ) { - let flag = basic_flag("flag"); - let mut user = User::with_key("alice".to_string()); - - if let Some(b) = is_anonymous { - user.anonymous(b); - } - - let event_factory = EventFactory::new(true); - let custom_event = event_factory.new_custom(user.build(), &flag.key.clone(), None, ""); + let mut feature_request_event = + event_factory.new_eval_event(&flag.key, context, &flag, fallthrough, default, None); + // fix creation date so JSON is predictable + feature_request_event.base_mut().unwrap().creation_date = 1234; - if let Ok(InputEvent::Custom(event)) = custom_event { - assert_eq!(event.context_kind, context_kind); - } else { - panic!("new_custom did not create a Custom event"); + if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event { + let output_event = OutputEvent::FeatureRequest( + feature_request_event.into_inline(false, HashSet::new()), + ); + let event_json = json!({ + "kind": "feature", + "creationDate": 1234, + "context": { + "key": "alice", + "kind": "user", + "anonymous": true + }, + "key": "flag", + "value": false, + "variation": 1, + "default": false, + "reason": { + "kind": "FALLTHROUGH" + }, + "version": 42 + }); + + assert_json_eq!(output_event, event_json); } } #[test] - fn serializes_feature_request_event() { + fn serializes_feature_request_event_with_global_private_attribute() { let flag = basic_flag("flag"); let default = FlagValue::from(false); - let user = User::with_key("alice".to_string()).anonymous(true).build(); + let context = ContextBuilder::new("alice") + .anonymous(true) + .set_value("foo", AttributeValue::Bool(true)) + .build() + .expect("Failed to create context"); let fallthrough = Detail { value: Some(FlagValue::from(false)), variation_index: Some(1), @@ -733,55 +634,49 @@ mod tests { }; let event_factory = EventFactory::new(true); - let mut feature_request_event = event_factory.new_eval_event( - &flag.key.clone(), - user, - &flag, - fallthrough.clone(), - default.clone(), - None, - ); + let mut feature_request_event = + event_factory.new_eval_event(&flag.key, context, &flag, fallthrough, default, None); // fix creation date so JSON is predictable feature_request_event.base_mut().unwrap().creation_date = 1234; if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event { let output_event = OutputEvent::FeatureRequest( - feature_request_event.into_inline(false, HashSet::new()), + feature_request_event.into_inline(false, hashset!["foo".into()]), ); - let event_json = r#"{ - "kind": "feature", - "creationDate": 1234, - "user": { - "key": "alice", - "anonymous": true - }, - "key": "flag", - "value": false, - "variation": 1, - "default": false, - "reason": { - "kind": "FALLTHROUGH" - }, - "version": 42, - "contextKind": "anonymousUser" -} - "# - .trim(); - - let json = serde_json::to_string_pretty(&output_event); - assert!(json.is_ok()); - assert_eq!(json.unwrap(), event_json.to_string()); + let event_json = json!({ + "kind": "feature", + "creationDate": 1234, + "context": { + "key": "alice", + "kind": "user", + "anonymous": true, + "_meta" : { + "redactedAttributes" : ["foo"] + } + }, + "key": "flag", + "value": false, + "variation": 1, + "default": false, + "reason": { + "kind": "FALLTHROUGH" + }, + "version": 42 + }); + + assert_json_eq!(output_event, event_json); } } #[test] - fn serializes_feature_request_event_with_private_attributes() { + fn serializes_feature_request_event_with_all_private_attributes() { let flag = basic_flag("flag"); let default = FlagValue::from(false); - let user = User::with_key("alice".to_string()) + let context = ContextBuilder::new("alice") .anonymous(true) - .secondary("Secondary") - .build(); + .set_value("foo", AttributeValue::Bool(true)) + .build() + .expect("Failed to create context"); let fallthrough = Detail { value: Some(FlagValue::from(false)), variation_index: Some(1), @@ -791,58 +686,50 @@ mod tests { }; let event_factory = EventFactory::new(true); - let mut feature_request_event = event_factory.new_eval_event( - &flag.key.clone(), - user, - &flag, - fallthrough.clone(), - default.clone(), - None, - ); + let mut feature_request_event = + event_factory.new_eval_event(&flag.key, context, &flag, fallthrough, default, None); // fix creation date so JSON is predictable feature_request_event.base_mut().unwrap().creation_date = 1234; if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event { let output_event = OutputEvent::FeatureRequest( - feature_request_event.into_inline(false, hashset!["secondary".into()]), + feature_request_event.into_inline(true, HashSet::new()), ); - let event_json = r#"{ - "kind": "feature", - "creationDate": 1234, - "user": { - "key": "alice", - "anonymous": true, - "privateAttrs": [ - "secondary" - ] - }, - "key": "flag", - "value": false, - "variation": 1, - "default": false, - "reason": { - "kind": "FALLTHROUGH" - }, - "version": 42, - "contextKind": "anonymousUser" -} - "# - .trim(); - - let json = serde_json::to_string_pretty(&output_event); - assert!(json.is_ok()); - assert_eq!(json.unwrap(), event_json.to_string()); + let event_json = json!({ + "kind": "feature", + "creationDate": 1234, + "context": { + "_meta": { + "redactedAttributes" : ["foo"] + }, + "key": "alice", + "kind": "user", + "anonymous": true + }, + "key": "flag", + "value": false, + "variation": 1, + "default": false, + "reason": { + "kind": "FALLTHROUGH" + }, + "version": 42 + }); + + assert_json_eq!(output_event, event_json); } } #[test] - fn serializes_feature_request_event_with_all_private_attributes() { + fn serializes_feature_request_event_with_local_private_attribute() { let flag = basic_flag("flag"); let default = FlagValue::from(false); - let user = User::with_key("alice".to_string()) + let context = ContextBuilder::new("alice") .anonymous(true) - .secondary("Secondary") - .build(); + .set_value("foo", AttributeValue::Bool(true)) + .add_private_attribute("foo") + .build() + .expect("Failed to create context"); let fallthrough = Detail { value: Some(FlagValue::from(false)), variation_index: Some(1), @@ -852,47 +739,37 @@ mod tests { }; let event_factory = EventFactory::new(true); - let mut feature_request_event = event_factory.new_eval_event( - &flag.key.clone(), - user, - &flag, - fallthrough.clone(), - default.clone(), - None, - ); + let mut feature_request_event = + event_factory.new_eval_event(&flag.key, context, &flag, fallthrough, default, None); // fix creation date so JSON is predictable feature_request_event.base_mut().unwrap().creation_date = 1234; if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event { let output_event = OutputEvent::FeatureRequest( - feature_request_event.into_inline(true, HashSet::new()), + feature_request_event.into_inline(false, HashSet::new()), ); - let event_json = r#"{ - "kind": "feature", - "creationDate": 1234, - "user": { - "key": "alice", - "privateAttrs": [ - "secondary", - "anonymous" - ] - }, - "key": "flag", - "value": false, - "variation": 1, - "default": false, - "reason": { - "kind": "FALLTHROUGH" - }, - "version": 42, - "contextKind": "anonymousUser" -} - "# - .trim(); - - let json = serde_json::to_string_pretty(&output_event); - assert!(json.is_ok()); - assert_eq!(json.unwrap(), event_json.to_string()); + let event_json = json!({ + "kind": "feature", + "creationDate": 1234, + "context": { + "_meta": { + "redactedAttributes" : ["foo"] + }, + "key": "alice", + "kind": "user", + "anonymous": true + }, + "key": "flag", + "value": false, + "variation": 1, + "default": false, + "reason": { + "kind": "FALLTHROUGH" + }, + "version": 42 + }); + + assert_json_eq!(output_event, event_json); } } @@ -900,7 +777,10 @@ mod tests { fn serializes_feature_request_event_without_inlining_user() { let flag = basic_flag("flag"); let default = FlagValue::from(false); - let user = User::with_key("alice".to_string()).anonymous(true).build(); + let context = ContextBuilder::new("alice") + .anonymous(true) + .build() + .expect("Failed to create context"); let fallthrough = Detail { value: Some(FlagValue::from(false)), variation_index: Some(1), @@ -910,156 +790,123 @@ mod tests { }; let event_factory = EventFactory::new(true); - let mut feature_request_event = event_factory.new_eval_event( - &flag.key.clone(), - user, - &flag, - fallthrough.clone(), - default.clone(), - None, - ); + let mut feature_request_event = + event_factory.new_eval_event(&flag.key, context, &flag, fallthrough, default, None); // fix creation date so JSON is predictable feature_request_event.base_mut().unwrap().creation_date = 1234; if let InputEvent::FeatureRequest(feature_request_event) = feature_request_event { let output_event = OutputEvent::FeatureRequest(feature_request_event); - let event_json = r#"{ - "kind": "feature", - "creationDate": 1234, - "userKey": "alice", - "key": "flag", - "value": false, - "variation": 1, - "default": false, - "reason": { - "kind": "FALLTHROUGH" - }, - "version": 42, - "contextKind": "anonymousUser" -} - "# - .trim(); - - let json = serde_json::to_string_pretty(&output_event); - assert!(json.is_ok()); - assert_eq!(json.unwrap(), event_json.to_string()); + let event_json = json!({ + "kind": "feature", + "creationDate": 1234, + "contextKeys": { + "user": "alice" + }, + "key": "flag", + "value": false, + "variation": 1, + "default": false, + "reason": { + "kind": "FALLTHROUGH" + }, + "version": 42 + }); + assert_json_eq!(output_event, event_json); } } #[test] fn serializes_identify_event() { - let user = User::with_key("alice".to_string()).anonymous(true).build(); + let context = ContextBuilder::new("alice") + .anonymous(true) + .build() + .expect("Failed to create context"); let event_factory = EventFactory::new(true); - let mut identify = event_factory.new_identify(user); + let mut identify = event_factory.new_identify(context); identify.base_mut().unwrap().creation_date = 1234; if let InputEvent::Identify(identify) = identify { let output_event = OutputEvent::Identify(identify.into_inline(false, HashSet::new())); - let event_json = r#"{ - "kind": "identify", - "creationDate": 1234, - "user": { - "key": "alice", - "anonymous": true - }, - "key": "alice" -} - "# - .trim(); - - let json = serde_json::to_string_pretty(&output_event); - assert!(json.is_ok()); - assert_eq!(json.unwrap(), event_json.to_string()); - } - } - - #[test] - fn serializes_alias_event() { - let user = User::with_key("alice".to_string()).anonymous(true).build(); - let previous_user = User::with_key("bob".to_string()).anonymous(true).build(); - let event_factory = EventFactory::new(true); - let alias = event_factory.new_alias(user, previous_user); - - if let InputEvent::Alias(mut alias) = alias { - alias.creation_date = 1234; - let output_event = OutputEvent::Alias(alias); - let event_json = r#"{ - "kind": "alias", - "creationDate": 1234, - "key": "alice", - "contextKind": "anonymousUser", - "previousKey": "bob", - "previousContextKind": "anonymousUser" -} - "# - .trim(); - - let json = serde_json::to_string_pretty(&output_event); - assert!(json.is_ok()); - assert_eq!(json.unwrap(), event_json.to_string()); + let event_json = json!({ + "kind": "identify", + "creationDate": 1234, + "context": { + "key": "alice", + "kind": "user", + "anonymous": true + }, + "key": "alice" + }); + assert_json_eq!(output_event, event_json); } } #[test] fn serializes_custom_event() { - let user = User::with_key("alice".to_string()).anonymous(true).build(); + let context = ContextBuilder::new("alice") + .anonymous(true) + .build() + .expect("Failed to create context"); let event_factory = EventFactory::new(true); let mut custom_event = event_factory - .new_custom(user, "custom-key", Some(12345.0), serde_json::Value::Null) + .new_custom( + context, + "custom-key", + Some(12345.0), + serde_json::Value::Null, + ) .unwrap(); // fix creation date so JSON is predictable custom_event.base_mut().unwrap().creation_date = 1234; if let InputEvent::Custom(custom_event) = custom_event { - let output_event = OutputEvent::Custom(custom_event.into_inline(false, HashSet::new())); - let event_json = r#"{ - "kind": "custom", - "creationDate": 1234, - "user": { - "key": "alice", - "anonymous": true - }, - "key": "custom-key", - "metricValue": 12345.0, - "contextKind": "anonymousUser" -} - "# - .trim(); - - let json = serde_json::to_string_pretty(&output_event); - assert!(json.is_ok()); - assert_eq!(json.unwrap(), event_json.to_string()); + let output_event = OutputEvent::Custom(custom_event); + let event_json = json!({ + "kind": "custom", + "creationDate": 1234, + "contextKeys": { + "user": "alice" + }, + "key": "custom-key", + "metricValue": 12345.0 + }); + assert_json_eq!(output_event, event_json); } } #[test] fn serializes_custom_event_without_inlining_user() { - let user = User::with_key("alice".to_string()).anonymous(true).build(); + let context = ContextBuilder::new("alice") + .anonymous(true) + .build() + .expect("Failed to create context"); let event_factory = EventFactory::new(true); let mut custom_event = event_factory - .new_custom(user, "custom-key", Some(12345.0), serde_json::Value::Null) + .new_custom( + context, + "custom-key", + Some(12345.0), + serde_json::Value::Null, + ) .unwrap(); // fix creation date so JSON is predictable custom_event.base_mut().unwrap().creation_date = 1234; if let InputEvent::Custom(custom_event) = custom_event { let output_event = OutputEvent::Custom(custom_event); - let event_json = r#"{ - "kind": "custom", - "creationDate": 1234, - "userKey": "alice", - "key": "custom-key", - "metricValue": 12345.0, - "contextKind": "anonymousUser" -} - "# - .trim(); - - let json = serde_json::to_string_pretty(&output_event); - assert!(json.is_ok()); - assert_eq!(json.unwrap(), event_json.to_string()); + let event_json = json!({ + "kind": "custom", + "creationDate": 1234, + "contextKeys": { + "user": "alice" + }, + "key": "custom-key", + "metricValue": 12345.0 + }); + assert_json_eq!(output_event, event_json); } } @@ -1069,36 +916,34 @@ mod tests { start_date: 1234, end_date: 4567, features: hashmap! { - VariationKey{flag_key: "f".into(), version: Some(2), variation: Some(1)} => VariationSummary{count: 1, value: true.into(), default: false.into()}, + "f".into() => FlagSummary { + counters: hashmap! { + VariationKey{version: Some(2), variation: Some(1)} => VariationSummary{count: 1, value: true.into()}, + }, + default: false.into(), + context_kinds: HashSet::new(), + } }, }; let summary_event = OutputEvent::Summary(summary); - let event_json = r#" -{ - "kind": "summary", - "startDate": 1234, - "endDate": 4567, - "features": { - "f": { - "default": false, - "counters": [ - { - "value": true, - "version": 2, - "count": 1, - "variation": 1 - } - ] - } - } -} - "# - .trim(); - - let json = serde_json::to_string_pretty(&summary_event); - assert!(json.is_ok()); - assert_eq!(json.unwrap(), event_json.to_string()); + let event_json = json!({ + "kind": "summary", + "startDate": 1234, + "endDate": 4567, + "features": { + "f": { + "default": false, + "contextKinds": [], + "counters": [{ + "value": true, + "version": 2, + "count": 1, + "variation": 1 + }] + } + }}); + assert_json_eq!(summary_event, event_json); } #[test] @@ -1107,40 +952,43 @@ mod tests { start_date: 1234, end_date: 4567, features: hashmap! { - VariationKey{flag_key: "f".into(), version: Some(2), variation: Some(1)} => VariationSummary{count: 1, value: true.into(), default: false.into()}, + "f".into() => FlagSummary { + counters: hashmap!{ + VariationKey{version: Some(2), variation: Some(1)} => VariationSummary{count: 1, value: true.into()} + }, + default: false.into(), + context_kinds: HashSet::new(), + } }, }; summary.reset(); assert!(summary.features.is_empty()); - assert_eq!(summary.start_date, std::u64::MAX); + assert_eq!(summary.start_date, u64::MAX); assert_eq!(summary.end_date, 0); - - assert_eq!(summary, EventSummary::default()); } #[test] fn serializes_index_event() { - let user = User::with_key("alice".to_string()).anonymous(true).build(); - let base_event = BaseEvent::new(1234, user); + let context = ContextBuilder::new("alice") + .anonymous(true) + .build() + .expect("Failed to create context"); + let base_event = BaseEvent::new(1234, context); let index_event = OutputEvent::Index(base_event.into()); - let event_json = r#" -{ - "kind": "index", - "creationDate": 1234, - "user": { - "key": "alice", - "anonymous": true - } -} - "# - .trim(); - - let json = serde_json::to_string_pretty(&index_event); - assert!(json.is_ok()); - assert_eq!(json.unwrap(), event_json.to_string()); + let event_json = json!({ + "kind": "index", + "creationDate": 1234, + "context": { + "key": "alice", + "kind": "user", + "anonymous": true + } + }); + + assert_json_eq!(index_event, event_json); } #[test] @@ -1151,7 +999,20 @@ mod tests { let flag = basic_flag("flag"); let default = FlagValue::from(false); - let user = User::with_key("alice".to_string()).build(); + let context = MultiContextBuilder::new() + .add_context( + ContextBuilder::new("alice") + .build() + .expect("Failed to create context"), + ) + .add_context( + ContextBuilder::new("LaunchDarkly") + .kind("org") + .build() + .expect("Failed to create context"), + ) + .build() + .expect("Failed to create multi-context"); let value = FlagValue::from(false); let variation_index = 1; @@ -1161,7 +1022,7 @@ mod tests { let eval_at = 1234; let fallthrough_request = FeatureRequestEvent { - base: BaseEvent::new(eval_at, user.clone()), + base: BaseEvent::new(eval_at, context), key: flag.key.clone(), value: value.clone(), variation: Some(variation_index), @@ -1169,7 +1030,6 @@ mod tests { version: Some(flag.version), reason: Some(reason), prereq_of: None, - context_kind: user.into(), track_events: false, debug_events_until_date: None, }; @@ -1180,41 +1040,54 @@ mod tests { assert_eq!(summary.end_date, eval_at); let fallthrough_key = VariationKey { - flag_key: flag.key, version: Some(flag.version), variation: Some(variation_index), }; - let fallthrough_summary = summary.features.get(&fallthrough_key); - if let Some(VariationSummary { - count: c, - value: v, - default: d, - }) = fallthrough_summary - { + let feature = summary.features.get(&flag.key); + assert!(feature.is_some()); + let feature = feature.unwrap(); + assert_eq!(feature.default, default); + assert_eq!(2, feature.context_kinds.len()); + assert!(feature.context_kinds.contains(&Kind::user())); + assert!(feature + .context_kinds + .contains(&Kind::try_from("org").unwrap())); + + let fallthrough_summary = feature.counters.get(&fallthrough_key); + if let Some(VariationSummary { count: c, value: v }) = fallthrough_summary { assert_eq!(*c, 1); assert_eq!(*v, value); - assert_eq!(*d, default); } else { panic!("Fallthrough summary is wrong type"); } summary.add(&fallthrough_request); - let fallthrough_summary = summary.features.get(&fallthrough_key).unwrap(); + let feature = summary + .features + .get(&flag.key) + .expect("Failed to get expected feature."); + let fallthrough_summary = feature + .counters + .get(&fallthrough_key) + .expect("Failed to get counters"); assert_eq!(fallthrough_summary.count, 2); + assert_eq!(2, feature.context_kinds.len()); } #[test] fn event_factory_unknown_flags_do_not_track_events() { let event_factory = EventFactory::new(true); - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); let detail = Detail { value: Some(FlagValue::from(false)), variation_index: Some(1), reason: Reason::Off, }; let event = - event_factory.new_unknown_flag_event("myFlag", user, detail, FlagValue::Bool(true)); + event_factory.new_unknown_flag_event("myFlag", context, detail, FlagValue::Bool(true)); if let InputEvent::FeatureRequest(event) = event { assert!(!event.track_events); @@ -1251,7 +1124,9 @@ mod tests { flag.track_events = flag_track_events; flag.track_events_fallthrough = flag_track_events_fallthrough; - let user = User::with_key("bob").build(); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); let detail = Detail { value: Some(FlagValue::from(false)), variation_index: Some(1), @@ -1259,7 +1134,7 @@ mod tests { }; let event = event_factory.new_eval_event( "myFlag", - user, + context, &flag, detail, FlagValue::Bool(true), @@ -1290,51 +1165,51 @@ mod tests { should_include_reason: bool, ) { let event_factory = EventFactory::new(event_factory_send_events); - let flag: Flag = serde_json::from_str( - r#"{ - "key": "with_rule", - "on": true, - "targets": [], - "prerequisites": [], - "rules": [ - { - "id": "rule-0", - "clauses": [{ - "attribute": "key", - "negate": false, - "op": "matches", - "values": ["do-track"] - }], - "trackEvents": true, - "variation": 1 - }, - { - "id": "rule-1", - "clauses": [{ - "attribute": "key", - "negate": false, - "op": "matches", - "values": ["no-track"] - }], - "trackEvents": false, - "variation": 1 - } - ], - "fallthrough": {"variation": 0}, - "trackEventsFallthrough": false, - "offVariation": 0, - "clientSideAvailability": { - "usingMobileKey": false, - "usingEnvironmentId": false - }, - "salt": "kosher", - "version": 2, - "variations": [false, true] - }"#, - ) - .expect("flag should parse"); - - let user = User::with_key("do-track").build(); + let flag: Flag = serde_json::from_value(json!({ + "key": "with_rule", + "on": true, + "targets": [], + "prerequisites": [], + "rules": [ + { + "id": "rule-0", + "clauses": [{ + "attribute": "key", + "negate": false, + "op": "matches", + "values": ["do-track"] + }], + "trackEvents": true, + "variation": 1 + }, + { + "id": "rule-1", + "clauses": [{ + "attribute": "key", + "negate": false, + "op": "matches", + "values": ["no-track"] + }], + "trackEvents": false, + "variation": 1 + } + ], + "fallthrough": {"variation": 0}, + "trackEventsFallthrough": false, + "offVariation": 0, + "clientSideAvailability": { + "usingMobileKey": false, + "usingEnvironmentId": false + }, + "salt": "kosher", + "version": 2, + "variations": [false, true] + })) + .unwrap(); + + let context = ContextBuilder::new("do-track") + .build() + .expect("Failed to create context"); let detail = Detail { value: Some(FlagValue::from(false)), variation_index: Some(1), @@ -1346,7 +1221,7 @@ mod tests { }; let event = event_factory.new_eval_event( "myFlag", - user, + context, &flag, detail, FlagValue::Bool(true), diff --git a/launchdarkly-server-sdk/src/events/mod.rs b/launchdarkly-server-sdk/src/events/mod.rs index a257ad4..5f28843 100644 --- a/launchdarkly-server-sdk/src/events/mod.rs +++ b/launchdarkly-server-sdk/src/events/mod.rs @@ -1,4 +1,6 @@ +use launchdarkly_server_sdk_evaluation::Reference; use std::collections::HashSet; +use std::num::NonZeroUsize; use std::sync::Arc; use std::time::Duration; @@ -14,26 +16,23 @@ pub struct EventsConfiguration { capacity: usize, event_sender: Arc, flush_interval: Duration, - inline_users_in_events: bool, - user_keys_capacity: usize, - user_keys_flush_interval: Duration, + context_keys_capacity: NonZeroUsize, + context_keys_flush_interval: Duration, all_attributes_private: bool, - private_attributes: HashSet, + private_attributes: HashSet, } #[cfg(test)] fn create_events_configuration( - event_sender: self::sender::InMemoryEventSender, - inline_users_in_events: bool, + event_sender: sender::InMemoryEventSender, flush_interval: Duration, ) -> EventsConfiguration { EventsConfiguration { capacity: 5, event_sender: Arc::new(event_sender), flush_interval, - inline_users_in_events, - user_keys_capacity: 5, - user_keys_flush_interval: Duration::from_secs(100), + context_keys_capacity: NonZeroUsize::new(5).expect("5 > 0"), + context_keys_flush_interval: Duration::from_secs(100), all_attributes_private: false, private_attributes: HashSet::new(), } @@ -41,9 +40,9 @@ fn create_events_configuration( #[cfg(test)] pub(super) fn create_event_sender() -> ( - self::sender::InMemoryEventSender, - crossbeam_channel::Receiver, + sender::InMemoryEventSender, + crossbeam_channel::Receiver, ) { let (event_tx, event_rx) = crossbeam_channel::unbounded(); - (self::sender::InMemoryEventSender::new(event_tx), event_rx) + (sender::InMemoryEventSender::new(event_tx), event_rx) } diff --git a/launchdarkly-server-sdk/src/events/processor.rs b/launchdarkly-server-sdk/src/events/processor.rs index 49501ff..636908b 100644 --- a/launchdarkly-server-sdk/src/events/processor.rs +++ b/launchdarkly-server-sdk/src/events/processor.rs @@ -109,7 +109,7 @@ impl EventProcessor for EventProcessorImpl { mod tests { use std::time::Duration; - use launchdarkly_server_sdk_evaluation::{Detail, Flag, FlagValue, Reason, User}; + use launchdarkly_server_sdk_evaluation::{ContextBuilder, Detail, Flag, FlagValue, Reason}; use test_case::test_case; use crate::{ @@ -126,25 +126,24 @@ mod tests { fn calling_close_on_processor_twice_returns() { let (event_sender, _) = create_event_sender(); let events_configuration = - create_events_configuration(event_sender, false, Duration::from_secs(100)); + create_events_configuration(event_sender, Duration::from_secs(100)); let event_processor = EventProcessorImpl::new(events_configuration).expect("failed to start ep"); event_processor.close(); event_processor.close(); } - #[test_case(true, false, vec!["index", "summary"])] - #[test_case(false, false, vec!["index", "summary"])] - #[test_case(true, true, vec!["feature", "summary"])] - #[test_case(false, true, vec!["index", "feature", "summary"])] + #[test_case(true, vec!["index", "feature", "summary"])] + #[test_case(false, vec!["index", "summary"])] fn sending_feature_event_emits_correct_events( - inline_users_in_events: bool, flag_track_events: bool, expected_event_types: Vec<&str>, ) { let mut flag = basic_flag("flag"); flag.track_events = flag_track_events; - let user = User::with_key("foo".to_string()).build(); + let context = ContextBuilder::new("foo") + .build() + .expect("Failed to create context"); let detail = Detail { value: Some(FlagValue::from(false)), variation_index: Some(1), @@ -156,19 +155,16 @@ mod tests { let event_factory = EventFactory::new(true); let feature_request = event_factory.new_eval_event( &flag.key, - user, + context, &flag, - detail.clone(), + detail, FlagValue::from(false), None, ); let (event_sender, event_rx) = create_event_sender(); - let events_configuration = create_events_configuration( - event_sender, - inline_users_in_events, - Duration::from_secs(100), - ); + let events_configuration = + create_events_configuration(event_sender, Duration::from_secs(100)); let event_processor = EventProcessorImpl::new(events_configuration).expect("failed to start ep"); event_processor.send(feature_request); @@ -179,7 +175,7 @@ mod tests { assert_eq!(expected_event_types.len(), events.len()); for event_type in expected_event_types { - assert!(events.iter().find(|e| e.kind() == event_type).is_some()); + assert!(events.iter().any(|e| e.kind() == event_type)); } } @@ -229,9 +225,15 @@ mod tests { ) .expect("flag should parse"); - let user_track_rule = User::with_key("do-track".to_string()).build(); - let user_notrack_rule = User::with_key("no-track".to_string()).build(); - let user_fallthrough = User::with_key("foo".to_string()).build(); + let context_track_rule = ContextBuilder::new("do-track") + .build() + .expect("Failed to create context"); + let context_notrack_rule = ContextBuilder::new("no-track") + .build() + .expect("Failed to create context"); + let context_fallthrough = ContextBuilder::new("foo") + .build() + .expect("Failed to create context"); let detail_track_rule = Detail { value: Some(FlagValue::from(true)), @@ -262,7 +264,7 @@ mod tests { let event_factory = EventFactory::new(true); let fre_track_rule = event_factory.new_eval_event( &flag.key, - user_track_rule, + context_track_rule, &flag, detail_track_rule, FlagValue::from(false), @@ -270,7 +272,7 @@ mod tests { ); let fre_notrack_rule = event_factory.new_eval_event( &flag.key, - user_notrack_rule, + context_notrack_rule, &flag, detail_notrack_rule, FlagValue::from(false), @@ -278,7 +280,7 @@ mod tests { ); let fre_fallthrough = event_factory.new_eval_event( &flag.key, - user_fallthrough, + context_fallthrough, &flag, detail_fallthrough, FlagValue::from(false), @@ -287,12 +289,12 @@ mod tests { let (event_sender, event_rx) = create_event_sender(); let events_configuration = - create_events_configuration(event_sender, true, Duration::from_secs(100)); + create_events_configuration(event_sender, Duration::from_secs(100)); let event_processor = EventProcessorImpl::new(events_configuration).expect("failed to start ep"); - for fre in vec![&fre_track_rule, &fre_notrack_rule, &fre_fallthrough] { - event_processor.send(fre.clone()); + for fre in [fre_track_rule, fre_notrack_rule, fre_fallthrough] { + event_processor.send(fre); } event_processor.flush(); @@ -300,8 +302,8 @@ mod tests { let events = event_rx.iter().collect::>(); - // detail_track_rule -> feature + index, detail_fallthrough -> feature, 1 summary - assert_eq!(events.len(), 2 + 1 + 1); + // detail_track_rule -> feature + index, detail_notrack_rule -> index, detail_fallthrough -> feature + index, 1 summary + assert_eq!(events.len(), 2 + 1 + 2 + 1); assert_eq!( events .iter() @@ -309,14 +311,16 @@ mod tests { .count(), 2 ); - assert!(events.iter().find(|e| e.kind() == "index").is_some()); - assert!(events.iter().find(|e| e.kind() == "summary").is_some()); + assert!(events.iter().any(|e| e.kind() == "index")); + assert!(events.iter().any(|e| e.kind() == "summary")); } #[test] fn feature_events_dedupe_index_events() { let flag = basic_flag("flag"); - let user = User::with_key("bar".to_string()).build(); + let context = ContextBuilder::new("bar") + .build() + .expect("Failed to create context"); let detail = Detail { value: Some(FlagValue::from(false)), variation_index: Some(1), @@ -328,16 +332,16 @@ mod tests { let event_factory = EventFactory::new(true); let feature_request = event_factory.new_eval_event( &flag.key, - user, + context, &flag, - detail.clone(), + detail, FlagValue::from(false), None, ); let (event_sender, event_rx) = create_event_sender(); let events_configuration = - create_events_configuration(event_sender, false, Duration::from_secs(100)); + create_events_configuration(event_sender, Duration::from_secs(100)); let event_processor = EventProcessorImpl::new(events_configuration).expect("failed to start ep"); event_processor.send(feature_request.clone()); diff --git a/launchdarkly-server-sdk/src/events/processor_builders.rs b/launchdarkly-server-sdk/src/events/processor_builders.rs index d439f7e..a23ac3b 100644 --- a/launchdarkly-server-sdk/src/events/processor_builders.rs +++ b/launchdarkly-server-sdk/src/events/processor_builders.rs @@ -1,19 +1,28 @@ +use std::collections::{HashMap, HashSet}; +use std::num::NonZeroUsize; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +use hyper_rustls::HttpsConnectorBuilder; +use launchdarkly_server_sdk_evaluation::Reference; +use thiserror::Error; + +use crate::events::sender::HyperEventSender; +use crate::{service_endpoints, LAUNCHDARKLY_TAGS_HEADER}; + use super::processor::{ EventProcessor, EventProcessorError, EventProcessorImpl, NullEventProcessor, }; -use super::sender::{EventSender, ReqwestEventSender}; +use super::sender::EventSender; use super::EventsConfiguration; -use crate::{service_endpoints, LAUNCHDARKLY_TAGS_HEADER}; -use std::collections::HashSet; -use std::sync::Arc; -use std::time::Duration; -use thiserror::Error; - const DEFAULT_FLUSH_POLL_INTERVAL: Duration = Duration::from_secs(5); const DEFAULT_EVENT_CAPACITY: usize = 500; -const DEFAULT_USER_KEY_SIZE: usize = 1000; -const DEFAULT_USER_KEYS_FLUSH_INTERVAL: Duration = Duration::from_secs(5 * 60); +// The capacity will be set to max(DEFAULT_CONTEXT_KEY_CAPACITY, 1), meaning +// caching cannot be entirely disabled. +const DEFAULT_CONTEXT_KEY_CAPACITY: Option = NonZeroUsize::new(1000); +const DEFAULT_CONTEXT_KEYS_FLUSH_INTERVAL: Duration = Duration::from_secs(5 * 60); /// Error type used to represent failures when building an [EventProcessor] instance. #[non_exhaustive] @@ -61,12 +70,11 @@ pub trait EventProcessorFactory { pub struct EventProcessorBuilder { capacity: usize, flush_interval: Duration, - user_keys_capacity: usize, - user_keys_flush_interval: Duration, - inline_users_in_events: bool, + context_keys_capacity: NonZeroUsize, + context_keys_flush_interval: Duration, event_sender: Option>, all_attributes_private: bool, - private_attributes: HashSet, + private_attributes: HashSet, // diagnostic_recording_interval: Duration } @@ -77,39 +85,39 @@ impl EventProcessorFactory for EventProcessorBuilder { sdk_key: &str, tags: Option, ) -> Result, BuildError> { - let url = format!("{}/bulk", endpoints.events_base_url()); - let url = reqwest::Url::parse(&url).map_err(|e| { - BuildError::InvalidConfig(format!("couldn't parse events_base_url: {}", e)) - })?; + let url_string = format!("{}/bulk", endpoints.events_base_url()); - let mut builder = reqwest::Client::builder(); + let mut default_headers = HashMap::<&str, String>::new(); if let Some(tags) = tags { - let mut headers = reqwest::header::HeaderMap::new(); - headers.append( - LAUNCHDARKLY_TAGS_HEADER, - reqwest::header::HeaderValue::from_str(&tags) - .map_err(|e| BuildError::InvalidConfig(e.to_string()))?, - ); - builder = builder.default_headers(headers); + default_headers.insert(LAUNCHDARKLY_TAGS_HEADER, tags); } - let http = builder.build().map_err(|e| { - BuildError::InvalidConfig(format!("unable to build reqwest client: {}", e)) - })?; - let event_sender = match &self.event_sender { Some(event_sender) => event_sender.clone(), - _ => Arc::new(ReqwestEventSender::new(http, url, sdk_key)), + _ => { + let connector = HttpsConnectorBuilder::new() + .with_native_roots() + .https_or_http() + .enable_http1() + .enable_http2() + .build(); + + Arc::new(HyperEventSender::new( + hyper::Client::builder().build(connector), + hyper::Uri::from_str(url_string.as_str()).unwrap(), + sdk_key, + default_headers, + )) + } }; let events_configuration = EventsConfiguration { event_sender, capacity: self.capacity, flush_interval: self.flush_interval, - inline_users_in_events: self.inline_users_in_events, - user_keys_capacity: self.user_keys_capacity, - user_keys_flush_interval: self.user_keys_flush_interval, + context_keys_capacity: self.context_keys_capacity, + context_keys_flush_interval: self.context_keys_flush_interval, all_attributes_private: self.all_attributes_private, private_attributes: self.private_attributes.clone(), }; @@ -131,9 +139,9 @@ impl EventProcessorBuilder { Self { capacity: DEFAULT_EVENT_CAPACITY, flush_interval: DEFAULT_FLUSH_POLL_INTERVAL, - user_keys_capacity: DEFAULT_USER_KEY_SIZE, - user_keys_flush_interval: DEFAULT_USER_KEYS_FLUSH_INTERVAL, - inline_users_in_events: false, + context_keys_capacity: DEFAULT_CONTEXT_KEY_CAPACITY + .unwrap_or_else(|| NonZeroUsize::new(1).unwrap()), + context_keys_flush_interval: DEFAULT_CONTEXT_KEYS_FLUSH_INTERVAL, event_sender: None, all_attributes_private: false, private_attributes: HashSet::new(), @@ -159,34 +167,28 @@ impl EventProcessorBuilder { self } - /// Sets the number of user keys that the event processor can remember at any one time. + /// Sets the number of context keys that the event processor can remember at any one time. /// - /// To avoid sending duplicate user details in analytics events, the SDK maintains a cache of - /// recently seen user keys. - pub fn user_keys_capacity(&mut self, user_keys_capacity: usize) -> &mut Self { - self.user_keys_capacity = user_keys_capacity; - self - } - - /// Sets the interval at which the event processor will reset its cache of known user keys. - pub fn user_keys_flush_interval(&mut self, user_keys_flush_interval: Duration) -> &mut Self { - self.user_keys_flush_interval = user_keys_flush_interval; + /// To avoid sending duplicate context details in analytics events, the SDK maintains a cache of + /// recently seen context keys. + pub fn context_keys_capacity(&mut self, context_keys_capacity: NonZeroUsize) -> &mut Self { + self.context_keys_capacity = context_keys_capacity; self } - /// Sets whether to include full user details in every analytics event. - /// - /// The default is false: events will only include the user key, except for one "index" event that provides - /// the full details for the user. - pub fn inline_users_in_events(&mut self, inline_users_in_events: bool) -> &mut Self { - self.inline_users_in_events = inline_users_in_events; + /// Sets the interval at which the event processor will reset its cache of known context keys. + pub fn context_keys_flush_interval( + &mut self, + context_keys_flush_interval: Duration, + ) -> &mut Self { + self.context_keys_flush_interval = context_keys_flush_interval; self } /// Sets whether or not all optional user attributes should be hidden from LaunchDarkly. /// /// If this is true, all user attribute values (other than the key) will be private, not just the attributes - /// specified with private_attribute_names or on a per-user basis with UserBuilder methods. By default, it is false. + /// specified with private_attributes or on a per-user basis with UserBuilder methods. By default, it is false. pub fn all_attributes_private(&mut self, all_attributes_private: bool) -> &mut Self { self.all_attributes_private = all_attributes_private; self @@ -197,8 +199,11 @@ impl EventProcessorBuilder { /// Any users sent to LaunchDarkly with this configuration active will have attributes with these /// names removed. This is in addition to any attributes that were marked as private for an /// individual user with UserBuilder methods. Setting all_attribute_private to true overrides this. - pub fn private_attribute_names(&mut self, attributes: HashSet) -> &mut Self { - self.private_attributes = attributes; + pub fn private_attributes(&mut self, attributes: HashSet) -> &mut Self + where + R: Into, + { + self.private_attributes = attributes.into_iter().map(|a| a.into()).collect(); self } @@ -250,7 +255,7 @@ impl Default for NullEventProcessorBuilder { #[cfg(test)] mod tests { - use launchdarkly_server_sdk_evaluation::User; + use launchdarkly_server_sdk_evaluation::ContextBuilder; use maplit::hashset; use mockito::{mock, Matcher}; use test_case::test_case; @@ -281,24 +286,21 @@ mod tests { } #[test] - fn user_keys_capacity_can_be_adjusted() { + fn context_keys_capacity_can_be_adjusted() { let mut builder = EventProcessorBuilder::new(); - builder.user_keys_capacity(1234); - assert_eq!(builder.user_keys_capacity, 1234); + let cap = NonZeroUsize::new(1234).expect("1234 > 0"); + builder.context_keys_capacity(cap); + assert_eq!(builder.context_keys_capacity, cap); } #[test] - fn user_keys_flush_interval_can_be_adjusted() { + fn context_keys_flush_interval_can_be_adjusted() { let mut builder = EventProcessorBuilder::new(); - builder.user_keys_flush_interval(Duration::from_secs(1000)); - assert_eq!(builder.user_keys_flush_interval, Duration::from_secs(1000)); - } - - #[test] - fn inline_users_in_events_can_be_adjusted() { - let mut builder = EventProcessorBuilder::new(); - builder.inline_users_in_events(true); - assert!(builder.inline_users_in_events); + builder.context_keys_flush_interval(Duration::from_secs(1000)); + assert_eq!( + builder.context_keys_flush_interval, + Duration::from_secs(1000) + ); } #[test] @@ -315,8 +317,8 @@ mod tests { let mut builder = EventProcessorBuilder::new(); assert!(builder.private_attributes.is_empty()); - builder.private_attribute_names(hashset!["name".to_string()]); - assert!(builder.private_attributes.contains("name")); + builder.private_attributes(hashset!["name"]); + assert!(builder.private_attributes.contains(&"name".into())); } #[test_case(Some("application-id/abc:application-sha/xyz".into()), "application-id/abc:application-sha/xyz")] @@ -342,8 +344,10 @@ mod tests { let event_factory = EventFactory::new(false); - let user = User::with_key("bob").build(); - let identify_event = event_factory.new_identify(user); + let context = ContextBuilder::new("bob") + .build() + .expect("Failed to create context"); + let identify_event = event_factory.new_identify(context); processor.send(identify_event); processor.close(); diff --git a/launchdarkly-server-sdk/src/events/sender.rs b/launchdarkly-server-sdk/src/events/sender.rs index b652323..512bd7d 100644 --- a/launchdarkly-server-sdk/src/events/sender.rs +++ b/launchdarkly-server-sdk/src/events/sender.rs @@ -2,11 +2,12 @@ use crate::{ reqwest::is_http_error_recoverable, LAUNCHDARKLY_EVENT_SCHEMA_HEADER, LAUNCHDARKLY_PAYLOAD_ID_HEADER, }; -use crossbeam_channel::Sender; +use std::collections::HashMap; use chrono::DateTime; -use r::{header::HeaderValue, Response}; -use reqwest as r; +use crossbeam_channel::Sender; +use futures::future::BoxFuture; +use tokio::time::{sleep, Duration}; use uuid::Uuid; use super::event::OutputEvent; @@ -18,30 +19,41 @@ pub struct EventSenderResult { } pub trait EventSender: Send + Sync { - fn send_event_data(&self, events: Vec, result_tx: Sender); + fn send_event_data( + &self, + events: Vec, + result_tx: Sender, + ) -> BoxFuture<()>; } #[derive(Clone)] -pub struct ReqwestEventSender { - url: r::Url, +pub struct HyperEventSender { + url: hyper::Uri, sdk_key: String, - http: r::Client, + http: hyper::Client, + default_headers: HashMap<&'static str, String>, } -impl ReqwestEventSender { - pub fn new(http: r::Client, url: r::Url, sdk_key: &str) -> Self { +impl HyperEventSender { + pub fn new( + http: hyper::Client, + url: hyper::Uri, + sdk_key: &str, + default_headers: HashMap<&'static str, String>, + ) -> Self { Self { - http, url, sdk_key: sdk_key.to_owned(), + http, + default_headers, } } - fn get_server_time_from_response(&self, response: &Response) -> u128 { + fn get_server_time_from_response(&self, response: &hyper::Response) -> u128 { let date_value = response .headers() - .get(r::header::DATE) - .map_or(HeaderValue::from_static(""), |v| v.to_owned()) + .get("date") + .unwrap_or(&crate::EMPTY_HEADER) .to_str() .unwrap_or("") .to_owned(); @@ -53,83 +65,106 @@ impl ReqwestEventSender { } } -impl EventSender for ReqwestEventSender { - fn send_event_data(&self, events: Vec, result_tx: Sender) { - let uuid = Uuid::new_v4(); - - debug!( - "Sending ({}): {}", - uuid, - serde_json::to_string_pretty(&events).unwrap_or_else(|e| e.to_string()) - ); - - let json = match serde_json::to_vec(&events) { - Ok(json) => json, - Err(e) => { - error!( - "Failed to serialize event payload. Some events were dropped: {:?}", - e - ); - return; - } - }; - - for _ in 0..2 { - let request = self - .http - .post(self.url.clone()) - .header("Content-Type", "application/json") - .header("Authorization", self.sdk_key.clone()) - .header("User-Agent", &*crate::USER_AGENT) - .header( - LAUNCHDARKLY_EVENT_SCHEMA_HEADER, - crate::CURRENT_EVENT_SCHEMA, - ) - .header(LAUNCHDARKLY_PAYLOAD_ID_HEADER, uuid.to_string()) - .body(json.clone()); - - let response = match request.send() { - Ok(response) => response, +impl EventSender for HyperEventSender +where + C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, +{ + fn send_event_data( + &self, + events: Vec, + result_tx: Sender, + ) -> BoxFuture<()> { + Box::pin(async move { + let uuid = Uuid::new_v4(); + + debug!( + "Sending ({}): {}", + uuid, + serde_json::to_string_pretty(&events).unwrap_or_else(|e| e.to_string()) + ); + + let json = match serde_json::to_vec(&events) { + Ok(json) => json, Err(e) => { - error!("Failed to send events. Some events were dropped: {:?}", e); - let must_shutdown = - !matches!(e.status(), Some(status) if is_http_error_recoverable(status)); + error!( + "Failed to serialize event payload. Some events were dropped: {:?}", + e + ); + return; + } + }; + + for attempt in 0..2 { + if attempt == 1 { + sleep(Duration::from_secs(1)).await; + } + let mut request_builder = hyper::Request::builder() + .uri(self.url.clone()) + .method("POST") + .header("Content-Type", "application/json") + .header("Authorization", self.sdk_key.clone()) + .header("User-Agent", &*crate::USER_AGENT) + .header( + LAUNCHDARKLY_EVENT_SCHEMA_HEADER, + crate::CURRENT_EVENT_SCHEMA, + ) + .header(LAUNCHDARKLY_PAYLOAD_ID_HEADER, uuid.to_string()); + + for default_header in &self.default_headers { + request_builder = + request_builder.header(*default_header.0, default_header.1.as_str()); + } + let request = request_builder.body(hyper::Body::from(json.clone())); + + let result = self.http.request(request.unwrap()).await; + + let response = match result { + Ok(response) => response, + Err(e) => { + // It appears this type of error will not be an HTTP error. + // It will be a closed connection, aborted write, timeout, etc. + error!("Failed to send events. Some events were dropped: {:?}", e); + result_tx + .send(EventSenderResult { + success: false, + time_from_server: 0, + must_shutdown: false, + }) + .unwrap(); + return; + } + }; + + if response.status().is_success() { let _ = result_tx.send(EventSenderResult { - success: false, - time_from_server: 0, - must_shutdown, + success: true, + time_from_server: self.get_server_time_from_response(&response), + must_shutdown: false, }); return; } - }; - - debug!("sent event: {:?}", response); - if response.status().is_success() { - let _ = result_tx.send(EventSenderResult { - success: true, - time_from_server: self.get_server_time_from_response(&response), - must_shutdown: false, - }); - return; + if !is_http_error_recoverable(response.status().as_u16()) { + result_tx + .send(EventSenderResult { + success: false, + time_from_server: 0, + must_shutdown: true, + }) + .unwrap(); + return; + } } - if !is_http_error_recoverable(response.status()) { - let _ = result_tx.send(EventSenderResult { + result_tx + .send(EventSenderResult { success: false, time_from_server: 0, - must_shutdown: true, - }); - return; - } - } - - let _ = result_tx.send(EventSenderResult { - success: false, - time_from_server: 0, - must_shutdown: false, - }); + must_shutdown: false, + }) + .unwrap(); + }) } } @@ -147,17 +182,24 @@ impl InMemoryEventSender { #[cfg(test)] impl EventSender for InMemoryEventSender { - fn send_event_data(&self, events: Vec, sender: Sender) { - events - .into_iter() - .for_each(|event| self.event_tx.send(event).expect("event send failed")); - sender - .send(EventSenderResult { - time_from_server: 0, - success: true, - must_shutdown: true, - }) - .expect("result send failed"); + fn send_event_data( + &self, + events: Vec, + sender: Sender, + ) -> BoxFuture<()> { + Box::pin(async move { + for event in events { + self.event_tx.send(event).unwrap(); + } + + sender + .send(EventSenderResult { + time_from_server: 0, + success: true, + must_shutdown: true, + }) + .unwrap(); + }) } } @@ -166,25 +208,25 @@ mod tests { use super::*; use crossbeam_channel::bounded; use mockito::mock; - use reqwest::StatusCode; + use std::str::FromStr; use test_case::test_case; - #[test_case(StatusCode::CONTINUE, true)] - #[test_case(StatusCode::OK, true)] - #[test_case(StatusCode::MULTIPLE_CHOICES, true)] - #[test_case(StatusCode::BAD_REQUEST, true)] - #[test_case(StatusCode::UNAUTHORIZED, false)] - #[test_case(StatusCode::REQUEST_TIMEOUT, true)] - #[test_case(StatusCode::CONFLICT, false)] - #[test_case(StatusCode::TOO_MANY_REQUESTS, true)] - #[test_case(StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE, false)] - #[test_case(StatusCode::INTERNAL_SERVER_ERROR, true)] - fn can_determine_recoverable_errors(status: StatusCode, is_recoverable: bool) { - assert_eq!(is_recoverable, is_http_error_recoverable(status)); + #[test_case(hyper::StatusCode::CONTINUE, true)] + #[test_case(hyper::StatusCode::OK, true)] + #[test_case(hyper::StatusCode::MULTIPLE_CHOICES, true)] + #[test_case(hyper::StatusCode::BAD_REQUEST, true)] + #[test_case(hyper::StatusCode::UNAUTHORIZED, false)] + #[test_case(hyper::StatusCode::REQUEST_TIMEOUT, true)] + #[test_case(hyper::StatusCode::CONFLICT, false)] + #[test_case(hyper::StatusCode::TOO_MANY_REQUESTS, true)] + #[test_case(hyper::StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE, false)] + #[test_case(hyper::StatusCode::INTERNAL_SERVER_ERROR, true)] + fn can_determine_recoverable_errors(status: hyper::StatusCode, is_recoverable: bool) { + assert_eq!(is_recoverable, is_http_error_recoverable(status.as_u16())); } - #[test] - fn can_parse_server_time_from_response() { + #[tokio::test] + async fn can_parse_server_time_from_response() { let _mock = mock("POST", "/bulk") .with_status(200) .with_header("date", "Fri, 13 Feb 2009 23:31:30 GMT") @@ -193,36 +235,36 @@ mod tests { let (tx, rx) = bounded::(5); let event_sender = build_event_sender(); - event_sender.send_event_data(vec![], tx); + event_sender.send_event_data(vec![], tx).await; - let sender_result = rx.recv().expect("Failed to receive sender_result"); + let sender_result = rx.recv().unwrap(); assert!(sender_result.success); assert!(!sender_result.must_shutdown); assert_eq!(sender_result.time_from_server, 1234567890000); } - #[test] - fn unrecoverable_failure_requires_shutdown() { + #[tokio::test] + async fn unrecoverable_failure_requires_shutdown() { let _mock = mock("POST", "/bulk").with_status(401).create(); let (tx, rx) = bounded::(5); let event_sender = build_event_sender(); - event_sender.send_event_data(vec![], tx); + event_sender.send_event_data(vec![], tx).await; let sender_result = rx.recv().expect("Failed to receive sender_result"); assert!(!sender_result.success); assert!(sender_result.must_shutdown); } - #[test] - fn recoverable_failures_are_attempted_multiple_times() { + #[tokio::test] + async fn recoverable_failures_are_attempted_multiple_times() { let mock = mock("POST", "/bulk").with_status(400).expect(2).create(); let (tx, rx) = bounded::(5); let event_sender = build_event_sender(); - event_sender.send_event_data(vec![], tx); + event_sender.send_event_data(vec![], tx).await; let sender_result = rx.recv().expect("Failed to receive sender_result"); assert!(!sender_result.success); @@ -230,8 +272,8 @@ mod tests { mock.assert(); } - #[test] - fn retrying_requests_can_eventually_succeed() { + #[tokio::test] + async fn retrying_requests_can_eventually_succeed() { let _failed = mock("POST", "/bulk").with_status(400).create(); let _succeed = mock("POST", "/bulk") .with_status(200) @@ -241,7 +283,7 @@ mod tests { let (tx, rx) = bounded::(5); let event_sender = build_event_sender(); - event_sender.send_event_data(vec![], tx); + event_sender.send_event_data(vec![], tx).await; let sender_result = rx.recv().expect("Failed to receive sender_result"); assert!(sender_result.success); @@ -249,13 +291,11 @@ mod tests { assert_eq!(sender_result.time_from_server, 1234567890000); } - fn build_event_sender() -> ReqwestEventSender { - let http = r::Client::builder() - .build() - .expect("Failed building the client"); + fn build_event_sender() -> HyperEventSender { + let http = hyper::Client::builder().build(hyper::client::HttpConnector::new()); let url = format!("{}/bulk", &mockito::server_url()); - let url = reqwest::Url::parse(&url).expect("Failed parsing the mock server url"); + let url = hyper::Uri::from_str(&url).expect("Failed parsing the mock server url"); - ReqwestEventSender::new(http, url, "sdk-key") + HyperEventSender::new(http, url, "sdk-key", HashMap::<&str, String>::new()) } } diff --git a/launchdarkly-server-sdk/src/feature_requester.rs b/launchdarkly-server-sdk/src/feature_requester.rs index 1673727..d9aa0c3 100644 --- a/launchdarkly-server-sdk/src/feature_requester.rs +++ b/launchdarkly-server-sdk/src/feature_requester.rs @@ -1,14 +1,14 @@ use crate::reqwest::is_http_error_recoverable; +use futures::future::BoxFuture; +use hyper::body::HttpBody; +use hyper::Body; +use std::collections::HashMap; +use std::sync::Arc; use super::stores::store_types::AllData; use launchdarkly_server_sdk_evaluation::{Flag, Segment}; -use r::{ - header::{HeaderValue, ETAG}, - StatusCode, -}; -use reqwest as r; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum FeatureRequesterError { Temporary, Permanent, @@ -18,102 +18,119 @@ pub enum FeatureRequesterError { struct CachedEntry(AllData, String); pub trait FeatureRequester: Send { - fn get_all(&mut self) -> Result, FeatureRequesterError>; + fn get_all(&mut self) -> BoxFuture, FeatureRequesterError>>; } -pub struct ReqwestFeatureRequester { - http: r::Client, - url: r::Url, +pub struct HyperFeatureRequester { + http: Arc>, + url: hyper::Uri, sdk_key: String, cache: Option, + default_headers: HashMap<&'static str, String>, } -impl ReqwestFeatureRequester { - pub fn new(http: r::Client, url: r::Url, sdk_key: String) -> Self { +impl HyperFeatureRequester { + pub fn new( + http: hyper::Client, + url: hyper::Uri, + sdk_key: String, + default_headers: HashMap<&'static str, String>, + ) -> Self { Self { - http, + http: Arc::new(http), url, sdk_key, cache: None, + default_headers, } } } -impl FeatureRequester for ReqwestFeatureRequester { - fn get_all(&mut self) -> Result, FeatureRequesterError> { - let mut request_builder = self - .http - .get(self.url.clone()) - .header("Content-Type", "application/json") - .header("Authorization", self.sdk_key.clone()) - .header("User-Agent", &*crate::USER_AGENT); - - if let Some(cache) = &self.cache { - request_builder = request_builder.header("If-None-Match", cache.1.clone()); - } +impl FeatureRequester for HyperFeatureRequester +where + C: hyper::client::connect::Connect + Clone + Send + Sync + 'static, +{ + fn get_all(&mut self) -> BoxFuture, FeatureRequesterError>> { + Box::pin(async { + let uri = self.url.clone(); + let key = self.sdk_key.clone(); + + let http = self.http.clone(); + let cache = self.cache.clone(); + + let mut request_builder = hyper::http::Request::builder() + .uri(uri) + .method("GET") + .header("Content-Type", "application/json") + .header("Authorization", key) + .header("User-Agent", &*crate::USER_AGENT); + + for default_header in &self.default_headers { + request_builder = + request_builder.header(*default_header.0, default_header.1.as_str()); + } - let resp = request_builder.send(); - - let mut response = match resp { - Ok(response) => response, - Err(e) => { - error!( - "An error occurred while retrieving flag information {} (status: {})", - e, - e.status() - .map_or(String::from("none"), |s| s.as_str().to_string()) - ); - return Err(match e.status() { - // If there is no status code, then the error is recoverable. - // If there is a status code, and the code is for a non-recoverable error, - // then the failure is permanent. - Some(status_code) if !is_http_error_recoverable(status_code) => { - FeatureRequesterError::Permanent - } - _ => FeatureRequesterError::Temporary, - }); + if let Some(cache) = &self.cache { + request_builder = request_builder.header("If-None-Match", cache.1.clone()); } - }; - if response.status() == StatusCode::NOT_MODIFIED && self.cache.is_some() { - let cache = self.cache.clone().unwrap(); - debug!("Returning cached data. Etag: {}", cache.1); - return Ok(cache.0); - } + let result = http + .request(request_builder.body(Body::empty()).unwrap()) + .await; - let etag: String = response - .headers() - .get(ETAG) - .unwrap_or(&HeaderValue::from_static("")) - .to_str() - .map_or_else(|_| "".into(), |s| s.into()); - - if response.status().is_success() { - return match response.json::>() { - Ok(all_data) => { - if !etag.is_empty() { - debug!("Caching data for future use with etag: {}", etag); - self.cache = Some(CachedEntry(all_data.clone(), etag)); - } - Ok(all_data) - } + let mut response = match result { + Ok(response) => response, Err(e) => { - error!("An error occurred while parsing the json response: {}", e); - Err(FeatureRequesterError::Permanent) + // It appears this type of error will not be an HTTP error. + // It will be a closed connection, aborted write, timeout, etc. + error!("An error occurred while retrieving flag information {}", e,); + return Err(FeatureRequesterError::Temporary); } }; - } - error!( - "An error occurred while retrieving flag information. (status: {})", - response.status().as_str() - ); + if response.status() == hyper::StatusCode::NOT_MODIFIED && cache.is_some() { + if let Some(entry) = cache { + return Ok(entry.0); + } + } - if !is_http_error_recoverable(response.status()) { - return Err(FeatureRequesterError::Permanent); - } + let etag: String = response + .headers() + .get("etag") + .unwrap_or(&crate::EMPTY_HEADER) + .to_str() + .map_or_else(|_| "".into(), |s| s.into()); + + if response.status().is_success() { + let bytes = response.body_mut().data().await.unwrap().unwrap(); + let json = serde_json::from_slice::>(bytes.as_ref()); + + return match json { + Ok(all_data) => { + if !etag.is_empty() { + debug!("Caching data for future use with etag: {}", etag); + self.cache = Some(CachedEntry(all_data.clone(), etag)); + } + Ok(all_data) + } + Err(e) => { + error!("An error occurred while parsing the json response: {}", e); + Err(FeatureRequesterError::Temporary) + } + }; + } + + error!( + "An error occurred while retrieving flag information. (status: {})", + response.status().as_str() + ); - Err(FeatureRequesterError::Temporary) + if !is_http_error_recoverable(response.status().as_u16()) { + return Err(FeatureRequesterError::Permanent); + } + + Err(FeatureRequesterError::Temporary) + }) } } @@ -121,10 +138,11 @@ impl FeatureRequester for ReqwestFeatureRequester { mod tests { use super::*; use mockito::mock; + use std::str::FromStr; use test_case::test_case; - #[test] - fn updates_etag_as_appropriate() { + #[tokio::test] + async fn updates_etag_as_appropriate() { let _initial_request = mock("GET", "/") .with_status(200) .with_header("etag", "INITIAL-TAG") @@ -144,20 +162,20 @@ mod tests { .create(); let mut requester = build_feature_requester(); - let result = requester.get_all(); + let result = requester.get_all().await; assert!(result.is_ok()); if let Some(cache) = &requester.cache { assert_eq!("INITIAL-TAG", cache.1); } - let result = requester.get_all(); + let result = requester.get_all().await; assert!(result.is_ok()); if let Some(cache) = &requester.cache { assert_eq!("INITIAL-TAG", cache.1); } - let result = requester.get_all(); + let result = requester.get_all().await; assert!(result.is_ok()); if let Some(cache) = &requester.cache { assert_eq!("UPDATED-TAG", cache.1); @@ -171,11 +189,15 @@ mod tests { #[test_case(429, FeatureRequesterError::Temporary)] #[test_case(430, FeatureRequesterError::Permanent)] #[test_case(500, FeatureRequesterError::Temporary)] - fn correctly_determines_unrecoverable_errors(status: usize, error: FeatureRequesterError) { + #[tokio::test] + async fn correctly_determines_unrecoverable_errors( + status: usize, + error: FeatureRequesterError, + ) { let _initial_request = mock("GET", "/").with_status(status).create(); let mut requester = build_feature_requester(); - let result = requester.get_all(); + let result = requester.get_all().await; if let Err(err) = result { assert_eq!(err, error); @@ -184,13 +206,16 @@ mod tests { } } - fn build_feature_requester() -> ReqwestFeatureRequester { - let http = r::Client::builder() - .build() - .expect("Failed building the client"); - let url = reqwest::Url::parse(&mockito::server_url()) + fn build_feature_requester() -> HyperFeatureRequester { + let http = hyper::Client::builder().build(hyper::client::HttpConnector::new()); + let url = hyper::Uri::from_str(&mockito::server_url()) .expect("Failed parsing the mock server url"); - ReqwestFeatureRequester::new(http, url, "sdk-key".to_string()) + HyperFeatureRequester::new( + http, + url, + "sdk-key".to_string(), + HashMap::<&str, String>::new(), + ) } } diff --git a/launchdarkly-server-sdk/src/feature_requester_builders.rs b/launchdarkly-server-sdk/src/feature_requester_builders.rs index 4a5c2aa..17323f8 100644 --- a/launchdarkly-server-sdk/src/feature_requester_builders.rs +++ b/launchdarkly-server-sdk/src/feature_requester_builders.rs @@ -1,7 +1,8 @@ -use crate::feature_requester::FeatureRequester; -use crate::feature_requester::ReqwestFeatureRequester; +use crate::feature_requester::{FeatureRequester, HyperFeatureRequester}; use crate::LAUNCHDARKLY_TAGS_HEADER; -use reqwest as r; +use hyper_rustls::HttpsConnectorBuilder; +use std::collections::HashMap; +use std::str::FromStr; use thiserror::Error; /// Error type used to represent failures when building a [FeatureRequesterFactory] instance. @@ -22,12 +23,12 @@ pub trait FeatureRequesterFactory: Send { fn build(&self, tags: Option) -> Result, BuildError>; } -pub struct ReqwestFeatureRequesterBuilder { +pub struct HyperFeatureRequesterBuilder { url: String, sdk_key: String, } -impl ReqwestFeatureRequesterBuilder { +impl HyperFeatureRequesterBuilder { pub fn new(url: &str, sdk_key: &str) -> Self { Self { url: url.into(), @@ -36,32 +37,35 @@ impl ReqwestFeatureRequesterBuilder { } } -impl FeatureRequesterFactory for ReqwestFeatureRequesterBuilder { +impl FeatureRequesterFactory for HyperFeatureRequesterBuilder { fn build(&self, tags: Option) -> Result, BuildError> { let url = format!("{}/sdk/latest-all", self.url); - let url = r::Url::parse(&url) - .map_err(|_| BuildError::InvalidConfig("Invalid base url provided".into()))?; - let mut builder = r::Client::builder(); + let builder = hyper::Client::builder(); + + let connector = HttpsConnectorBuilder::new() + .with_native_roots() + .https_or_http() + .enable_http1() + .enable_http2() + .build(); + + let http = builder.build(connector); + + let mut default_headers = HashMap::<&str, String>::new(); if let Some(tags) = tags { - let mut headers = r::header::HeaderMap::new(); - headers.append( - LAUNCHDARKLY_TAGS_HEADER, - r::header::HeaderValue::from_str(&tags) - .map_err(|e| BuildError::InvalidConfig(e.to_string()))?, - ); - builder = builder.default_headers(headers); + default_headers.insert(LAUNCHDARKLY_TAGS_HEADER, tags); } - let http = builder - .build() - .map_err(|e| BuildError::InvalidConfig(e.to_string()))?; + let url = hyper::Uri::from_str(url.as_str()) + .map_err(|_| BuildError::InvalidConfig("Invalid base url provided".into()))?; - Ok(Box::new(ReqwestFeatureRequester::new( + Ok(Box::new(HyperFeatureRequester::new( http, url, self.sdk_key.clone(), + default_headers, ))) } } @@ -73,7 +77,7 @@ mod tests { #[test] fn factory_handles_url_parsing_failure() { let builder = - ReqwestFeatureRequesterBuilder::new("This is clearly not a valid URL", "sdk-key"); + HyperFeatureRequesterBuilder::new("This is clearly not a valid URL", "sdk-key"); let result = builder.build(None); match result { diff --git a/launchdarkly-server-sdk/src/lib.rs b/launchdarkly-server-sdk/src/lib.rs index c5b1ad1..2e44d6a 100644 --- a/launchdarkly-server-sdk/src/lib.rs +++ b/launchdarkly-server-sdk/src/lib.rs @@ -18,7 +18,8 @@ extern crate serde_json; pub use launchdarkly_server_sdk_evaluation::Error as EvalError; pub use launchdarkly_server_sdk_evaluation::{ - AttributeValue, Detail, FlagValue, Reason, TypeError, User, UserBuilder, + AttributeValue, Context, ContextBuilder, Detail, FlagValue, Kind, MultiContextBuilder, Reason, + Reference, }; use lazy_static::lazy_static; @@ -64,11 +65,15 @@ mod version; static LAUNCHDARKLY_EVENT_SCHEMA_HEADER: &str = "x-launchdarkly-event-schema"; static LAUNCHDARKLY_PAYLOAD_ID_HEADER: &str = "x-launchdarkly-payload-id"; static LAUNCHDARKLY_TAGS_HEADER: &str = "x-launchdarkly-tags"; -static CURRENT_EVENT_SCHEMA: &str = "3"; +static CURRENT_EVENT_SCHEMA: &str = "4"; lazy_static! { pub(crate) static ref USER_AGENT: String = "RustServerClient/".to_owned() + built_info::PKG_VERSION; + + // For cases where a statically empty header value are needed. + pub(crate) static ref EMPTY_HEADER: hyper::header::HeaderValue = + hyper::header::HeaderValue::from_static(""); } #[allow(dead_code)] diff --git a/launchdarkly-server-sdk/src/reqwest.rs b/launchdarkly-server-sdk/src/reqwest.rs index d5c5499..9fcc2b9 100644 --- a/launchdarkly-server-sdk/src/reqwest.rs +++ b/launchdarkly-server-sdk/src/reqwest.rs @@ -1,20 +1,25 @@ -use reqwest::StatusCode; +use hyper::StatusCode; -pub fn is_http_error_recoverable(status: StatusCode) -> bool { - if !status.is_client_error() { - return true; +pub fn is_http_error_recoverable(status: u16) -> bool { + if let Ok(status) = StatusCode::from_u16(status) { + if !status.is_client_error() { + return true; + } + + return matches!( + status, + StatusCode::BAD_REQUEST | StatusCode::REQUEST_TIMEOUT | StatusCode::TOO_MANY_REQUESTS + ); } - matches!( - status, - StatusCode::BAD_REQUEST | StatusCode::REQUEST_TIMEOUT | StatusCode::TOO_MANY_REQUESTS - ) + warn!("Unable to determine if status code is recoverable"); + false } #[cfg(test)] mod tests { use super::*; - use reqwest::StatusCode; + use hyper::StatusCode; use test_case::test_case; #[test_case("130.65331632653061", 130.65331632653061)] @@ -36,6 +41,6 @@ mod tests { #[test_case(StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE, false)] #[test_case(StatusCode::INTERNAL_SERVER_ERROR, true)] fn can_determine_recoverable_errors(status: StatusCode, is_recoverable: bool) { - assert_eq!(is_recoverable, is_http_error_recoverable(status)); + assert_eq!(is_recoverable, is_http_error_recoverable(status.as_u16())); } } diff --git a/launchdarkly-server-sdk/src/stores/persistent_store_builders.rs b/launchdarkly-server-sdk/src/stores/persistent_store_builders.rs index 876618f..4158288 100644 --- a/launchdarkly-server-sdk/src/stores/persistent_store_builders.rs +++ b/launchdarkly-server-sdk/src/stores/persistent_store_builders.rs @@ -102,7 +102,7 @@ mod tests { impl PersistentDataStoreFactory for InMemoryPersistentDataStoreFactory { fn create_persistent_data_store( &self, - ) -> std::result::Result, std::io::Error> { + ) -> Result, std::io::Error> { Ok(Box::new(InMemoryPersistentDataStore { data: AllData { flags: HashMap::new(), diff --git a/launchdarkly-server-sdk/src/stores/persistent_store_cache.rs b/launchdarkly-server-sdk/src/stores/persistent_store_cache.rs index f44b147..e3a2caf 100644 --- a/launchdarkly-server-sdk/src/stores/persistent_store_cache.rs +++ b/launchdarkly-server-sdk/src/stores/persistent_store_cache.rs @@ -12,7 +12,7 @@ pub(super) struct CachePair { cache_name: String, } -impl CachePair { +impl CachePair { pub fn new(cache_name: String, cache_ttl: Option) -> CachePair { match cache_ttl { Some(ttl) => CachePair { @@ -29,7 +29,7 @@ impl Cac } pub fn cache_is_infinite(&self) -> bool { - self.all.time_to_live().is_none() + self.all.policy().time_to_live().is_none() } pub fn get_all(&self) -> Option>> { @@ -53,7 +53,7 @@ impl Cac K: Hash + Eq + Send + Sync + 'static, V: Clone + Send + Sync + 'static, { - match cache.time_to_live() { + match cache.policy().time_to_live() { None => cache.insert(key, value), Some(duration) if !duration.is_zero() => cache.insert(key, value), _ => (), diff --git a/launchdarkly-server-sdk/src/stores/persistent_store_wrapper.rs b/launchdarkly-server-sdk/src/stores/persistent_store_wrapper.rs index ae41d1b..dea2ce3 100644 --- a/launchdarkly-server-sdk/src/stores/persistent_store_wrapper.rs +++ b/launchdarkly-server-sdk/src/stores/persistent_store_wrapper.rs @@ -56,7 +56,7 @@ impl PersistentDataStoreWrapper { Ok(was_updated) } - fn add_to_cache( + fn add_to_cache( was_updated: bool, cache: &CachePair, key: &str, diff --git a/launchdarkly-server-sdk/src/stores/store.rs b/launchdarkly-server-sdk/src/stores/store.rs index 5f7455d..28d87c2 100644 --- a/launchdarkly-server-sdk/src/stores/store.rs +++ b/launchdarkly-server-sdk/src/stores/store.rs @@ -31,8 +31,7 @@ pub trait DataStore: Store + Send + Sync { fn to_store(&self) -> &dyn Store; } -// TODO(ch108602) implement Error::ClientNotReady -/// Default implementation of the DataStore which holds information in an in-memory data store. +/// Default implementation of [DataStore] which holds information in-memory. pub struct InMemoryDataStore { pub data: AllData, StorageItem>, } @@ -190,8 +189,8 @@ mod tests { let mut data_store = InMemoryDataStore::new(); data_store.init(basic_data()); - let mut flag = data_store.flag("flag-key").unwrap().clone(); - flag.version = flag.version + 1; + let mut flag = data_store.flag("flag-key").unwrap(); + flag.version += 1; assert!(data_store .upsert( @@ -201,13 +200,13 @@ mod tests { .is_ok()); assert!(data_store.flag("flag-key").is_none()); - flag.version = flag.version - 1; + flag.version -= 1; let patch_target = PatchTarget::Flag(StorageItem::Item(flag.clone())); assert!(data_store.upsert("flag-key", patch_target).is_ok()); assert!(data_store.flag("flag-key").is_none()); - flag.version = flag.version + 2; + flag.version += 2; let patch_target = PatchTarget::Flag(StorageItem::Item(flag)); assert!(data_store.upsert("flag-key", patch_target).is_ok()); assert!(data_store.flag("flag-key").is_some()); @@ -222,10 +221,9 @@ mod tests { let mut data_store = InMemoryDataStore::new(); data_store.init(basic_data()); - let flag = data_store.flag("flag-key").unwrap(); + let mut flag = data_store.flag("flag-key").unwrap(); assert_eq!(42, flag.version); - let mut flag = flag.clone(); flag.version = updated_version; let patch_target = PatchTarget::Flag(StorageItem::Item(flag)); @@ -267,8 +265,8 @@ mod tests { let mut data_store = InMemoryDataStore::new(); data_store.init(basic_data()); - let mut segment = data_store.segment("segment-key").unwrap().clone(); - segment.version = segment.version + 1; + let mut segment = data_store.segment("segment-key").unwrap(); + segment.version += 1; assert!(data_store .upsert( @@ -278,13 +276,13 @@ mod tests { .is_ok()); assert!(data_store.segment("segment-key").is_none()); - segment.version = segment.version - 1; + segment.version -= 1; let patch_target = PatchTarget::Segment(StorageItem::Item(segment.clone())); assert!(data_store.upsert("segment-key", patch_target).is_ok()); assert!(data_store.segment("segment-key").is_none()); - segment.version = segment.version + 2; + segment.version += 2; let patch_target = PatchTarget::Segment(StorageItem::Item(segment)); assert!(data_store.upsert("segment-key", patch_target).is_ok()); assert!(data_store.segment("segment-key").is_some()); @@ -299,10 +297,9 @@ mod tests { let mut data_store = InMemoryDataStore::new(); data_store.init(basic_data()); - let segment = data_store.segment("segment-key").unwrap(); + let mut segment = data_store.segment("segment-key").unwrap(); assert_eq!(1, segment.version); - let mut segment = segment.clone(); segment.version = updated_version; let patch_target = PatchTarget::Segment(StorageItem::Item(segment));