statsig_rust/persistent_storage/
persistent_storage_trait.rs

1use std::{collections::HashMap, sync::Weak};
2
3use chrono::Utc;
4use serde::{Deserialize, 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::{
11    evaluation::evaluation_types::{ExperimentEvaluation, LayerEvaluation},
12    statsig_type_factories::{extract_from_experiment_evaluation, make_layer},
13    statsig_types::{Experiment, Layer},
14    unwrap_or_return,
15    user::StatsigUserInternal,
16    EvaluationDetails, SecondaryExposure,
17};
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
39#[derive(Serialize, Deserialize, Debug, Clone, Default)]
40pub struct StickyValues {
41    pub value: bool,
42    pub json_value: Option<HashMap<String, Value>>,
43    pub rule_id: Option<String>,
44    pub group_name: Option<String>,
45    pub secondary_exposures: Vec<SecondaryExposure>,
46    pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
47    pub config_delegate: Option<String>,
48    pub explicit_parameters: Option<Vec<String>>,
49    pub time: Option<u64>,
50    pub config_version: Option<u32>,
51}
52
53pub fn make_layer_from_sticky_value(
54    name: &str,
55    user: &StatsigUserInternal,
56    evaluation: LayerEvaluation,
57    sticky_value: StickyValues,
58    event_logger_ptr: Option<Weak<EventLogger>>,
59    disable_exposure: bool,
60) -> Layer {
61    let details = EvaluationDetails {
62        reason: "Persisted".to_owned(),
63        lcut: sticky_value.time,
64        received_at: Some(Utc::now().timestamp_millis() as u64),
65    };
66
67    make_layer(
68        user.to_loggable(),
69        name,
70        Some(evaluation),
71        details,
72        event_logger_ptr,
73        disable_exposure,
74    )
75}
76
77pub fn make_sticky_value_from_layer(layer: &Layer) -> Option<StickyValues> {
78    let layer_evaluation = unwrap_or_return!(layer.__evaluation.as_ref(), None);
79    let config_version = extract_config_version(&layer_evaluation.base);
80
81    Some(StickyValues {
82        value: true,
83        json_value: Some(layer_evaluation.value.clone()),
84        rule_id: Some(layer_evaluation.base.rule_id.clone()),
85        group_name: layer_evaluation.group_name.clone(),
86        secondary_exposures: layer_evaluation.base.secondary_exposures.clone(),
87        undelegated_secondary_exposures: layer_evaluation.undelegated_secondary_exposures.clone(),
88        config_delegate: layer_evaluation.allocated_experiment_name.clone(),
89        explicit_parameters: Some(layer_evaluation.explicit_parameters.clone()),
90        time: layer.details.lcut,
91        config_version,
92    })
93}
94
95pub fn make_experiment_from_sticky_value(
96    evaluation: ExperimentEvaluation,
97    sticky_value: StickyValues,
98) -> Experiment {
99    let name = evaluation.base.name.clone();
100    let maybe_evaluation = Some(evaluation);
101    let (value, rule_id, id_type, group_name) =
102        extract_from_experiment_evaluation(&maybe_evaluation);
103    let details = EvaluationDetails {
104        reason: "Persisted".to_owned(),
105        lcut: sticky_value.time,
106        received_at: Some(Utc::now().timestamp_millis() as u64),
107    };
108    Experiment {
109        name,
110        value,
111        rule_id,
112        id_type,
113        group_name,
114        details,
115        __evaluation: maybe_evaluation,
116    }
117}
118
119pub fn make_sticky_value_from_experiment(experiment: &Experiment) -> Option<StickyValues> {
120    let experiment_evaluation = unwrap_or_return!(&experiment.__evaluation, None);
121    let config_version = extract_config_version(&experiment_evaluation.base);
122
123    Some(StickyValues {
124        value: true, // For sticky value, if it's being saved, it should always be true
125        json_value: Some(experiment_evaluation.value.clone()),
126        rule_id: Some(experiment_evaluation.base.rule_id.clone()),
127        group_name: experiment_evaluation.group_name.clone(),
128        secondary_exposures: experiment_evaluation.base.secondary_exposures.clone(),
129        undelegated_secondary_exposures: experiment_evaluation
130            .undelegated_secondary_exposures
131            .clone(),
132        config_delegate: None,
133        explicit_parameters: experiment_evaluation.explicit_parameters.clone(),
134        time: experiment.details.lcut,
135        config_version,
136    })
137}
138
139fn extract_config_version(evaluation: &BaseEvaluation) -> Option<u32> {
140    evaluation
141        .exposure_info
142        .as_ref()
143        .and_then(|info| info.version)
144}