diff --git a/Cargo.lock b/Cargo.lock index 65be1ae..ebf68f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -694,9 +694,9 @@ dependencies = [ [[package]] name = "feature-probe-event" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef547aae6c8bf890c41149452fdd3bcd5a971054079b42a089fd5752ee8b31c" +checksum = "7b67f319a69b42c5fc57fe692a19f5f846f0a9f56f8d488748c0992884676113" dependencies = [ "axum 0.5.17", "headers", @@ -771,7 +771,7 @@ dependencies = [ [[package]] name = "feature-probe-server-sdk" -version = "2.1.1" +version = "2.2.0" dependencies = [ "anyhow", "approx", @@ -1055,9 +1055,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", diff --git a/Cargo.toml b/Cargo.toml index 0df31df..a0e85a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] edition = "2021" name = "feature-probe-server-sdk" -version = "2.1.1" +version = "2.2.0" license = "Apache-2.0" authors = ["maintain@featureprobe.com"] description = "FeatureProbe Server Side SDK for Rust" @@ -40,7 +40,7 @@ thiserror = "1.0" tracing = "0.1" url = "2" -feature-probe-event = { version = "1.1.3", features = [ +feature-probe-event = { version = "1.2.0", features = [ "use_tokio", ], default-features = false} diff --git a/resources/fixtures/repo.json b/resources/fixtures/repo.json index 7741fa0..c2380b4 100644 --- a/resources/fixtures/repo.json +++ b/resources/fixtures/repo.json @@ -1,5 +1,6 @@ { "version": 1, + "debugUntilTime": 1681289908000, "segments": { "some_segment1-fjoaefjaam": { "key": "some_segment1", diff --git a/resources/fixtures/spec b/resources/fixtures/spec index 9e8e9e0..8cbca35 160000 --- a/resources/fixtures/spec +++ b/resources/fixtures/spec @@ -1 +1 @@ -Subproject commit 9e8e9e041ceb4a75408cc337ff4ebf9c4f3eb386 +Subproject commit 8cbca353a9cc2c5cc1a99182c473a1c51123a678 diff --git a/src/evaluate.rs b/src/evaluate.rs index 9103424..45cda75 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -78,8 +78,8 @@ impl Distribution { }; let salt = match &self.salt { - None => eval_param.key, - Some(s) => s, + Some(s) if !s.is_empty() => s, + _ => eval_param.key, }; let bucket_index = salt_hash(&hash_key, salt, 10000); @@ -112,7 +112,6 @@ fn salt_hash(key: &str, salt: &str, bucket_size: u64) -> u32 { v.push(hax_value[i]); } let mut v = v.as_slice(); - let value = v.read_u32::().expect("can not be here"); value % bucket_size as u32 } @@ -124,14 +123,16 @@ pub struct EvalParams<'a> { variations: &'a [Value], segment_repo: &'a HashMap, toggle_repo: &'a HashMap, + debug_until_time: Option, } -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Default)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Default, Clone)] #[serde(rename_all = "camelCase")] pub struct EvalDetail { pub value: Option, pub rule_index: Option, pub track_access_events: Option, + pub debug_until_time: Option, pub last_modified: Option, pub variation_index: Option, pub version: Option, @@ -169,6 +170,7 @@ impl Toggle { toggle_repo: &HashMap, is_detail: bool, deep: u8, + debug_until_time: Option, ) -> EvalDetail { let eval_param = EvalParams { user, @@ -177,6 +179,7 @@ impl Toggle { key: &self.key, is_detail, variations: &self.variations, + debug_until_time, }; match self.do_eval(&eval_param, deep) { @@ -192,20 +195,41 @@ impl Toggle { ) -> Result, PrerequisiteError> { if !self.enabled { let v = self.disabled_serve.select_variation(eval_param).ok(); - return Ok(self.serve_variation(v, "disabled".to_owned(), None)); + return Ok(self.serve_variation( + v, + "disabled".to_owned(), + None, + eval_param.debug_until_time, + )); } - self.check_prerequisites(eval_param, max_deep)?; + match self.check_prerequisites(eval_param, max_deep) { + Ok(is_match) if !is_match => { + return Ok(self.default_variation(eval_param, None)); + } + Ok(_is_match) => {} + Err(e) => return Err(e), + } for (i, rule) in self.rules.iter().enumerate() { match rule.serve_variation(eval_param) { Ok(v) => { if v.is_some() { - return Ok(self.serve_variation(v, format!("rule {i}"), Some(i))); + return Ok(self.serve_variation( + v, + format!("rule {i}"), + Some(i), + eval_param.debug_until_time, + )); } } Err(e) => { - return Ok(self.serve_variation(None, format!("{e:?}"), Some(i))); + return Ok(self.serve_variation( + None, + format!("{e:?}"), + Some(i), + eval_param.debug_until_time, + )); } } } @@ -217,7 +241,7 @@ impl Toggle { &self, eval_param: &EvalParams, deep: u8, - ) -> Result<(), PrerequisiteError> { + ) -> Result { if deep == 0 { return Err(PrerequisiteError::DeepOverflow); } @@ -236,6 +260,7 @@ impl Toggle { user: eval_param.user, segment_repo: eval_param.segment_repo, toggle_repo: eval_param.toggle_repo, + debug_until_time: eval_param.debug_until_time, }, deep - 1, )?, @@ -243,12 +268,12 @@ impl Toggle { match eval.value { Some(v) if v == pre.value => continue, - _ => return Err(PrerequisiteError::NotMatch(pre.key.to_string())), + _ => return Ok(false), } } + return Ok(true); } - - Ok(()) + Ok(true) } fn serve_variation( @@ -256,12 +281,14 @@ impl Toggle { v: Option, reason: String, rule_index: Option, + debug_until_time: Option, ) -> EvalDetail { EvalDetail { variation_index: v.as_ref().map(|v| v.index), value: v.map(|v| v.value), version: Some(self.version), track_access_events: self.track_access_events, + debug_until_time, last_modified: self.last_modified, rule_index, reason, @@ -274,10 +301,18 @@ impl Toggle { reason: Option, ) -> EvalDetail { match self.default_serve.select_variation(eval_param) { - Ok(v) => { - self.serve_variation(Some(v), concat_reason("default".to_owned(), reason), None) - } - Err(e) => self.serve_variation(None, concat_reason(format!("{e:?}"), reason), None), + Ok(v) => self.serve_variation( + Some(v), + concat_reason("default".to_owned(), reason), + None, + eval_param.debug_until_time, + ), + Err(e) => self.serve_variation( + None, + concat_reason(format!("{e:?}"), reason), + None, + eval_param.debug_until_time, + ), } } @@ -525,12 +560,14 @@ impl Segment { } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] pub struct Repository { pub segments: HashMap, pub toggles: HashMap, pub events: Option, // TODO: remove option next release pub version: Option, + pub debug_until_time: Option, } impl Default for Repository { @@ -540,6 +577,7 @@ impl Default for Repository { toggles: Default::default(), events: Default::default(), version: Some(0), + debug_until_time: None, } } } @@ -613,7 +651,7 @@ mod tests { let user = FPUser::new().with("city", "4"); let toggle = repo.toggles.get("json_toggle").unwrap(); - let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP); + let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None); let r = r.value.unwrap(); let r = r.as_object().unwrap(); assert!(r.get("variation_1").is_some()); @@ -630,7 +668,7 @@ mod tests { let user = FPUser::new().with("city", "100"); let toggle = repo.toggles.get("not_in_segment").unwrap(); - let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP); + let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None); let r = r.value.unwrap(); let r = r.as_object().unwrap(); assert!(r.get("not_in").is_some()); @@ -647,19 +685,19 @@ mod tests { let user = FPUser::new().with("city", "1").with("os", "linux"); let toggle = repo.toggles.get("multi_condition_toggle").unwrap(); - let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP); + let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None); let r = r.value.unwrap(); let r = r.as_object().unwrap(); assert!(r.get("variation_0").is_some()); let user = FPUser::new().with("os", "linux"); let toggle = repo.toggles.get("multi_condition_toggle").unwrap(); - let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP); + let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None); assert!(r.reason.starts_with("default")); let user = FPUser::new().with("city", "1"); let toggle = repo.toggles.get("multi_condition_toggle").unwrap(); - let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP); + let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None); assert!(r.reason.starts_with("default")); } @@ -679,7 +717,7 @@ mod tests { let mut variation_1 = 0; let mut variation_2 = 0; for user in &users { - let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP); + let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None); let r = r.value.unwrap(); let r = r.as_object().unwrap(); if r.get("variation_0").is_some() { @@ -710,7 +748,7 @@ mod tests { let user = FPUser::new().with("city", "100"); let toggle = repo.toggles.get("disabled_toggle").unwrap(); - let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP); + let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None); assert!(r .value .unwrap() @@ -732,7 +770,7 @@ mod tests { let user = FPUser::new().with("city", "4"); let toggle = repo.toggles.get("prerequisite_toggle").unwrap(); - let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP); + let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None); assert!(r.value.unwrap().as_object().unwrap().get("2").is_some()); } @@ -749,7 +787,7 @@ mod tests { let user = FPUser::new().with("city", "4"); let toggle = repo.toggles.get("prerequisite_toggle_not_exist").unwrap(); - let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP); + let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None); assert!(r.value.unwrap().as_object().unwrap().get("1").is_some()); assert!(r.reason.contains("not exist")); @@ -767,10 +805,10 @@ mod tests { let user = FPUser::new().with("city", "4"); let toggle = repo.toggles.get("prerequisite_toggle_not_match").unwrap(); - let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP); + let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None); assert!(r.value.unwrap().as_object().unwrap().get("1").is_some()); - assert!(r.reason.contains("not match")); + assert!(r.reason.contains("default.")); } #[test] @@ -785,7 +823,7 @@ mod tests { let user = FPUser::new().with("city", "4"); let toggle = repo.toggles.get("prerequisite_toggle").unwrap(); - let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, 1); + let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, 1, None); assert!(r.value.unwrap().as_object().unwrap().get("1").is_some()); assert!(r.reason.contains("deep overflow")); @@ -829,6 +867,7 @@ mod distribution_tests { variations: &[], segment_repo: &Default::default(), toggle_repo: &Default::default(), + debug_until_time: None, }; let result = distribution.find_index(¶ms); @@ -855,6 +894,7 @@ mod distribution_tests { variations: &[], segment_repo: &Default::default(), toggle_repo: &Default::default(), + debug_until_time: None, }; let result = distribution.find_index(¶ms); @@ -867,6 +907,7 @@ mod distribution_tests { variations: &[], segment_repo: &Default::default(), toggle_repo: &Default::default(), + debug_until_time: None, }; let result = distribution.find_index(¶ms_no_detail); assert!(result.is_err()); @@ -896,6 +937,7 @@ mod distribution_tests { ], segment_repo: &Default::default(), toggle_repo: &Default::default(), + debug_until_time: None, }; let result = serve.select_variation(¶ms).expect_err("e"); @@ -1246,7 +1288,7 @@ mod condition_tests { let user = FPUser::new().with("city", "1"); let toggle = repo.toggles.get("json_toggle").unwrap(); - let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP); + let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None); let r = r.value.unwrap(); let r = r.as_object().unwrap(); assert!(r.get("variation_0").is_some()); diff --git a/src/feature_probe.rs b/src/feature_probe.rs index 23425dd..e308392 100644 --- a/src/feature_probe.rs +++ b/src/feature_probe.rs @@ -8,6 +8,7 @@ use crate::{sync::UpdateCallback, user::FPUser}; use crate::{FPDetail, SdkAuthorization, Toggle}; use event::event::AccessEvent; use event::event::CustomEvent; +use event::event::DebugEvent; use event::event::Event; use event::recorder::unix_timestamp; use event::recorder::EventRecorder; @@ -207,6 +208,7 @@ impl FeatureProbe { fn eval(&self, toggle: &str, user: &FPUser, is_detail: bool) -> Option> { let repo = self.repo.read(); + let debug_until_time = repo.debug_until_time; let detail = repo.toggles.get(toggle).map(|toggle| { toggle.eval( user, @@ -214,42 +216,80 @@ impl FeatureProbe { &repo.toggles, is_detail, self.config.max_prerequisites_deep, + debug_until_time, ) }); let track_access_events = match repo.toggles.get(toggle) { Some(toggle) => toggle.track_access_events(), None => false, }; - self.record_detail(toggle, user, track_access_events, &detail); + let ts = unix_timestamp(); + self.record_access(toggle, user, track_access_events, &detail, ts); + self.record_debug(toggle, user, debug_until_time, &detail, ts); + if let Some(mut detail) = detail { + detail.debug_until_time = debug_until_time; + return Some(detail); + } detail } - fn record_detail( + fn record_access( &self, toggle: &str, user: &FPUser, track_access_events: bool, detail: &Option>, + ts: u128, ) -> Option<()> { let recorder = self.event_recorder.as_ref()?; let detail = detail.as_ref()?; let value = detail.value.as_ref()?; let event = AccessEvent { kind: "access".to_string(), - time: unix_timestamp(), + time: ts, key: toggle.to_owned(), user: user.key(), value: value.clone(), variation_index: detail.variation_index.unwrap(), version: detail.version, rule_index: detail.rule_index, - reason: Some(detail.reason.to_string()), track_access_events, }; recorder.record_event(Event::AccessEvent(event)); None } + fn record_debug( + &self, + toggle: &str, + user: &FPUser, + debug_until_time: Option, + detail: &Option>, + ts: u128, + ) -> Option<()> { + let recorder = self.event_recorder.as_ref()?; + let detail = detail.as_ref()?; + let value = detail.value.as_ref()?; + if let Some(debug_until_time) = debug_until_time { + if debug_until_time as u128 >= ts { + let debug = DebugEvent { + kind: "debug".to_string(), + time: ts, + key: toggle.to_owned(), + user: user.key(), + user_detail: serde_json::to_value(user).unwrap(), + value: value.clone(), + variation_index: detail.variation_index.unwrap(), + version: detail.version, + rule_index: detail.rule_index, + reason: Some(detail.reason.to_string()), + }; + recorder.record_event(Event::DebugEvent(debug)); + } + } + None + } + fn start(&mut self) { self.sync(); @@ -456,6 +496,16 @@ mod tests { assert_eq!(fp.string_value("toggle_3", &u, "val".to_owned()), "value"); } + #[test] + fn test_feature_probe_track() { + let json = load_local_json("resources/fixtures/repo.json"); + let mut repo = json.unwrap(); + repo.debug_until_time = Some(unix_timestamp() as u64 + 60 * 1000); + let fp = FeatureProbe::new_with("secret key".to_string(), repo); + let u = FPUser::new().with("name", "bob").with("city", "1"); + fp.bool_value("bool_toggle", &u, false); + } + fn load_local_json(file: &str) -> Result { let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); path.push(file); diff --git a/src/lib.rs b/src/lib.rs index bf6aeb2..67c6e4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,8 +56,6 @@ enum PrerequisiteError { DeepOverflow, #[error("prerequisite not exist: {0}")] NotExist(String), - #[error("prerequisite not match: {0}")] - NotMatch(String), } #[derive(Debug, Deserialize)]