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