statsig_rust/persistent_storage/
persistent_storage_trait.rs1use crate::evaluation::dynamic_string::DynamicString;
2use crate::evaluation::evaluation_types::BaseEvaluation;
3use crate::interned_string::InternedString;
4use crate::specs_response::explicit_params::ExplicitParameters;
5use crate::{log_e, DynamicReturnable, StatsigUser};
6use crate::{
7 statsig_types::{Experiment, Layer},
8 unwrap_or_return, SecondaryExposure,
9};
10use serde::{Deserialize, Deserializer, Serialize};
11use serde_json::Value;
12use std::collections::HashMap;
13
14pub type UserPersistedValues = HashMap<String, StickyValues>;
15
16pub trait PersistentStorage: Send + Sync {
17 fn load(&self, key: String) -> Option<UserPersistedValues>;
18 fn save(&self, key: &str, config_name: &str, data: StickyValues);
19 fn delete(&self, key: &str, config_name: &str);
20
21 fn get_values_for_user(
22 &self,
23 user: &StatsigUser,
24 id_type: &str,
25 ) -> Option<UserPersistedValues> {
26 let key = get_persistent_storage_key(user, id_type)?;
27 self.load(key)
28 }
29}
30
31pub fn get_persistent_storage_key(user: &StatsigUser, id_type: &str) -> Option<String> {
32 let dyn_str_id_type = DynamicString::from(id_type.to_string());
33 user.get_unit_id(&dyn_str_id_type).map(|id| {
34 let id_str = id
35 .string_value
36 .as_ref()
37 .map(|value| value.value.as_str())
38 .unwrap_or("");
39
40 format!("{id_str}:{id_type}")
41 })
42}
43
44const TAG: &str = "PersistentStorageTrait";
45
46#[derive(Serialize, Deserialize, Debug, Clone, Default)]
47pub struct StickyValues {
48 pub value: bool,
49 pub json_value: Option<DynamicReturnable>,
50 pub rule_id: Option<InternedString>,
51 pub group_name: Option<InternedString>,
52 pub secondary_exposures: Vec<SecondaryExposure>,
53 pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
54 pub config_delegate: Option<InternedString>,
55 pub explicit_parameters: Option<ExplicitParameters>,
56 #[serde(deserialize_with = "deserialize_safe_timestamp")]
57 pub time: Option<u64>,
58 pub config_version: Option<u32>,
59}
60
61fn deserialize_safe_timestamp<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
62where
63 D: Deserializer<'de>,
64{
65 let value = Value::deserialize(deserializer)?;
66
67 if value.is_null() {
68 return Ok(None);
69 }
70
71 if let Value::Number(num) = value {
72 if let Some(timestamp) = num.as_u64() {
73 return Ok(Some(timestamp));
74 }
75
76 if let Some(timestamp) = num.as_i64() {
77 return Ok(Some(timestamp as u64));
78 }
79
80 if let Some(timestamp) = num.as_f64() {
81 return Ok(Some(timestamp as u64));
82 }
83 }
84
85 log_e!(TAG, "Non-numeric 'timestamp' field");
86
87 Ok(None)
88}
89
90pub fn make_sticky_value_from_layer(layer: &Layer) -> Option<StickyValues> {
91 let layer_evaluation = unwrap_or_return!(layer.__evaluation.as_ref(), None);
92 let config_version = extract_config_version(&layer_evaluation.base);
93
94 Some(StickyValues {
95 value: true,
96 json_value: Some(layer_evaluation.value.clone()),
97 rule_id: Some(layer_evaluation.base.rule_id.clone()),
98 group_name: layer_evaluation.group_name.clone(),
99 secondary_exposures: layer_evaluation.base.secondary_exposures.clone(),
100 undelegated_secondary_exposures: layer_evaluation.undelegated_secondary_exposures.clone(),
101 config_delegate: layer_evaluation.allocated_experiment_name.clone(),
102 explicit_parameters: Some(layer_evaluation.explicit_parameters.clone()),
103 time: layer.details.lcut,
104 config_version,
105 })
106}
107
108pub fn make_sticky_value_from_experiment(experiment: &Experiment) -> Option<StickyValues> {
109 let evaluation = unwrap_or_return!(&experiment.__evaluation, None);
110 let config_version = extract_config_version(&evaluation.base);
111
112 Some(StickyValues {
113 value: true, json_value: Some(evaluation.value.clone()),
115 rule_id: Some(evaluation.base.rule_id.clone()),
116 group_name: evaluation.group_name.clone(),
117 secondary_exposures: evaluation.base.secondary_exposures.clone(),
118 undelegated_secondary_exposures: evaluation.undelegated_secondary_exposures.clone(),
119 config_delegate: None,
120 explicit_parameters: evaluation.explicit_parameters.as_ref().cloned(),
121 time: experiment.details.lcut,
122 config_version,
123 })
124}
125
126fn extract_config_version(evaluation: &BaseEvaluation) -> Option<u32> {
127 evaluation
128 .exposure_info
129 .as_ref()
130 .and_then(|info| info.version)
131}