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    #[deprecated(
211        note = "This field is deprecated and will be removed in a future release. It is no longer consumed and can be removed safely."
212    )]
213    pub fn config_compression_mode(
214        mut self,
215        config_compression_mode: Option<ConfigCompressionMode>,
216    ) -> Self {
217        self.inner.config_compression_mode = config_compression_mode;
218        self
219    }
220
221    #[must_use]
222    pub fn output_log_level(mut self, output_log_level: Option<u32>) -> Self {
223        if let Some(level) = output_log_level {
224            self.inner.output_log_level = Some(LogLevel::from(level));
225        }
226        self
227    }
228
229    #[must_use]
230    pub fn output_logger_provider(
231        mut self,
232        output_logger_provider: Option<Arc<dyn OutputLogProvider>>,
233    ) -> Self {
234        self.inner.output_logger_provider = output_logger_provider;
235        self
236    }
237
238    #[must_use]
239    pub fn wait_for_country_lookup_init(
240        mut self,
241        wait_for_country_lookup_init: Option<bool>,
242    ) -> Self {
243        self.inner.wait_for_country_lookup_init = wait_for_country_lookup_init;
244        self
245    }
246
247    #[must_use]
248    pub fn wait_for_user_agent_init(mut self, wait_for_user_agent_init: Option<bool>) -> Self {
249        self.inner.wait_for_user_agent_init = wait_for_user_agent_init;
250        self
251    }
252
253    #[must_use]
254    pub fn disable_user_agent_parsing(mut self, disable_user_agent_parsing: Option<bool>) -> Self {
255        self.inner.disable_user_agent_parsing = disable_user_agent_parsing;
256        self
257    }
258
259    #[must_use]
260    pub fn disable_country_lookup(mut self, disable_country_lookup: Option<bool>) -> Self {
261        self.inner.disable_country_lookup = disable_country_lookup;
262        self
263    }
264
265    #[must_use]
266    pub fn service_name(mut self, service_name: Option<String>) -> Self {
267        self.inner.service_name = service_name;
268        self
269    }
270
271    #[must_use]
272    pub fn fallback_to_statsig_api(mut self, fallback_to_statsig_api: Option<bool>) -> Self {
273        self.inner.fallback_to_statsig_api = fallback_to_statsig_api;
274        self
275    }
276
277    #[must_use]
278    pub fn global_custom_fields(
279        mut self,
280        global_custom_fields: Option<HashMap<String, DynamicValue>>,
281    ) -> Self {
282        self.inner.global_custom_fields = global_custom_fields;
283        self
284    }
285
286    pub fn disable_network(mut self, disable_network: Option<bool>) -> Self {
287        self.inner.disable_network = disable_network;
288        self
289    }
290
291    #[must_use]
292    pub fn init_timeout_ms(mut self, init_timeout_ms: Option<u64>) -> Self {
293        self.inner.init_timeout_ms = init_timeout_ms;
294        self
295    }
296
297    #[must_use]
298    pub fn build(self) -> StatsigOptions {
299        self.inner
300    }
301
302    // interface related options
303
304    #[must_use]
305    pub fn observability_client(mut self, client: Option<Weak<dyn ObservabilityClient>>) -> Self {
306        self.inner.observability_client = client;
307        self
308    }
309
310    #[must_use]
311    pub fn data_store(mut self, data_store: Option<Arc<dyn DataStoreTrait>>) -> Self {
312        self.inner.data_store = data_store;
313        self
314    }
315}
316
317impl Serialize for StatsigOptions {
318    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
319    where
320        S: Serializer,
321    {
322        let mut state = serializer.serialize_struct("StatsigOptions", 20)?;
323        serialize_if_not_none!(state, "spec_url", &self.specs_url);
324        serialize_if_not_none!(
325            state,
326            "spec_adapter",
327            &get_display_name(&self.specs_adapter)
328        );
329        serialize_if_not_none!(state, "spec_adapter_configs", &self.spec_adapters_config);
330        serialize_if_not_none!(
331            state,
332            "specs_sync_interval_ms",
333            &self.specs_sync_interval_ms
334        );
335        serialize_if_not_none!(state, "init_timeout_ms", &self.init_timeout_ms);
336
337        serialize_if_not_none!(state, "data_store", &get_if_set(&self.data_store));
338
339        serialize_if_not_none!(state, "log_event_url", &self.log_event_url);
340        serialize_if_not_none!(state, "disable_all_logging", &self.disable_all_logging);
341        serialize_if_not_none!(state, "disable_network", &self.disable_network);
342
343        serialize_if_not_none!(state, "id_lists_url", &self.id_lists_url);
344        serialize_if_not_none!(state, "enable_id_lists", &self.enable_id_lists);
345        serialize_if_not_none!(
346            state,
347            "wait_for_user_agent_init",
348            &self.wait_for_user_agent_init
349        );
350        serialize_if_not_none!(
351            state,
352            "wait_for_country_lookup_init",
353            &self.wait_for_country_lookup_init
354        );
355        serialize_if_not_none!(
356            state,
357            "id_lists_sync_interval",
358            &self.id_lists_sync_interval_ms
359        );
360        serialize_if_not_none!(state, "environment", &self.environment);
361        serialize_if_not_none!(
362            state,
363            "id_list_adapter",
364            &get_display_name(&self.id_lists_adapter)
365        );
366        serialize_if_not_none!(
367            state,
368            "fallback_to_statsig_api",
369            &self.fallback_to_statsig_api
370        );
371        serialize_if_not_none!(
372            state,
373            "override_adapter",
374            &get_if_set(&self.override_adapter)
375        );
376        serialize_if_not_none!(state, "service_name", &get_if_set(&self.service_name));
377        serialize_if_not_none!(state, "global_custom_fields", &self.global_custom_fields);
378
379        state.end()
380    }
381}
382
383fn get_if_set<T>(s: &Option<T>) -> Option<&str> {
384    s.as_ref().map(|_| "set")
385}
386
387fn get_display_name<T: fmt::Debug>(s: &Option<T>) -> Option<String> {
388    s.as_ref().map(|st| format!("{st:?}"))
389}
390
391//-------------------------------Validator---------------------------------
392
393const TAG: &str = "StatsigOptionValidator";
394impl StatsigOptions {
395    pub fn validate_and_fix(self: Arc<Self>) -> Arc<Self> {
396        if std::env::var(TEST_ENV_FLAG).is_ok() {
397            log_d!(
398                TAG,
399                "Skipping StatsigOptions validation in testing environment"
400            );
401            return self;
402        }
403
404        let mut opts_clone: Arc<StatsigOptions> = self.clone();
405        let mut_ref = Arc::make_mut(&mut opts_clone);
406
407        if is_sync_interval_invalid(&self.specs_sync_interval_ms) {
408            log_w!(
409                TAG,
410                "Invalid 'specs_sync_interval_ms', value must be greater than {}, received {:?}",
411                MIN_SYNC_INTERVAL,
412                &self.specs_sync_interval_ms
413            );
414            mut_ref.specs_sync_interval_ms = None;
415        }
416
417        if is_sync_interval_invalid(&self.id_lists_sync_interval_ms) {
418            log_w!(
419                TAG,
420                "Invalid 'id_lists_sync_interval_ms', value must be greater than {}, received {:?}",
421                MIN_SYNC_INTERVAL,
422                &self.id_lists_sync_interval_ms
423            );
424            mut_ref.id_lists_sync_interval_ms = None;
425        }
426
427        if should_fix_null_url(&self.specs_url) {
428            log_d!(TAG, "Setting specs_url to be default url");
429            mut_ref.specs_url = None;
430        }
431
432        if should_fix_null_url(&self.id_lists_url) {
433            log_d!(TAG, "Setting id_lists_url to be default url");
434            mut_ref.id_lists_url = None;
435        }
436
437        if should_fix_null_url(&self.log_event_url) {
438            log_d!(TAG, "Setting log_event_url to be default url");
439            mut_ref.log_event_url = None;
440        }
441
442        opts_clone
443    }
444}
445
446fn is_sync_interval_invalid(interval_ms: &Option<u32>) -> bool {
447    if let Some(interval) = interval_ms {
448        return *interval < MIN_SYNC_INTERVAL;
449    }
450    false
451}
452
453fn should_fix_null_url(maybe_url: &Option<String>) -> bool {
454    if let Some(url) = maybe_url {
455        return url.is_empty() || url.eq_ignore_ascii_case("null");
456    }
457
458    false
459}