From a9344fddcfc0b8b8b8419a87fb5efafca5da8033 Mon Sep 17 00:00:00 2001 From: ssebo Date: Tue, 4 Apr 2023 12:29:37 +0800 Subject: [PATCH 1/7] fix: salt empty string --- src/evaluate.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/evaluate.rs b/src/evaluate.rs index 9103424..06daf9e 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 } From a7cca3530ad4511a5283c399df22af2e1005933e Mon Sep 17 00:00:00 2001 From: jianggang Date: Thu, 20 Apr 2023 12:10:08 +0800 Subject: [PATCH 2/7] :sparkles: feat: support debug event --- Cargo.lock | 4 +- Cargo.toml | 4 +- src/evaluate.rs | 96 ++++++++++++++++++++++++++++++++------------ src/feature_probe.rs | 43 ++++++++++++++++++-- src/lib.rs | 2 - 5 files changed, 114 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65be1ae..db80a7b 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", 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/src/evaluate.rs b/src/evaluate.rs index 06daf9e..463d658 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -123,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, @@ -168,6 +170,7 @@ impl Toggle { toggle_repo: &HashMap, is_detail: bool, deep: u8, + debug_until_time: Option, ) -> EvalDetail { let eval_param = EvalParams { user, @@ -176,6 +179,7 @@ impl Toggle { key: &self.key, is_detail, variations: &self.variations, + debug_until_time, }; match self.do_eval(&eval_param, deep) { @@ -191,20 +195,42 @@ 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_pass) => { + if !is_pass { + return Ok(self.default_variation(eval_param, None)); + } + } + 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, + )); } } } @@ -216,7 +242,7 @@ impl Toggle { &self, eval_param: &EvalParams, deep: u8, - ) -> Result<(), PrerequisiteError> { + ) -> Result { if deep == 0 { return Err(PrerequisiteError::DeepOverflow); } @@ -235,6 +261,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, )?, @@ -242,12 +269,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( @@ -255,12 +282,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, @@ -273,10 +302,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, + ), } } @@ -524,12 +561,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 { @@ -539,6 +578,7 @@ impl Default for Repository { toggles: Default::default(), events: Default::default(), version: Some(0), + debug_until_time: None, } } } @@ -612,7 +652,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()); @@ -629,7 +669,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()); @@ -646,19 +686,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")); } @@ -678,7 +718,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() { @@ -709,7 +749,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() @@ -731,7 +771,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()); } @@ -748,7 +788,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")); @@ -766,10 +806,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] @@ -784,7 +824,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")); @@ -828,6 +868,7 @@ mod distribution_tests { variations: &[], segment_repo: &Default::default(), toggle_repo: &Default::default(), + debug_until_time: None, }; let result = distribution.find_index(¶ms); @@ -854,6 +895,7 @@ mod distribution_tests { variations: &[], segment_repo: &Default::default(), toggle_repo: &Default::default(), + debug_until_time: None, }; let result = distribution.find_index(¶ms); @@ -866,6 +908,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()); @@ -895,6 +938,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"); @@ -1245,7 +1289,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..abec9ce 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,17 +216,23 @@ 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); + self.record_access(toggle, user, track_access_events, &detail); + self.record_debug(toggle, user, debug_until_time, &detail); + 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, @@ -243,13 +251,42 @@ impl FeatureProbe { 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>, + ) -> 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 >= unix_timestamp() { + let debug = DebugEvent { + kind: "debug".to_string(), + time: unix_timestamp(), + 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(); 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)] From c634f2ba44faeddcaa313659847223ae96ba190a Mon Sep 17 00:00:00 2001 From: jianggang Date: Thu, 20 Apr 2023 14:13:06 +0800 Subject: [PATCH 3/7] :sparkles: feat: support debug event --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index db80a7b..995da17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -771,7 +771,7 @@ dependencies = [ [[package]] name = "feature-probe-server-sdk" -version = "2.1.1" +version = "2.2.0" dependencies = [ "anyhow", "approx", From 40f1181ebfb40fd3408e67bee28bc16d58b12d3e Mon Sep 17 00:00:00 2001 From: jianggang Date: Thu, 20 Apr 2023 14:53:05 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20feat:=20support=20debug=20event?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/fixtures/spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e500b80214ff155fc174a9f608d136a6cdc6a945 Mon Sep 17 00:00:00 2001 From: jianggang Date: Thu, 20 Apr 2023 16:31:53 +0800 Subject: [PATCH 5/7] :sparkles: feat: support debug event --- resources/fixtures/repo.json | 1 + src/feature_probe.rs | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) 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/src/feature_probe.rs b/src/feature_probe.rs index abec9ce..e308392 100644 --- a/src/feature_probe.rs +++ b/src/feature_probe.rs @@ -223,8 +223,9 @@ impl FeatureProbe { Some(toggle) => toggle.track_access_events(), None => false, }; - self.record_access(toggle, user, track_access_events, &detail); - self.record_debug(toggle, user, debug_until_time, &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); @@ -238,13 +239,14 @@ impl FeatureProbe { 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(), @@ -263,15 +265,16 @@ impl FeatureProbe { 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 >= unix_timestamp() { + if debug_until_time as u128 >= ts { let debug = DebugEvent { kind: "debug".to_string(), - time: unix_timestamp(), + time: ts, key: toggle.to_owned(), user: user.key(), user_detail: serde_json::to_value(user).unwrap(), @@ -493,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); From 74639a1ae46a39c3372de69d23be28bf22296f5d Mon Sep 17 00:00:00 2001 From: jianggang Date: Thu, 20 Apr 2023 16:38:41 +0800 Subject: [PATCH 6/7] :sparkles: feat: support debug event --- src/evaluate.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/evaluate.rs b/src/evaluate.rs index 463d658..45cda75 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -204,11 +204,10 @@ impl Toggle { } match self.check_prerequisites(eval_param, max_deep) { - Ok(is_pass) => { - if !is_pass { - return Ok(self.default_variation(eval_param, None)); - } + Ok(is_match) if !is_match => { + return Ok(self.default_variation(eval_param, None)); } + Ok(_is_match) => {} Err(e) => return Err(e), } From 171f0f8f3b11cbcf7ae0b42ab34ac1d0c4bcb59c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Apr 2023 08:45:49 +0000 Subject: [PATCH 7/7] chore(deps): bump http from 0.2.8 to 0.2.9 Bumps [http](https://github.com/hyperium/http) from 0.2.8 to 0.2.9. - [Release notes](https://github.com/hyperium/http/releases) - [Changelog](https://github.com/hyperium/http/blob/master/CHANGELOG.md) - [Commits](https://github.com/hyperium/http/compare/v0.2.8...v0.2.9) --- updated-dependencies: - dependency-name: http dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 995da17..ebf68f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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",