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