statsig_rust/user/
statsig_user_loggable.rs1use std::collections::HashMap;
2use std::sync::{Arc, RwLock, Weak};
3
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6
7use crate::{log_e, user::StatsigUserInternal};
8
9const TAG: &str = "StatsigUserLoggable";
10
11lazy_static::lazy_static! {
12 static ref LOGGABLE_USER_STORE: RwLock<HashMap<String, Weak<UserLoggableData>>> =
13 RwLock::new(HashMap::new());
14}
15
16#[derive(Serialize, Deserialize)]
17pub struct UserLoggableData {
18 pub key: String,
19 pub value: Value,
20}
21
22#[derive(Clone)]
23pub struct StatsigUserLoggable {
24 pub data: Arc<UserLoggableData>,
25}
26
27impl Serialize for StatsigUserLoggable {
28 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
29 where
30 S: serde::Serializer,
31 {
32 self.data.value.serialize(serializer)
33 }
34}
35
36impl<'de> Deserialize<'de> for StatsigUserLoggable {
37 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
38 where
39 D: serde::Deserializer<'de>,
40 {
41 let value = Value::deserialize(deserializer)?;
42 Ok(StatsigUserLoggable {
43 data: Arc::new(UserLoggableData {
44 key: "".to_string(),
45 value,
46 }),
47 })
48 }
49}
50
51fn make_loggable(
52 full_user_key: String,
53 user_internal: &StatsigUserInternal,
54) -> Arc<UserLoggableData> {
55 let result = Arc::new(UserLoggableData {
56 key: full_user_key.clone(),
57 value: json!(user_internal),
58 });
59
60 let mut store = match LOGGABLE_USER_STORE.write() {
61 Ok(store) => store,
62 Err(e) => {
63 log_e!(TAG, "Error locking user loggable store: {:?}", e);
64 return result;
65 }
66 };
67
68 store.insert(full_user_key, Arc::downgrade(&result));
69
70 result
71}
72
73impl StatsigUserLoggable {
74 pub fn new(user_internal: &StatsigUserInternal) -> Self {
75 let user_key = user_internal.get_full_user_key();
76
77 let mut existing = None;
78
79 match LOGGABLE_USER_STORE.read() {
80 Ok(store) => existing = store.get(&user_key).map(|x| x.upgrade()),
81 Err(e) => {
82 log_e!(TAG, "Error locking user loggable store: {:?}", e);
83 }
84 };
85
86 let data = match existing {
87 Some(Some(x)) => x,
88 _ => make_loggable(user_key, user_internal),
89 };
90
91 Self { data }
92 }
93
94 pub fn null_user() -> Self {
95 Self {
96 data: Arc::new(UserLoggableData {
97 key: "".to_string(),
98 value: Value::Null,
99 }),
100 }
101 }
102
103 pub fn create_sampling_key(&self) -> String {
104 let user_data = &self.data.value;
105 let user_id = user_data
106 .get("userID")
107 .map(|x| x.as_str())
108 .unwrap_or_default()
109 .unwrap_or_default();
110
111 let mut user_key = String::from("u:");
113 user_key += user_id;
114 user_key += ";";
115
116 let custom_ids = user_data
117 .get("customIDs")
118 .map(|x| x.as_object())
119 .unwrap_or_default();
120
121 if let Some(custom_ids) = custom_ids {
122 for (key, val) in custom_ids.iter() {
123 if let Some(string_value) = &val.as_str() {
124 user_key += key;
125 user_key += ":";
126 user_key += string_value;
127 user_key += ";";
128 }
129 }
130 };
131
132 user_key
133 }
134}
135
136impl StatsigUserInternal<'_, '_> {
137 pub fn to_loggable(&self) -> StatsigUserLoggable {
138 StatsigUserLoggable::new(self)
139 }
140}
141
142impl Drop for StatsigUserLoggable {
143 fn drop(&mut self) {
144 let strong_count = match LOGGABLE_USER_STORE.read() {
145 Ok(store) => match store.get(&self.data.key) {
146 Some(weak_ref) => weak_ref.strong_count(),
147 None => return,
148 },
149 Err(e) => {
150 log_e!(TAG, "Error locking user loggable store: {:?}", e);
151 return;
152 }
153 };
154
155 if strong_count > 1 {
156 return;
157 }
158
159 match LOGGABLE_USER_STORE.write() {
160 Ok(mut store) => {
161 store.remove(&self.data.key);
162 }
163 Err(e) => {
164 log_e!(TAG, "Error locking user loggable store: {:?}", e);
165 }
166 };
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use std::time::Duration;
173
174 use futures::future::join_all;
175
176 use crate::StatsigUser;
177
178 use super::*;
179
180 #[tokio::test]
181 async fn test_creating_many_loggable_users_and_map_growth() {
182 let mut handles = vec![];
183
184 for _ in 0..10 {
185 let handle = tokio::spawn(async move {
186 for i in 0..1000 {
187 let user_data = StatsigUser::with_user_id(format!("user{}", i));
188 let user_internal = StatsigUserInternal::new(&user_data, None);
189 let loggable = user_internal.to_loggable();
190 tokio::time::sleep(Duration::from_micros(1)).await;
191 let _ = loggable; }
193 });
194
195 handles.push(handle);
196 }
197
198 join_all(handles).await;
199
200 assert_eq!(LOGGABLE_USER_STORE.read().unwrap().len(), 0);
201 }
202}