statsig_rust/persistent_storage/
persistent_storage_trait.rs

1use crate::evaluation::dynamic_string::DynamicString;
2use crate::evaluation::evaluation_types::BaseEvaluation;
3use crate::event_logging::event_logger::EventLogger;
4use crate::event_logging::exposable_string::ExposableString;
5use crate::log_e;
6use crate::{
7    evaluation::evaluation_types::{ExperimentEvaluation, LayerEvaluation},
8    statsig_type_factories::{extract_from_experiment_evaluation, make_layer},
9    statsig_types::{Experiment, Layer},
10    unwrap_or_return,
11    user::StatsigUserInternal,
12    EvaluationDetails, SecondaryExposure,
13};
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
27pub fn get_persistent_storage_key(user: &StatsigUserInternal, id_type: &String) -> Option<String> {
28    let dyn_str_id_type = DynamicString::from(id_type.clone());
29    user.get_unit_id(&dyn_str_id_type).map(|id| {
30        let mut id_str = "";
31        if let Some(id) = id.string_value.as_ref().map(|s| &s.value) {
32            id_str = id;
33        }
34
35        format!("{id_str}:{id_type}")
36    })
37}
38
39const TAG: &str = "PersistentStorageTrait";
40
41#[derive(Serialize, Deserialize, Debug, Clone, Default)]
42pub struct StickyValues {
43    pub value: bool,
44    pub json_value: Option<HashMap<String, Value>>,
45    pub rule_id: Option<ExposableString>,
46    pub group_name: Option<String>,
47    pub secondary_exposures: Vec<SecondaryExposure>,
48    pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
49    pub config_delegate: Option<String>,
50    pub explicit_parameters: Option<Vec<String>>,
51    #[serde(deserialize_with = "deserialize_safe_timestamp")]
52    pub time: Option<u64>,
53    pub config_version: Option<u32>,
54}
55
56fn deserialize_safe_timestamp<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
57where
58    D: Deserializer<'de>,
59{
60    let value = Value::deserialize(deserializer)?;
61
62    if value.is_null() {
63        return Ok(None);
64    }
65
66    if let Value::Number(num) = value {
67        if let Some(timestamp) = num.as_u64() {
68            return Ok(Some(timestamp));
69        }
70
71        if let Some(timestamp) = num.as_i64() {
72            return Ok(Some(timestamp as u64));
73        }
74
75        if let Some(timestamp) = num.as_f64() {
76            return Ok(Some(timestamp as u64));
77        }
78    }
79
80    log_e!(TAG, "Non-numeric 'timestamp' field");
81
82    Ok(None)
83}
84
85pub fn make_layer_from_sticky_value(
86    name: &str,
87    user: &StatsigUserInternal,
88    evaluation: LayerEvaluation,
89    sticky_value: StickyValues,
90    event_logger_ptr: Option<Weak<EventLogger>>,
91    disable_exposure: bool,
92) -> Layer {
93    let details = EvaluationDetails {
94        reason: "Persisted".to_owned(),
95        lcut: sticky_value.time,
96        received_at: Some(Utc::now().timestamp_millis() as u64),
97    };
98
99    make_layer(
100        user.to_loggable(),
101        name,
102        Some(evaluation),
103        details,
104        event_logger_ptr,
105        disable_exposure,
106    )
107}
108
109pub fn make_sticky_value_from_layer(layer: &Layer) -> Option<StickyValues> {
110    let layer_evaluation = unwrap_or_return!(layer.__evaluation.as_ref(), None);
111    let config_version = extract_config_version(&layer_evaluation.base);
112
113    Some(StickyValues {
114        value: true,
115        json_value: Some(layer_evaluation.value.get_json().unwrap_or_default()),
116        rule_id: Some(layer_evaluation.base.rule_id.clone()),
117        group_name: layer_evaluation.group_name.clone(),
118        secondary_exposures: layer_evaluation.base.secondary_exposures.clone(),
119        undelegated_secondary_exposures: layer_evaluation.undelegated_secondary_exposures.clone(),
120        config_delegate: layer_evaluation.allocated_experiment_name.clone(),
121        explicit_parameters: Some(layer_evaluation.explicit_parameters.clone()),
122        time: layer.details.lcut,
123        config_version,
124    })
125}
126
127pub fn make_experiment_from_sticky_value(
128    evaluation: ExperimentEvaluation,
129    sticky_value: StickyValues,
130) -> Experiment {
131    let name = evaluation.base.name.clone();
132    let maybe_evaluation = Some(evaluation);
133    let (value, rule_id, id_type, group_name) =
134        extract_from_experiment_evaluation(&maybe_evaluation);
135    let details = EvaluationDetails {
136        reason: "Persisted".to_owned(),
137        lcut: sticky_value.time,
138        received_at: Some(Utc::now().timestamp_millis() as u64),
139    };
140    Experiment {
141        name: name.unperformant_to_string(),
142        value: value.get_json().unwrap_or_default(),
143        rule_id: rule_id.unperformant_to_string(),
144        id_type,
145        group_name,
146        details,
147        __evaluation: maybe_evaluation,
148    }
149}
150
151pub fn make_sticky_value_from_experiment(experiment: &Experiment) -> Option<StickyValues> {
152    let evaluation = unwrap_or_return!(&experiment.__evaluation, None);
153    let config_version = extract_config_version(&evaluation.base);
154
155    Some(StickyValues {
156        value: true, // For sticky value, if it's being saved, it should always be true
157        json_value: Some(evaluation.value.get_json().unwrap_or_default()),
158        rule_id: Some(evaluation.base.rule_id.clone()),
159        group_name: evaluation.group_name.clone(),
160        secondary_exposures: evaluation.base.secondary_exposures.clone(),
161        undelegated_secondary_exposures: evaluation.undelegated_secondary_exposures.clone(),
162        config_delegate: None,
163        explicit_parameters: evaluation.explicit_parameters.clone(),
164        time: experiment.details.lcut,
165        config_version,
166    })
167}
168
169fn extract_config_version(evaluation: &BaseEvaluation) -> Option<u32> {
170    evaluation
171        .exposure_info
172        .as_ref()
173        .and_then(|info| info.version)
174}