Skip to main content

posthog_rs/
endpoints.rs

1use std::fmt;
2
3/// US ingestion endpoint
4pub const US_INGESTION_ENDPOINT: &str = "https://us.i.posthog.com";
5
6/// EU ingestion endpoint
7pub const EU_INGESTION_ENDPOINT: &str = "https://eu.i.posthog.com";
8
9/// Default host (US by default)
10pub const DEFAULT_HOST: &str = US_INGESTION_ENDPOINT;
11
12/// API endpoints used by the SDK for different operations.
13#[derive(Debug, Clone)]
14pub enum Endpoint {
15    /// Event capture endpoint
16    Capture,
17    /// Batch event capture endpoint
18    Batch,
19    /// Feature flags endpoint
20    Flags,
21    /// Local evaluation endpoint
22    LocalEvaluation,
23}
24
25impl Endpoint {
26    /// Get the URL path for this endpoint.
27    pub fn path(&self) -> &str {
28        match self {
29            Endpoint::Capture => "/i/v0/e/",
30            Endpoint::Batch => "/batch/",
31            Endpoint::Flags => "/flags/?v=2",
32            Endpoint::LocalEvaluation => "/flags/definitions/?send_cohorts",
33        }
34    }
35}
36
37impl fmt::Display for Endpoint {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(f, "{}", self.path())
40    }
41}
42
43/// Manages PostHog API endpoints and host configuration.
44///
45/// This low-level helper normalizes app hosts such as
46/// `https://us.posthog.com` to ingestion hosts such as
47/// [`US_INGESTION_ENDPOINT`].
48#[derive(Debug, Clone)]
49pub struct EndpointManager {
50    base_host: String,
51    raw_host: String,
52}
53
54impl EndpointManager {
55    /// Create a new endpoint manager for `host`.
56    ///
57    /// `host` may be an app host (for example `https://eu.posthog.com`), an
58    /// ingestion host, or a custom reverse-proxy host.
59    pub fn new(host: String) -> Self {
60        let base_host = Self::determine_server_host(&host);
61
62        Self {
63            base_host,
64            raw_host: host,
65        }
66    }
67
68    /// Determine the ingestion host used for API calls.
69    ///
70    /// Maps PostHog app hosts to their ingestion equivalents and removes a
71    /// trailing slash. Custom hosts are returned unchanged except for the
72    /// trailing slash.
73    pub fn determine_server_host(host: &str) -> String {
74        let trimmed_host = host.trim_end_matches('/');
75
76        match trimmed_host {
77            "https://app.posthog.com" | "https://us.posthog.com" => {
78                US_INGESTION_ENDPOINT.to_string()
79            }
80            "https://eu.posthog.com" => EU_INGESTION_ENDPOINT.to_string(),
81            _ => trimmed_host.to_string(),
82        }
83    }
84
85    /// Get the normalized base host URL used for API calls.
86    pub fn base_host(&self) -> &str {
87        &self.base_host
88    }
89
90    /// Get the raw host as provided by the user.
91    pub fn raw_host(&self) -> &str {
92        &self.raw_host
93    }
94
95    /// Build a full URL for a given SDK endpoint.
96    pub fn build_url(&self, endpoint: Endpoint) -> String {
97        format!(
98            "{}{}",
99            self.base_host.trim_end_matches('/'),
100            endpoint.path()
101        )
102    }
103
104    /// Build a URL with a custom path relative to the base host.
105    ///
106    /// The path may be passed with or without a leading `/`.
107    pub fn build_custom_url(&self, path: &str) -> String {
108        let normalized_path = if path.starts_with('/') {
109            path.to_string()
110        } else {
111            format!("/{path}")
112        };
113        format!(
114            "{}{}",
115            self.base_host.trim_end_matches('/'),
116            normalized_path
117        )
118    }
119
120    /// Build the local evaluation definitions URL with a project token.
121    ///
122    /// The token is included as a query parameter, so avoid logging this URL in
123    /// production diagnostics.
124    pub fn build_local_eval_url(&self, token: &str) -> String {
125        format!(
126            "{}/flags/definitions/?token={}&send_cohorts",
127            self.base_host.trim_end_matches('/'),
128            token
129        )
130    }
131
132    /// Get the base host for API operations without a trailing slash or path.
133    pub fn api_host(&self) -> String {
134        self.base_host.trim_end_matches('/').to_string()
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_determine_server_host() {
144        assert_eq!(
145            EndpointManager::determine_server_host("https://app.posthog.com"),
146            US_INGESTION_ENDPOINT
147        );
148
149        assert_eq!(
150            EndpointManager::determine_server_host("https://us.posthog.com"),
151            US_INGESTION_ENDPOINT
152        );
153
154        assert_eq!(
155            EndpointManager::determine_server_host("https://eu.posthog.com"),
156            EU_INGESTION_ENDPOINT
157        );
158
159        assert_eq!(
160            EndpointManager::determine_server_host("https://custom.domain.com"),
161            "https://custom.domain.com"
162        );
163
164        assert_eq!(
165            EndpointManager::determine_server_host("https://eu.posthog.com/"),
166            EU_INGESTION_ENDPOINT
167        );
168    }
169
170    #[test]
171    fn test_build_url() {
172        let manager = EndpointManager::new(DEFAULT_HOST.to_string());
173
174        assert_eq!(
175            manager.build_url(Endpoint::Capture),
176            format!("{}/i/v0/e/", US_INGESTION_ENDPOINT)
177        );
178
179        assert_eq!(
180            manager.build_url(Endpoint::Flags),
181            format!("{}/flags/?v=2", US_INGESTION_ENDPOINT)
182        );
183    }
184
185    #[test]
186    fn test_build_custom_url() {
187        let manager = EndpointManager::new("https://custom.com/".to_string());
188
189        assert_eq!(
190            manager.build_custom_url("/api/test"),
191            "https://custom.com/api/test"
192        );
193
194        assert_eq!(
195            manager.build_custom_url("api/test"),
196            "https://custom.com/api/test"
197        );
198    }
199}