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