Skip to main content

statsig_rust/
statsig_options.rs

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