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::event_logging::exposable_string::ExposableString;
11use crate::{
12    evaluation::evaluation_types::{ExperimentEvaluation, LayerEvaluation},
13    statsig_type_factories::{extract_from_experiment_evaluation, make_layer},
14    statsig_types::{Experiment, Layer},
15    unwrap_or_return,
16    user::StatsigUserInternal,
17    EvaluationDetails, SecondaryExposure,
18};
19
20pub type UserPersistedValues = HashMap<String, StickyValues>;
21
22pub trait PersistentStorage: Send + Sync {
23    fn load(&self, key: String) -> Option<UserPersistedValues>;
24    fn save(&self, key: &str, config_name: &str, data: StickyValues);
25    fn delete(&self, key: &str, config_name: &str);
26}
27
28pub fn get_persistent_storage_key(user: &StatsigUserInternal, id_type: &String) -> Option<String> {
29    let dyn_str_id_type = DynamicString::from(id_type.clone());
30    user.get_unit_id(&dyn_str_id_type).map(|id| {
31        let mut id_str = "";
32        if let Some(id) = id.string_value.as_ref().map(|s| &s.value) {
33            id_str = id;
34        }
35
36        format!("{}:{}", id_str, id_type)
37    })
38}
39
40#[derive(Serialize, Deserialize, Debug, Clone, Default)]
41pub struct StickyValues {
42    pub value: bool,
43    pub json_value: Option<HashMap<String, Value>>,
44    pub rule_id: Option<ExposableString>,
45    pub group_name: Option<String>,
46    pub secondary_exposures: Vec<SecondaryExposure>,
47    pub undelegated_secondary_exposures: Option<Vec<SecondaryExposure>>,
48    pub config_delegate: Option<String>,
49    pub explicit_parameters: Option<Vec<String>>,
50    pub time: Option<u64>,
51    pub config_version: Option<u32>,
52}
53
54pub fn make_layer_from_sticky_value(
55    name: &str,
56    user: &StatsigUserInternal,
57    evaluation: LayerEvaluation,
58    sticky_value: StickyValues,
59    event_logger_ptr: Option<Weak<EventLogger>>,
60    disable_exposure: bool,
61) -> Layer {
62    let details = EvaluationDetails {
63        reason: "Persisted".to_owned(),
64        lcut: sticky_value.time,
65        received_at: Some(Utc::now().timestamp_millis() as u64),
66    };
67
68    make_layer(
69        user.to_loggable(),
70        name,
71        Some(evaluation),
72        details,
73        event_logger_ptr,
74        disable_exposure,
75    )
76}
77
78pub fn make_sticky_value_from_layer(layer: &Layer) -> Option<StickyValues> {
79    let layer_evaluation = unwrap_or_return!(layer.__evaluation.as_ref(), None);
80    let config_version = extract_config_version(&layer_evaluation.base);
81
82    Some(StickyValues {
83        value: true,
84        json_value: Some(layer_evaluation.value.clone()),
85        rule_id: Some(layer_evaluation.base.rule_id.clone()),
86        group_name: layer_evaluation.group_name.clone(),
87        secondary_exposures: layer_evaluation.base.secondary_exposures.clone(),
88        undelegated_secondary_exposures: layer_evaluation.undelegated_secondary_exposures.clone(),
89        config_delegate: layer_evaluation.allocated_experiment_name.clone(),
90        explicit_parameters: Some(layer_evaluation.explicit_parameters.clone()),
91        time: layer.details.lcut,
92        config_version,
93    })
94}
95
96pub fn make_experiment_from_sticky_value(
97    evaluation: ExperimentEvaluation,
98    sticky_value: StickyValues,
99) -> Experiment {
100    let name = evaluation.base.name.clone();
101    let maybe_evaluation = Some(evaluation);
102    let (value, rule_id, id_type, group_name) =
103        extract_from_experiment_evaluation(&maybe_evaluation);
104    let details = EvaluationDetails {
105        reason: "Persisted".to_owned(),
106        lcut: sticky_value.time,
107        received_at: Some(Utc::now().timestamp_millis() as u64),
108    };
109    Experiment {
110        name: name.unperformant_to_string(),
111        value,
112        rule_id: rule_id.unperformant_to_string(),
113        id_type,
114        group_name,
115        details,
116        __evaluation: maybe_evaluation,
117    }
118}
119
120pub fn make_sticky_value_from_experiment(experiment: &Experiment) -> Option<StickyValues> {
121    let experiment_evaluation = unwrap_or_return!(&experiment.__evaluation, None);
122    let config_version = extract_config_version(&experiment_evaluation.base);
123
124    Some(StickyValues {
125        value: true, // For sticky value, if it's being saved, it should always be true
126        json_value: Some(experiment_evaluation.value.clone()),
127        rule_id: Some(experiment_evaluation.base.rule_id.clone()),
128        group_name: experiment_evaluation.group_name.clone(),
129        secondary_exposures: experiment_evaluation.base.secondary_exposures.clone(),
130        undelegated_secondary_exposures: experiment_evaluation
131            .undelegated_secondary_exposures
132            .clone(),
133        config_delegate: None,
134        explicit_parameters: experiment_evaluation.explicit_parameters.clone(),
135        time: experiment.details.lcut,
136        config_version,
137    })
138}
139
140fn extract_config_version(evaluation: &BaseEvaluation) -> Option<u32> {
141    evaluation
142        .exposure_info
143        .as_ref()
144        .and_then(|info| info.version)
145}