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, u64, u64, u64, u64, u64, u64, Vec<u64>, Vec<u64>, Vec<u64>, Vec<u64>, );
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 if user_sdk_version.as_ptr() == current_version.as_ptr() {
344 return;
345 }
346
347 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}