use std::sync::OnceLock;
use std::time::Duration;
use serde_json::{json, Value};
use super::client::QueuedEvent;
const POST_TIMEOUT: Duration = Duration::from_secs(5);
pub fn posthog_key() -> &'static str {
static KEY: OnceLock<String> = OnceLock::new();
KEY.get_or_init(|| {
std::env::var("OPENLATCH_POSTHOG_KEY")
.unwrap_or_else(|_| env!("OPENLATCH_POSTHOG_KEY").to_string())
})
}
pub fn key_is_present() -> bool {
!posthog_key().is_empty()
}
pub fn posthog_host() -> String {
std::env::var("OPENLATCH_POSTHOG_HOST")
.unwrap_or_else(|_| env!("OPENLATCH_POSTHOG_HOST").to_string())
}
pub fn build_client() -> Option<reqwest::Client> {
reqwest::Client::builder()
.timeout(POST_TIMEOUT)
.use_rustls_tls()
.build()
.ok()
}
pub async fn post_batch(client: &reqwest::Client, batch: &[QueuedEvent]) -> bool {
if batch.is_empty() {
return true;
}
let url = format!("{}/batch/", posthog_host().trim_end_matches('/'));
let body = build_body(batch);
match client.post(&url).json(&body).send().await {
Ok(resp) => resp.status().is_success(),
Err(_) => false,
}
}
fn build_body(batch: &[QueuedEvent]) -> Value {
let events: Vec<Value> = batch.iter().map(event_to_payload).collect();
json!({
"api_key": posthog_key(),
"batch": events,
})
}
fn event_to_payload(event: &QueuedEvent) -> Value {
let distinct_id = event
.properties
.get("agent_id")
.and_then(|v| v.as_str())
.unwrap_or("agt_unknown")
.to_string();
let timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
json!({
"event": event.name,
"distinct_id": distinct_id,
"properties": event.properties,
"timestamp": timestamp,
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Map;
fn make_event(name: &str, agent: &str) -> QueuedEvent {
let mut props = Map::new();
props.insert("agent_id".into(), json!(agent));
props.insert("os".into(), json!("linux-x64"));
QueuedEvent {
name: name.into(),
properties: props,
}
}
#[test]
fn test_build_body_wraps_with_api_key_and_batch() {
let events = vec![make_event("cli_initialized", "agt_a")];
let body = build_body(&events);
assert!(body["api_key"].is_string());
let batch = body["batch"].as_array().unwrap();
assert_eq!(batch.len(), 1);
assert_eq!(batch[0]["event"], "cli_initialized");
assert_eq!(batch[0]["distinct_id"], "agt_a");
assert!(batch[0]["timestamp"].is_string());
}
#[test]
fn test_event_to_payload_falls_back_when_agent_id_missing() {
let mut props = Map::new();
props.insert("os".into(), json!("linux-x64"));
let ev = QueuedEvent {
name: "test".into(),
properties: props,
};
let p = event_to_payload(&ev);
assert_eq!(p["distinct_id"], "agt_unknown");
}
#[test]
fn test_post_batch_empty_short_circuits() {
let rt = tokio::runtime::Runtime::new().unwrap();
let client = build_client().unwrap();
let ok = rt.block_on(post_batch(&client, &[]));
assert!(ok);
}
}