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