statsig_rust/event_logging/
statsig_event_internal.rs

1use crate::event_logging::statsig_event::StatsigEvent;
2use crate::sdk_diagnostics::diagnostics::DIAGNOSTICS_EVENT;
3use crate::user::StatsigUserLoggable;
4use crate::{evaluation::evaluation_types::SecondaryExposure, statsig_metadata::StatsigMetadata};
5
6use chrono::Utc;
7use serde::{Deserialize, Serialize};
8use serde_json::{json, Value};
9use std::collections::{HashMap, HashSet};
10
11pub const GATE_EXPOSURE_EVENT_NAME: &str = "statsig::gate_exposure";
12pub const CONFIG_EXPOSURE_EVENT_NAME: &str = "statsig::config_exposure";
13pub const LAYER_EXPOSURE_EVENT_NAME: &str = "statsig::layer_exposure";
14pub const STATSIG_LOG_LINE_EVENT_NAME: &str = "statsig::log_line";
15
16#[derive(Debug)]
17pub enum StatsigLogLineLevel {
18    Trace,
19    Debug,
20    Log,
21    Info,
22    Warn,
23    Error,
24}
25
26impl StatsigLogLineLevel {
27    pub fn to_status_string(&self) -> String {
28        match self {
29            StatsigLogLineLevel::Trace => "trace".to_string(),
30            StatsigLogLineLevel::Debug => "debug".to_string(),
31            StatsigLogLineLevel::Log => "info".to_string(), // info and log map to the same status
32            StatsigLogLineLevel::Info => "info".to_string(),
33            StatsigLogLineLevel::Warn => "warn".to_string(),
34            StatsigLogLineLevel::Error => "error".to_string(),
35        }
36    }
37}
38
39#[derive(Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct StatsigEventInternal {
42    #[serde(flatten)]
43    pub event_data: StatsigEvent,
44
45    pub user: StatsigUserLoggable,
46    pub time: u64,
47    pub secondary_exposures: Option<Vec<SecondaryExposure>>,
48}
49
50impl StatsigEventInternal {
51    pub fn new(
52        time: u64,
53        user: StatsigUserLoggable,
54        event: StatsigEvent,
55        secondary_exposures: Option<Vec<SecondaryExposure>>,
56    ) -> Self {
57        StatsigEventInternal {
58            event_data: event,
59            user,
60            time,
61            secondary_exposures: secondary_exposure_keys_to_expos(secondary_exposures),
62        }
63    }
64
65    pub fn new_custom_event(
66        user: StatsigUserLoggable,
67        event_name: String,
68        value: Option<Value>,
69        metadata: Option<HashMap<String, String>>,
70    ) -> Self {
71        StatsigEventInternal::new(
72            Utc::now().timestamp_millis() as u64,
73            user,
74            StatsigEvent {
75                event_name,
76                value,
77                metadata,
78                statsig_metadata: None,
79            },
80            None,
81        )
82    }
83
84    pub fn new_diagnostic_event(metadata: HashMap<String, String>) -> Self {
85        StatsigEventInternal {
86            event_data: StatsigEvent {
87                event_name: DIAGNOSTICS_EVENT.to_string(),
88                value: None,
89                metadata: Some(metadata),
90                statsig_metadata: None,
91            },
92            user: StatsigUserLoggable::null(),
93            time: Utc::now().timestamp_millis() as u64,
94            secondary_exposures: None,
95        }
96    }
97
98    pub fn new_non_exposed_checks_event(checks: HashMap<String, u64>) -> Self {
99        let checks_json = match serde_json::to_string(&checks) {
100            Ok(json) => json,
101            Err(_) => "STATSIG_ERROR_SERIALIZING_NON_EXPOSED_CHECKS".into(),
102        };
103
104        let event = StatsigEvent {
105            event_name: "statsig::non_exposed_checks".to_string(),
106            value: None,
107            metadata: Some(HashMap::from([("checks".into(), checks_json)])),
108            statsig_metadata: None,
109        };
110
111        StatsigEventInternal {
112            event_data: event,
113            user: StatsigUserLoggable::null(),
114            time: Utc::now().timestamp_millis() as u64,
115            secondary_exposures: None,
116        }
117    }
118
119    pub fn new_statsig_log_line_event(
120        user: StatsigUserLoggable,
121        log_level: StatsigLogLineLevel,
122        value: Option<String>,
123        metadata: Option<HashMap<String, String>>,
124    ) -> Self {
125        let mut populated_metadata = metadata.unwrap_or_default();
126        populated_metadata.insert("status".to_string(), log_level.to_status_string());
127        populated_metadata.insert(
128            "source".to_string(),
129            StatsigMetadata::get_metadata().sdk_type.to_string(),
130        );
131        populated_metadata.insert("log_level".to_string(), format!("{:?}", log_level));
132
133        StatsigEventInternal {
134            event_data: StatsigEvent {
135                event_name: STATSIG_LOG_LINE_EVENT_NAME.to_string(),
136                value: value.map(|v| json!(v)),
137                metadata: Some(populated_metadata),
138                statsig_metadata: None,
139            },
140            user,
141            time: Utc::now().timestamp_millis() as u64,
142            secondary_exposures: None,
143        }
144    }
145
146    pub fn is_exposure_event(&self) -> bool {
147        self.event_data.event_name == GATE_EXPOSURE_EVENT_NAME
148            || self.event_data.event_name == CONFIG_EXPOSURE_EVENT_NAME
149            || self.event_data.event_name == LAYER_EXPOSURE_EVENT_NAME
150    }
151
152    pub fn is_diagnostic_event(&self) -> bool {
153        self.event_data.event_name == DIAGNOSTICS_EVENT
154    }
155}
156
157fn secondary_exposure_keys_to_expos(
158    secondary_exposures: Option<Vec<SecondaryExposure>>,
159) -> Option<Vec<SecondaryExposure>> {
160    match secondary_exposures.as_ref() {
161        Some(secondary_exposures) => {
162            let mut seen = HashSet::new();
163            let mut filtered = Vec::new();
164            for expo in secondary_exposures {
165                let key = format!(
166                    "{}.{}.{}",
167                    expo.gate,
168                    expo.rule_id.as_str(),
169                    expo.gate_value
170                );
171                if !seen.contains(&key) {
172                    seen.insert(key);
173                    filtered.push(expo);
174                }
175            }
176
177            Some(secondary_exposures.clone())
178        }
179        None => None,
180    }
181}
182
183#[cfg(test)]
184mod statsig_event_internal_tests {
185    use crate::event_logging::statsig_event::StatsigEvent;
186    use crate::event_logging::statsig_event_internal::StatsigEventInternal;
187    use crate::user::StatsigUserInternal;
188    use crate::StatsigUser;
189    use chrono::Utc;
190    use serde_json::{json, Value};
191    use std::collections::HashMap;
192
193    fn create_test_event() -> StatsigEventInternal {
194        let user_data = StatsigUser::with_user_id("a-user");
195        let user = StatsigUserInternal::new(&user_data, None);
196        let mut sampling_statsig_metadata: HashMap<String, Value> = HashMap::new();
197        sampling_statsig_metadata.insert("samplingMode".into(), "on".into());
198        sampling_statsig_metadata.insert("samplingRate".into(), 101.into());
199        sampling_statsig_metadata.insert("shadowLogged".into(), "logged".into());
200
201        StatsigEventInternal::new(
202            Utc::now().timestamp_millis() as u64,
203            user.to_loggable(),
204            StatsigEvent {
205                event_name: "foo".into(),
206                value: Some(json!("bar")),
207                metadata: Some(HashMap::from([("key".into(), "value".into())])),
208                statsig_metadata: Some(sampling_statsig_metadata),
209            },
210            None,
211        )
212    }
213
214    #[test]
215    fn test_custom_event_fields() {
216        let event = create_test_event();
217        let data = event.event_data;
218
219        assert_eq!(data.event_name, "foo");
220        assert_eq!(data.value.unwrap().as_str(), Some("bar"));
221        assert_eq!(data.metadata.unwrap().get("key").unwrap(), "value");
222    }
223
224    #[test]
225    fn test_custom_event_serialization() {
226        let event = create_test_event();
227
228        let value = json!(event).as_object().cloned().unwrap();
229        assert_eq!(value.get("eventName").unwrap(), "foo");
230        assert_eq!(value.get("value").unwrap(), "bar");
231        assert_eq!(
232            value.get("metadata").unwrap().to_string(),
233            "{\"key\":\"value\"}"
234        );
235    }
236}