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