statsig_rust/user/
statsig_user_internal.rs1use std::collections::HashMap;
2
3use crate::evaluation::dynamic_value::DynamicValue;
4use crate::StatsigUser;
5use crate::{evaluation::dynamic_string::DynamicString, Statsig};
6use serde::ser::SerializeMap;
7use serde::Serialize;
8use serde_json::json;
9
10macro_rules! append_string_value {
11 ($values:expr, $user_data:expr, $field:ident) => {
12 if let Some(field) = &$user_data.$field {
13 if let Some(string_value) = &field.string_value {
14 $values += string_value;
15 $values += "|";
16 }
17 }
18 };
19}
20
21macro_rules! append_sorted_string_values {
22 ($values:expr, $map:expr) => {
23 if let Some(map) = $map {
24 let mut keys: Vec<&String> = map.keys().collect();
25 keys.sort();
26
27 for key in keys {
28 if let Some(string_value) = &map[key].string_value {
29 $values += key;
30 $values += string_value;
31 }
32 }
33 }
34 };
35}
36
37#[derive(Clone)]
38pub struct StatsigUserInternal<'statsig, 'user> {
39 pub user_data: &'user StatsigUser,
40
41 pub statsig_instance: Option<&'statsig Statsig>,
42}
43
44impl<'statsig, 'user> StatsigUserInternal<'statsig, 'user> {
45 pub fn new(user: &'user StatsigUser, statsig_instance: Option<&'statsig Statsig>) -> Self {
46 Self {
47 user_data: user,
48 statsig_instance,
49 }
50 }
51
52 pub fn get_unit_id(&self, id_type: &DynamicString) -> Option<&DynamicValue> {
53 if id_type.lowercased_value.eq("userid") {
54 return self.user_data.user_id.as_ref();
55 }
56
57 let custom_ids = self.user_data.custom_ids.as_ref()?;
58
59 if let Some(custom_id) = custom_ids.get(&id_type.value) {
60 return Some(custom_id);
61 }
62
63 custom_ids.get(&id_type.lowercased_value)
64 }
65
66 pub fn get_user_value(&self, field: &Option<DynamicString>) -> Option<&DynamicValue> {
67 let field = field.as_ref()?;
68
69 let lowered_field = &field.lowercased_value;
70
71 let str_value = match lowered_field as &str {
72 "userid" => &self.user_data.user_id,
73 "email" => &self.user_data.email,
74 "ip" => &self.user_data.ip,
75 "country" => &self.user_data.country,
76 "locale" => &self.user_data.locale,
77 "appversion" => &self.user_data.app_version,
78 "useragent" => &self.user_data.user_agent,
79 _ => &None,
80 };
81
82 if str_value.is_some() {
83 return str_value.as_ref();
84 }
85
86 if let Some(custom) = &self.user_data.custom {
87 if let Some(found) = custom.get(&field.value) {
88 return Some(found);
89 }
90 if let Some(lowered_found) = custom.get(lowered_field) {
91 return Some(lowered_found);
92 }
93 }
94
95 if let Some(instance) = &self.statsig_instance {
96 if let Some(val) = instance.get_value_from_global_custom_fields(&field.value) {
97 return Some(val);
98 }
99
100 if let Some(val) = instance.get_value_from_global_custom_fields(&field.lowercased_value)
101 {
102 return Some(val);
103 }
104 }
105
106 if let Some(private_attributes) = &self.user_data.private_attributes {
107 if let Some(found) = private_attributes.get(&field.value) {
108 return Some(found);
109 }
110 if let Some(lowered_found) = private_attributes.get(lowered_field) {
111 return Some(lowered_found);
112 }
113 }
114
115 let str_value_alt = match lowered_field as &str {
116 "user_id" => &self.user_data.user_id,
117 "app_version" => &self.user_data.app_version,
118 "user_agent" => &self.user_data.user_agent,
119 _ => &None,
120 };
121
122 if str_value_alt.is_some() {
123 return str_value_alt.as_ref();
124 }
125
126 None
127 }
128
129 pub fn get_value_from_environment(
130 &self,
131 field: &Option<DynamicString>,
132 ) -> Option<DynamicValue> {
133 let field = field.as_ref()?;
134
135 if let Some(result) = self.statsig_instance?.get_from_statsig_env(&field.value) {
136 return Some(result);
137 }
138
139 self.statsig_instance?
140 .get_from_statsig_env(&field.lowercased_value)
141 }
142
143 pub fn get_sampling_key(&self) -> String {
144 let user_data = &self.user_data;
145
146 let mut user_key = format!(
147 "u:{};",
148 user_data
149 .user_id
150 .as_ref()
151 .and_then(|id| id.string_value.as_deref())
152 .unwrap_or("")
153 );
154
155 if let Some(custom_ids) = user_data.custom_ids.as_ref() {
156 for (key, val) in custom_ids {
157 if let Some(string_value) = &val.string_value {
158 user_key.push_str(&format!("{key}:{string_value};"));
159 }
160 }
161 };
162
163 user_key
164 }
165
166 pub fn get_full_user_key(&self) -> String {
167 let mut values = String::new();
168
169 append_string_value!(values, self.user_data, app_version);
170 append_string_value!(values, self.user_data, country);
171 append_string_value!(values, self.user_data, email);
172 append_string_value!(values, self.user_data, ip);
173 append_string_value!(values, self.user_data, locale);
174 append_string_value!(values, self.user_data, user_agent);
175 append_string_value!(values, self.user_data, user_id);
176
177 append_sorted_string_values!(values, &self.user_data.custom_ids);
178 append_sorted_string_values!(values, &self.user_data.custom);
179 append_sorted_string_values!(values, &self.user_data.private_attributes);
180
181 if let Some(statsig_instance) = self.statsig_instance {
182 statsig_instance.use_statsig_env(|env| {
183 append_sorted_string_values!(values, env);
184 });
185 }
186
187 values
188 }
189}
190
191impl Serialize for StatsigUserInternal<'_, '_> {
192 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
193 where
194 S: serde::Serializer,
195 {
196 let inner_json = serde_json::to_value(self.user_data).map_err(serde::ser::Error::custom)?;
197
198 let mut len = 1;
199 if let serde_json::Value::Object(obj) = &inner_json {
200 len += obj.len();
201 }
202
203 let mut state = serializer.serialize_map(Some(len))?;
204
205 if let serde_json::Value::Object(obj) = &inner_json {
206 for (k, v) in obj {
207 state.serialize_entry(k, v)?;
208 }
209 }
210
211 if let Some(statsig_instance) = self.statsig_instance {
212 statsig_instance.use_global_custom_fields(|global_fields| {
213 if is_none_or_empty(&self.user_data.custom.as_ref())
214 && is_none_or_empty(&global_fields)
215 {
216 return Ok(());
217 }
218
219 let mut merged = HashMap::new();
220 if let Some(user_custom) = &self.user_data.custom {
221 merged.extend(user_custom.iter());
222 }
223
224 if let Some(global) = global_fields {
225 merged.extend(global.iter());
226 }
227
228 state.serialize_entry("custom", &json!(merged))?;
229
230 Ok(())
231 })?;
232
233 statsig_instance.use_statsig_env(|env| {
234 if let Some(env) = env {
235 state.serialize_entry("statsigEnvironment", &json!(env))?;
236 }
237
238 Ok(())
239 })?;
240 }
241
242 state.end()
243 }
244}
245
246fn is_none_or_empty(opt_vec: &Option<&HashMap<String, DynamicValue>>) -> bool {
247 match opt_vec {
248 None => true,
249 Some(vec) => vec.is_empty(),
250 }
251}