Skip to main content

statsig_rust/persistent_storage/
persistent_storage_trait.rs

1use 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 mut id_str = "";
35        if let Some(id) = id.string_value.as_ref().map(|s| &s.value) {
36            id_str = id;
37        }
38
39        format!("{id_str}:{id_type}")
40    })
41}
42
43const TAG: &str = "PersistentStorageTrait";
44
45#[derive(Serialize, Deserialize, Debug, Clone, Default)]
46pub struct StickyValues {
47    pub value: bool,
48    pub json_value: Option<DynamicReturnable>,
49    pub rule_id: Option<InternedString>,
50    pub group_name: Option<InternedString>,
51    pub secondary_exposures: Vec<SecondaryExposure>,
52    pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
53    pub config_delegate: Option<InternedString>,
54    pub explicit_parameters: Option<ExplicitParameters>,
55    #[serde(deserialize_with = "deserialize_safe_timestamp")]
56    pub time: Option<u64>,
57    pub config_version: Option<u32>,
58}
59
60fn deserialize_safe_timestamp<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
61where
62    D: Deserializer<'de>,
63{
64    let value = Value::deserialize(deserializer)?;
65
66    if value.is_null() {
67        return Ok(None);
68    }
69
70    if let Value::Number(num) = value {
71        if let Some(timestamp) = num.as_u64() {
72            return Ok(Some(timestamp));
73        }
74
75        if let Some(timestamp) = num.as_i64() {
76            return Ok(Some(timestamp as u64));
77        }
78
79        if let Some(timestamp) = num.as_f64() {
80            return Ok(Some(timestamp as u64));
81        }
82    }
83
84    log_e!(TAG, "Non-numeric 'timestamp' field");
85
86    Ok(None)
87}
88
89pub fn make_sticky_value_from_layer(layer: &Layer) -> Option<StickyValues> {
90    let layer_evaluation = unwrap_or_return!(layer.__evaluation.as_ref(), None);
91    let config_version = extract_config_version(&layer_evaluation.base);
92
93    Some(StickyValues {
94        value: true,
95        json_value: Some(layer_evaluation.value.clone()),
96        rule_id: Some(layer_evaluation.base.rule_id.clone()),
97        group_name: layer_evaluation.group_name.clone(),
98        secondary_exposures: layer_evaluation.base.secondary_exposures.clone(),
99        undelegated_secondary_exposures: layer_evaluation.undelegated_secondary_exposures.clone(),
100        config_delegate: layer_evaluation.allocated_experiment_name.clone(),
101        explicit_parameters: Some(layer_evaluation.explicit_parameters.clone()),
102        time: layer.details.lcut,
103        config_version,
104    })
105}
106
107pub fn make_sticky_value_from_experiment(experiment: &Experiment) -> Option<StickyValues> {
108    let evaluation = unwrap_or_return!(&experiment.__evaluation, None);
109    let config_version = extract_config_version(&evaluation.base);
110
111    Some(StickyValues {
112        value: true, // For sticky value, if it's being saved, it should always be true
113        json_value: Some(evaluation.value.clone()),
114        rule_id: Some(evaluation.base.rule_id.clone()),
115        group_name: evaluation.group_name.clone(),
116        secondary_exposures: evaluation.base.secondary_exposures.clone(),
117        undelegated_secondary_exposures: evaluation.undelegated_secondary_exposures.clone(),
118        config_delegate: None,
119        explicit_parameters: evaluation.explicit_parameters.as_ref().cloned(),
120        time: experiment.details.lcut,
121        config_version,
122    })
123}
124
125fn extract_config_version(evaluation: &BaseEvaluation) -> Option<u32> {
126    evaluation
127        .exposure_info
128        .as_ref()
129        .and_then(|info| info.version)
130}