Skip to main content

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}