statsig_rust/
statsig_options.rs

1use serde::ser::SerializeStruct;
2use serde::{Serialize, Serializer};
3
4use crate::data_store_interface::DataStoreTrait;
5use crate::evaluation::dynamic_value::DynamicValue;
6use crate::event_logging_adapter::EventLoggingAdapter;
7use crate::id_lists_adapter::IdListsAdapter;
8use crate::networking::proxy_config::ProxyConfig;
9use crate::output_logger::{LogLevel, OutputLogProvider};
10use crate::persistent_storage::persistent_storage_trait::PersistentStorage;
11use crate::{
12    log_d, log_w, serialize_if_not_none, ConfigCompressionMode, ObservabilityClient,
13    OverrideAdapter, SpecAdapterConfig, SpecsAdapter,
14};
15use std::collections::HashMap;
16use std::fmt;
17use std::sync::{Arc, Weak};
18
19pub const DEFAULT_INIT_TIMEOUT_MS: u64 = 3000;
20const MIN_SYNC_INTERVAL: u32 = 1000;
21const TEST_ENV_FLAG: &str = "STATSIG_RUNNING_TESTS";
22
23#[derive(Clone, Default)]
24pub struct StatsigOptions {
25    pub data_store: Option<Arc<dyn DataStoreTrait>>, // External DataStore
26
27    pub disable_all_logging: Option<bool>,
28    pub disable_country_lookup: Option<bool>,
29    pub disable_network: Option<bool>, // Disable all out-going network including get configs, log_events...
30    pub disable_user_agent_parsing: Option<bool>,
31
32    pub enable_id_lists: Option<bool>,
33    pub environment: Option<String>,
34    pub config_compression_mode: Option<ConfigCompressionMode>,
35
36    pub event_logging_adapter: Option<Arc<dyn EventLoggingAdapter>>,
37
38    #[deprecated]
39    pub event_logging_flush_interval_ms: Option<u32>,
40    pub event_logging_max_pending_batch_queue_size: Option<u32>,
41    pub event_logging_max_queue_size: Option<u32>,
42
43    pub fallback_to_statsig_api: Option<bool>,
44    pub global_custom_fields: Option<HashMap<String, DynamicValue>>,
45
46    pub id_lists_adapter: Option<Arc<dyn IdListsAdapter>>,
47    pub id_lists_sync_interval_ms: Option<u32>,
48    pub id_lists_url: Option<String>,
49
50    pub init_timeout_ms: Option<u64>,
51    pub log_event_url: Option<String>,
52    pub observability_client: Option<Weak<dyn ObservabilityClient>>,
53    pub output_log_level: Option<LogLevel>,
54    pub output_logger_provider: Option<Arc<dyn OutputLogProvider>>,
55    pub override_adapter: Option<Arc<dyn OverrideAdapter>>,
56    pub persistent_storage: Option<Arc<dyn PersistentStorage>>,
57    pub service_name: Option<String>,
58
59    pub spec_adapters_config: Option<Vec<SpecAdapterConfig>>, // Specs to customized spec adapter, order matters, reflecting priority of trying
60    pub specs_adapter: Option<Arc<dyn SpecsAdapter>>,
61    pub specs_sync_interval_ms: Option<u32>,
62    pub specs_url: Option<String>,
63
64    pub wait_for_country_lookup_init: Option<bool>,
65    pub wait_for_user_agent_init: Option<bool>,
66
67    pub proxy_config: Option<ProxyConfig>,
68
69    pub __experimental_ua_parsing_enabled: Option<bool>,
70}
71
72impl StatsigOptions {
73    #[must_use]
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    // The builder method for more complex initialization
79    #[must_use]
80    pub fn builder() -> StatsigOptionsBuilder {
81        StatsigOptionsBuilder::default()
82    }
83}
84
85#[derive(Default)]
86pub struct StatsigOptionsBuilder {
87    inner: StatsigOptions,
88}
89
90impl StatsigOptionsBuilder {
91    #[must_use]
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    // Specs
97
98    #[must_use]
99    pub fn specs_url(mut self, specs_url: Option<String>) -> Self {
100        self.inner.specs_url = specs_url;
101        self
102    }
103
104    #[must_use]
105    pub fn specs_adapter(mut self, specs_adapter: Option<Arc<dyn SpecsAdapter>>) -> Self {
106        self.inner.specs_adapter = specs_adapter;
107        self
108    }
109
110    #[must_use]
111    pub fn specs_sync_interval_ms(mut self, specs_sync_interval_ms: Option<u32>) -> Self {
112        self.inner.specs_sync_interval_ms = specs_sync_interval_ms;
113        self
114    }
115
116    // Event Logging
117
118    #[must_use]
119    pub fn log_event_url(mut self, log_event_url: Option<String>) -> Self {
120        self.inner.log_event_url = log_event_url;
121        self
122    }
123
124    #[must_use]
125    pub fn disable_all_logging(mut self, disable_all_logging: Option<bool>) -> Self {
126        self.inner.disable_all_logging = disable_all_logging;
127        self
128    }
129
130    #[must_use]
131    pub fn event_logging_adapter(
132        mut self,
133        event_logging_adapter: Option<Arc<dyn EventLoggingAdapter>>,
134    ) -> Self {
135        self.inner.event_logging_adapter = event_logging_adapter;
136        self
137    }
138
139    #[must_use]
140    #[deprecated(
141        note = "This field is deprecated in favor of smart log event. It is no longer consumed and can be removed safely."
142    )]
143    #[allow(deprecated)]
144    pub fn event_logging_flush_interval_ms(
145        mut self,
146        event_logging_flush_interval_ms: Option<u32>,
147    ) -> Self {
148        self.inner.event_logging_flush_interval_ms = event_logging_flush_interval_ms;
149        self
150    }
151
152    #[must_use]
153    pub fn event_logging_max_queue_size(
154        mut self,
155        event_logging_max_queue_size: Option<u32>,
156    ) -> Self {
157        self.inner.event_logging_max_queue_size = event_logging_max_queue_size;
158        self
159    }
160
161    #[must_use]
162    pub fn event_logging_max_pending_batch_queue_size(
163        mut self,
164        event_logging_max_pending_batch_queue_size: Option<u32>,
165    ) -> Self {
166        self.inner.event_logging_max_pending_batch_queue_size =
167            event_logging_max_pending_batch_queue_size;
168        self
169    }
170
171    // ID Lists
172
173    #[must_use]
174    pub fn enable_id_lists(mut self, enable_id_lists: Option<bool>) -> Self {
175        self.inner.enable_id_lists = enable_id_lists;
176        self
177    }
178
179    #[must_use]
180    pub fn id_lists_url(mut self, id_lists_url: Option<String>) -> Self {
181        self.inner.id_lists_url = id_lists_url;
182        self
183    }
184
185    #[must_use]
186    pub fn id_lists_adapter(mut self, id_lists_adapter: Option<Arc<dyn IdListsAdapter>>) -> Self {
187        self.inner.id_lists_adapter = id_lists_adapter;
188        self
189    }
190
191    #[must_use]
192    pub fn id_lists_sync_interval_ms(mut self, id_lists_sync_interval_ms: Option<u32>) -> Self {
193        self.inner.id_lists_sync_interval_ms = id_lists_sync_interval_ms;
194        self
195    }
196
197    // Other
198
199    #[must_use]
200    pub fn proxy_config(mut self, proxy_config: Option<ProxyConfig>) -> Self {
201        self.inner.proxy_config = proxy_config;
202        self
203    }
204
205    #[must_use]
206    pub fn environment(mut self, environment: Option<String>) -> Self {
207        self.inner.environment = environment;
208        self
209    }
210
211    #[must_use]
212    #[deprecated(
213        note = "This field is deprecated and will be removed in a future release. It is no longer consumed and can be removed safely."
214    )]
215    pub fn config_compression_mode(
216        mut self,
217        config_compression_mode: Option<ConfigCompressionMode>,
218    ) -> Self {
219        self.inner.config_compression_mode = config_compression_mode;
220        self
221    }
222
223    #[must_use]
224    pub fn output_log_level(mut self, output_log_level: Option<u32>) -> Self {
225        if let Some(level) = output_log_level {
226            self.inner.output_log_level = Some(LogLevel::from(level));
227        }
228        self
229    }
230
231    #[must_use]
232    pub fn output_logger_provider(
233        mut self,
234        output_logger_provider: Option<Arc<dyn OutputLogProvider>>,
235    ) -> Self {
236        self.inner.output_logger_provider = output_logger_provider;
237        self
238    }
239
240    #[must_use]
241    pub fn wait_for_country_lookup_init(
242        mut self,
243        wait_for_country_lookup_init: Option<bool>,
244    ) -> Self {
245        self.inner.wait_for_country_lookup_init = wait_for_country_lookup_init;
246        self
247    }
248
249    #[must_use]
250    pub fn wait_for_user_agent_init(mut self, wait_for_user_agent_init: Option<bool>) -> Self {
251        self.inner.wait_for_user_agent_init = wait_for_user_agent_init;
252        self
253    }
254
255    #[must_use]
256    pub fn disable_user_agent_parsing(mut self, disable_user_agent_parsing: Option<bool>) -> Self {
257        self.inner.disable_user_agent_parsing = disable_user_agent_parsing;
258        self
259    }
260
261    #[must_use]
262    pub fn disable_country_lookup(mut self, disable_country_lookup: Option<bool>) -> Self {
263        self.inner.disable_country_lookup = disable_country_lookup;
264        self
265    }
266
267    #[must_use]
268    pub fn service_name(mut self, service_name: Option<String>) -> Self {
269        self.inner.service_name = service_name;
270        self
271    }
272
273    #[must_use]
274    pub fn fallback_to_statsig_api(mut self, fallback_to_statsig_api: Option<bool>) -> Self {
275        self.inner.fallback_to_statsig_api = fallback_to_statsig_api;
276        self
277    }
278
279    #[must_use]
280    pub fn global_custom_fields(
281        mut self,
282        global_custom_fields: Option<HashMap<String, DynamicValue>>,
283    ) -> Self {
284        self.inner.global_custom_fields = global_custom_fields;
285        self
286    }
287
288    pub fn disable_network(mut self, disable_network: Option<bool>) -> Self {
289        self.inner.disable_network = disable_network;
290        self
291    }
292
293    #[must_use]
294    pub fn init_timeout_ms(mut self, init_timeout_ms: Option<u64>) -> Self {
295        self.inner.init_timeout_ms = init_timeout_ms;
296        self
297    }
298
299    #[must_use]
300    pub fn build(self) -> StatsigOptions {
301        self.inner
302    }
303
304    // interface related options
305
306    #[must_use]
307    pub fn observability_client(mut self, client: Option<Weak<dyn ObservabilityClient>>) -> Self {
308        self.inner.observability_client = client;
309        self
310    }
311
312    #[must_use]
313    pub fn data_store(mut self, data_store: Option<Arc<dyn DataStoreTrait>>) -> Self {
314        self.inner.data_store = data_store;
315        self
316    }
317}
318
319impl Serialize for StatsigOptions {
320    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
321    where
322        S: Serializer,
323    {
324        let mut state = serializer.serialize_struct("StatsigOptions", 20)?;
325        serialize_if_not_none!(state, "spec_url", &self.specs_url);
326        serialize_if_not_none!(
327            state,
328            "spec_adapter",
329            &get_display_name(&self.specs_adapter)
330        );
331        serialize_if_not_none!(state, "spec_adapter_configs", &self.spec_adapters_config);
332        serialize_if_not_none!(
333            state,
334            "specs_sync_interval_ms",
335            &self.specs_sync_interval_ms
336        );
337        serialize_if_not_none!(state, "init_timeout_ms", &self.init_timeout_ms);
338
339        serialize_if_not_none!(state, "data_store", &get_if_set(&self.data_store));
340
341        serialize_if_not_none!(state, "log_event_url", &self.log_event_url);
342        serialize_if_not_none!(state, "disable_all_logging", &self.disable_all_logging);
343        serialize_if_not_none!(state, "disable_network", &self.disable_network);
344
345        serialize_if_not_none!(state, "id_lists_url", &self.id_lists_url);
346        serialize_if_not_none!(state, "enable_id_lists", &self.enable_id_lists);
347        serialize_if_not_none!(
348            state,
349            "wait_for_user_agent_init",
350            &self.wait_for_user_agent_init
351        );
352        serialize_if_not_none!(
353            state,
354            "wait_for_country_lookup_init",
355            &self.wait_for_country_lookup_init
356        );
357        serialize_if_not_none!(
358            state,
359            "id_lists_sync_interval",
360            &self.id_lists_sync_interval_ms
361        );
362        serialize_if_not_none!(state, "environment", &self.environment);
363        serialize_if_not_none!(
364            state,
365            "id_list_adapter",
366            &get_display_name(&self.id_lists_adapter)
367        );
368        serialize_if_not_none!(
369            state,
370            "fallback_to_statsig_api",
371            &self.fallback_to_statsig_api
372        );
373        serialize_if_not_none!(
374            state,
375            "override_adapter",
376            &get_if_set(&self.override_adapter)
377        );
378        serialize_if_not_none!(state, "service_name", &get_if_set(&self.service_name));
379        serialize_if_not_none!(state, "global_custom_fields", &self.global_custom_fields);
380
381        state.end()
382    }
383}
384
385fn get_if_set<T>(s: &Option<T>) -> Option<&str> {
386    s.as_ref().map(|_| "set")
387}
388
389fn get_display_name<T: fmt::Debug>(s: &Option<T>) -> Option<String> {
390    s.as_ref().map(|st| format!("{st:?}"))
391}
392
393//-------------------------------Validator---------------------------------
394
395const TAG: &str = "StatsigOptionValidator";
396impl StatsigOptions {
397    pub fn validate_and_fix(self: Arc<Self>) -> Arc<Self> {
398        if std::env::var(TEST_ENV_FLAG).is_ok() {
399            log_d!(
400                TAG,
401                "Skipping StatsigOptions validation in testing environment"
402            );
403            return self;
404        }
405
406        let mut opts_clone: Arc<StatsigOptions> = self.clone();
407        let mut_ref = Arc::make_mut(&mut opts_clone);
408
409        if is_sync_interval_invalid(&self.specs_sync_interval_ms) {
410            log_w!(
411                TAG,
412                "Invalid 'specs_sync_interval_ms', value must be greater than {}, received {:?}",
413                MIN_SYNC_INTERVAL,
414                &self.specs_sync_interval_ms
415            );
416            mut_ref.specs_sync_interval_ms = None;
417        }
418
419        if is_sync_interval_invalid(&self.id_lists_sync_interval_ms) {
420            log_w!(
421                TAG,
422                "Invalid 'id_lists_sync_interval_ms', value must be greater than {}, received {:?}",
423                MIN_SYNC_INTERVAL,
424                &self.id_lists_sync_interval_ms
425            );
426            mut_ref.id_lists_sync_interval_ms = None;
427        }
428
429        if should_fix_null_url(&self.specs_url) {
430            log_d!(TAG, "Setting specs_url to be default url");
431            mut_ref.specs_url = None;
432        }
433
434        if should_fix_null_url(&self.id_lists_url) {
435            log_d!(TAG, "Setting id_lists_url to be default url");
436            mut_ref.id_lists_url = None;
437        }
438
439        if should_fix_null_url(&self.log_event_url) {
440            log_d!(TAG, "Setting log_event_url to be default url");
441            mut_ref.log_event_url = None;
442        }
443
444        opts_clone
445    }
446}
447
448fn is_sync_interval_invalid(interval_ms: &Option<u32>) -> bool {
449    if let Some(interval) = interval_ms {
450        return *interval < MIN_SYNC_INTERVAL;
451    }
452    false
453}
454
455fn should_fix_null_url(maybe_url: &Option<String>) -> bool {
456    if let Some(url) = maybe_url {
457        return url.is_empty() || url.eq_ignore_ascii_case("null");
458    }
459
460    false
461}