statsig_rust/event_logging/
statsig_event_internal.rs1use 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(), 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}