Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .sampo/changesets/noble-wavetamer-sampsa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
cargo/posthog-rs: patch
---

Generate (and allow overrides of) event UUID, allow for properties pass through.
117 changes: 89 additions & 28 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ use crate::Error;
/// website. Examples include button clicks, pageviews, query completions, and signups.
/// See the [PostHog documentation](https://posthog.com/docs/data/events)
/// for a detailed explanation of PostHog Events.
#[derive(Serialize, Debug, PartialEq, Eq)]
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You added Clone here, but I don't see it being used. We should remove it? Cloning large data objects is not great

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah sorry, i can break that out into a separate pr. clone is needed for the retry logic added in PostHog/posthog@71d7c0e

pub struct Event {
event: String,
#[serde(rename = "$distinct_id")]
distinct_id: String,
properties: HashMap<String, serde_json::Value>,
groups: HashMap<String, String>,
timestamp: Option<NaiveDateTime>,
uuid: Uuid,
}

impl Event {
Expand All @@ -31,6 +32,7 @@ impl Event {
properties: HashMap::new(),
groups: HashMap::new(),
timestamp: None,
uuid: Uuid::now_v7(),
}
}

Expand All @@ -43,6 +45,7 @@ impl Event {
properties: HashMap::new(),
groups: HashMap::new(),
timestamp: None,
uuid: Uuid::now_v7(),
};
res.insert_prop("$process_person_profile", false)
.expect("bools are safe for serde");
Expand Down Expand Up @@ -88,14 +91,19 @@ impl Event {
self.timestamp = Some(timestamp.naive_utc());
Ok(())
}

/// Override the auto-generated UUID for this event. Useful for
/// deduplication when re-importing historical data.
pub fn set_uuid(&mut self, uuid: Uuid) {
self.uuid = uuid;
}
}

// This exists so that the client doesn't have to specify the API key over and over
#[derive(Serialize)]
pub struct InnerEvent {
api_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
uuid: Option<Uuid>,
uuid: Uuid,
event: String,
#[serde(rename = "$distinct_id")]
distinct_id: String,
Expand All @@ -105,39 +113,43 @@ pub struct InnerEvent {

impl InnerEvent {
pub fn new(event: Event, api_key: String) -> Self {
Self::new_with_uuid(event, api_key, None)
}

pub fn new_with_uuid(event: Event, api_key: String, uuid: Option<Uuid>) -> Self {
let uuid = event.uuid;
let mut properties = event.properties;

// Add $lib_name and $lib_version to the properties
properties.insert(
"$lib".into(),
serde_json::Value::String("posthog-rs".into()),
);

let version_str = env!("CARGO_PKG_VERSION");
properties.insert(
"$lib_version".into(),
serde_json::Value::String(version_str.into()),
);

if let Ok(version) = version_str.parse::<Version>() {
properties.insert(
"$lib_version__major".into(),
serde_json::Value::Number(version.major.into()),
);
// Set $lib and $lib_version if not already present, so callers
// forwarding events from other SDKs can preserve the original values.
if !properties.contains_key("$lib") {
properties.insert(
"$lib_version__minor".into(),
serde_json::Value::Number(version.minor.into()),
"$lib".into(),
serde_json::Value::String("posthog-rs".into()),
);
}

let version_str = env!("CARGO_PKG_VERSION");
if !properties.contains_key("$lib_version") {
properties.insert(
"$lib_version__patch".into(),
serde_json::Value::Number(version.patch.into()),
"$lib_version".into(),
serde_json::Value::String(version_str.into()),
);
}

if !properties.contains_key("$lib_version__major") {
if let Ok(version) = version_str.parse::<Version>() {
properties.insert(
"$lib_version__major".into(),
serde_json::Value::Number(version.major.into()),
);
properties.insert(
"$lib_version__minor".into(),
serde_json::Value::Number(version.minor.into()),
);
properties.insert(
"$lib_version__patch".into(),
serde_json::Value::Number(version.patch.into()),
);
}
}

if !event.groups.is_empty() {
properties.insert(
"$groups".into(),
Expand All @@ -164,6 +176,8 @@ impl InnerEvent {

#[cfg(test)]
pub mod tests {
use uuid::Uuid;

use crate::{event::InnerEvent, Event};

#[test]
Expand All @@ -183,6 +197,53 @@ pub mod tests {
Some(&serde_json::Value::String("posthog-rs".to_string()))
);
}

#[test]
fn inner_event_includes_auto_generated_uuid() {
let event = Event::new("test", "user1");

let inner = InnerEvent::new(event, "key".to_string());
let json = serde_json::to_value(&inner).unwrap();

let uuid_str = json["uuid"].as_str().expect("uuid should be present");
Uuid::parse_str(uuid_str).expect("uuid should be valid");
}

#[test]
fn inner_event_preserves_overridden_uuid() {
let uuid = Uuid::now_v7();
let mut event = Event::new("test", "user1");
event.set_uuid(uuid);

let inner = InnerEvent::new(event, "key".to_string());
let json = serde_json::to_value(&inner).unwrap();

assert_eq!(json["uuid"], uuid.to_string());
}

#[test]
fn inner_event_preserves_existing_lib_properties() {
let mut event = Event::new("forwarded event", "user1");
event.insert_prop("$lib", "posthog-js").unwrap();
event.insert_prop("$lib_version", "1.42.0").unwrap();
event.insert_prop("$lib_version__major", 1u64).unwrap();

let inner = InnerEvent::new(event, "key".to_string());
let props = &inner.properties;

assert_eq!(
props.get("$lib"),
Some(&serde_json::Value::String("posthog-js".to_string()))
);
assert_eq!(
props.get("$lib_version"),
Some(&serde_json::Value::String("1.42.0".to_string()))
);
assert_eq!(
props.get("$lib_version__major"),
Some(&serde_json::Value::Number(1u64.into()))
);
}
}

#[cfg(test)]
Expand Down