1use std::collections::HashMap;
2
3use chrono::{DateTime, Duration, NaiveDateTime, TimeZone, Utc};
4use semver::Version;
5use serde::Serialize;
6use uuid::Uuid;
7
8use crate::Error;
9
10#[derive(Serialize, Debug, PartialEq, Eq)]
15pub struct Event {
16 event: String,
17 #[serde(rename = "$distinct_id")]
18 distinct_id: String,
19 properties: HashMap<String, serde_json::Value>,
20 groups: HashMap<String, String>,
21 timestamp: Option<NaiveDateTime>,
22}
23
24impl Event {
25 pub fn new<S: Into<String>>(event: S, distinct_id: S) -> Self {
28 Self {
29 event: event.into(),
30 distinct_id: distinct_id.into(),
31 properties: HashMap::new(),
32 groups: HashMap::new(),
33 timestamp: None,
34 }
35 }
36
37 pub fn new_anon<S: Into<String>>(event: S) -> Self {
40 let mut res = Self {
41 event: event.into(),
42 distinct_id: Uuid::now_v7().to_string(),
43 properties: HashMap::new(),
44 groups: HashMap::new(),
45 timestamp: None,
46 };
47 res.insert_prop("$process_person_profile", false)
48 .expect("bools are safe for serde");
49 res
50 }
51
52 pub fn insert_prop<K: Into<String>, P: Serialize>(
56 &mut self,
57 key: K,
58 prop: P,
59 ) -> Result<(), Error> {
60 let as_json =
61 serde_json::to_value(prop).map_err(|e| Error::Serialization(e.to_string()))?;
62 let _ = self.properties.insert(key.into(), as_json);
63 Ok(())
64 }
65
66 pub fn add_group(&mut self, group_name: &str, group_id: &str) {
70 self.insert_prop("$process_person_profile", true)
72 .expect("bools are safe for serde");
73 self.groups.insert(group_name.into(), group_id.into());
74 }
75
76 pub fn set_timestamp<Tz>(&mut self, timestamp: DateTime<Tz>) -> Result<(), Error>
78 where
79 Tz: TimeZone,
80 {
81 if timestamp > Utc::now() + Duration::seconds(1) {
82 return Err(Error::InvalidTimestamp(String::from(
83 "Events cannot occur in the future",
84 )));
85 }
86 self.timestamp = Some(timestamp.naive_utc());
87 Ok(())
88 }
89}
90
91#[derive(Serialize)]
93pub struct InnerEvent {
94 api_key: String,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 uuid: Option<Uuid>,
97 event: String,
98 #[serde(rename = "$distinct_id")]
99 distinct_id: String,
100 properties: HashMap<String, serde_json::Value>,
101 timestamp: Option<NaiveDateTime>,
102}
103
104impl InnerEvent {
105 pub fn new(event: Event, api_key: String) -> Self {
106 Self::new_with_uuid(event, api_key, None)
107 }
108
109 pub fn new_with_uuid(event: Event, api_key: String, uuid: Option<Uuid>) -> Self {
110 let mut properties = event.properties;
111
112 properties.insert(
114 "$lib".into(),
115 serde_json::Value::String("posthog-rs".into()),
116 );
117
118 let version_str = env!("CARGO_PKG_VERSION");
119 properties.insert(
120 "$lib_version".into(),
121 serde_json::Value::String(version_str.into()),
122 );
123
124 if let Ok(version) = version_str.parse::<Version>() {
125 properties.insert(
126 "$lib_version__major".into(),
127 serde_json::Value::Number(version.major.into()),
128 );
129 properties.insert(
130 "$lib_version__minor".into(),
131 serde_json::Value::Number(version.minor.into()),
132 );
133 properties.insert(
134 "$lib_version__patch".into(),
135 serde_json::Value::Number(version.patch.into()),
136 );
137 }
138
139 if !event.groups.is_empty() {
140 properties.insert(
141 "$groups".into(),
142 serde_json::Value::Object(
143 event
144 .groups
145 .into_iter()
146 .map(|(k, v)| (k, serde_json::Value::String(v)))
147 .collect(),
148 ),
149 );
150 }
151
152 Self {
153 api_key,
154 uuid,
155 event: event.event,
156 distinct_id: event.distinct_id,
157 properties,
158 timestamp: event.timestamp,
159 }
160 }
161}
162
163#[cfg(test)]
164pub mod tests {
165 use crate::{event::InnerEvent, Event};
166
167 #[test]
168 fn inner_event_adds_lib_properties_correctly() {
169 let mut event = Event::new("unit test event", "1234");
171 event.insert_prop("key1", "value1").unwrap();
172 let api_key = "test_api_key".to_string();
173
174 let inner_event = InnerEvent::new(event, api_key);
176
177 let props = &inner_event.properties;
179 assert_eq!(
180 props.get("$lib"),
181 Some(&serde_json::Value::String("posthog-rs".to_string()))
182 );
183 }
184}
185
186#[cfg(test)]
187mod test {
188 use std::time::Duration;
189
190 use chrono::{DateTime, Utc};
191
192 use super::Event;
193
194 #[test]
195 fn test_timestamp_is_correctly_set() {
196 let mut event = Event::new_anon("test");
197 let ts = DateTime::parse_from_rfc3339("2023-01-01T10:00:00+03:00").unwrap();
198 event
199 .set_timestamp(ts.clone())
200 .expect("Date is not in the future");
201 let expected = DateTime::parse_from_rfc3339("2023-01-01T07:00:00Z").unwrap();
202 assert_eq!(event.timestamp.unwrap(), expected.naive_utc())
203 }
204
205 #[test]
206 fn test_timestamp_is_correctly_set_with_future_date() {
207 let mut event = Event::new_anon("test");
208 let ts = Utc::now() + Duration::from_secs(60);
209 event
210 .set_timestamp(ts.clone())
211 .expect_err("Date is in the future, should be rejected");
212
213 assert!(event.timestamp.is_none())
214 }
215}