better_posthog/client.rs
1use std::fmt;
2use std::sync::OnceLock;
3use std::time::Duration;
4
5use crate::Event;
6use crate::worker::Worker;
7
8/// Hook that can modify or discard events before sending.
9///
10/// Each hook receives an owned [`Event`] and can:
11/// - Return `Some(event)` to pass the (possibly modified) event to the next hook
12/// - Return `None` to discard the event and stop further processing
13///
14/// Hooks are executed in order after the event is enriched with library/OS context.
15/// If any hook panics, the event is discarded and an error is logged.
16///
17/// # Thread Safety
18///
19/// Hooks run in a background worker thread, so they must be `Send + 'static`.
20/// If you need randomness (e.g., for sampling), use a `Send`-compatible RNG like [`fastrand::Rng`](https://docs.rs/fastrand) initialized before the closure.
21///
22/// # Example
23///
24/// ```
25/// let options = better_posthog::ClientOptions {
26/// api_key: Some("phc_your_api_key".into()),
27/// before_send: vec![{
28/// // Initialize a scoped `Send`-compatible RNG.
29/// let mut rng = fastrand::Rng::new();
30///
31/// // Return a `before_send` hook.
32/// Box::new(move |event| {
33/// let sample_rate = match event.event.as_str() {
34/// "button_click" => 0.5, // Process only a half of `button_click` events.
35/// _ => 1.0, // Process all other events.
36/// };
37/// if rng.f64() < sample_rate { Some(event) } else { None }
38/// })
39/// }],
40/// ..Default::default()
41/// };
42/// ```
43pub type BeforeSendFn = Box<dyn FnMut(Event) -> Option<Event> + Send + 'static>;
44
45/// Global client instance.
46pub static CLIENT: OnceLock<Client> = OnceLock::new();
47
48/// Internal client state holding the worker.
49pub struct Client {
50 pub worker: Worker,
51}
52
53impl Client {
54 /// Creates a new client from the given configuration.
55 pub fn new(options: ClientOptions) -> Self {
56 let worker = Worker::new(options);
57 Self { worker }
58 }
59}
60
61/// Configuration for the PostHog client.
62pub struct ClientOptions {
63 /// The PostHog API key. If `None`, the client will not be initialized.
64 pub api_key: Option<ApiKey>,
65 /// The target PostHog host.
66 pub host: Host,
67 /// Timeout for graceful shutdown (default: 2 seconds).
68 pub shutdown_timeout: Duration,
69 /// Hooks to modify or filter events before sending.
70 pub before_send: Vec<BeforeSendFn>,
71}
72
73impl fmt::Debug for ClientOptions {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 f.debug_struct("ClientOptions")
76 .field("api_key", &self.api_key)
77 .field("host", &self.host)
78 .field("shutdown_timeout", &self.shutdown_timeout)
79 .field("before_send", &format!("[{} hooks]", self.before_send.len()))
80 .finish()
81 }
82}
83
84impl Default for ClientOptions {
85 fn default() -> Self {
86 Self {
87 api_key: None,
88 host: Host::default(),
89 shutdown_timeout: Duration::from_secs(2),
90 before_send: Vec::new(),
91 }
92 }
93}
94
95impl ClientOptions {
96 /// Creates a new `ClientOptions` with the given API key and default settings.
97 pub fn new<T: Into<ApiKey>>(api_key: T) -> Self {
98 Self {
99 api_key: Some(api_key.into()),
100 ..Default::default()
101 }
102 }
103}
104
105impl<T: Into<ApiKey>> From<T> for ClientOptions {
106 fn from(api_key: T) -> Self {
107 Self::new(api_key)
108 }
109}
110
111impl<T: Into<ApiKey>> From<(T, Self)> for ClientOptions {
112 fn from((api_key, mut options): (T, Self)) -> Self {
113 options.api_key = Some(api_key.into());
114 options
115 }
116}
117
118/// PostHog API key newtype.
119#[derive(Debug, Clone)]
120pub struct ApiKey(String);
121
122impl ApiKey {
123 /// Returns the API key as a string slice.
124 #[must_use]
125 pub fn as_str(&self) -> &str {
126 &self.0
127 }
128}
129
130impl From<&str> for ApiKey {
131 fn from(key: &str) -> Self {
132 Self(key.to_owned())
133 }
134}
135
136impl From<String> for ApiKey {
137 fn from(key: String) -> Self {
138 Self(key)
139 }
140}
141
142/// Target PostHog environment for event submission.
143#[derive(Debug, Clone, Default)]
144pub enum Host {
145 /// US PostHog cloud instance (<https://us.i.posthog.com>).
146 #[default]
147 US,
148 /// EU PostHog cloud instance (<https://eu.i.posthog.com>).
149 EU,
150 /// Custom self-hosted PostHog instance.
151 Custom(String),
152}
153
154impl Host {
155 /// Returns the base URL for this host.
156 #[must_use]
157 pub const fn base_url(&self) -> &str {
158 match self {
159 Self::US => "https://us.i.posthog.com",
160 Self::EU => "https://eu.i.posthog.com",
161 Self::Custom(url) => url.as_str(),
162 }
163 }
164
165 /// Returns the single event capture endpoint URL.
166 #[must_use]
167 pub fn capture_url(&self) -> String {
168 format!("{}/i/v0/e/", self.base_url())
169 }
170
171 /// Returns the batch event endpoint URL.
172 #[must_use]
173 pub fn batch_url(&self) -> String {
174 format!("{}/batch/", self.base_url())
175 }
176}