1use std::collections::HashMap;
2use std::fmt::Debug;
3use std::sync::{Arc, RwLock};
4use std::time::Duration;
5
6use tokio::time::sleep;
7use tracing::error;
8
9use crate::cache::{FeatureCache, InMemoryCache};
10use crate::dto::GrowthBookResponse;
11use crate::env::Environment;
12use crate::error::GrowthbookError;
13use crate::gateway::GrowthbookGateway;
14use crate::growthbook::GrowthBook;
15use crate::model_public::{ExperimentResult, FeatureResult, GrowthBookAttribute};
16
17pub type OnFeatureUsageCallback = Arc<dyn Fn(String, FeatureResult) + Send + Sync>;
18pub type OnExperimentViewedCallback = Arc<dyn Fn(ExperimentResult) + Send + Sync>;
19pub type OnRefreshCallback = Arc<dyn Fn() + Send + Sync>; #[derive(Clone)]
22pub struct GrowthBookClient {
23 pub gb: Arc<RwLock<GrowthBook>>,
24 pub cache: Option<Arc<dyn FeatureCache>>,
25 gateway: Option<Arc<GrowthbookGateway>>,
26 auto_refresh: bool,
27 refresh_interval: Duration,
28 pub on_feature_usage: Option<OnFeatureUsageCallback>,
29 pub on_experiment_viewed: Option<OnExperimentViewedCallback>,
30 pub on_refresh: Vec<OnRefreshCallback>,
31 pub decryption_key: Option<String>,
32}
33
34impl Debug for GrowthBookClient {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("GrowthBookClient")
37 .field("gb", &self.gb)
38 .field("auto_refresh", &self.auto_refresh)
39 .field("refresh_interval", &self.refresh_interval)
40 .field("on_feature_usage", &self.on_feature_usage.is_some())
41 .field("on_experiment_viewed", &self.on_experiment_viewed.is_some())
42 .field("on_refresh", &self.on_refresh.len())
43 .field("decryption_key", &self.decryption_key.is_some())
44 .finish()
45 }
46}
47
48pub struct GrowthBookClientBuilder {
49 api_url: Option<String>,
50 client_key: Option<String>,
51 cache: Option<Arc<dyn FeatureCache>>,
52 ttl: Option<Duration>,
53 auto_refresh: bool,
54 refresh_interval: Option<Duration>,
55 attributes: Option<HashMap<String, GrowthBookAttribute>>,
56 on_feature_usage: Option<OnFeatureUsageCallback>,
57 on_experiment_viewed: Option<OnExperimentViewedCallback>,
58 on_refresh: Vec<OnRefreshCallback>,
59 features: Option<HashMap<String, crate::dto::GrowthBookFeature>>,
60 decryption_key: Option<String>,
61}
62
63impl GrowthBookClientBuilder {
64 pub fn new() -> Self {
65 Self {
66 api_url: None,
67 client_key: None,
68 cache: None,
69 ttl: None,
70 auto_refresh: false,
71 refresh_interval: None,
72 attributes: None,
73 on_feature_usage: None,
74 on_experiment_viewed: None,
75 on_refresh: Vec::new(),
76 features: None,
77 decryption_key: None,
78 }
79 }
80
81 pub fn api_url(mut self, api_url: String) -> Self {
82 self.api_url = Some(api_url);
83 self
84 }
85
86 pub fn client_key(mut self, client_key: String) -> Self {
87 self.client_key = Some(client_key);
88 self
89 }
90
91 pub fn cache(mut self, cache: Arc<dyn FeatureCache>) -> Self {
92 self.cache = Some(cache);
93 self
94 }
95
96 pub fn ttl(mut self, ttl: Duration) -> Self {
97 self.ttl = Some(ttl);
98 self
99 }
100
101 pub fn auto_refresh(mut self, auto_refresh: bool) -> Self {
102 self.auto_refresh = auto_refresh;
103 self
104 }
105
106 pub fn refresh_interval(mut self, interval: Duration) -> Self {
107 self.refresh_interval = Some(interval);
108 self
109 }
110
111 pub fn attributes(mut self, attributes: HashMap<String, GrowthBookAttribute>) -> Self {
112 self.attributes = Some(attributes);
113 self
114 }
115
116 pub fn on_feature_usage(mut self, callback: Box<dyn Fn(String, FeatureResult) + Send + Sync>) -> Self {
117 self.on_feature_usage = Some(Arc::from(callback));
118 self
119 }
120
121 pub fn on_experiment_viewed(mut self, callback: Box<dyn Fn(ExperimentResult) + Send + Sync>) -> Self {
122 self.on_experiment_viewed = Some(Arc::from(callback));
123 self
124 }
125
126 pub fn add_on_refresh(mut self, callback: Box<dyn Fn() + Send + Sync>) -> Self {
127 self.on_refresh.push(Arc::from(callback));
128 self
129 }
130
131 pub fn features(mut self, features: HashMap<String, crate::dto::GrowthBookFeature>) -> Self {
132 self.features = Some(features);
133 self
134 }
135
136 pub fn features_json(mut self, features_json: serde_json::Value) -> Result<Self, serde_json::Error> {
137 let features: HashMap<String, crate::dto::GrowthBookFeature> = serde_json::from_value(features_json)?;
138 self.features = Some(features);
139 Ok(self)
140 }
141
142 pub fn decryption_key(mut self, decryption_key: String) -> Self {
143 self.decryption_key = Some(decryption_key);
144 self
145 }
146
147 pub async fn build(self) -> Result<GrowthBookClient, GrowthbookError> {
148 let api_url = self.api_url.ok_or(GrowthbookError::new(crate::error::GrowthbookErrorCode::ConfigError, "API URL is required"))?;
149 let client_key = self.client_key.ok_or(GrowthbookError::new(crate::error::GrowthbookErrorCode::ConfigError, "Client Key is required"))?;
150
151 let refresh_interval = self.refresh_interval.unwrap_or_else(|| {
152 let seconds = Environment::u64_or_default("GB_UPDATE_INTERVAL", 60);
153 Duration::from_secs(seconds)
154 });
155
156 let gateway = GrowthbookGateway::new(&api_url, &client_key, Duration::from_secs(10))?;
157 let gateway_arc = Arc::new(gateway);
158
159 let cache = self.cache.unwrap_or_else(|| {
160 let ttl = self.ttl.unwrap_or(Duration::from_secs(60));
161 Arc::new(InMemoryCache::new(ttl))
162 });
163
164 let client = GrowthBookClient {
165 gb: Arc::new(RwLock::new(GrowthBook {
166 forced_variations: None,
167 features: self.features.unwrap_or_default(),
168 attributes: self.attributes,
169 })),
170 cache: Some(cache),
171 gateway: Some(gateway_arc),
172 auto_refresh: self.auto_refresh,
173 refresh_interval,
174 on_feature_usage: self.on_feature_usage,
175 on_experiment_viewed: self.on_experiment_viewed,
176 on_refresh: self.on_refresh,
177 decryption_key: self.decryption_key,
178 };
179
180 client.refresh().await;
182
183 if client.auto_refresh {
184 client.start_auto_refresh();
185 }
186
187 Ok(client)
188 }
189}
190
191impl GrowthBookClient {
192 pub async fn refresh(&self) {
193 if let Some(gateway) = &self.gateway {
194 let cache_key = "features";
195
196 if let Some(cache) = &self.cache {
198 if let Some(response) = cache.get(cache_key).await {
199 self.update_gb(response);
200 return;
201 }
202 }
203
204 match gateway.get_features(None).await {
206 Ok(response) => {
207 if let Some(cache) = &self.cache {
209 cache.set(cache_key, response.clone()).await;
210 }
211 self.update_gb(response);
212 },
213 Err(e) => {
214 error!("[growthbook-sdk] Failed to fetch features: {:?}", e);
215 }
216 }
217 }
218 }
219
220 fn update_gb(&self, response: GrowthBookResponse) {
221 let mut features = response.features;
222
223 if let Some(encrypted_features) = response.encrypted_features {
224 if let Some(key) = &self.decryption_key {
225 match decrypt_features(&encrypted_features, key) {
226 Ok(decrypted) => {
227 if let Ok(parsed_features) = serde_json::from_str(&decrypted) {
228 features = Some(parsed_features);
229 } else {
230 error!("[growthbook-sdk] Failed to parse decrypted features");
231 }
232 },
233 Err(e) => {
234 error!("[growthbook-sdk] Failed to decrypt features: {:?}", e);
235 }
236 }
237 } else {
238 error!("[growthbook-sdk] Encrypted features received but no decryption key provided");
239 }
240 }
241
242 let mut writable_config = self.gb.write().expect("problem to create mutex for gb data");
243 let attributes = writable_config.attributes.clone();
244 *writable_config = GrowthBook {
245 forced_variations: response.forced_variations,
246 features: features.unwrap_or_default(),
247 attributes,
248 };
249
250 for callback in &self.on_refresh {
251 callback();
252 }
253 }
254
255 pub fn start_auto_refresh(&self) {
256 let client = self.clone();
257 tokio::spawn(async move {
258 loop {
259 sleep(client.refresh_interval).await;
260 client.refresh().await;
261 }
262 });
263 }
264
265 pub async fn new(
268 api_url: &str,
269 sdk_key: &str,
270 update_interval: Option<Duration>,
271 _http_timeout: Option<Duration>,
272 ) -> Result<Self, GrowthbookError> {
273 let mut builder = GrowthBookClientBuilder::new()
274 .api_url(api_url.to_string())
275 .client_key(sdk_key.to_string())
276 .auto_refresh(true)
277 .ttl(Duration::from_secs(0)); if let Some(interval) = update_interval {
281 builder = builder.refresh_interval(interval);
282 }
283
284 builder.build().await
285 }
286
287 fn read_gb(&self) -> GrowthBook {
288 match self.gb.read() {
289 Ok(rw_read_guard) => (*rw_read_guard).clone(),
290 Err(e) => {
291 error!("{}", format!("[growthbook-sdk] problem to reading gb mutex data returning empty {:?}", e));
292 GrowthBook {
293 forced_variations: None,
294 features: HashMap::new(),
295 attributes: None,
296 }
297 },
298 }
299 }
300 fn resolve_feature(
301 &self,
302 feature_name: &str,
303 user_attributes: Option<Vec<GrowthBookAttribute>>,
304 ) -> FeatureResult {
305 let result = self.read_gb().check(feature_name, &user_attributes);
306
307 let invalid_sources = ["unknownFeature", "prerequisite", "cyclicPrerequisite"];
310 if !invalid_sources.contains(&result.source.as_str()) {
311 if let Some(cb) = &self.on_feature_usage {
312 cb(feature_name.to_string(), result.clone());
313 }
314 }
315
316 if let Some(cb) = &self.on_experiment_viewed {
318 if let Some(experiment_result) = &result.experiment_result {
319 if experiment_result.in_experiment {
320 cb(experiment_result.clone());
321 }
322 }
323 }
324
325 result
326 }
327}
328
329pub trait GrowthBookClientTrait: Debug + Send + Sync {
330 fn is_on(
331 &self,
332 feature_name: &str,
333 user_attributes: Option<Vec<GrowthBookAttribute>>,
334 ) -> bool;
335
336 fn is_off(
337 &self,
338 feature_name: &str,
339 user_attributes: Option<Vec<GrowthBookAttribute>>,
340 ) -> bool;
341
342 fn feature_result(
343 &self,
344 feature_name: &str,
345 user_attributes: Option<Vec<GrowthBookAttribute>>,
346 ) -> FeatureResult;
347
348 fn total_features(&self) -> usize;
349}
350
351impl GrowthBookClientTrait for GrowthBookClient {
352 fn is_on(
353 &self,
354 feature_name: &str,
355 user_attributes: Option<Vec<GrowthBookAttribute>>,
356 ) -> bool {
357 self.resolve_feature(feature_name, user_attributes).on
358 }
359
360 fn is_off(
361 &self,
362 feature_name: &str,
363 user_attributes: Option<Vec<GrowthBookAttribute>>,
364 ) -> bool {
365 self.resolve_feature(feature_name, user_attributes).off
366 }
367
368 fn feature_result(
369 &self,
370 feature_name: &str,
371 user_attributes: Option<Vec<GrowthBookAttribute>>,
372 ) -> FeatureResult {
373 self.resolve_feature(feature_name, user_attributes)
374 }
375
376 fn total_features(&self) -> usize {
377 let gb_data = self.read_gb();
378 gb_data.features.len()
379 }
380}
381
382use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
383use base64::{engine::general_purpose, Engine as _};
384
385type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
386
387fn decrypt_features(encrypted_features: &str, key: &str) -> Result<String, Box<dyn std::error::Error>> {
388 let parts: Vec<&str> = encrypted_features.split('.').collect();
389 if parts.len() != 2 {
390 return Err("Invalid encrypted features format".into());
391 }
392
393 let iv = general_purpose::STANDARD.decode(parts[0])?;
394 let mut ciphertext = general_purpose::STANDARD.decode(parts[1])?; let key_bytes = general_purpose::STANDARD.decode(key)?;
396
397 if key_bytes.len() != 16 {
398 return Err("Invalid key length".into());
399 }
400
401 let decryptor = Aes128CbcDec::new_from_slices(&key_bytes, &iv)
402 .map_err(|_| "Invalid key or IV length")?;
403
404 let plaintext_len = decryptor
406 .decrypt_padded_mut::<Pkcs7>(&mut ciphertext)
407 .map_err(|_| "Decryption failed (padding error)")?
408 .len();
409
410 ciphertext.truncate(plaintext_len);
412
413 Ok(String::from_utf8(ciphertext)?)
414}