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::{
7    fast_statsig_user::FastStatsigUser,
8    user_data::UserDataMap,
9    user_value::{UserValue, UserValueRef},
10    StatsigUserLoggable,
11};
12use crate::evaluation::{dynamic_value::DynamicValue, evaluation_data::DynamicStringRef};
13use crate::hashing::djb2_number;
14use crate::{evaluation::dynamic_string::DynamicString, Statsig};
15use crate::{log_w, statsig_metadata, StatsigUser};
16
17pub type FullUserKey = (
18    u64,      // app_version
19    u64,      // country
20    u64,      // email
21    u64,      // ip
22    u64,      // locale
23    u64,      // user_agent
24    u64,      // user_id
25    Vec<u64>, // custom_ids
26    Vec<u64>, // custom
27    Vec<u64>, // private_attributes
28    Vec<u64>, // statsig_env
29);
30
31const TAG: &str = stringify!(StatsigUserInternal);
32const VERSION_CHECK_THROTTLE_MS: u64 = 60_000;
33
34#[derive(Clone)]
35pub struct StatsigUserInternal<'statsig, 'user> {
36    pub(crate) user_ref: InternalUserRef<'user>,
37    pub statsig_instance: Option<&'statsig Statsig>,
38}
39
40#[derive(Clone, Copy)]
41pub(crate) enum InternalUserRef<'user> {
42    Public(&'user StatsigUser),
43    Fast(&'user FastStatsigUser),
44}
45
46static LAST_VERSION_CHECK: AtomicU64 = AtomicU64::new(0);
47
48impl<'statsig, 'user> StatsigUserInternal<'statsig, 'user> {
49    pub fn new(user: &'user StatsigUser, statsig_instance: Option<&'statsig Statsig>) -> Self {
50        throttled_version_check(user.sdk_version);
51
52        Self {
53            user_ref: InternalUserRef::Public(user),
54            statsig_instance,
55        }
56    }
57
58    pub fn from_fast_user(
59        user: &'user FastStatsigUser,
60        statsig_instance: Option<&'statsig Statsig>,
61    ) -> Self {
62        throttled_version_check(user.sdk_version);
63
64        Self {
65            user_ref: InternalUserRef::Fast(user),
66            statsig_instance,
67        }
68    }
69
70    pub fn get_unit_id(&self, id_type: &DynamicString) -> Option<UserValueRef<'_>> {
71        self.get_unit_id_ref(id_type.into())
72    }
73
74    pub(crate) fn get_unit_id_ref(
75        &self,
76        id_type: DynamicStringRef<'_>,
77    ) -> Option<UserValueRef<'_>> {
78        match self.user_ref {
79            InternalUserRef::Public(user) => user
80                .get_unit_id_by_name(id_type.value(), id_type.lowercased_value())
81                .map(UserValueRef::Dynamic),
82            InternalUserRef::Fast(user) => user
83                .get_unit_id_by_name(id_type.value(), id_type.lowercased_value())
84                .map(UserValueRef::User),
85        }
86    }
87
88    pub fn get_user_value(&self, field: &Option<DynamicString>) -> Option<UserValueRef<'_>> {
89        let field = field.as_ref()?;
90
91        let lowered_field = &field.lowercased_value;
92
93        if let Some(value) = self.get_primary_user_value(lowered_field) {
94            if value.string_value().is_some_and(|value| !value.is_empty()) {
95                return Some(value);
96            }
97        }
98
99        if let Some(found) = self.get_custom_value(field.value.as_str()) {
100            return Some(found);
101        }
102        if let Some(lowered_found) = self.get_custom_value(lowered_field.as_str()) {
103            return Some(lowered_found);
104        }
105
106        if let Some(instance) = &self.statsig_instance {
107            if let Some(val) = instance.get_value_from_global_custom_fields(&field.value) {
108                return Some(UserValueRef::Dynamic(val));
109            }
110
111            if let Some(val) = instance.get_value_from_global_custom_fields(&field.lowercased_value)
112            {
113                return Some(UserValueRef::Dynamic(val));
114            }
115        }
116
117        if let Some(found) = self.get_private_attribute_value(field.value.as_str()) {
118            return Some(UserValueRef::Dynamic(found));
119        }
120        if let Some(lowered_found) = self.get_private_attribute_value(lowered_field.as_str()) {
121            return Some(UserValueRef::Dynamic(lowered_found));
122        }
123
124        self.get_primary_user_value_alt(lowered_field)
125    }
126
127    pub fn get_value_from_environment(
128        &self,
129        field: &Option<DynamicString>,
130    ) -> Option<DynamicValue> {
131        let field = field.as_ref()?;
132
133        if let Some(statsig_environment) = self.statsig_environment() {
134            if let Some(result) = statsig_environment.get(field.value.as_str()) {
135                return Some(result.clone());
136            }
137        }
138
139        if let Some(result) = self.statsig_instance?.get_from_statsig_env(&field.value) {
140            return Some(result);
141        }
142
143        self.statsig_instance?
144            .get_from_statsig_env(&field.lowercased_value)
145    }
146
147    pub fn to_loggable(&self) -> StatsigUserLoggable {
148        let mut environment = self.statsig_environment().cloned();
149        let mut global_custom: Option<HashMap<String, DynamicValue>> = None;
150
151        if let Some(statsig_instance) = &self.statsig_instance {
152            if environment.is_none() {
153                environment = statsig_instance.use_statsig_env(hashmap_to_user_data_map);
154            }
155            global_custom = statsig_instance.use_global_custom_fields(|gc| gc.cloned());
156        }
157
158        match self.user_ref {
159            InternalUserRef::Public(user) => {
160                StatsigUserLoggable::new(&user.data, environment, global_custom)
161            }
162            InternalUserRef::Fast(user) => {
163                StatsigUserLoggable::new_fast(&user.data, environment, global_custom)
164            }
165        }
166    }
167
168    pub fn get_hashed_private_attributes(&self) -> Option<String> {
169        let private_attributes = self.private_attributes()?;
170
171        if private_attributes.is_empty() {
172            return None;
173        }
174
175        let mut val: i64 = 0;
176        for (key, value) in private_attributes {
177            let hash_key = match value.string_value {
178                Some(ref s) => key.to_owned() + ":" + &s.value,
179                None => key.to_owned() + ":",
180            };
181            val += djb2_number(&hash_key);
182            val &= 0xFFFF_FFFF;
183        }
184        Some(val.to_string())
185    }
186
187    pub(crate) fn create_exposure_dedupe_user_hash(&self, unit_id_type: Option<&str>) -> u64 {
188        match self.user_ref {
189            InternalUserRef::Public(user) => {
190                user.data.create_exposure_dedupe_user_hash(unit_id_type)
191            }
192            InternalUserRef::Fast(user) => user.data.create_exposure_dedupe_user_hash(unit_id_type),
193        }
194    }
195
196    pub(crate) fn get_persistent_storage_key(&self, id_type: &str) -> Option<String> {
197        let dyn_str_id_type = DynamicString::from(id_type.to_string());
198        self.get_unit_id(&dyn_str_id_type).map(|id| {
199            let id_str = id.string_value().unwrap_or("");
200            format!("{id_str}:{id_type}")
201        })
202    }
203
204    pub(crate) fn get_user_id_str(&self) -> Option<&str> {
205        match self.user_ref {
206            InternalUserRef::Public(user) => user
207                .data
208                .user_id
209                .as_ref()
210                .and_then(|value| value.string_value.as_ref())
211                .map(|value| value.value.as_str()),
212            InternalUserRef::Fast(user) => {
213                user.data.user_id.as_ref().and_then(UserValue::string_value)
214            }
215        }
216    }
217
218    pub(crate) fn custom_id_pairs(&self) -> Vec<(&str, &str)> {
219        match self.user_ref {
220            InternalUserRef::Public(user) => user
221                .data
222                .custom_ids
223                .as_ref()
224                .map(|custom_ids| {
225                    custom_ids
226                        .iter()
227                        .map(|(key, value)| {
228                            (
229                                key.as_str(),
230                                value
231                                    .string_value
232                                    .as_ref()
233                                    .map(|value| value.value.as_str())
234                                    .unwrap_or(""),
235                            )
236                        })
237                        .collect()
238                })
239                .unwrap_or_default(),
240            InternalUserRef::Fast(user) => user
241                .data
242                .custom_ids
243                .as_ref()
244                .map(|custom_ids| {
245                    custom_ids
246                        .iter()
247                        .map(|(key, value)| (key.as_str(), value.string_value().unwrap_or("")))
248                        .collect()
249                })
250                .unwrap_or_default(),
251        }
252    }
253
254    pub(crate) fn with_public_user<R>(&self, f: impl FnOnce(&StatsigUser) -> R) -> R {
255        match self.user_ref {
256            InternalUserRef::Public(user) => f(user),
257            InternalUserRef::Fast(user) => {
258                let materialized = user.to_public_user();
259                f(&materialized)
260            }
261        }
262    }
263
264    fn get_primary_user_value(&self, lowered_field: &str) -> Option<UserValueRef<'_>> {
265        match self.user_ref {
266            InternalUserRef::Public(user) => match lowered_field {
267                "userid" => user.data.user_id.as_ref().map(UserValueRef::Dynamic),
268                "email" => user.data.email.as_ref().map(UserValueRef::Dynamic),
269                "ip" => user.data.ip.as_ref().map(UserValueRef::Dynamic),
270                "country" => user.data.country.as_ref().map(UserValueRef::Dynamic),
271                "locale" => user.data.locale.as_ref().map(UserValueRef::Dynamic),
272                "appversion" => user.data.app_version.as_ref().map(UserValueRef::Dynamic),
273                "useragent" => user.data.user_agent.as_ref().map(UserValueRef::Dynamic),
274                _ => None,
275            },
276            InternalUserRef::Fast(user) => match lowered_field {
277                "userid" => user.data.user_id.as_ref().map(UserValueRef::User),
278                "email" => user.data.email.as_ref().map(UserValueRef::User),
279                "ip" => user.data.ip.as_ref().map(UserValueRef::User),
280                "country" => user.data.country.as_ref().map(UserValueRef::User),
281                "locale" => user.data.locale.as_ref().map(UserValueRef::User),
282                "appversion" => user.data.app_version.as_ref().map(UserValueRef::User),
283                "useragent" => user.data.user_agent.as_ref().map(UserValueRef::User),
284                _ => None,
285            },
286        }
287    }
288
289    fn get_primary_user_value_alt(&self, lowered_field: &str) -> Option<UserValueRef<'_>> {
290        match lowered_field {
291            "user_id" => self.get_primary_user_value("userid"),
292            "app_version" => self.get_primary_user_value("appversion"),
293            "user_agent" => self.get_primary_user_value("useragent"),
294            _ => None,
295        }
296    }
297
298    fn get_custom_value(&self, key: &str) -> Option<UserValueRef<'_>> {
299        match self.user_ref {
300            InternalUserRef::Public(user) => user
301                .data
302                .custom
303                .as_ref()?
304                .get(key)
305                .map(UserValueRef::Dynamic),
306            InternalUserRef::Fast(user) => {
307                user.data.custom.as_ref()?.get(key).map(UserValueRef::User)
308            }
309        }
310    }
311
312    fn get_private_attribute_value(&self, key: &str) -> Option<&DynamicValue> {
313        self.private_attributes()?.get(key)
314    }
315
316    fn private_attributes(&self) -> Option<&UserDataMap> {
317        match self.user_ref {
318            InternalUserRef::Public(user) => user.data.private_attributes.as_ref(),
319            InternalUserRef::Fast(user) => user.data.private_attributes.as_ref(),
320        }
321    }
322
323    fn statsig_environment(&self) -> Option<&UserDataMap> {
324        match self.user_ref {
325            InternalUserRef::Public(user) => user.data.statsig_environment.as_ref(),
326            InternalUserRef::Fast(user) => user.data.statsig_environment.as_ref(),
327        }
328    }
329}
330
331fn hashmap_to_user_data_map(map: Option<&HashMap<String, DynamicValue>>) -> Option<UserDataMap> {
332    map.map(|map| {
333        map.iter()
334            .map(|(key, value)| (key.clone(), value.clone()))
335            .collect()
336    })
337}
338
339fn throttled_version_check(user_sdk_version: &'static str) {
340    let current_version = statsig_metadata::SDK_VERSION;
341
342    // compare pointers (faster than string comparison)
343    if user_sdk_version.as_ptr() == current_version.as_ptr() {
344        return;
345    }
346
347    // compare the values
348    if user_sdk_version == current_version {
349        return;
350    }
351
352    let now = Utc::now().timestamp_millis() as u64;
353    let last = LAST_VERSION_CHECK.load(Ordering::Relaxed);
354
355    if now.saturating_sub(last) < VERSION_CHECK_THROTTLE_MS {
356        return;
357    }
358
359    if LAST_VERSION_CHECK
360        .compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed)
361        .is_ok()
362    {
363        log_w!(
364            TAG,
365            "Multiple SDK versions detected. This may cause unexpected behavior. Expected: {}, Got: {}",
366            statsig_metadata::SDK_VERSION,
367            user_sdk_version
368        );
369    }
370}