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