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