Skip to main content

statsig_rust/event_logging/
statsig_event_internal.rs

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