statsig_rust/persistent_storage/
persistent_storage_trait.rs

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