Skip to main content

statsig_rust/user/
statsig_user_internal.rs

1use std::collections::HashMap;
2use std::sync::atomic::{AtomicU64, Ordering};
3
4use chrono::Utc;
5
6use super::StatsigUserLoggable;
7use crate::evaluation::dynamic_value::DynamicValue;
8use crate::hashing::djb2_number;
9use crate::{evaluation::dynamic_string::DynamicString, Statsig};
10use crate::{log_w, statsig_metadata, StatsigUser};
11
12pub type FullUserKey = (
13    u64,      // app_version
14    u64,      // country
15    u64,      // email
16    u64,      // ip
17    u64,      // locale
18    u64,      // user_agent
19    u64,      // user_id
20    Vec<u64>, // custom_ids
21    Vec<u64>, // custom
22    Vec<u64>, // private_attributes
23    Vec<u64>, // statsig_env
24);
25
26const TAG: &str = stringify!(StatsigUserInternal);
27const VERSION_CHECK_THROTTLE_MS: u64 = 60_000;
28
29#[derive(Clone)]
30pub struct StatsigUserInternal<'statsig, 'user> {
31    pub user_ref: &'user StatsigUser,
32    pub statsig_instance: Option<&'statsig Statsig>,
33}
34
35static LAST_VERSION_CHECK: AtomicU64 = AtomicU64::new(0);
36
37impl<'statsig, 'user> StatsigUserInternal<'statsig, 'user> {
38    pub fn new(user: &'user StatsigUser, statsig_instance: Option<&'statsig Statsig>) -> Self {
39        throttled_version_check(user);
40
41        Self {
42            user_ref: user,
43            statsig_instance,
44        }
45    }
46
47    pub fn get_unit_id(&self, id_type: &DynamicString) -> Option<&DynamicValue> {
48        self.user_ref.get_unit_id(id_type)
49    }
50
51    pub fn get_user_value(&self, field: &Option<DynamicString>) -> Option<&DynamicValue> {
52        let field = field.as_ref()?;
53
54        let lowered_field = &field.lowercased_value;
55
56        let str_value = match lowered_field as &str {
57            "userid" => &self.user_ref.data.user_id,
58            "email" => &self.user_ref.data.email,
59            "ip" => &self.user_ref.data.ip,
60            "country" => &self.user_ref.data.country,
61            "locale" => &self.user_ref.data.locale,
62            "appversion" => &self.user_ref.data.app_version,
63            "useragent" => &self.user_ref.data.user_agent,
64            _ => &None,
65        };
66
67        if let Some(value) = str_value {
68            if let Some(str_val) = &value.string_value {
69                if !str_val.value.is_empty() {
70                    return Some(value);
71                }
72            }
73        }
74
75        if let Some(custom) = &self.user_ref.data.custom {
76            if let Some(found) = custom.get(field.value.as_str()) {
77                return Some(found);
78            }
79            if let Some(lowered_found) = custom.get(lowered_field.as_str()) {
80                return Some(lowered_found);
81            }
82        }
83
84        if let Some(instance) = &self.statsig_instance {
85            if let Some(val) = instance.get_value_from_global_custom_fields(&field.value) {
86                return Some(val);
87            }
88
89            if let Some(val) = instance.get_value_from_global_custom_fields(&field.lowercased_value)
90            {
91                return Some(val);
92            }
93        }
94
95        if let Some(private_attributes) = &self.user_ref.data.private_attributes {
96            if let Some(found) = private_attributes.get(field.value.as_str()) {
97                return Some(found);
98            }
99            if let Some(lowered_found) = private_attributes.get(lowered_field.as_str()) {
100                return Some(lowered_found);
101            }
102        }
103
104        let str_value_alt = match lowered_field as &str {
105            "user_id" => &self.user_ref.data.user_id,
106            "app_version" => &self.user_ref.data.app_version,
107            "user_agent" => &self.user_ref.data.user_agent,
108            _ => &None,
109        };
110
111        if str_value_alt.is_some() {
112            return str_value_alt.as_ref();
113        }
114
115        None
116    }
117
118    pub fn get_value_from_environment(
119        &self,
120        field: &Option<DynamicString>,
121    ) -> Option<DynamicValue> {
122        let field = field.as_ref()?;
123
124        if let Some(statsig_environment) = &self.user_ref.data.statsig_environment {
125            if let Some(result) = statsig_environment.get(field.value.as_str()) {
126                return Some(result.clone());
127            }
128        }
129
130        if let Some(result) = self.statsig_instance?.get_from_statsig_env(&field.value) {
131            return Some(result);
132        }
133
134        self.statsig_instance?
135            .get_from_statsig_env(&field.lowercased_value)
136    }
137
138    pub fn to_loggable(&self) -> StatsigUserLoggable {
139        let mut environment = self.user_ref.data.statsig_environment.clone();
140        let mut global_custom: Option<HashMap<String, DynamicValue>> = None;
141
142        if let Some(statsig_instance) = &self.statsig_instance {
143            if environment.is_none() {
144                environment = statsig_instance.use_statsig_env(|e| e.cloned());
145            }
146            global_custom = statsig_instance.use_global_custom_fields(|gc| gc.cloned());
147        }
148
149        StatsigUserLoggable::new(&self.user_ref.data, environment, global_custom)
150    }
151
152    pub fn get_hashed_private_attributes(&self) -> Option<String> {
153        let private_attributes = match &self.user_ref.data.private_attributes {
154            Some(attrs) => attrs,
155            None => return None,
156        };
157
158        if private_attributes.is_empty() {
159            return None;
160        }
161
162        let mut val: i64 = 0;
163        for (key, value) in private_attributes {
164            let hash_key = match value.string_value {
165                Some(ref s) => key.to_owned() + ":" + &s.value,
166                None => key.to_owned() + ":",
167            };
168            val += djb2_number(&hash_key);
169            val &= 0xFFFF_FFFF;
170        }
171        Some(val.to_string())
172    }
173}
174
175fn throttled_version_check(user: &StatsigUser) {
176    let current_version = statsig_metadata::SDK_VERSION;
177
178    // compare pointers (faster than string comparison)
179    if user.sdk_version.as_ptr() == current_version.as_ptr() {
180        return;
181    }
182
183    // compare the values
184    if user.sdk_version == current_version {
185        return;
186    }
187
188    let now = Utc::now().timestamp_millis() as u64;
189    let last = LAST_VERSION_CHECK.load(Ordering::Relaxed);
190
191    if now.saturating_sub(last) < VERSION_CHECK_THROTTLE_MS {
192        return;
193    }
194
195    if LAST_VERSION_CHECK
196        .compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed)
197        .is_ok()
198    {
199        log_w!(
200            TAG,
201            "Multiple SDK versions detected. This may cause unexpected behavior. Expected: {}, Got: {}",
202            statsig_metadata::SDK_VERSION,
203            user.sdk_version
204        );
205    }
206}