statsig_rust/user/
statsig_user_internal.rs1use 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, u64, u64, u64, u64, u64, u64, Vec<u64>, Vec<u64>, Vec<u64>, Vec<u64>, );
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 if user.sdk_version.as_ptr() == current_version.as_ptr() {
180 return;
181 }
182
183 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}