1use std::collections::HashMap;
2
3use chrono::{DateTime, Duration, NaiveDateTime, TimeZone, Utc};
4use semver::Version;
5use serde::Serialize;
6use uuid::Uuid;
7
8use crate::feature_flag_evaluations::FeatureFlagEvaluations;
9use crate::Error;
10
11#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
16pub struct Event {
17 event: String,
18 #[serde(rename = "$distinct_id")]
19 distinct_id: String,
20 properties: HashMap<String, serde_json::Value>,
21 groups: HashMap<String, String>,
22 timestamp: Option<NaiveDateTime>,
23 uuid: Uuid,
24}
25
26impl Event {
27 pub fn new<S: Into<String>>(event: S, distinct_id: S) -> Self {
30 Self {
31 event: event.into(),
32 distinct_id: distinct_id.into(),
33 properties: HashMap::new(),
34 groups: HashMap::new(),
35 timestamp: None,
36 uuid: Uuid::now_v7(),
37 }
38 }
39
40 pub fn new_anon<S: Into<String>>(event: S) -> Self {
43 let mut res = Self {
44 event: event.into(),
45 distinct_id: Uuid::now_v7().to_string(),
46 properties: HashMap::new(),
47 groups: HashMap::new(),
48 timestamp: None,
49 uuid: Uuid::now_v7(),
50 };
51 res.insert_prop("$process_person_profile", false)
52 .expect("bools are safe for serde");
53 res
54 }
55
56 pub fn insert_prop<K: Into<String>, P: Serialize>(
60 &mut self,
61 key: K,
62 prop: P,
63 ) -> Result<(), Error> {
64 let as_json =
65 serde_json::to_value(prop).map_err(|e| Error::Serialization(e.to_string()))?;
66 let _ = self.properties.insert(key.into(), as_json);
67 Ok(())
68 }
69
70 pub fn add_group(&mut self, group_name: &str, group_id: &str) {
74 self.insert_prop("$process_person_profile", true)
76 .expect("bools are safe for serde");
77 self.groups.insert(group_name.into(), group_id.into());
78 }
79
80 pub fn set_timestamp<Tz>(&mut self, timestamp: DateTime<Tz>) -> Result<(), Error>
84 where
85 Tz: TimeZone,
86 {
87 if timestamp > Utc::now() + Duration::seconds(1) {
88 return Err(Error::InvalidTimestamp(String::from(
89 "Events cannot occur in the future",
90 )));
91 }
92 self.timestamp = Some(timestamp.naive_utc());
93 Ok(())
94 }
95
96 pub fn set_uuid(&mut self, uuid: Uuid) {
99 self.uuid = uuid;
100 }
101
102 pub fn with_flags(&mut self, flags: &FeatureFlagEvaluations) -> &mut Self {
108 for (key, value) in flags.event_properties() {
109 self.properties.insert(key, value);
110 }
111 self
112 }
113}
114
115#[derive(Serialize)]
118pub struct BatchRequest {
119 pub api_key: String,
120 pub historical_migration: bool,
121 pub batch: Vec<InnerEvent>,
122}
123
124#[derive(Serialize)]
126pub struct InnerEvent {
127 api_key: String,
128 uuid: Uuid,
129 event: String,
130 #[serde(rename = "$distinct_id")]
131 distinct_id: String,
132 properties: HashMap<String, serde_json::Value>,
133 timestamp: Option<NaiveDateTime>,
134}
135
136impl InnerEvent {
137 pub fn new(event: Event, api_key: String) -> Self {
138 let uuid = event.uuid;
139 let mut properties = event.properties;
140
141 if !properties.contains_key("$lib") {
144 properties.insert(
145 "$lib".into(),
146 serde_json::Value::String("posthog-rs".into()),
147 );
148 }
149
150 let version_str = env!("CARGO_PKG_VERSION");
151 if !properties.contains_key("$lib_version") {
152 properties.insert(
153 "$lib_version".into(),
154 serde_json::Value::String(version_str.into()),
155 );
156 }
157
158 if !properties.contains_key("$lib_version__major") {
159 if let Ok(version) = version_str.parse::<Version>() {
160 properties.insert(
161 "$lib_version__major".into(),
162 serde_json::Value::Number(version.major.into()),
163 );
164 properties.insert(
165 "$lib_version__minor".into(),
166 serde_json::Value::Number(version.minor.into()),
167 );
168 properties.insert(
169 "$lib_version__patch".into(),
170 serde_json::Value::Number(version.patch.into()),
171 );
172 }
173 }
174
175 if !event.groups.is_empty() {
176 properties.insert(
177 "$groups".into(),
178 serde_json::Value::Object(
179 event
180 .groups
181 .into_iter()
182 .map(|(k, v)| (k, serde_json::Value::String(v)))
183 .collect(),
184 ),
185 );
186 }
187
188 Self {
189 api_key,
190 uuid,
191 event: event.event,
192 distinct_id: event.distinct_id,
193 properties,
194 timestamp: event.timestamp,
195 }
196 }
197}
198
199#[cfg(test)]
200pub mod tests {
201 use uuid::Uuid;
202
203 use crate::{event::InnerEvent, Event};
204
205 #[test]
206 fn inner_event_adds_lib_properties_correctly() {
207 let mut event = Event::new("unit test event", "1234");
209 event.insert_prop("key1", "value1").unwrap();
210 let api_key = "test_api_key".to_string();
211
212 let inner_event = InnerEvent::new(event, api_key);
214
215 let props = &inner_event.properties;
217 assert_eq!(
218 props.get("$lib"),
219 Some(&serde_json::Value::String("posthog-rs".to_string()))
220 );
221 }
222
223 #[test]
224 fn inner_event_includes_auto_generated_uuid() {
225 let event = Event::new("test", "user1");
226
227 let inner = InnerEvent::new(event, "key".to_string());
228 let json = serde_json::to_value(&inner).unwrap();
229
230 let uuid_str = json["uuid"].as_str().expect("uuid should be present");
231 Uuid::parse_str(uuid_str).expect("uuid should be valid");
232 }
233
234 #[test]
235 fn inner_event_preserves_overridden_uuid() {
236 let uuid = Uuid::now_v7();
237 let mut event = Event::new("test", "user1");
238 event.set_uuid(uuid);
239
240 let inner = InnerEvent::new(event, "key".to_string());
241 let json = serde_json::to_value(&inner).unwrap();
242
243 assert_eq!(json["uuid"], uuid.to_string());
244 }
245
246 #[test]
247 fn inner_event_preserves_existing_lib_properties() {
248 let mut event = Event::new("forwarded event", "user1");
249 event.insert_prop("$lib", "posthog-js").unwrap();
250 event.insert_prop("$lib_version", "1.42.0").unwrap();
251 event.insert_prop("$lib_version__major", 1u64).unwrap();
252
253 let inner = InnerEvent::new(event, "key".to_string());
254 let props = &inner.properties;
255
256 assert_eq!(
257 props.get("$lib"),
258 Some(&serde_json::Value::String("posthog-js".to_string()))
259 );
260 assert_eq!(
261 props.get("$lib_version"),
262 Some(&serde_json::Value::String("1.42.0".to_string()))
263 );
264 assert_eq!(
265 props.get("$lib_version__major"),
266 Some(&serde_json::Value::Number(1u64.into()))
267 );
268 }
269}
270
271#[cfg(test)]
272mod test {
273 use std::time::Duration;
274
275 use chrono::{DateTime, Utc};
276
277 use super::Event;
278
279 #[test]
280 fn test_timestamp_is_correctly_set() {
281 let mut event = Event::new_anon("test");
282 let ts = DateTime::parse_from_rfc3339("2023-01-01T10:00:00+03:00").unwrap();
283 event.set_timestamp(ts).expect("Date is not in the future");
284 let expected = DateTime::parse_from_rfc3339("2023-01-01T07:00:00Z").unwrap();
285 assert_eq!(event.timestamp.unwrap(), expected.naive_utc())
286 }
287
288 #[test]
289 fn test_timestamp_is_correctly_set_with_future_date() {
290 let mut event = Event::new_anon("test");
291 let ts = Utc::now() + Duration::from_secs(60);
292 event
293 .set_timestamp(ts)
294 .expect_err("Date is in the future, should be rejected");
295
296 assert!(event.timestamp.is_none())
297 }
298}