statsig_rust/persistent_storage/
persistent_storage_trait.rs1use crate::evaluation::dynamic_string::DynamicString;
2use crate::evaluation::evaluation_types::BaseEvaluation;
3use crate::event_logging::event_logger::EventLogger;
4use crate::interned_string::InternedString;
5use crate::{
6 evaluation::evaluation_types::{ExperimentEvaluation, LayerEvaluation},
7 statsig_type_factories::{extract_from_experiment_evaluation, make_layer},
8 statsig_types::{Experiment, Layer},
9 unwrap_or_return,
10 user::StatsigUserInternal,
11 EvaluationDetails, SecondaryExposure,
12};
13use crate::{log_e, StatsigUser};
14use chrono::Utc;
15use serde::{Deserialize, Deserializer, Serialize};
16use serde_json::Value;
17use std::{collections::HashMap, sync::Weak};
18
19pub type UserPersistedValues = HashMap<String, StickyValues>;
20
21pub trait PersistentStorage: Send + Sync {
22 fn load(&self, key: String) -> Option<UserPersistedValues>;
23 fn save(&self, key: &str, config_name: &str, data: StickyValues);
24 fn delete(&self, key: &str, config_name: &str);
25
26 fn get_values_for_user(
27 &self,
28 user: &StatsigUser,
29 id_type: &String,
30 ) -> Option<UserPersistedValues> {
31 let key = get_persistent_storage_key(user, id_type)?;
32 self.load(key)
33 }
34}
35
36pub fn get_persistent_storage_key(user: &StatsigUser, id_type: &String) -> Option<String> {
37 let dyn_str_id_type = DynamicString::from(id_type.clone());
38 user.get_unit_id(&dyn_str_id_type).map(|id| {
39 let mut id_str = "";
40 if let Some(id) = id.string_value.as_ref().map(|s| &s.value) {
41 id_str = id;
42 }
43
44 format!("{id_str}:{id_type}")
45 })
46}
47
48const TAG: &str = "PersistentStorageTrait";
49
50#[derive(Serialize, Deserialize, Debug, Clone, Default)]
51pub struct StickyValues {
52 pub value: bool,
53 pub json_value: Option<HashMap<String, Value>>,
54 pub rule_id: Option<InternedString>,
55 pub group_name: Option<String>,
56 pub secondary_exposures: Vec<SecondaryExposure>,
57 pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
58 pub config_delegate: Option<String>,
59 pub explicit_parameters: Option<Vec<String>>,
60 #[serde(deserialize_with = "deserialize_safe_timestamp")]
61 pub time: Option<u64>,
62 pub config_version: Option<u32>,
63}
64
65fn deserialize_safe_timestamp<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
66where
67 D: Deserializer<'de>,
68{
69 let value = Value::deserialize(deserializer)?;
70
71 if value.is_null() {
72 return Ok(None);
73 }
74
75 if let Value::Number(num) = value {
76 if let Some(timestamp) = num.as_u64() {
77 return Ok(Some(timestamp));
78 }
79
80 if let Some(timestamp) = num.as_i64() {
81 return Ok(Some(timestamp as u64));
82 }
83
84 if let Some(timestamp) = num.as_f64() {
85 return Ok(Some(timestamp as u64));
86 }
87 }
88
89 log_e!(TAG, "Non-numeric 'timestamp' field");
90
91 Ok(None)
92}
93
94pub fn make_layer_from_sticky_value(
95 name: &str,
96 user: &StatsigUserInternal,
97 evaluation: LayerEvaluation,
98 sticky_value: StickyValues,
99 event_logger_ptr: Option<Weak<EventLogger>>,
100 disable_exposure: bool,
101) -> Layer {
102 let details = EvaluationDetails {
103 reason: "Persisted".to_owned(),
104 lcut: sticky_value.time,
105 received_at: Some(Utc::now().timestamp_millis() as u64),
106 };
107
108 make_layer(
109 user.to_loggable(),
110 name,
111 Some(evaluation),
112 details,
113 event_logger_ptr,
114 disable_exposure,
115 )
116}
117
118pub fn make_sticky_value_from_layer(layer: &Layer) -> Option<StickyValues> {
119 let layer_evaluation = unwrap_or_return!(layer.__evaluation.as_ref(), None);
120 let config_version = extract_config_version(&layer_evaluation.base);
121
122 Some(StickyValues {
123 value: true,
124 json_value: Some(layer_evaluation.value.get_json().unwrap_or_default()),
125 rule_id: Some(layer_evaluation.base.rule_id.clone()),
126 group_name: layer_evaluation
127 .group_name
128 .as_ref()
129 .map(|g| g.unperformant_to_string()),
130 secondary_exposures: layer_evaluation.base.secondary_exposures.clone(),
131 undelegated_secondary_exposures: layer_evaluation.undelegated_secondary_exposures.clone(),
132 config_delegate: layer_evaluation
133 .allocated_experiment_name
134 .as_ref()
135 .map(|g| g.unperformant_to_string()),
136 explicit_parameters: Some(
137 layer_evaluation
138 .explicit_parameters
139 .iter()
140 .map(|g| g.unperformant_to_string())
141 .collect(),
142 ),
143 time: layer.details.lcut,
144 config_version,
145 })
146}
147
148pub fn make_experiment_from_sticky_value(
149 evaluation: ExperimentEvaluation,
150 sticky_value: StickyValues,
151) -> Experiment {
152 let name = evaluation.base.name.clone();
153 let maybe_evaluation = Some(evaluation);
154 let (value, rule_id, id_type, group_name, is_experiment_active) =
155 extract_from_experiment_evaluation(&maybe_evaluation);
156 let details = EvaluationDetails {
157 reason: "Persisted".to_owned(),
158 lcut: sticky_value.time,
159 received_at: Some(Utc::now().timestamp_millis() as u64),
160 };
161 Experiment {
162 name: name.unperformant_to_string(),
163 value: value.get_json().unwrap_or_default(),
164 rule_id: rule_id.unperformant_to_string(),
165 id_type,
166 group_name,
167 details,
168 is_experiment_active,
169 __evaluation: maybe_evaluation,
170 }
171}
172
173pub fn make_sticky_value_from_experiment(experiment: &Experiment) -> Option<StickyValues> {
174 let evaluation = unwrap_or_return!(&experiment.__evaluation, None);
175 let config_version = extract_config_version(&evaluation.base);
176
177 Some(StickyValues {
178 value: true, json_value: Some(evaluation.value.get_json().unwrap_or_default()),
180 rule_id: Some(evaluation.base.rule_id.clone()),
181 group_name: evaluation
182 .group_name
183 .as_ref()
184 .map(|g| g.unperformant_to_string()),
185 secondary_exposures: evaluation.base.secondary_exposures.clone(),
186 undelegated_secondary_exposures: evaluation.undelegated_secondary_exposures.clone(),
187 config_delegate: None,
188 explicit_parameters: evaluation
189 .explicit_parameters
190 .as_ref()
191 .map(|g| g.iter().map(|g| g.unperformant_to_string()).collect()),
192 time: experiment.details.lcut,
193 config_version,
194 })
195}
196
197fn extract_config_version(evaluation: &BaseEvaluation) -> Option<u32> {
198 evaluation
199 .exposure_info
200 .as_ref()
201 .and_then(|info| info.version)
202}