diff --git a/Cargo.toml b/Cargo.toml index 2378cf3..aad08c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,7 @@ -[package] -name = "posthog-rs" -license = "MIT" -version = "0.2.3" -authors = ["christos "] -description = "An unofficial Rust client for Posthog (https://posthog.com/)." -repository = "https://github.com/openquery-io/posthog-rs" -edition = "2018" +[workspace] +members = [ + "async", + "core", + "sync", +] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -reqwest = { version = "0.11.3", default-features = false, features = ["blocking", "rustls-tls"] } -serde = { version = "1.0.125", features = ["derive"] } -chrono = {version = "0.4.19", features = ["serde"] } -serde_json = "1.0.64" diff --git a/README.md b/README.md index a5feac0..b95fec2 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,14 @@ Add `posthog-rs` to your `Cargo.toml`. ```toml [dependencies] -posthog_rs = "0.2.0" +posthog-rs = "0.2.0" # for sync client +async-posthog = "0.2.0" # for async client ``` +## Events + +Capture events with `capture`. + ```rust let client = crate::client(env!("POSTHOG_API_KEY")); @@ -21,6 +26,36 @@ event.insert_prop("key1", "value1").unwrap(); event.insert_prop("key2", vec!["a", "b"]).unwrap(); client.capture(event).unwrap(); +``` + +## Groups + +[Group analytics](https://posthog.com/docs/product-analytics/group-analytics) are supported. + +### Identifying Groups + +Groups can be created with [`group_identify`](https://posthog.com/docs/product-analytics/group-analytics#how-to-create-groups). + +```rust +let client = crate::client(env!("POSTHOG_API_KEY")); + +let mut event = GroupIdentify::new("organisation", "some_id"); +event.insert_prop("status", "active").unwrap(); + +client.group_identify(event).unwrap(); ``` +### Associating Events with a Group + +```rust +let client = crate::client(env!("POSTHOG_API_KEY")); + +let mut event = Event::new("test", "1234"); + +// Optionally associate this event with a group (in this case, +// a "company" group type with key "company_id_123"). +event.insert_group("company", "company_id_123"); + +client.capture(event).unwrap(); +``` diff --git a/async/Cargo.toml b/async/Cargo.toml new file mode 100644 index 0000000..8c9840d --- /dev/null +++ b/async/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "async-posthog" +license = "MIT" +version = "0.2.3" +description = "An unofficial Rust client for Posthog (https://posthog.com/)." +repository = "https://github.com/openquery-io/posthog-rs" +edition = "2021" + +[dependencies] +posthog-core = { path = "../core" } +reqwest = { version = "0.11.3", default-features = false, features = ["json", "rustls-tls"] } +tokio = { version = "1", features = ["full"] } +serde = { version = "1.0.125", features = ["derive"] } +serde_json = "1.0.64" +thiserror = "1.0.38" diff --git a/async/src/client.rs b/async/src/client.rs new file mode 100644 index 0000000..724b2eb --- /dev/null +++ b/async/src/client.rs @@ -0,0 +1,75 @@ +use posthog_core::event::{Event, InnerEvent, InnerEventBatch}; +use posthog_core::group_identify::GroupIdentify; +use reqwest::{Client as HttpClient, Method}; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::client_options::ClientOptions; +use crate::error::Error; + +pub struct Client { + options: ClientOptions, + http_client: HttpClient, +} + +impl Client { + pub(crate) fn new(options: ClientOptions) -> Self { + let http_client = HttpClient::builder() + .timeout(options.timeout) + .build() + .unwrap(); // Unwrap here is as safe as `HttpClient::new` + Client { + options, + http_client, + } + } + + async fn send_request, Body: Serialize, Res: DeserializeOwned>( + &self, + method: Method, + path: P, + body: &Body, + ) -> Result { + let res = self + .http_client + .request( + method, + format!("{}{}", self.options.api_endpoint, path.as_ref()), + ) + .json(body) + .send() + .await + .map_err(|source| Error::SendRequest { source })? + .error_for_status() + .map_err(|source| Error::ResponseStatus { source })? + .json::() + .await + .map_err(|source| Error::DecodeResponse { source })?; + Ok(res) + } + + pub async fn capture(&self, event: Event) -> Result<(), Error> { + let inner_event = InnerEvent::new(event, self.options.api_key.clone()); + self.send_request::<_, _, serde_json::Value>(Method::POST, "/capture/", &inner_event) + .await?; + Ok(()) + } + + pub async fn capture_batch(&self, events: Vec) -> Result<(), Error> { + let inner_event_batch = InnerEventBatch::new(events, self.options.api_key.clone()); + self.send_request::<_, _, serde_json::Value>(Method::POST, "/batch/", &inner_event_batch) + .await?; + Ok(()) + } + + pub async fn group_identify(&self, identify: GroupIdentify) -> Result<(), Error> { + let inner_event = InnerEvent::new( + identify + .try_into() + .map_err(|source| Error::PostHogCore { source })?, + self.options.api_key.clone(), + ); + self.send_request::<_, _, serde_json::Value>(Method::POST, "/capture/", &inner_event) + .await?; + Ok(()) + } +} diff --git a/async/src/client_options.rs b/async/src/client_options.rs new file mode 100644 index 0000000..b4019cc --- /dev/null +++ b/async/src/client_options.rs @@ -0,0 +1,42 @@ +use std::time::Duration; + +use crate::client::Client; + +const API_ENDPOINT: &str = "https://app.posthog.com"; +const TIMEOUT: Duration = Duration::from_millis(800); // This should be specified by the user + +pub struct ClientOptions { + pub(crate) api_endpoint: String, + pub(crate) api_key: String, + pub(crate) timeout: Duration, +} + +impl ClientOptions { + pub fn new(api_key: impl ToString) -> ClientOptions { + ClientOptions { + api_endpoint: API_ENDPOINT.to_string(), + api_key: api_key.to_string(), + timeout: TIMEOUT, + } + } + + pub fn api_endpoint(&mut self, api_endpoint: impl ToString) -> &mut Self { + self.api_endpoint = api_endpoint.to_string(); + self + } + + pub fn timeout(&mut self, timeout: Duration) -> &mut Self { + self.timeout = timeout; + self + } + + pub fn build(self) -> Client { + Client::new(self) + } +} + +impl From<&str> for ClientOptions { + fn from(api_key: &str) -> Self { + ClientOptions::new(api_key) + } +} diff --git a/async/src/error.rs b/async/src/error.rs new file mode 100644 index 0000000..74024e6 --- /dev/null +++ b/async/src/error.rs @@ -0,0 +1,11 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{source}")] + PostHogCore { source: posthog_core::error::Error }, + #[error("send request: {source}")] + SendRequest { source: reqwest::Error }, + #[error("response status: {source}")] + ResponseStatus { source: reqwest::Error }, + #[error("decode response: {source}")] + DecodeResponse { source: reqwest::Error }, +} diff --git a/async/src/lib.rs b/async/src/lib.rs new file mode 100644 index 0000000..fb45065 --- /dev/null +++ b/async/src/lib.rs @@ -0,0 +1,14 @@ +mod client; +mod client_options; +mod error; + +pub use client::Client; +pub use client_options::ClientOptions; +pub use error::Error; + +pub use posthog_core::event::{Event, Properties}; +pub use posthog_core::group_identify::GroupIdentify; + +pub fn client>(options: C) -> Client { + options.into().build() +} diff --git a/async/tests/basic.rs b/async/tests/basic.rs new file mode 100644 index 0000000..840ff51 --- /dev/null +++ b/async/tests/basic.rs @@ -0,0 +1,54 @@ +use async_posthog::{Event, GroupIdentify}; +use std::collections::HashMap; + +fn build_client() -> async_posthog::Client { + async_posthog::client(env!("POSTHOG_API_KEY")) +} + +#[tokio::test] +async fn capture() { + let client = build_client(); + + let mut child_map = HashMap::new(); + child_map.insert("child_key1", "child_value1"); + + let mut event = Event::new("test async capture", "1234"); + event.insert_prop("key1", "value1").unwrap(); + event.insert_prop("key2", vec!["a", "b"]).unwrap(); + event.insert_prop("key3", child_map).unwrap(); + + event.insert_group("company", "company_key"); + + client.capture(event).await.unwrap(); +} + +#[tokio::test] +async fn capture_batch() { + let client = build_client(); + + let events = (0..16) + .map(|_| { + let mut child_map = HashMap::new(); + child_map.insert("child_key1", "child_value1"); + + let mut event = Event::new("test async capture batch", "1234"); + event.insert_prop("key1", "value1").unwrap(); + event.insert_prop("key2", vec!["a", "b"]).unwrap(); + event.insert_prop("key3", child_map).unwrap(); + + event + }) + .collect::>(); + + client.capture_batch(events).await.unwrap(); +} + +#[tokio::test] +async fn group_identify() { + let client = build_client(); + + let mut event = GroupIdentify::new("organisation", "some_id"); + event.insert_prop("status", "active").unwrap(); + + client.group_identify(event).await.unwrap(); +} diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..176e7c3 --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "posthog-core" +license = "MIT" +version = "0.1.0" +description = "An unofficial Rust client for Posthog (https://posthog.com/)." +repository = "https://github.com/openquery-io/posthog-rs" +edition = "2021" + +[dependencies] +chrono = {version = "0.4.19", features = ["serde"] } +serde = { version = "1.0.125", features = ["derive"] } +serde_json = "1.0.64" +thiserror = "1.0.38" diff --git a/core/src/error.rs b/core/src/error.rs new file mode 100644 index 0000000..88312e4 --- /dev/null +++ b/core/src/error.rs @@ -0,0 +1,8 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("serialization: {source}")] + Serialization { + #[from] + source: serde_json::Error, + }, +} diff --git a/core/src/event.rs b/core/src/event.rs new file mode 100644 index 0000000..64252a1 --- /dev/null +++ b/core/src/event.rs @@ -0,0 +1,99 @@ +use chrono::NaiveDateTime; +use serde::Serialize; +use std::collections::HashMap; + +use crate::error::Error; + +#[derive(Serialize)] +pub struct InnerEvent { + api_key: String, + #[serde(flatten)] + event: Event, +} + +impl InnerEvent { + pub fn new(event: Event, api_key: String) -> Self { + Self { api_key, event } + } +} + +#[derive(Serialize)] +pub struct InnerEventBatch { + api_key: String, + batch: Vec, +} + +impl InnerEventBatch { + pub fn new(batch: Vec, api_key: String) -> Self { + Self { api_key, batch } + } +} + +#[derive(Serialize, Debug, PartialEq, Eq)] +pub struct Event { + pub(crate) event: String, + pub(crate) properties: Properties, + pub(crate) timestamp: Option, +} + +#[derive(Serialize, Debug, PartialEq, Eq)] +pub struct Properties { + pub(crate) distinct_id: String, + #[serde(rename = "$groups", skip_serializing_if = "Option::is_none")] + pub(crate) groups: Option>, + #[serde(flatten)] + pub(crate) props: HashMap, +} + +impl Properties { + fn new>(distinct_id: S) -> Self { + Self { + distinct_id: distinct_id.into(), + props: Default::default(), + groups: None, + } + } +} + +impl Event { + pub fn new>(event: S, distinct_id: S) -> Self { + Self { + event: event.into(), + properties: Properties::new(distinct_id), + timestamp: None, + } + } + + pub fn with_timestamp>( + event: S, + distinct_id: S, + timestamp: NaiveDateTime, + ) -> Self { + Self { + event: event.into(), + properties: Properties::new(distinct_id), + timestamp: Some(timestamp), + } + } + + /// Errors if `prop` fails to serialize + pub fn insert_prop, P: Serialize>( + &mut self, + key: K, + prop: P, + ) -> Result<(), Error> { + let as_json = + serde_json::to_value(prop).map_err(|source| Error::Serialization { source })?; + let _ = self.properties.props.insert(key.into(), as_json); + Ok(()) + } + + pub fn insert_group, P: Into>( + &mut self, + group_type: K, + group_key: P, + ) -> () { + let groups = self.properties.groups.get_or_insert_with(HashMap::new); + groups.insert(group_type.into(), group_key.into()); + } +} diff --git a/core/src/group_identify.rs b/core/src/group_identify.rs new file mode 100644 index 0000000..06128d3 --- /dev/null +++ b/core/src/group_identify.rs @@ -0,0 +1,84 @@ +use chrono::NaiveDateTime; +use serde::Serialize; +use std::collections::HashMap; + +use crate::{ + error::Error, + event::{Event, Properties}, +}; + +#[derive(Serialize, Debug, PartialEq, Eq)] +pub struct GroupIdentify { + group_type: String, + group_key: String, + group_properties: Option, + timestamp: Option, +} + +impl GroupIdentify { + pub fn new>(group_type: S, group_id: S) -> Self { + Self { + group_type: group_type.into(), + group_key: group_id.into(), + group_properties: None, + timestamp: None, + } + } + + pub fn with_timestamp>( + group_type: S, + group_id: S, + timestamp: NaiveDateTime, + ) -> Self { + Self { + group_type: group_type.into(), + group_key: group_id.into(), + group_properties: None, + timestamp: Some(timestamp), + } + } + + pub fn insert_prop, P: Serialize>( + &mut self, + key: K, + prop: P, + ) -> Result<(), Error> { + let as_json = + serde_json::to_value(prop).map_err(|source| Error::Serialization { source })?; + let key = key.into(); + + let group_properties = self.group_properties.get_or_insert_with(|| Properties { + distinct_id: self.group_key.clone(), + props: HashMap::new(), + groups: None, + }); + + group_properties.props.insert(key, as_json); + + Ok(()) + } +} + +impl TryFrom for Event { + type Error = Error; + fn try_from(group_identify: GroupIdentify) -> Result { + let distinct_id = format!("{}_{}", group_identify.group_type, group_identify.group_key); + + let mut props: HashMap = HashMap::with_capacity(3); + props.insert("$group_type".into(), group_identify.group_type.into()); + props.insert("$group_key".into(), group_identify.group_key.into()); + if let Some(properties) = group_identify.group_properties { + props.insert("$group_set".into(), serde_json::to_value(properties.props)?); + } + + Ok(Self { + event: "$groupidentify".into(), + properties: Properties { + distinct_id, + props, + groups: None, + }, + timestamp: group_identify.timestamp, + }) + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..9f5d4d2 --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod event; +pub mod group_identify; diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 98630fa..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -use chrono::{NaiveDateTime}; -use reqwest::blocking::Client as HttpClient; -use reqwest::header::CONTENT_TYPE; -use serde::{Serialize}; -use std::time::Duration; - -extern crate serde_json; - -const API_ENDPOINT: &str = "https://app.posthog.com/capture/"; -const TIMEOUT: &Duration = &Duration::from_millis(800); // This should be specified by the user - -pub fn client>(options: C) -> Client { - let client = HttpClient::builder().timeout(Some(TIMEOUT.clone())).build().unwrap(); // Unwrap here is as safe as `HttpClient::new` - Client { - options: options.into(), - client, - } -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Error::Connection(msg) => write!(f, "Connection Error: {}", msg), - Error::Serialization(msg) => write!(f, "Serialization Error: {}", msg) - } - } -} - -impl std::error::Error for Error { - -} - -#[derive(Debug)] -pub enum Error { - Connection(String), - Serialization(String) -} - -pub struct ClientOptions { - api_endpoint: String, - api_key: String, -} - -impl From<&str> for ClientOptions { - fn from(api_key: &str) -> Self { - ClientOptions { - api_endpoint: API_ENDPOINT.to_string(), - api_key: api_key.to_string(), - } - } -} - -pub struct Client { - options: ClientOptions, - client: HttpClient, -} - -impl Client { - pub fn capture(&self, event: Event) -> Result<(), Error> { - let inner_event = InnerEvent::new(event, self.options.api_key.clone()); - let _res = self.client.post(self.options.api_endpoint.clone()) - .header(CONTENT_TYPE, "application/json") - .body(serde_json::to_string(&inner_event).expect("unwrap here is safe")) - .send() - .map_err(|e| Error::Connection(e.to_string()))?; - Ok(()) - } - - pub fn capture_batch(&self, events: Vec) -> Result<(), Error> { - for event in events { - self.capture(event)?; - } - Ok(()) - } -} - -// This exists so that the client doesn't have to specify the API key over and over -#[derive(Serialize)] -struct InnerEvent { - api_key: String, - event: String, - properties: Properties, - timestamp: Option, -} - -impl InnerEvent { - fn new(event: Event, api_key: String) -> Self { - Self { - api_key, - event: event.event, - properties: event.properties, - timestamp: event.timestamp, - } - } -} - - -#[derive(Serialize, Debug, PartialEq, Eq)] -pub struct Event { - event: String, - properties: Properties, - timestamp: Option, -} - -#[derive(Serialize, Debug, PartialEq, Eq)] -pub struct Properties { - distinct_id: String, - props: HashMap, -} - -impl Properties { - fn new>(distinct_id: S) -> Self { - Self { - distinct_id: distinct_id.into(), - props: Default::default() - } - } -} - -impl Event { - pub fn new>(event: S, distinct_id: S) -> Self { - Self { - event: event.into(), - properties: Properties::new(distinct_id), - timestamp: None - } - } - - /// Errors if `prop` fails to serialize - pub fn insert_prop, P: Serialize>(&mut self, key: K, prop: P) -> Result<(), Error> { - let as_json = serde_json::to_value(prop).map_err(|e| Error::Serialization(e.to_string()))?; - let _ = self.properties.props.insert(key.into(), as_json); - Ok(()) - } -} - - -#[cfg(test)] -pub mod tests { - use super::*; - use chrono::{Utc}; - - #[test] - fn get_client() { - let client = crate::client(env!("POSTHOG_API_KEY")); - - let mut child_map = HashMap::new(); - child_map.insert("child_key1", "child_value1"); - - - let mut event = Event::new("test", "1234"); - event.insert_prop("key1", "value1").unwrap(); - event.insert_prop("key2", vec!["a", "b"]).unwrap(); - event.insert_prop("key3", child_map).unwrap(); - - client.capture(event).unwrap(); - } -} \ No newline at end of file diff --git a/sync/Cargo.toml b/sync/Cargo.toml new file mode 100644 index 0000000..75fdf9b --- /dev/null +++ b/sync/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "posthog-rs" +license = "MIT" +version = "0.2.3" +authors = ["christos "] +description = "An unofficial Rust client for Posthog (https://posthog.com/)." +repository = "https://github.com/openquery-io/posthog-rs" +edition = "2021" + +[dependencies] +posthog-core = { path = "../core" } +reqwest = { version = "0.11.3", default-features = false, features = ["blocking", "json", "rustls-tls"] } +serde = { version = "1.0.125", features = ["derive"] } +serde_json = "1.0.64" +thiserror = "1.0.38" diff --git a/sync/src/client.rs b/sync/src/client.rs new file mode 100644 index 0000000..d3f81b5 --- /dev/null +++ b/sync/src/client.rs @@ -0,0 +1,70 @@ +use posthog_core::event::{Event, InnerEvent, InnerEventBatch}; +use posthog_core::group_identify::GroupIdentify; +use reqwest::{blocking::Client as HttpClient, Method}; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::client_options::ClientOptions; +use crate::error::Error; + +pub struct Client { + options: ClientOptions, + http_client: HttpClient, +} + +impl Client { + pub(crate) fn new(options: ClientOptions) -> Self { + let http_client = HttpClient::builder() + .timeout(Some(options.timeout)) + .build() + .unwrap(); // Unwrap here is as safe as `HttpClient::new` + Client { + options, + http_client, + } + } + + fn send_request, Body: Serialize, Res: DeserializeOwned>( + &self, + method: Method, + path: P, + body: &Body, + ) -> Result { + let res = self + .http_client + .request( + method, + format!("{}{}", self.options.api_endpoint, path.as_ref()), + ) + .json(body) + .send() + .map_err(|source| Error::SendRequest { source })? + .error_for_status() + .map_err(|source| Error::ResponseStatus { source })? + .json::() + .map_err(|source| Error::DecodeResponse { source })?; + Ok(res) + } + + pub fn capture(&self, event: Event) -> Result<(), Error> { + let inner_event = InnerEvent::new(event, self.options.api_key.clone()); + self.send_request::<_, _, serde_json::Value>(Method::POST, "/capture/", &inner_event)?; + Ok(()) + } + + pub fn capture_batch(&self, events: Vec) -> Result<(), Error> { + let inner_event_batch = InnerEventBatch::new(events, self.options.api_key.clone()); + self.send_request::<_, _, serde_json::Value>(Method::POST, "/batch/", &inner_event_batch)?; + Ok(()) + } + + pub fn group_identify(&self, identify: GroupIdentify) -> Result<(), Error> { + let inner_event = InnerEvent::new( + identify + .try_into() + .map_err(|source| Error::PostHogCore { source })?, + self.options.api_key.clone(), + ); + self.send_request::<_, _, serde_json::Value>(Method::POST, "/capture/", &inner_event)?; + Ok(()) + } +} diff --git a/sync/src/client_options.rs b/sync/src/client_options.rs new file mode 100644 index 0000000..b4019cc --- /dev/null +++ b/sync/src/client_options.rs @@ -0,0 +1,42 @@ +use std::time::Duration; + +use crate::client::Client; + +const API_ENDPOINT: &str = "https://app.posthog.com"; +const TIMEOUT: Duration = Duration::from_millis(800); // This should be specified by the user + +pub struct ClientOptions { + pub(crate) api_endpoint: String, + pub(crate) api_key: String, + pub(crate) timeout: Duration, +} + +impl ClientOptions { + pub fn new(api_key: impl ToString) -> ClientOptions { + ClientOptions { + api_endpoint: API_ENDPOINT.to_string(), + api_key: api_key.to_string(), + timeout: TIMEOUT, + } + } + + pub fn api_endpoint(&mut self, api_endpoint: impl ToString) -> &mut Self { + self.api_endpoint = api_endpoint.to_string(); + self + } + + pub fn timeout(&mut self, timeout: Duration) -> &mut Self { + self.timeout = timeout; + self + } + + pub fn build(self) -> Client { + Client::new(self) + } +} + +impl From<&str> for ClientOptions { + fn from(api_key: &str) -> Self { + ClientOptions::new(api_key) + } +} diff --git a/sync/src/error.rs b/sync/src/error.rs new file mode 100644 index 0000000..74024e6 --- /dev/null +++ b/sync/src/error.rs @@ -0,0 +1,11 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{source}")] + PostHogCore { source: posthog_core::error::Error }, + #[error("send request: {source}")] + SendRequest { source: reqwest::Error }, + #[error("response status: {source}")] + ResponseStatus { source: reqwest::Error }, + #[error("decode response: {source}")] + DecodeResponse { source: reqwest::Error }, +} diff --git a/sync/src/lib.rs b/sync/src/lib.rs new file mode 100644 index 0000000..fb45065 --- /dev/null +++ b/sync/src/lib.rs @@ -0,0 +1,14 @@ +mod client; +mod client_options; +mod error; + +pub use client::Client; +pub use client_options::ClientOptions; +pub use error::Error; + +pub use posthog_core::event::{Event, Properties}; +pub use posthog_core::group_identify::GroupIdentify; + +pub fn client>(options: C) -> Client { + options.into().build() +} diff --git a/sync/tests/basic.rs b/sync/tests/basic.rs new file mode 100644 index 0000000..8c57396 --- /dev/null +++ b/sync/tests/basic.rs @@ -0,0 +1,54 @@ +use posthog_rs::{Event, GroupIdentify}; +use std::collections::HashMap; + +fn build_client() -> posthog_rs::Client { + posthog_rs::client(env!("POSTHOG_API_KEY")) +} + +#[test] +fn capture() { + let client = build_client(); + + let mut child_map = HashMap::new(); + child_map.insert("child_key1", "child_value1"); + + let mut event = Event::new("test sync capture", "1234"); + event.insert_prop("key1", "value1").unwrap(); + event.insert_prop("key2", vec!["a", "b"]).unwrap(); + event.insert_prop("key3", child_map).unwrap(); + + event.insert_group("company", "company_key"); + + client.capture(event).unwrap(); +} + +#[test] +fn capture_batch() { + let client = build_client(); + + let events = (0..16) + .map(|_| { + let mut child_map = HashMap::new(); + child_map.insert("child_key1", "child_value1"); + + let mut event = Event::new("test sync capture batch", "1234"); + event.insert_prop("key1", "value1").unwrap(); + event.insert_prop("key2", vec!["a", "b"]).unwrap(); + event.insert_prop("key3", child_map).unwrap(); + + event + }) + .collect::>(); + + client.capture_batch(events).unwrap(); +} + +#[test] +fn group_identify() { + let client = build_client(); + + let mut event = GroupIdentify::new("organisation", "some_id"); + event.insert_prop("status", "active").unwrap(); + + client.group_identify(event).unwrap(); +}