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::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        version: sticky_value.config_version,
107    };
108
109    make_layer(
110        user.to_loggable(),
111        name,
112        Some(evaluation),
113        details,
114        event_logger_ptr,
115        disable_exposure,
116    )
117}
118
119pub fn make_sticky_value_from_layer(layer: &Layer) -> Option<StickyValues> {
120    let layer_evaluation = unwrap_or_return!(layer.__evaluation.as_ref(), None);
121    let config_version = extract_config_version(&layer_evaluation.base);
122
123    Some(StickyValues {
124        value: true,
125        json_value: Some(layer_evaluation.value.get_json().unwrap_or_default()),
126        rule_id: Some(layer_evaluation.base.rule_id.clone()),
127        group_name: layer_evaluation
128            .group_name
129            .as_ref()
130            .map(|g| g.unperformant_to_string()),
131        secondary_exposures: layer_evaluation.base.secondary_exposures.clone(),
132        undelegated_secondary_exposures: layer_evaluation.undelegated_secondary_exposures.clone(),
133        config_delegate: layer_evaluation
134            .allocated_experiment_name
135            .as_ref()
136            .map(|g| g.unperformant_to_string()),
137        explicit_parameters: Some(
138            layer_evaluation
139                .explicit_parameters
140                .iter()
141                .map(|g| g.unperformant_to_string())
142                .collect(),
143        ),
144        time: layer.details.lcut,
145        config_version,
146    })
147}
148
149pub fn make_experiment_from_sticky_value(
150    evaluation: ExperimentEvaluation,
151    sticky_value: StickyValues,
152) -> Experiment {
153    let name = evaluation.base.name.clone();
154    let maybe_evaluation = Some(evaluation);
155    let (value, rule_id, id_type, group_name, is_experiment_active) =
156        extract_from_experiment_evaluation(&maybe_evaluation);
157    let details = EvaluationDetails {
158        reason: "Persisted".to_owned(),
159        lcut: sticky_value.time,
160        received_at: Some(Utc::now().timestamp_millis() as u64),
161        version: sticky_value.config_version,
162    };
163    Experiment {
164        name: name.unperformant_to_string(),
165        value: value.get_json().unwrap_or_default(),
166        rule_id: rule_id.unperformant_to_string(),
167        id_type,
168        group_name,
169        details,
170        is_experiment_active,
171        __evaluation: maybe_evaluation,
172    }
173}
174
175pub fn make_sticky_value_from_experiment(experiment: &Experiment) -> Option<StickyValues> {
176    let evaluation = unwrap_or_return!(&experiment.__evaluation, None);
177    let config_version = extract_config_version(&evaluation.base);
178
179    Some(StickyValues {
180        value: true, // For sticky value, if it's being saved, it should always be true
181        json_value: Some(evaluation.value.get_json().unwrap_or_default()),
182        rule_id: Some(evaluation.base.rule_id.clone()),
183        group_name: evaluation
184            .group_name
185            .as_ref()
186            .map(|g| g.unperformant_to_string()),
187        secondary_exposures: evaluation.base.secondary_exposures.clone(),
188        undelegated_secondary_exposures: evaluation.undelegated_secondary_exposures.clone(),
189        config_delegate: None,
190        explicit_parameters: evaluation
191            .explicit_parameters
192            .as_ref()
193            .map(|g| g.iter().map(|g| g.unperformant_to_string()).collect()),
194        time: experiment.details.lcut,
195        config_version,
196    })
197}
198
199fn extract_config_version(evaluation: &BaseEvaluation) -> Option<u32> {
200    evaluation
201        .exposure_info
202        .as_ref()
203        .and_then(|info| info.version)
204}