togglr_sdk/
client.rs

1use std::time::Duration;
2
3use reqwest::{Client, ClientBuilder};
4use tracing::debug;
5
6use crate::config::Config;
7use crate::context::RequestContext;
8use crate::errors::{TogglrError, TogglrResult};
9use crate::track_event::TrackEvent;
10use crate::types::{EvaluateResponse, HealthResponse, FeatureHealth, FeatureErrorReport};
11
12use crate::generated::apis::default_api::{
13    get_feature_health, report_feature_error, sdk_v1_features_feature_key_evaluate_post,
14    sdk_v1_health_get, track_feature_event
15};
16use crate::generated::apis::configuration::Configuration;
17use crate::generated::models::{
18    TrackRequest, FeatureErrorReport as ApiFeatureErrorReport
19};
20
21pub struct TogglrClient {
22    config: Config,
23    http_client: Client,
24    configuration: Configuration,
25}
26
27impl TogglrClient {
28    pub async fn new(config: Config) -> TogglrResult<Self> {
29        let http_client = Self::build_http_client(&config).await?;
30        let mut configuration = Configuration::default();
31        configuration.base_path = config.base_url.clone();
32        configuration.client = http_client.clone();
33        configuration.api_key = Some(crate::generated::apis::configuration::ApiKey {
34            prefix: None,
35            key: config.api_key.clone(),
36        });
37
38        Ok(Self {
39            config,
40            http_client,
41            configuration,
42        })
43    }
44
45    async fn build_http_client(config: &Config) -> TogglrResult<Client> {
46        let mut client_builder = ClientBuilder::new()
47            .timeout(config.timeout)
48            .pool_max_idle_per_host(config.max_connections);
49
50        if config.insecure {
51            client_builder = client_builder.danger_accept_invalid_certs(true);
52        }
53
54        Ok(client_builder.build()?)
55    }
56
57
58    pub async fn health_check(&self) -> TogglrResult<HealthResponse> {
59        let response = sdk_v1_health_get(&self.configuration).await
60            .map_err(|e| TogglrError::Unknown(format!("Health check failed: {:?}", e)))?;
61        Ok(HealthResponse {
62            status: match response.status {
63                crate::generated::models::health_response::Status::Ok => "ok".to_string(),
64            },
65            server_time: response.server_time,
66        })
67    }
68
69    pub async fn evaluate_feature(
70        &self,
71        feature_key: &str,
72        context: RequestContext,
73    ) -> TogglrResult<EvaluateResponse> {
74        let response = sdk_v1_features_feature_key_evaluate_post(
75            &self.configuration,
76            feature_key,
77            context.to_map(),
78        ).await
79        .map_err(|e| TogglrError::Unknown(format!("Feature evaluation failed: {:?}", e)))?;
80
81        Ok(EvaluateResponse {
82            feature_key: response.feature_key,
83            enabled: response.enabled,
84            value: response.value,
85        })
86    }
87
88    pub async fn track_event(
89        &self,
90        feature_key: &str,
91        event: TrackEvent,
92    ) -> TogglrResult<()> {
93        let api_request = event.to_api_request();
94        
95        self.track_event_with_retries(feature_key, api_request).await
96    }
97
98    async fn track_event_with_retries(
99        &self,
100        feature_key: &str,
101        request: TrackRequest,
102    ) -> TogglrResult<()> {
103        let mut last_error = None;
104
105        for attempt in 0..=self.config.retries {
106            if attempt > 0 {
107                let delay = self.calculate_backoff_delay(attempt);
108                debug!("Retrying track event after delay: {:?} (attempt {})", delay, attempt);
109                tokio::time::sleep(delay).await;
110            }
111
112            match self.track_event_single(feature_key, &request).await {
113                Ok(()) => return Ok(()),
114                Err(e) => {
115                    last_error = Some(e);
116                    if !self.should_retry(&last_error.as_ref().unwrap()) {
117                        debug!("Not retrying track event due to error type: {:?}", last_error);
118                        break;
119                    }
120                    debug!("Retrying track event due to error: {:?} (attempt {})", last_error, attempt);
121                }
122            }
123        }
124
125        Err(last_error.unwrap())
126    }
127
128    async fn track_event_single(
129        &self,
130        feature_key: &str,
131        request: &TrackRequest,
132    ) -> TogglrResult<()> {
133        track_feature_event(&self.configuration, feature_key, request.clone())
134            .await
135            .map_err(|e| match e {
136                crate::generated::apis::Error::Reqwest(reqwest_err) => {
137                    if reqwest_err.is_timeout() {
138                        TogglrError::Timeout(reqwest_err.to_string())
139                    } else if reqwest_err.is_connect() {
140                        TogglrError::NetworkError(reqwest_err.to_string())
141                    } else {
142                        TogglrError::HttpError(reqwest_err)
143                    }
144                }
145                crate::generated::apis::Error::Serde(serde_err) => {
146                    TogglrError::SerializationError(serde_err)
147                }
148                crate::generated::apis::Error::ResponseError(response) => {
149                    TogglrError::Unknown(format!("Response error: {:?}", response))
150                }
151                _ => TogglrError::Unknown(format!("Unknown error: {:?}", e)),
152            })?;
153
154        Ok(())
155    }
156
157    pub async fn report_feature_error(
158        &self,
159        feature_key: &str,
160        error_report: FeatureErrorReport,
161    ) -> TogglrResult<()> {
162        let api_request = ApiFeatureErrorReport {
163            error_type: error_report.error_type,
164            error_message: error_report.error_message,
165            context: error_report.context,
166        };
167
168        report_feature_error(&self.configuration, feature_key, api_request)
169            .await
170            .map_err(|e| TogglrError::Unknown(format!("Failed to report error: {:?}", e)))?;
171
172        Ok(())
173    }
174
175    pub async fn get_feature_health(&self, feature_key: &str) -> TogglrResult<FeatureHealth> {
176        let response = get_feature_health(&self.configuration, feature_key)
177            .await
178            .map_err(|e| TogglrError::Unknown(format!("Failed to get feature health: {:?}", e)))?;
179
180        Ok(FeatureHealth {
181            feature_key: response.feature_key,
182            environment_key: response.environment_key,
183            enabled: response.enabled,
184            auto_disabled: response.auto_disabled,
185            error_rate: response.error_rate,
186            threshold: response.threshold,
187            last_error_at: response.last_error_at,
188        })
189    }
190
191    fn calculate_backoff_delay(&self, attempt: u32) -> Duration {
192        let delay_ms = (self.config.backoff.base_delay.as_millis() as f64
193            * self.config.backoff.factor.powi(attempt as i32 - 1))
194            .min(self.config.backoff.max_delay.as_millis() as f64) as u64;
195
196        Duration::from_millis(delay_ms)
197    }
198
199    fn should_retry(&self, error: &TogglrError) -> bool {
200        match error {
201            TogglrError::Timeout(_) | TogglrError::NetworkError(_) | TogglrError::HttpError(_) => true,
202            TogglrError::InternalServerError(_) => true,
203            _ => false,
204        }
205    }
206}
207