Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement async PostHog client #4

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
21 changes: 6 additions & 15 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
[package]
name = "posthog-rs"
license = "MIT"
version = "0.2.3"
authors = ["christos <[email protected]>"]
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"
15 changes: 15 additions & 0 deletions async/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 = ["rustls-tls"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0.125", features = ["derive"] }
serde_json = "1.0.64"
thiserror = "1.0.38"
45 changes: 45 additions & 0 deletions async/src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use posthog_core::event::{Event, InnerEvent};
use reqwest::header::CONTENT_TYPE;
use reqwest::Client as HttpClient;

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,
}
}

pub async fn capture(&self, event: Event) -> Result<(), Error> {
let inner_event = InnerEvent::new(event, self.options.api_key.clone());
let _res = self
.http_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()
.await
.map_err(|source| Error::Connection { source })?;
Ok(())
}

pub async fn capture_batch(&self, events: Vec<Event>) -> Result<(), Error> {
// TODO: Use batch endpoint
for event in events {
self.capture(event).await?;
}
Ok(())
}
}
42 changes: 42 additions & 0 deletions async/src/client_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use std::time::Duration;

use crate::client::Client;

const API_ENDPOINT: &str = "https://app.posthog.com/capture/";
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)
}
}
7 changes: 7 additions & 0 deletions async/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{source}")]
PostHogCore { source: posthog_core::error::Error },
#[error("connection: {source}")]
Connection { source: reqwest::Error },
}
13 changes: 13 additions & 0 deletions async/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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 fn client<C: Into<ClientOptions>>(options: C) -> Client {
options.into().build()
}
17 changes: 17 additions & 0 deletions async/tests/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use async_posthog::Event;
use std::collections::HashMap;

#[tokio::test]
async fn get_client() {
let client = async_posthog::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).await.unwrap();
}
13 changes: 13 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
8 changes: 8 additions & 0 deletions core/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("serialization: {source}")]
Serialization {
#[from]
source: serde_json::Error,
},
}
69 changes: 69 additions & 0 deletions core/src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use chrono::NaiveDateTime;
use serde::Serialize;
use std::collections::HashMap;

use crate::error::Error;

// 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,
event: String,
properties: Properties,
timestamp: Option<NaiveDateTime>,
}

impl InnerEvent {
pub 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<NaiveDateTime>,
}

#[derive(Serialize, Debug, PartialEq, Eq)]
pub struct Properties {
distinct_id: String,
props: HashMap<String, serde_json::Value>,
}

impl Properties {
fn new<S: Into<String>>(distinct_id: S) -> Self {
Self {
distinct_id: distinct_id.into(),
props: Default::default(),
}
}
}

impl Event {
pub fn new<S: Into<String>>(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<K: Into<String>, 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(())
}
}
2 changes: 2 additions & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod error;
pub mod event;
Loading