Skip to main content

statsig_rust/user/
statsig_user_loggable.rs

1use super::user_data::{UserData, UserDataMap};
2use crate::{DynamicValue, StatsigUser};
3use serde::{
4    ser::{SerializeMap, SerializeStruct},
5    Deserialize, Serialize,
6};
7use serde_json::Value;
8use std::{collections::HashMap, sync::Arc};
9
10const TAG: &str = "StatsigUserLoggable";
11
12#[derive(Clone, Default)]
13pub struct StatsigUserLoggable {
14    pub data: Arc<UserData>,
15    pub environment: Option<UserDataMap>,
16    pub global_custom: Option<HashMap<String, DynamicValue>>,
17}
18
19impl StatsigUserLoggable {
20    pub fn new(
21        user_inner: &Arc<UserData>,
22        environment: Option<UserDataMap>,
23        global_custom: Option<HashMap<String, DynamicValue>>,
24    ) -> Self {
25        Self {
26            data: user_inner.clone(),
27            environment,
28            global_custom,
29        }
30    }
31
32    pub fn null() -> Self {
33        Self::default()
34    }
35
36    pub fn default_console_capture_user(
37        environment: Option<UserDataMap>,
38        global_custom: Option<HashMap<String, DynamicValue>>,
39    ) -> Self {
40        Self::new(
41            &StatsigUser::with_user_id("console-capture-user").data,
42            environment,
43            global_custom,
44        )
45    }
46}
47
48// ----------------------------------------------------- [Serialization]
49
50impl Serialize for StatsigUserLoggable {
51    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
52    where
53        S: serde::Serializer,
54    {
55        let mut state = serializer.serialize_struct(TAG, 10)?;
56
57        let data = self.data.as_ref();
58        serialize_data_field(&mut state, "userID", &data.user_id)?;
59        serialize_data_field(&mut state, "customIDs", &data.custom_ids)?;
60        serialize_data_field(&mut state, "email", &data.email)?;
61        serialize_data_field(&mut state, "ip", &data.ip)?;
62        serialize_data_field(&mut state, "userAgent", &data.user_agent)?;
63        serialize_data_field(&mut state, "country", &data.country)?;
64        serialize_data_field(&mut state, "locale", &data.locale)?;
65        serialize_data_field(&mut state, "appVersion", &data.app_version)?;
66
67        serialize_custom_field(&mut state, &data.custom, &self.global_custom)?;
68        serialize_data_field(&mut state, "statsigEnvironment", &self.environment)?;
69
70        // DO NOT SERIALIZE "privateAttributes"
71
72        state.end()
73    }
74}
75
76impl<'de> Deserialize<'de> for StatsigUserLoggable {
77    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
78    where
79        D: serde::Deserializer<'de>,
80    {
81        let mut value = Value::deserialize(deserializer)?;
82        let env = value["statsigEnvironment"].take();
83        let data = serde_json::from_value::<UserData>(value).map_err(|e| {
84            serde::de::Error::custom(format!("Error deserializing StatsigUserInner: {e}"))
85        })?;
86
87        let environment = serde_json::from_value::<Option<UserDataMap>>(env).map_err(|e| {
88            serde::de::Error::custom(format!("Error deserializing StatsigUserInner: {e}"))
89        })?;
90
91        Ok(StatsigUserLoggable {
92            data: Arc::new(data),
93            environment,
94            global_custom: None, // there is no way to discern between user-defined and global custom fields
95        })
96    }
97}
98
99fn serialize_data_field<S, T>(
100    state: &mut S,
101    field: &'static str,
102    value: &Option<T>,
103) -> Result<(), S::Error>
104where
105    S: SerializeStruct,
106    T: Serialize,
107{
108    if let Some(value) = value {
109        state.serialize_field(field, value)?;
110    }
111    Ok(())
112}
113
114fn serialize_custom_field<S>(
115    state: &mut S,
116    custom: &Option<UserDataMap>,
117    global_custom: &Option<HashMap<String, DynamicValue>>,
118) -> Result<(), S::Error>
119where
120    S: SerializeStruct,
121{
122    if global_custom.is_none() && custom.is_none() {
123        return Ok(());
124    }
125
126    state.serialize_field(
127        "custom",
128        &MergedCustomFields {
129            custom: custom.as_ref(),
130            global_custom: global_custom.as_ref(),
131        },
132    )
133}
134
135struct MergedCustomFields<'a> {
136    custom: Option<&'a UserDataMap>,
137    global_custom: Option<&'a HashMap<String, DynamicValue>>,
138}
139
140impl MergedCustomFields<'_> {
141    fn serialized_len(&self) -> usize {
142        let global_len = self.global_custom.map_or(0, HashMap::len);
143        let custom_only_len = self.custom.map_or(0, |custom| {
144            custom
145                .keys()
146                .filter(|key| {
147                    !self
148                        .global_custom
149                        .is_some_and(|global_custom| global_custom.contains_key(*key))
150                })
151                .count()
152        });
153
154        global_len + custom_only_len
155    }
156}
157
158impl Serialize for MergedCustomFields<'_> {
159    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
160    where
161        S: serde::Serializer,
162    {
163        let mut map = serializer.serialize_map(Some(self.serialized_len()))?;
164
165        if let Some(global_custom) = self.global_custom {
166            for (key, global_value) in global_custom {
167                let value = self
168                    .custom
169                    .and_then(|custom| custom.get(key))
170                    .unwrap_or(global_value);
171                map.serialize_entry(key, value)?;
172            }
173        }
174
175        if let Some(custom) = self.custom {
176            for (key, value) in custom {
177                if self
178                    .global_custom
179                    .is_some_and(|global_custom| global_custom.contains_key(key))
180                {
181                    continue;
182                }
183
184                map.serialize_entry(key, value)?;
185            }
186        }
187
188        map.end()
189    }
190}