statsig_rust/user/
statsig_user_internal.rs

1use 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(dyn_str) = &field.string_value {
14                $values += &dyn_str.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(dyn_str) = &map[key].string_value {
29                    $values += key;
30                    $values += &dyn_str.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 = "u:".to_string();
147        if let Some(user_id) = user_data
148            .user_id
149            .as_ref()
150            .and_then(|id| id.string_value.as_ref().map(|s| &s.value))
151        {
152            user_key += user_id;
153        }
154
155        let custom_ids = match user_data.custom_ids.as_ref() {
156            Some(custom_ids) => custom_ids,
157            None => return user_key,
158        };
159
160        for (key, val) in custom_ids {
161            if let Some(dyn_str) = &val.string_value {
162                let string_value = &dyn_str.value;
163                user_key.push_str(&format!("{key}:{string_value};"));
164            }
165        }
166
167        user_key
168    }
169
170    pub fn get_full_user_key(&self) -> String {
171        let mut values = String::new();
172
173        append_string_value!(values, self.user_data, app_version);
174        append_string_value!(values, self.user_data, country);
175        append_string_value!(values, self.user_data, email);
176        append_string_value!(values, self.user_data, ip);
177        append_string_value!(values, self.user_data, locale);
178        append_string_value!(values, self.user_data, user_agent);
179        append_string_value!(values, self.user_data, user_id);
180
181        append_sorted_string_values!(values, &self.user_data.custom_ids);
182        append_sorted_string_values!(values, &self.user_data.custom);
183        append_sorted_string_values!(values, &self.user_data.private_attributes);
184
185        if let Some(statsig_instance) = self.statsig_instance {
186            statsig_instance.use_statsig_env(|env| {
187                append_sorted_string_values!(values, env);
188            });
189        }
190
191        values
192    }
193}
194
195impl Serialize for StatsigUserInternal<'_, '_> {
196    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
197    where
198        S: serde::Serializer,
199    {
200        let inner_json = serde_json::to_value(self.user_data).map_err(serde::ser::Error::custom)?;
201
202        let mut len = 1;
203        if let serde_json::Value::Object(obj) = &inner_json {
204            len += obj.len();
205        }
206
207        let mut state = serializer.serialize_map(Some(len))?;
208
209        if let serde_json::Value::Object(obj) = &inner_json {
210            for (k, v) in obj {
211                state.serialize_entry(k, v)?;
212            }
213        }
214
215        if let Some(statsig_instance) = self.statsig_instance {
216            statsig_instance.use_global_custom_fields(|global_fields| {
217                if is_none_or_empty(&self.user_data.custom.as_ref())
218                    && is_none_or_empty(&global_fields)
219                {
220                    return Ok(());
221                }
222
223                let mut merged = HashMap::new();
224                if let Some(user_custom) = &self.user_data.custom {
225                    merged.extend(user_custom.iter());
226                }
227
228                if let Some(global) = global_fields {
229                    merged.extend(global.iter());
230                }
231
232                state.serialize_entry("custom", &json!(merged))?;
233
234                Ok(())
235            })?;
236
237            statsig_instance.use_statsig_env(|env| {
238                if let Some(env) = env {
239                    state.serialize_entry("statsigEnvironment", &json!(env))?;
240                }
241
242                Ok(())
243            })?;
244        }
245
246        state.end()
247    }
248}
249
250fn is_none_or_empty(opt_vec: &Option<&HashMap<String, DynamicValue>>) -> bool {
251    match opt_vec {
252        None => true,
253        Some(vec) => vec.is_empty(),
254    }
255}