Skip to main content

statsig_rust/
statsig.rs

1use crate::console_capture::console_capture_handler::ConsoleCaptureHandler;
2use crate::console_capture::console_capture_instances::{
3    ConsoleCaptureInstance, CONSOLE_CAPTURE_REGISTRY,
4};
5use crate::console_capture::console_log_line_levels::StatsigLogLineLevel;
6use crate::data_store_interface::{get_data_store_key, RequestPath};
7use crate::evaluation::cmab_evaluator::{get_cmab_ranked_list, CMABRankedGroup};
8use crate::evaluation::country_lookup::CountryLookup;
9use crate::evaluation::dynamic_value::DynamicValue;
10use crate::evaluation::evaluation_data::{RuleRef, SpecView};
11use crate::evaluation::evaluation_details::EvaluationDetails;
12use crate::evaluation::evaluation_types::GateEvaluation;
13use crate::evaluation::evaluator::{Evaluator, Recognition, SpecType};
14use crate::evaluation::evaluator_context::{EvaluatorContext, IdListResolution};
15use crate::evaluation::evaluator_result::{
16    result_to_dynamic_config_eval, result_to_experiment_eval, result_to_gate_eval,
17    result_to_layer_eval, EvaluatorResult,
18};
19use crate::evaluation::user_agent_parsing::{ParsedUserAgentValue, UserAgentParser};
20#[cfg(feature = "ffi-support")]
21use crate::event_logging::delayed_exposure_store::DelayedExposureStore;
22use crate::event_logging::event_logger::{EventLogger, ExposureTrigger};
23use crate::event_logging::event_queue::queued_config_expo::EnqueueConfigExpoOp;
24use crate::event_logging::event_queue::queued_experiment_expo::EnqueueExperimentExpoOp;
25use crate::event_logging::event_queue::queued_expo::EnqueueExposureOp;
26use crate::event_logging::event_queue::queued_gate_expo::EnqueueGateExpoOp;
27use crate::event_logging::event_queue::queued_layer_param_expo::EnqueueLayerParamExpoOp;
28use crate::event_logging::event_queue::queued_passthrough::EnqueuePassthroughOp;
29use crate::event_logging::statsig_event_internal::StatsigEventInternal;
30use crate::event_logging_adapter::EventLoggingAdapter;
31use crate::event_logging_adapter::StatsigHttpEventLoggingAdapter;
32use crate::gcir::gcir_formatter::GCIRFormatter;
33use crate::gcir::target_app_id_utils::select_app_id_for_gcir;
34use crate::hashing::HashUtil;
35use crate::initialize_evaluations_response::InitializeEvaluationsResponse;
36use crate::initialize_response::InitializeResponse;
37use crate::initialize_v2_response::InitializeV2Response;
38use crate::interned_string::InternedString;
39use crate::observability::console_capture_observer::ConsoleCaptureObserver;
40use crate::observability::diagnostics_observer::DiagnosticsObserver;
41use crate::observability::observability_client_adapter::{MetricType, ObservabilityEvent};
42use crate::observability::ops_stats::{OpsStatsForInstance, OPS_STATS};
43use crate::observability::sdk_errors_observer::{ErrorBoundaryEvent, SDKErrorsObserver};
44use crate::output_logger::{initialize_output_logger, shutdown_output_logger};
45use crate::persistent_storage::persistent_values_manager::PersistentValuesManager;
46use crate::sdk_diagnostics::diagnostics::{ContextType, Diagnostics};
47use crate::sdk_diagnostics::marker::{ActionType, KeyType, Marker};
48use crate::sdk_event_emitter::SdkEventEmitter;
49use crate::spec_store::{SpecStore, SpecStoreData};
50use crate::specs_adapter::{StatsigCustomizedSpecsAdapter, StatsigHttpSpecsAdapter};
51use crate::specs_response::param_store_types::Parameter;
52use crate::statsig_err::StatsigErr;
53use crate::statsig_metadata::StatsigMetadata;
54use crate::statsig_options::StatsigOptions;
55use crate::statsig_runtime::StatsigRuntime;
56use crate::statsig_type_factories::{
57    make_dynamic_config, make_experiment, make_feature_gate, make_layer,
58};
59#[cfg(feature = "ffi-support")]
60use crate::statsig_types::{
61    BulkDynamicConfigEvaluation, BulkEvaluationCommonFields, BulkEvaluationOptions,
62    BulkEvaluationResponse, BulkExperimentEvaluation, BulkFeatureGateEvaluation,
63    BulkLayerEvaluation, ResolvedBulkEvaluationOptions,
64};
65use crate::statsig_types::{DynamicConfig, Experiment, FeatureGate, Layer, ParameterStore};
66#[cfg(feature = "ffi-support")]
67use crate::statsig_types_raw::{DynamicConfigRaw, ExperimentRaw, FeatureGateRaw, LayerRaw};
68use crate::user::StatsigUserInternal;
69use crate::utils::get_loggable_sdk_key;
70use crate::{
71    dyn_value, log_d, log_e, log_w, ClientInitResponseOptions, GCIRResponseFormat, IdListsAdapter,
72    InitializeDetails, ObservabilityClient, OpsStatsEventObserver, OverrideAdapter, SpecsAdapter,
73    SpecsInfo, SpecsSource, SpecsUpdateListener, StatsigHttpIdListsAdapter,
74    StatsigLocalOverrideAdapter, StatsigUser,
75};
76use crate::{
77    log_error_to_statsig_and_console,
78    statsig_core_api_options::{
79        DynamicConfigEvaluationOptions, ExperimentEvaluationOptions, FeatureGateEvaluationOptions,
80        LayerEvaluationOptions, ParameterStoreEvaluationOptions,
81    },
82};
83use chrono::Utc;
84use parking_lot::Mutex;
85use serde::de::DeserializeOwned;
86use serde::Serialize;
87use serde_json::json;
88use serde_json::Value;
89use std::borrow::Cow;
90use std::collections::HashMap;
91use std::sync::atomic::{AtomicBool, Ordering};
92use std::sync::{Arc, Weak};
93use std::time::{Duration, Instant};
94use tokio::time::sleep;
95use tokio::try_join;
96
97const TAG: &str = stringify!(Statsig);
98
99#[cfg(feature = "ffi-support")]
100enum RawExposureLogging {
101    Immediate,
102    Delayed,
103}
104
105#[cfg(feature = "ffi-support")]
106struct RawExperimentExposureOptions {
107    include_local_override: bool,
108    exposure_logging: RawExposureLogging,
109    exposure_metadata: Option<HashMap<String, Value>>,
110}
111const ERROR_SDK_KEY: &str = "__STATSIG_ERROR_SDK_KEY__";
112const INIT_IP_TAG: &str = "INIT_COUNTRY_LOOKUP";
113const INIT_UA_TAG: &str = "INIT_UA";
114
115lazy_static::lazy_static! {
116    static ref SHARED_INSTANCE: Mutex<Option<Arc<Statsig>>> = Mutex::new(None);
117}
118
119pub struct Statsig {
120    pub statsig_runtime: Arc<StatsigRuntime>,
121    pub options: Arc<StatsigOptions>,
122    pub event_emitter: Arc<SdkEventEmitter>,
123
124    sdk_key: String,
125    event_logger: Arc<EventLogger>,
126    #[cfg(feature = "ffi-support")]
127    delayed_exposure_store: Arc<DelayedExposureStore>,
128    specs_adapter: SpecsAdapterHousing,
129    event_logging_adapter: Arc<dyn EventLoggingAdapter>,
130    id_lists_adapter: IdListsAdapterHousing,
131    override_adapter: Option<Arc<dyn OverrideAdapter>>,
132    spec_store: Arc<SpecStore>,
133    hashing: Arc<HashUtil>,
134    statsig_environment: Option<HashMap<String, DynamicValue>>,
135    fallback_environment: Mutex<Option<HashMap<String, DynamicValue>>>,
136    ops_stats: Arc<OpsStatsForInstance>,
137    console_capture: Arc<ConsoleCaptureInstance>,
138    error_observer: Arc<dyn OpsStatsEventObserver>,
139    diagnostics_observer: Arc<dyn OpsStatsEventObserver>,
140    console_capture_observer: Arc<dyn OpsStatsEventObserver>,
141    background_tasks_started: Arc<AtomicBool>,
142    persistent_values_manager: Option<Arc<PersistentValuesManager>>,
143    initialize_details: Mutex<InitializeDetails>,
144}
145
146pub struct StatsigContext {
147    pub sdk_key: String,
148    pub options: Arc<StatsigOptions>,
149    pub local_override_adapter: Option<Arc<dyn OverrideAdapter>>,
150    pub error_observer: Arc<dyn OpsStatsEventObserver>,
151    pub diagnostics_observer: Arc<dyn OpsStatsEventObserver>,
152    pub console_capture_observer: Arc<dyn OpsStatsEventObserver>,
153    pub spec_store: Arc<SpecStore>,
154    pub console_capture: Arc<ConsoleCaptureInstance>,
155}
156
157impl Drop for Statsig {
158    fn drop(&mut self) {
159        #[cfg(feature = "ffi-support")]
160        self.delayed_exposure_store.clear();
161
162        self.event_logger.force_shutdown();
163
164        if let Some(adapter) = &self.id_lists_adapter.as_default_adapter {
165            adapter.force_shutdown();
166        }
167
168        if let Some(adapter) = &self.specs_adapter.as_default_adapter {
169            adapter.force_shutdown();
170        }
171
172        shutdown_output_logger();
173
174        log_d!(TAG, "Statsig instance dropped");
175    }
176}
177
178impl Statsig {
179    pub fn new(sdk_key: &str, options: Option<Arc<StatsigOptions>>) -> Self {
180        let options = options.map(|o| o.validate_and_fix()).unwrap_or_default();
181        let statsig_runtime = StatsigRuntime::get_runtime_with_options(
182            options.sdk_runtime_thread_count,
183            options.runtime_thread_start_callback.clone(),
184        );
185
186        initialize_output_logger(
187            &options.output_log_level,
188            options.output_logger_provider.clone(),
189        );
190
191        let hashing = Arc::new(HashUtil::new());
192        let sdk_instance_id = options.get_sdk_instance_id(sdk_key).to_string();
193
194        let data_store_key =
195            get_data_store_key(RequestPath::RulesetsV2, sdk_key, &hashing, &options);
196
197        let specs_adapter = initialize_specs_adapter(sdk_key, &data_store_key, &options);
198        let id_lists_adapter = initialize_id_lists_adapter(sdk_key, &options);
199        let event_logging_adapter = initialize_event_logging_adapter(sdk_key, &options);
200        let override_adapter = match options.override_adapter.as_ref() {
201            Some(adapter) => Some(Arc::clone(adapter)),
202            None => Some(Arc::new(StatsigLocalOverrideAdapter::new()) as Arc<dyn OverrideAdapter>),
203        };
204
205        let event_logger =
206            EventLogger::new(sdk_key, &options, &event_logging_adapter, &statsig_runtime);
207
208        let diagnostics = Arc::new(Diagnostics::new(
209            event_logger.clone(),
210            sdk_instance_id.as_str(),
211        ));
212        let diagnostics_observer: Arc<dyn OpsStatsEventObserver> =
213            Arc::new(DiagnosticsObserver::new(diagnostics));
214        let error_observer: Arc<dyn OpsStatsEventObserver> =
215            Arc::new(SDKErrorsObserver::new(sdk_key, &options));
216        let console_capture = Arc::new(ConsoleCaptureHandler::new(event_logger.clone()));
217        let console_capture_observer: Arc<dyn OpsStatsEventObserver> =
218            Arc::new(ConsoleCaptureObserver::new(console_capture));
219
220        let ops_stats = setup_ops_stats(
221            sdk_instance_id.as_str(),
222            statsig_runtime.clone(),
223            &error_observer,
224            &diagnostics_observer,
225            &console_capture_observer,
226            &options.observability_client,
227        );
228
229        let event_emitter = Arc::new(SdkEventEmitter::default());
230
231        let spec_store = Arc::new(SpecStore::new(
232            sdk_key,
233            data_store_key,
234            statsig_runtime.clone(),
235            event_emitter.clone(),
236            Some(&options),
237        ));
238
239        let environment = options
240            .environment
241            .as_ref()
242            .map(|env| HashMap::from([("tier".into(), dyn_value!(env.as_str()))]));
243
244        let persistent_values_manager = options.persistent_storage.clone().map(|storage| {
245            Arc::new(PersistentValuesManager {
246                persistent_storage: storage,
247            })
248        });
249
250        StatsigMetadata::update_service_name(options.service_name.clone());
251
252        let console_capture =
253            CONSOLE_CAPTURE_REGISTRY.get_for_instance(sdk_key, &options, &environment);
254
255        Statsig {
256            sdk_key: sdk_key.to_string(),
257            options,
258            hashing,
259            statsig_environment: environment,
260            fallback_environment: Mutex::new(None),
261            override_adapter,
262            spec_store,
263            specs_adapter,
264            event_logging_adapter,
265            event_logger,
266            #[cfg(feature = "ffi-support")]
267            delayed_exposure_store: Arc::new(DelayedExposureStore::new(ops_stats.clone())),
268            id_lists_adapter,
269            statsig_runtime,
270            ops_stats,
271            console_capture,
272            error_observer,
273            diagnostics_observer,
274            console_capture_observer,
275            background_tasks_started: Arc::new(AtomicBool::new(false)),
276            persistent_values_manager,
277            initialize_details: Mutex::new(InitializeDetails::default()),
278            event_emitter,
279        }
280    }
281
282    /// Initializes the Statsig client and returns an error if initialization fails.
283    ///
284    /// This method performs the client initialization and returns `Ok(())` if successful.
285    /// If the initialization completes with failure details, it returns a [`StatsigErr`]
286    /// describing the failure.
287    ///
288    /// For detailed information about the initialization process—regardless of success or failure—
289    /// use [`initialize_with_details`] instead.
290    ///
291    /// # Errors
292    ///
293    /// Returns a [`StatsigErr`] if the client fails to initialize successfully.
294    pub async fn initialize(&self) -> Result<(), StatsigErr> {
295        let details = self.initialize_with_details().await?;
296
297        if let Some(failure_details) = details.failure_details {
298            Err(failure_details
299                .error
300                .unwrap_or(StatsigErr::InitializationError(failure_details.reason)))
301        } else {
302            Ok(())
303        }
304    }
305
306    /// Initializes the Statsig client and returns detailed information about the process.
307    ///
308    /// This method returns a [`InitializeDetails`] struct, which includes metadata such as
309    /// the success status, initialization source, and any failure details. Even if initialization
310    /// fails, this method does not return an error; instead, the `init_success` field will be `false`
311    /// and `failure_details` may be populated.
312    ///
313    /// # Returns
314    ///
315    /// Returns a [`InitializeDetails`] struct, which includes metadata such as
316    /// the success status, initialization source, and any failure details.
317    pub async fn initialize_with_details(&self) -> Result<InitializeDetails, StatsigErr> {
318        self.ops_stats.add_marker(
319            Marker::new(KeyType::Overall, ActionType::Start, None),
320            Some(ContextType::Initialize),
321        );
322
323        let init_details = if let Some(timeout_ms) = self.options.init_timeout_ms {
324            self.apply_timeout_to_init(timeout_ms).await
325        } else {
326            self.initialize_impl_with_details().await
327        };
328        self.log_init_details(&init_details);
329        if let Ok(details) = &init_details {
330            match self.initialize_details.try_lock_for(Duration::from_secs(5)) {
331                Some(mut curr_init_details) => {
332                    *curr_init_details = details.clone();
333                }
334                None => {
335                    log_e!(TAG, "Failed to lock initialize_details");
336                }
337            }
338        }
339        init_details
340    }
341
342    pub fn get_initialize_details(&self) -> InitializeDetails {
343        match self.initialize_details.try_lock_for(Duration::from_secs(5)) {
344            Some(details) => details.clone(),
345            None => InitializeDetails::from_error(
346                "Failed to lock initialize_details",
347                Some(StatsigErr::LockFailure(
348                    "Failed to lock initialize_details".to_string(),
349                )),
350            ),
351        }
352    }
353
354    pub fn is_initialized(&self) -> bool {
355        match self.initialize_details.try_lock_for(Duration::from_secs(5)) {
356            Some(details) => details.init_success,
357            None => false,
358        }
359    }
360
361    pub fn is_config_spec_ready(&self) -> bool {
362        let specs_info = self.spec_store.get_current_specs_info();
363        matches!(specs_info.lcut, Some(lcut) if lcut != 0)
364    }
365
366    pub async fn shutdown(&self) -> Result<(), StatsigErr> {
367        self.shutdown_with_timeout(Duration::from_secs(3)).await
368    }
369
370    pub async fn shutdown_with_timeout(&self, timeout: Duration) -> Result<(), StatsigErr> {
371        log_d!(
372            TAG,
373            "Shutting down Statsig with timeout {}ms",
374            timeout.as_millis()
375        );
376
377        let start = Instant::now();
378        let shutdown_result = tokio::select! {
379            () = tokio::time::sleep(timeout) => {
380                log_w!(TAG, "Statsig shutdown timed out. {}", start.elapsed().as_millis());
381                Err(StatsigErr::ShutdownFailure(
382                    "Shutdown timed out".to_string()
383                ))
384            }
385            sub_result = async {
386                let id_list_shutdown: Pin<Box<_>> = if let Some(adapter) = &self.id_lists_adapter.inner {
387                    adapter.shutdown(timeout)
388                } else {
389                    Box::pin(async { Ok(()) })
390                };
391
392                shutdown_output_logger();
393
394                try_join!(
395                    id_list_shutdown,
396                    self.event_logger.shutdown(),
397                    self.specs_adapter.inner.shutdown(timeout, &self.statsig_runtime),
398                )
399            } => {
400                match sub_result {
401                    Ok(_) => {
402                        log_d!(TAG, "All Statsig tasks shutdown successfully");
403                        Ok(())
404                    }
405                    Err(e) => {
406                        log_w!(TAG, "Error during shutdown: {:?}", e);
407                        Err(e)
408                    }
409                }
410            }
411        };
412
413        #[cfg(feature = "ffi-support")]
414        self.delayed_exposure_store.clear();
415
416        self.statsig_runtime.shutdown();
417        shutdown_result
418    }
419
420    pub fn get_context(&self) -> StatsigContext {
421        StatsigContext {
422            sdk_key: self.sdk_key.clone(),
423            options: self.options.clone(),
424            local_override_adapter: self.override_adapter.clone(),
425            error_observer: self.error_observer.clone(),
426            diagnostics_observer: self.diagnostics_observer.clone(),
427            console_capture_observer: self.console_capture_observer.clone(),
428            spec_store: self.spec_store.clone(),
429            console_capture: self.console_capture.clone(),
430        }
431    }
432}
433
434// ------------------------------------------------------------------------------- [ Shared Instance ]
435
436impl Statsig {
437    pub fn shared() -> Arc<Statsig> {
438        let lock = match SHARED_INSTANCE.try_lock_for(Duration::from_secs(5)) {
439            Some(lock) => lock,
440            None => {
441                log_e!(
442                    TAG,
443                    "Statsig::shared() mutex error: Failed to lock SHARED_INSTANCE"
444                );
445                return Arc::new(Statsig::new(ERROR_SDK_KEY, None));
446            }
447        };
448
449        match lock.as_ref() {
450            Some(statsig) => statsig.clone(),
451            None => {
452                log_e!(
453                    TAG,
454                    "Statsig::shared() called, but no instance has been set with Statsig::new_shared(...)"
455                );
456                Arc::new(Statsig::new(ERROR_SDK_KEY, None))
457            }
458        }
459    }
460
461    pub fn new_shared(
462        sdk_key: &str,
463        options: Option<Arc<StatsigOptions>>,
464    ) -> Result<Arc<Statsig>, StatsigErr> {
465        match SHARED_INSTANCE.try_lock_for(Duration::from_secs(5)) {
466            Some(mut lock) => {
467                if lock.is_some() {
468                    let message = "Statsig shared instance already exists. Call Statsig::remove_shared() before creating a new instance.";
469                    log_e!(TAG, "{}", message);
470                    return Err(StatsigErr::SharedInstanceFailure(message.to_string()));
471                }
472
473                let statsig = Arc::new(Statsig::new(sdk_key, options));
474                *lock = Some(statsig.clone());
475                Ok(statsig)
476            }
477            None => {
478                let message = "Statsig::new_shared() mutex error: Failed to lock SHARED_INSTANCE";
479                log_e!(TAG, "{}", message);
480                Err(StatsigErr::SharedInstanceFailure(message.to_string()))
481            }
482        }
483    }
484
485    pub fn remove_shared() {
486        match SHARED_INSTANCE.try_lock_for(Duration::from_secs(5)) {
487            Some(mut lock) => {
488                *lock = None;
489            }
490            None => {
491                log_e!(
492                    TAG,
493                    "Statsig::remove_shared() mutex error: Failed to lock SHARED_INSTANCE"
494                );
495            }
496        }
497    }
498
499    pub fn has_shared_instance() -> bool {
500        match SHARED_INSTANCE.try_lock_for(Duration::from_secs(5)) {
501            Some(lock) => lock.is_some(),
502            None => false,
503        }
504    }
505}
506
507// ------------------------------------------------------------------------------- [ Client Init Response ]
508
509impl Statsig {
510    pub fn get_client_init_response(&self, user: &StatsigUser) -> InitializeResponse {
511        self.get_client_init_response_with_options(user, &ClientInitResponseOptions::default())
512    }
513
514    pub fn get_client_init_response_with_options(
515        &self,
516        user: &StatsigUser,
517        options: &ClientInitResponseOptions,
518    ) -> InitializeResponse {
519        let user_internal = self.internalize_user(user);
520        self.get_client_init_response_with_options_for_internal_user(&user_internal, options)
521    }
522
523    #[doc(hidden)]
524    pub fn get_client_init_response_with_options_for_internal_user(
525        &self,
526        user_internal: &StatsigUserInternal<'_, '_>,
527        options: &ClientInitResponseOptions,
528    ) -> InitializeResponse {
529        let data = self.spec_store.load_data();
530        let plan = data.gcir_evaluation_plan(&self.hashing);
531
532        let mut context = self.create_gcir_eval_context(user_internal, &data, options);
533
534        match GCIRFormatter::generate_v1_format_with_plan(&mut context, options, plan) {
535            Ok(mut response) => {
536                if response.has_updates {
537                    response.time = data.lcut();
538                }
539                response
540            }
541            Err(e) => {
542                log_error_to_statsig_and_console!(
543                    &self.ops_stats,
544                    TAG,
545                    StatsigErr::GCIRError(e.to_string())
546                );
547                InitializeResponse::blank(user_internal.clone())
548            }
549        }
550    }
551
552    pub fn get_client_init_response_as_string(&self, user: &StatsigUser) -> String {
553        serde_json::to_string(&self.get_client_init_response(user)).unwrap_or_default()
554    }
555
556    pub fn get_client_init_response_with_options_as_string(
557        &self,
558        user: &StatsigUser,
559        options: &ClientInitResponseOptions,
560    ) -> String {
561        let user_internal = self.internalize_user(user);
562        self.get_client_init_response_with_options_as_string_for_internal_user(
563            &user_internal,
564            options,
565        )
566    }
567
568    #[doc(hidden)]
569    pub fn get_client_init_response_with_options_as_string_for_internal_user(
570        &self,
571        user_internal: &StatsigUserInternal<'_, '_>,
572        options: &ClientInitResponseOptions,
573    ) -> String {
574        let data = self.spec_store.load_data();
575
576        let mut context = self.create_gcir_eval_context(user_internal, &data, options);
577        let response_lcut = data.lcut();
578
579        match options.response_format {
580            Some(GCIRResponseFormat::InitializeWithSecondaryExposureMapping) => self
581                .stringify_gcir_response(
582                    GCIRFormatter::generate_v2_format(&mut context, options).map(|mut response| {
583                        if response.has_updates {
584                            response.time = response_lcut;
585                        }
586                        response
587                    }),
588                    || InitializeEvaluationsResponse::blank(user_internal.clone()),
589                ),
590            Some(GCIRResponseFormat::InitializeV2) => self.stringify_gcir_response(
591                GCIRFormatter::generate_init_v2_format(&mut context, options).map(
592                    |mut response| {
593                        if response.has_updates {
594                            response.time = response_lcut;
595                        }
596                        response
597                    },
598                ),
599                || InitializeV2Response::blank(user_internal.clone()),
600            ),
601            _ => {
602                let plan = data.gcir_evaluation_plan(&self.hashing);
603
604                self.stringify_gcir_response(
605                    GCIRFormatter::generate_v1_format_with_plan(&mut context, options, plan).map(
606                        |mut response| {
607                            if response.has_updates {
608                                response.time = response_lcut;
609                            }
610                            response
611                        },
612                    ),
613                    || InitializeResponse::blank(user_internal.clone()),
614                )
615            }
616        }
617    }
618}
619
620// ------------------------------------------------------------------------------- [ Logging ]
621
622impl Statsig {
623    pub fn log_event(
624        &self,
625        user: &StatsigUser,
626        event_name: &str,
627        value: Option<String>,
628        metadata: Option<HashMap<String, String>>,
629    ) {
630        let user_internal = self.internalize_user(user);
631
632        self.event_logger.enqueue(EnqueuePassthroughOp {
633            event: StatsigEventInternal::new_custom_event(
634                user_internal.to_loggable(),
635                event_name.to_string(),
636                value.map(|v| json!(v)),
637                metadata,
638            ),
639        });
640    }
641
642    pub fn log_event_with_number(
643        &self,
644        user: &StatsigUser,
645        event_name: &str,
646        value: Option<f64>,
647        metadata: Option<HashMap<String, String>>,
648    ) {
649        let user_internal = self.internalize_user(user);
650        self.event_logger.enqueue(EnqueuePassthroughOp {
651            event: StatsigEventInternal::new_custom_event(
652                user_internal.to_loggable(),
653                event_name.to_string(),
654                value.map(|v| json!(v)),
655                metadata,
656            ),
657        });
658    }
659
660    pub fn log_event_with_typed_metadata(
661        &self,
662        user: &StatsigUser,
663        event_name: &str,
664        value: Option<String>,
665        metadata: Option<HashMap<String, Value>>,
666    ) {
667        let user_internal = self.internalize_user(user);
668        self.log_event_with_typed_metadata_for_internal_user(
669            &user_internal,
670            event_name,
671            value,
672            metadata,
673        );
674    }
675
676    #[doc(hidden)]
677    pub fn log_event_with_typed_metadata_for_internal_user(
678        &self,
679        user_internal: &StatsigUserInternal<'_, '_>,
680        event_name: &str,
681        value: Option<String>,
682        metadata: Option<HashMap<String, Value>>,
683    ) {
684        self.event_logger.enqueue(EnqueuePassthroughOp {
685            event: StatsigEventInternal::new_custom_event_with_typed_metadata(
686                user_internal.to_loggable(),
687                event_name.to_string(),
688                value.map(|v| json!(v)),
689                metadata,
690            ),
691        });
692    }
693
694    pub fn log_event_with_number_and_typed_metadata(
695        &self,
696        user: &StatsigUser,
697        event_name: &str,
698        value: Option<f64>,
699        metadata: Option<HashMap<String, Value>>,
700    ) {
701        let user_internal = self.internalize_user(user);
702        self.log_event_with_number_and_typed_metadata_for_internal_user(
703            &user_internal,
704            event_name,
705            value,
706            metadata,
707        );
708    }
709
710    #[doc(hidden)]
711    pub fn log_event_with_number_and_typed_metadata_for_internal_user(
712        &self,
713        user_internal: &StatsigUserInternal<'_, '_>,
714        event_name: &str,
715        value: Option<f64>,
716        metadata: Option<HashMap<String, Value>>,
717    ) {
718        self.event_logger.enqueue(EnqueuePassthroughOp {
719            event: StatsigEventInternal::new_custom_event_with_typed_metadata(
720                user_internal.to_loggable(),
721                event_name.to_string(),
722                value.map(|v| json!(v)),
723                metadata,
724            ),
725        });
726    }
727
728    pub fn forward_log_line_event(
729        &self,
730        user: &StatsigUser,
731        log_level: StatsigLogLineLevel,
732        value: Option<String>,
733        metadata: Option<HashMap<String, String>>,
734    ) {
735        let user_internal = self.internalize_user(user);
736        self.event_logger.enqueue(EnqueuePassthroughOp {
737            event: StatsigEventInternal::new_statsig_log_line_event(
738                user_internal.to_loggable(),
739                log_level,
740                value,
741                metadata,
742                None,
743            ),
744        });
745    }
746
747    pub fn log_layer_param_exposure_with_layer_json(
748        &self,
749        layer_json: String,
750        parameter_name: String,
751    ) {
752        let layer = match serde_json::from_str::<Layer>(&layer_json) {
753            Ok(layer) => layer,
754            Err(e) => {
755                log_error_to_statsig_and_console!(
756                    self.ops_stats.clone(),
757                    TAG,
758                    StatsigErr::ShutdownFailure(e.to_string())
759                );
760                return;
761            }
762        };
763
764        self.log_layer_param_exposure_with_layer(layer, parameter_name);
765    }
766
767    pub fn log_layer_param_exposure_with_layer(&self, layer: Layer, parameter_name: String) {
768        if layer.__disable_exposure {
769            self.event_logger.increment_non_exposure_checks(&layer.name);
770            return;
771        }
772
773        self.event_logger
774            .enqueue(EnqueueLayerParamExpoOp::LayerOwned(
775                Utc::now().timestamp_millis() as u64,
776                Box::new(layer),
777                parameter_name,
778                ExposureTrigger::Auto,
779            ));
780    }
781
782    pub async fn flush_events(&self) {
783        let _ = self.event_logger.flush_all_pending_events().await;
784    }
785}
786
787// ------------------------------------------------------------------------------- [ Parameter Store ]
788
789impl Statsig {
790    pub fn get_string_parameter_from_store(
791        &self,
792        user: &StatsigUser,
793        parameter_store_name: &str,
794        parameter_name: &str,
795        fallback: Option<String>,
796        options: Option<ParameterStoreEvaluationOptions>,
797    ) -> Option<String> {
798        self.get_parameter_from_store(
799            user,
800            parameter_store_name,
801            parameter_name,
802            fallback,
803            options,
804        )
805    }
806
807    pub fn get_boolean_parameter_from_store(
808        &self,
809        user: &StatsigUser,
810        parameter_store_name: &str,
811        parameter_name: &str,
812        fallback: Option<bool>,
813        options: Option<ParameterStoreEvaluationOptions>,
814    ) -> Option<bool> {
815        self.get_parameter_from_store(
816            user,
817            parameter_store_name,
818            parameter_name,
819            fallback,
820            options,
821        )
822    }
823
824    pub fn get_float_parameter_from_store(
825        &self,
826        user: &StatsigUser,
827        parameter_store_name: &str,
828        parameter_name: &str,
829        fallback: Option<f64>,
830        options: Option<ParameterStoreEvaluationOptions>,
831    ) -> Option<f64> {
832        self.get_parameter_from_store(
833            user,
834            parameter_store_name,
835            parameter_name,
836            fallback,
837            options,
838        )
839    }
840
841    pub fn get_integer_parameter_from_store(
842        &self,
843        user: &StatsigUser,
844        parameter_store_name: &str,
845        parameter_name: &str,
846        fallback: Option<i64>,
847        options: Option<ParameterStoreEvaluationOptions>,
848    ) -> Option<i64> {
849        self.get_parameter_from_store(
850            user,
851            parameter_store_name,
852            parameter_name,
853            fallback,
854            options,
855        )
856    }
857
858    pub fn get_array_parameter_from_store(
859        &self,
860        user: &StatsigUser,
861        parameter_store_name: &str,
862        parameter_name: &str,
863        fallback: Option<Vec<Value>>,
864        options: Option<ParameterStoreEvaluationOptions>,
865    ) -> Option<Vec<Value>> {
866        self.get_parameter_from_store(
867            user,
868            parameter_store_name,
869            parameter_name,
870            fallback,
871            options,
872        )
873    }
874
875    pub fn get_object_parameter_from_store(
876        &self,
877        user: &StatsigUser,
878        parameter_store_name: &str,
879        parameter_name: &str,
880        fallback: Option<HashMap<String, Value>>,
881        options: Option<ParameterStoreEvaluationOptions>,
882    ) -> Option<HashMap<String, Value>> {
883        self.get_parameter_from_store(
884            user,
885            parameter_store_name,
886            parameter_name,
887            fallback,
888            options,
889        )
890    }
891
892    pub fn get_parameter_from_store<T: DeserializeOwned>(
893        &self,
894        user: &StatsigUser,
895        parameter_store_name: &str,
896        parameter_name: &str,
897        fallback: Option<T>,
898        options: Option<ParameterStoreEvaluationOptions>,
899    ) -> Option<T> {
900        let store = self.get_parameter_store_with_user_and_options(
901            Some(user),
902            parameter_store_name,
903            options.unwrap_or_default(),
904        );
905        match fallback {
906            Some(fallback) => Some(store.get(user, parameter_name, fallback)),
907            None => store.get_opt(user, parameter_name),
908        }
909    }
910
911    pub fn get_parameter_store(&self, parameter_store_name: &str) -> ParameterStore<'_> {
912        self.get_parameter_store_with_options(
913            parameter_store_name,
914            ParameterStoreEvaluationOptions::default(),
915        )
916    }
917
918    pub fn get_parameter_store_for_user(
919        &self,
920        user: &StatsigUser,
921        parameter_store_name: &str,
922    ) -> ParameterStore<'_> {
923        self.get_parameter_store_with_user_and_options(
924            Some(user),
925            parameter_store_name,
926            ParameterStoreEvaluationOptions::default(),
927        )
928    }
929
930    pub fn get_parameter_store_with_options(
931        &self,
932        parameter_store_name: &str,
933        options: ParameterStoreEvaluationOptions,
934    ) -> ParameterStore<'_> {
935        self.get_parameter_store_with_user_and_options(None, parameter_store_name, options)
936    }
937
938    fn get_parameter_store_with_user_and_options(
939        &self,
940        user: Option<&StatsigUser>,
941        parameter_store_name: &str,
942        options: ParameterStoreEvaluationOptions,
943    ) -> ParameterStore<'_> {
944        let store_name_intern = InternedString::from_str_ref(parameter_store_name);
945
946        self.event_logger
947            .increment_non_exposure_checks(parameter_store_name);
948
949        let data = self.spec_store.load_data();
950
951        if let Some(user) = user {
952            if let Some((override_result, parameters)) =
953                self.get_parameter_store_override(user, parameter_store_name)
954            {
955                let details = EvaluationDetails::recognized_but_overridden(
956                    data.lcut(),
957                    data.time_received_at,
958                    override_result.override_reason.unwrap_or("Override"),
959                    override_result.version,
960                );
961
962                return ParameterStore {
963                    name: parameter_store_name.to_string(),
964                    parameters,
965                    details,
966                    options,
967                    _statsig_ref: self,
968                };
969            }
970        }
971
972        let stores = &data.snapshot.param_stores;
973        let store = match stores {
974            Some(stores) => stores.get(&store_name_intern),
975            None => {
976                return ParameterStore {
977                    name: parameter_store_name.to_string(),
978                    parameters: HashMap::new(),
979                    details: EvaluationDetails::unrecognized(
980                        &data.source,
981                        data.lcut(),
982                        data.time_received_at,
983                    ),
984                    options,
985                    _statsig_ref: self,
986                };
987            }
988        };
989        match store {
990            Some(store) => ParameterStore {
991                name: parameter_store_name.to_string(),
992                parameters: store.parameters.clone(),
993                details: EvaluationDetails::recognized(
994                    &data.source,
995                    data.lcut(),
996                    data.time_received_at,
997                    &EvaluatorResult::default(),
998                ),
999                options,
1000                _statsig_ref: self,
1001            },
1002            None => ParameterStore {
1003                name: parameter_store_name.to_string(),
1004                parameters: HashMap::new(),
1005                details: EvaluationDetails::unrecognized(
1006                    &data.source,
1007                    data.lcut(),
1008                    data.time_received_at,
1009                ),
1010                options,
1011                _statsig_ref: self,
1012            },
1013        }
1014    }
1015
1016    pub(crate) fn get_parameter_store_override(
1017        &self,
1018        user: &StatsigUser,
1019        parameter_store_name: &str,
1020    ) -> Option<(EvaluatorResult, HashMap<String, Parameter>)> {
1021        let adapter = self.override_adapter.as_ref()?;
1022
1023        let mut result = EvaluatorResult::default();
1024        if !adapter.get_parameter_store_override(user, parameter_store_name, &mut result) {
1025            return None;
1026        }
1027
1028        let mut parameters = HashMap::new();
1029        if let Some(json_value) = &result.json_value {
1030            if let Some(map) = json_value.get_json() {
1031                for (param_name, param_value) in map {
1032                    if let Ok(parameter) = serde_json::from_value::<Parameter>(param_value) {
1033                        parameters.insert(param_name, parameter);
1034                    }
1035                }
1036            }
1037        }
1038
1039        Some((result, parameters))
1040    }
1041}
1042
1043// ------------------------------------------------------------------------------- [ User Store ]
1044
1045impl Statsig {
1046    pub fn identify(&self, user: &StatsigUser) {
1047        let user_internal = self.internalize_user(user);
1048        self.identify_internal_user(&user_internal);
1049    }
1050
1051    #[doc(hidden)]
1052    pub fn identify_internal_user(&self, user_internal: &StatsigUserInternal<'_, '_>) {
1053        self.event_logger.enqueue(EnqueuePassthroughOp {
1054            event: StatsigEventInternal::new_custom_event(
1055                user_internal.to_loggable(),
1056                "statsig::identify".to_string(),
1057                None,
1058                None,
1059            ),
1060        });
1061    }
1062}
1063
1064// ------------------------------------------------------------------------------- [ CMAB ]
1065
1066impl Statsig {
1067    pub fn get_cmab_ranked_groups(
1068        &self,
1069        user: &StatsigUser,
1070        cmab_name: &str,
1071    ) -> Vec<CMABRankedGroup> {
1072        self.event_logger.increment_non_exposure_checks(cmab_name);
1073
1074        let data = self.spec_store.load_data();
1075        let user_internal = self.internalize_user(user);
1076        let mut context = self.create_standard_eval_context(
1077            &user_internal,
1078            &data,
1079            data.snapshot.app_id.as_ref(),
1080            self.override_adapter.as_ref(),
1081            true,
1082        );
1083        get_cmab_ranked_list(&mut context, cmab_name)
1084    }
1085
1086    pub fn log_cmab_exposure_for_group(
1087        &self,
1088        user: &StatsigUser,
1089        cmab_name: &str,
1090        group_id: String,
1091    ) {
1092        let user_internal = self.internalize_user(user);
1093
1094        let mut experiment = self.get_experiment_impl(&user_internal, cmab_name, None);
1095        experiment.rule_id = group_id;
1096
1097        self.event_logger.enqueue(EnqueueExperimentExpoOp {
1098            exposure_time: Utc::now().timestamp_millis() as u64,
1099            user: &user_internal,
1100            experiment: &experiment,
1101            trigger: ExposureTrigger::Manual,
1102        });
1103    }
1104}
1105
1106// ------------------------------------------------------------------------------- [ Override ]
1107
1108impl Statsig {
1109    pub fn override_gate(&self, gate_name: &str, value: bool, id: Option<&str>) {
1110        if let Some(adapter) = &self.override_adapter {
1111            adapter.override_gate(gate_name, value, id);
1112        }
1113    }
1114
1115    pub fn override_dynamic_config(
1116        &self,
1117        config_name: &str,
1118        value: HashMap<String, serde_json::Value>,
1119        id: Option<&str>,
1120    ) {
1121        if let Some(adapter) = &self.override_adapter {
1122            adapter.override_dynamic_config(config_name, value, id);
1123        }
1124    }
1125
1126    pub fn override_layer(
1127        &self,
1128        layer_name: &str,
1129        value: HashMap<String, serde_json::Value>,
1130        id: Option<&str>,
1131    ) {
1132        if let Some(adapter) = &self.override_adapter {
1133            adapter.override_layer(layer_name, value, id);
1134        }
1135    }
1136
1137    pub fn override_parameter_store(
1138        &self,
1139        param_name: &str,
1140        value: HashMap<String, serde_json::Value>,
1141        id: Option<&str>,
1142    ) {
1143        if let Some(adapter) = &self.override_adapter {
1144            adapter.override_parameter_store(param_name, value, id);
1145        }
1146    }
1147
1148    pub fn override_experiment(
1149        &self,
1150        experiment_name: &str,
1151        value: HashMap<String, serde_json::Value>,
1152        id: Option<&str>,
1153    ) {
1154        if let Some(adapter) = &self.override_adapter {
1155            adapter.override_experiment(experiment_name, value, id);
1156        }
1157    }
1158
1159    pub fn override_experiment_by_group_name(
1160        &self,
1161        experiment_name: &str,
1162        group_name: &str,
1163        id: Option<&str>,
1164    ) {
1165        if let Some(adapter) = &self.override_adapter {
1166            adapter.override_experiment_by_group_name(experiment_name, group_name, id);
1167        }
1168    }
1169
1170    pub fn remove_gate_override(&self, gate_name: &str, id: Option<&str>) {
1171        if let Some(adapter) = &self.override_adapter {
1172            adapter.remove_gate_override(gate_name, id);
1173        }
1174    }
1175
1176    pub fn remove_dynamic_config_override(&self, config_name: &str, id: Option<&str>) {
1177        if let Some(adapter) = &self.override_adapter {
1178            adapter.remove_dynamic_config_override(config_name, id);
1179        }
1180    }
1181
1182    pub fn remove_experiment_override(&self, experiment_name: &str, id: Option<&str>) {
1183        if let Some(adapter) = &self.override_adapter {
1184            adapter.remove_experiment_override(experiment_name, id);
1185        }
1186    }
1187
1188    pub fn remove_layer_override(&self, layer_name: &str, id: Option<&str>) {
1189        if let Some(adapter) = &self.override_adapter {
1190            adapter.remove_layer_override(layer_name, id);
1191        }
1192    }
1193
1194    pub fn remove_parameter_store_override(&self, parameter_store_name: &str, id: Option<&str>) {
1195        if let Some(adapter) = &self.override_adapter {
1196            adapter.remove_parameter_store_override(parameter_store_name, id);
1197        }
1198    }
1199
1200    pub fn remove_all_overrides(&self) {
1201        if let Some(adapter) = &self.override_adapter {
1202            adapter.remove_all_overrides();
1203        }
1204    }
1205}
1206
1207// ------------------------------------------------------------------------------- [ Debugging ]
1208
1209impl Statsig {
1210    pub fn get_feature_gate_list(&self) -> Vec<String> {
1211        self.spec_store
1212            .unperformant_keys_entity_filter("feature_gates", "feature_gate")
1213    }
1214
1215    pub fn get_dynamic_config_list(&self) -> Vec<String> {
1216        self.spec_store
1217            .unperformant_keys_entity_filter("dynamic_configs", "dynamic_config")
1218    }
1219
1220    pub fn get_experiment_list(&self) -> Vec<String> {
1221        self.spec_store
1222            .unperformant_keys_entity_filter("dynamic_configs", "experiment")
1223    }
1224
1225    pub fn get_autotune_list(&self) -> Vec<String> {
1226        self.spec_store
1227            .unperformant_keys_entity_filter("dynamic_configs", "autotune")
1228    }
1229
1230    pub fn get_parameter_store_list(&self) -> Vec<String> {
1231        self.spec_store
1232            .unperformant_keys_entity_filter("param_stores", "*")
1233    }
1234
1235    pub fn get_layer_list(&self) -> Vec<String> {
1236        self.spec_store
1237            .unperformant_keys_entity_filter("layer_configs", "*")
1238    }
1239
1240    pub fn __get_parsed_user_agent_value(
1241        &self,
1242        user: &StatsigUser,
1243    ) -> Option<ParsedUserAgentValue> {
1244        UserAgentParser::get_parsed_user_agent_value_for_user(user, &self.options)
1245    }
1246}
1247
1248// ------------------------------------------------------------------------------- [ Feature Gate ]
1249
1250impl Statsig {
1251    pub fn check_gate(&self, user: &StatsigUser, gate_name: &str) -> bool {
1252        self.check_gate_with_options(user, gate_name, FeatureGateEvaluationOptions::default())
1253    }
1254
1255    pub fn check_gate_with_options(
1256        &self,
1257        user: &StatsigUser,
1258        gate_name: &str,
1259        options: FeatureGateEvaluationOptions,
1260    ) -> bool {
1261        let user_internal = self.internalize_user(user);
1262        self.check_gate_with_options_for_internal_user(&user_internal, gate_name, options)
1263    }
1264
1265    #[doc(hidden)]
1266    pub fn check_gate_with_options_for_internal_user(
1267        &self,
1268        user_internal: &StatsigUserInternal<'_, '_>,
1269        gate_name: &str,
1270        options: FeatureGateEvaluationOptions,
1271    ) -> bool {
1272        let disable_exposure_logging = options.disable_exposure_logging;
1273        let (details, evaluation) = self.get_gate_evaluation(
1274            user_internal,
1275            gate_name,
1276            Some(options.disable_exposure_logging),
1277        );
1278
1279        let value = evaluation.as_ref().map(|e| e.value).unwrap_or_default();
1280        let rule_id = evaluation
1281            .as_ref()
1282            .map(|e| e.base.rule_id.clone())
1283            .unwrap_or_default();
1284
1285        if disable_exposure_logging {
1286            log_d!(TAG, "Exposure logging is disabled for gate {}", gate_name);
1287            self.event_logger.increment_non_exposure_checks(gate_name);
1288        } else {
1289            self.event_logger.enqueue(EnqueueGateExpoOp {
1290                exposure_time: Utc::now().timestamp_millis() as u64,
1291                user: user_internal,
1292                queried_gate_name: gate_name,
1293                evaluation: evaluation.map(Cow::Owned),
1294                details: details.clone(),
1295                trigger: ExposureTrigger::Auto,
1296            });
1297        }
1298
1299        self.emit_gate_evaluated(gate_name, rule_id.as_str(), value, &details.reason);
1300
1301        value
1302    }
1303
1304    pub fn get_feature_gate(&self, user: &StatsigUser, gate_name: &str) -> FeatureGate {
1305        self.get_feature_gate_with_options(user, gate_name, FeatureGateEvaluationOptions::default())
1306    }
1307
1308    pub fn get_feature_gate_with_options(
1309        &self,
1310        user: &StatsigUser,
1311        gate_name: &str,
1312        options: FeatureGateEvaluationOptions,
1313    ) -> FeatureGate {
1314        let user_internal = self.internalize_user(user);
1315        let disable_exposure_logging = options.disable_exposure_logging;
1316        let (details, evaluation) = self.get_gate_evaluation(
1317            &user_internal,
1318            gate_name,
1319            Some(options.disable_exposure_logging),
1320        );
1321
1322        if disable_exposure_logging {
1323            log_d!(TAG, "Exposure logging is disabled for gate {}", gate_name);
1324            self.event_logger.increment_non_exposure_checks(gate_name);
1325        } else {
1326            self.event_logger.enqueue(EnqueueGateExpoOp {
1327                exposure_time: Utc::now().timestamp_millis() as u64,
1328                user: &user_internal,
1329                queried_gate_name: gate_name,
1330                evaluation: evaluation.as_ref().map(Cow::Borrowed),
1331                details: details.clone(),
1332                trigger: ExposureTrigger::Auto,
1333            });
1334        }
1335
1336        let gate = make_feature_gate(gate_name, evaluation, details);
1337        self.emit_gate_evaluated(gate_name, &gate.rule_id, gate.value, &gate.details.reason);
1338        gate
1339    }
1340
1341    pub fn manually_log_gate_exposure(&self, user: &StatsigUser, gate_name: &str) {
1342        let interned_gate_name = InternedString::from_str_ref(gate_name);
1343        let user_internal = self.internalize_user(user);
1344        self.manually_log_gate_exposure_for_internal_user(&user_internal, &interned_gate_name);
1345    }
1346
1347    #[doc(hidden)]
1348    pub fn manually_log_gate_exposure_for_internal_user(
1349        &self,
1350        user_internal: &StatsigUserInternal<'_, '_>,
1351        gate_name: &InternedString,
1352    ) {
1353        let (details, evaluation) =
1354            self.evaluate_spec_raw(user_internal, gate_name.as_str(), &SpecType::Gate, None);
1355
1356        self.event_logger.enqueue(EnqueueExposureOp::gate_exposure(
1357            user_internal,
1358            gate_name,
1359            ExposureTrigger::Manual,
1360            details,
1361            evaluation,
1362        ));
1363    }
1364
1365    pub fn get_fields_needed_for_gate(&self, gate_name: &str) -> Vec<String> {
1366        self.spec_store
1367            .get_fields_used_for_entity(gate_name, SpecType::Gate)
1368    }
1369}
1370
1371// ------------------------------------------------------------------------------- [ Dynamic Config ]
1372
1373impl Statsig {
1374    pub fn get_dynamic_config(
1375        &self,
1376        user: &StatsigUser,
1377        dynamic_config_name: &str,
1378    ) -> DynamicConfig {
1379        self.get_dynamic_config_with_options(
1380            user,
1381            dynamic_config_name,
1382            DynamicConfigEvaluationOptions::default(),
1383        )
1384    }
1385
1386    pub fn get_dynamic_config_with_options(
1387        &self,
1388        user: &StatsigUser,
1389        dynamic_config_name: &str,
1390        options: DynamicConfigEvaluationOptions,
1391    ) -> DynamicConfig {
1392        let user_internal = self.internalize_user(user);
1393        let disable_exposure_logging = options.disable_exposure_logging;
1394        let dynamic_config = self.get_dynamic_config_impl(
1395            &user_internal,
1396            dynamic_config_name,
1397            Some(options.disable_exposure_logging),
1398        );
1399
1400        if disable_exposure_logging {
1401            log_d!(
1402                TAG,
1403                "Exposure logging is disabled for Dynamic Config {}",
1404                dynamic_config_name
1405            );
1406            self.event_logger
1407                .increment_non_exposure_checks(dynamic_config_name);
1408        } else {
1409            self.event_logger.enqueue(EnqueueConfigExpoOp {
1410                exposure_time: Utc::now().timestamp_millis() as u64,
1411                user: &user_internal,
1412                config: &dynamic_config,
1413                trigger: ExposureTrigger::Auto,
1414            });
1415        }
1416
1417        self.emit_dynamic_config_evaluated(&dynamic_config);
1418
1419        dynamic_config
1420    }
1421
1422    pub fn manually_log_dynamic_config_exposure(
1423        &self,
1424        user: &StatsigUser,
1425        dynamic_config_name: &str,
1426    ) {
1427        let interned_dynamic_config_name = InternedString::from_str_ref(dynamic_config_name);
1428        let user_internal = self.internalize_user(user);
1429        self.manually_log_dynamic_config_exposure_for_internal_user(
1430            &user_internal,
1431            &interned_dynamic_config_name,
1432        );
1433    }
1434
1435    #[doc(hidden)]
1436    pub fn manually_log_dynamic_config_exposure_for_internal_user(
1437        &self,
1438        user_internal: &StatsigUserInternal<'_, '_>,
1439        dynamic_config_name: &InternedString,
1440    ) {
1441        let (details, evaluation) = self.evaluate_spec_raw(
1442            user_internal,
1443            dynamic_config_name.as_str(),
1444            &SpecType::DynamicConfig,
1445            None,
1446        );
1447
1448        self.event_logger
1449            .enqueue(EnqueueExposureOp::dynamic_config_exposure(
1450                user_internal,
1451                dynamic_config_name,
1452                ExposureTrigger::Manual,
1453                details,
1454                evaluation,
1455            ));
1456    }
1457
1458    pub fn get_fields_needed_for_dynamic_config(&self, config_name: &str) -> Vec<String> {
1459        self.spec_store
1460            .get_fields_used_for_entity(config_name, SpecType::DynamicConfig)
1461    }
1462}
1463
1464// ------------------------------------------------------------------------------- [ Experiment ]
1465
1466impl Statsig {
1467    pub fn get_experiment(&self, user: &StatsigUser, experiment_name: &str) -> Experiment {
1468        self.get_experiment_with_options(
1469            user,
1470            experiment_name,
1471            ExperimentEvaluationOptions::default(),
1472        )
1473    }
1474
1475    pub fn get_experiment_with_options(
1476        &self,
1477        user: &StatsigUser,
1478        experiment_name: &str,
1479        options: ExperimentEvaluationOptions,
1480    ) -> Experiment {
1481        let user_internal = self.internalize_user(user);
1482        let disable_exposure_logging = options.disable_exposure_logging;
1483        let mut experiment = self.get_experiment_impl(
1484            &user_internal,
1485            experiment_name,
1486            Some(options.disable_exposure_logging),
1487        );
1488
1489        experiment = PersistentValuesManager::try_apply_sticky_value_to_experiment(
1490            &self.persistent_values_manager,
1491            &user_internal,
1492            &options,
1493            experiment,
1494        );
1495
1496        if disable_exposure_logging {
1497            log_d!(
1498                TAG,
1499                "Exposure logging is disabled for experiment {}",
1500                experiment_name
1501            );
1502            self.event_logger
1503                .increment_non_exposure_checks(experiment_name);
1504        } else {
1505            self.event_logger.enqueue(EnqueueExperimentExpoOp {
1506                exposure_time: Utc::now().timestamp_millis() as u64,
1507                user: &user_internal,
1508                experiment: &experiment,
1509                trigger: ExposureTrigger::Auto,
1510            });
1511        }
1512
1513        self.emit_experiment_evaluated(&experiment);
1514
1515        experiment
1516    }
1517
1518    pub fn manually_log_experiment_exposure(&self, user: &StatsigUser, experiment_name: &str) {
1519        let interned_experiment_name = InternedString::from_str_ref(experiment_name);
1520        let user_internal = self.internalize_user(user);
1521        self.manually_log_experiment_exposure_for_internal_user(
1522            &user_internal,
1523            &interned_experiment_name,
1524        );
1525    }
1526
1527    #[doc(hidden)]
1528    pub fn manually_log_experiment_exposure_for_internal_user(
1529        &self,
1530        user_internal: &StatsigUserInternal<'_, '_>,
1531        experiment_name: &InternedString,
1532    ) {
1533        self.manually_log_experiment_exposure_for_internal_user_with_metadata(
1534            user_internal,
1535            experiment_name,
1536            None,
1537        );
1538    }
1539
1540    #[doc(hidden)]
1541    pub fn manually_log_experiment_exposure_for_internal_user_with_metadata(
1542        &self,
1543        user_internal: &StatsigUserInternal<'_, '_>,
1544        experiment_name: &InternedString,
1545        exposure_metadata: Option<HashMap<String, Value>>,
1546    ) {
1547        let (details, evaluation) = self.evaluate_spec_raw(
1548            user_internal,
1549            experiment_name.as_str(),
1550            &SpecType::Experiment,
1551            None,
1552        );
1553
1554        self.event_logger.enqueue(
1555            EnqueueExposureOp::experiment_exposure(
1556                user_internal,
1557                experiment_name,
1558                ExposureTrigger::Manual,
1559                details,
1560                evaluation,
1561            )
1562            .with_extra_metadata(exposure_metadata),
1563        );
1564    }
1565
1566    pub fn get_fields_needed_for_experiment(&self, experiment_name: &str) -> Vec<String> {
1567        self.spec_store
1568            .get_fields_used_for_entity(experiment_name, SpecType::Experiment)
1569    }
1570
1571    pub fn get_experiment_by_group_name(
1572        &self,
1573        experiment_name: &str,
1574        group_name: &str,
1575    ) -> Experiment {
1576        self.get_experiment_by_group_name_impl(
1577            experiment_name,
1578            group_name,
1579            |spec, rule, details| {
1580                if let (Some(spec), Some(rule)) = (spec, rule) {
1581                    let value = rule.return_value().json_value().unwrap_or_default();
1582                    let rule_id = rule.id().as_str().to_string();
1583                    let id_type = rule.id_type().value().to_string();
1584                    let group_name = rule.group_name().map(|name| name.as_str().to_string());
1585
1586                    return Experiment {
1587                        name: experiment_name.to_string(),
1588                        value,
1589                        rule_id,
1590                        id_type,
1591                        group_name,
1592                        details,
1593                        is_experiment_active: spec.is_active().unwrap_or(false),
1594                        __evaluation: None,
1595                    };
1596                }
1597
1598                make_experiment(experiment_name, None, details)
1599            },
1600        )
1601    }
1602
1603    pub fn get_experiment_by_group_id_advanced(
1604        &self,
1605        experiment_name: &str,
1606        group_id: &str,
1607    ) -> Experiment {
1608        self.get_experiment_by_group_id_advanced_impl(
1609            experiment_name,
1610            group_id,
1611            |spec, rule, details| {
1612                if let (Some(spec), Some(rule)) = (spec, rule) {
1613                    let value = rule.return_value().json_value().unwrap_or_default();
1614                    let rule_id = rule.id().as_str().to_string();
1615                    let id_type = rule.id_type().value().to_string();
1616                    let group_name = rule.group_name().map(|name| name.as_str().to_string());
1617
1618                    return Experiment {
1619                        name: experiment_name.to_string(),
1620                        value,
1621                        rule_id,
1622                        id_type,
1623                        group_name,
1624                        details,
1625                        is_experiment_active: spec.is_active().unwrap_or(false),
1626                        __evaluation: None,
1627                    };
1628                }
1629
1630                make_experiment(experiment_name, None, details)
1631            },
1632        )
1633    }
1634
1635    fn get_experiment_by_group_name_impl<T>(
1636        &self,
1637        experiment_name: &str,
1638        group_name: &str,
1639        result_factory: impl FnOnce(Option<SpecView<'_>>, Option<RuleRef<'_>>, EvaluationDetails) -> T,
1640    ) -> T {
1641        self.get_experiment_by_rule_match_impl(
1642            experiment_name,
1643            |rule| {
1644                rule.group_name()
1645                    .is_some_and(|name| name.as_str() == group_name)
1646            },
1647            result_factory,
1648        )
1649    }
1650
1651    fn get_experiment_by_group_id_advanced_impl<T>(
1652        &self,
1653        experiment_name: &str,
1654        rule_id: &str,
1655        result_factory: impl FnOnce(Option<SpecView<'_>>, Option<RuleRef<'_>>, EvaluationDetails) -> T,
1656    ) -> T {
1657        self.get_experiment_by_rule_match_impl(
1658            experiment_name,
1659            |rule| rule.id().as_str() == rule_id,
1660            result_factory,
1661        )
1662    }
1663
1664    fn get_experiment_by_rule_match_impl<T, P>(
1665        &self,
1666        experiment_name: &str,
1667        rule_predicate: P,
1668        result_factory: impl FnOnce(Option<SpecView<'_>>, Option<RuleRef<'_>>, EvaluationDetails) -> T,
1669    ) -> T
1670    where
1671        P: Fn(RuleRef<'_>) -> bool,
1672    {
1673        let data = self.spec_store.load_data();
1674
1675        let experiment_name = InternedString::from_str_ref(experiment_name);
1676        let experiment = data.snapshot.dynamic_configs.get(&experiment_name);
1677
1678        let Some(exp) = experiment else {
1679            return result_factory(
1680                None,
1681                None,
1682                EvaluationDetails::unrecognized(&data.source, data.lcut(), data.time_received_at),
1683            );
1684        };
1685
1686        let spec = exp.view();
1687        for index in 0..spec.rules_len() {
1688            let rule = spec.rule(index);
1689            if rule_predicate(rule) {
1690                return result_factory(
1691                    Some(spec),
1692                    Some(rule),
1693                    EvaluationDetails::recognized_without_eval_result(
1694                        &data.source,
1695                        data.lcut(),
1696                        data.time_received_at,
1697                    ),
1698                );
1699            }
1700        }
1701
1702        result_factory(
1703            None,
1704            None,
1705            EvaluationDetails::unrecognized(&data.source, data.lcut(), data.time_received_at),
1706        )
1707    }
1708}
1709
1710#[cfg(feature = "ffi-support")]
1711fn experiment_group_to_raw_json(
1712    experiment_name: &str,
1713    spec: Option<SpecView<'_>>,
1714    rule: Option<RuleRef<'_>>,
1715    details: EvaluationDetails,
1716) -> String {
1717    use crate::statsig_types_raw::{ExperimentRaw, SuffixedRuleId};
1718
1719    let (Some(spec), Some(rule)) = (spec, rule) else {
1720        return ExperimentRaw::empty(experiment_name, &details).unperformant_to_json_string();
1721    };
1722
1723    let value = rule.return_value().to_owned();
1724    let rule_id = rule.id().to_interned();
1725    let id_type = InternedString::from_str_ref(rule.id_type().value());
1726    let group_name = rule.group_name().map(|name| name.to_interned());
1727    ExperimentRaw {
1728        name: experiment_name,
1729        value: Some(&value),
1730        rule_id: SuffixedRuleId {
1731            rule_id: &rule_id,
1732            rule_id_suffix: None,
1733        },
1734        id_type: Some(&id_type),
1735        group_name: group_name.as_ref(),
1736        is_experiment_active: Some(spec.is_active().unwrap_or(false)),
1737        details: &details,
1738        secondary_exposures: None,
1739    }
1740    .unperformant_to_json_string()
1741}
1742
1743// ------------------------------------------------------------------------------- [ Layer ]
1744
1745impl Statsig {
1746    pub fn get_layer(&self, user: &StatsigUser, layer_name: &str) -> Layer {
1747        self.get_layer_with_options(user, layer_name, LayerEvaluationOptions::default())
1748    }
1749
1750    pub fn get_layer_with_options(
1751        &self,
1752        user: &StatsigUser,
1753        layer_name: &str,
1754        options: LayerEvaluationOptions,
1755    ) -> Layer {
1756        let user_internal = self.internalize_user(user);
1757        self.get_layer_impl(user_internal, layer_name, options)
1758    }
1759
1760    pub fn manually_log_layer_parameter_exposure(
1761        &self,
1762        user: &StatsigUser,
1763        layer_name: &str,
1764        parameter_name: String,
1765    ) {
1766        let interned_layer_name = InternedString::from_str_ref(layer_name);
1767        let interned_parameter_name = InternedString::from_string(parameter_name);
1768        let user_internal = self.internalize_user(user);
1769        self.manually_log_layer_parameter_exposure_for_internal_user(
1770            &user_internal,
1771            &interned_layer_name,
1772            interned_parameter_name,
1773        );
1774    }
1775
1776    #[doc(hidden)]
1777    pub fn manually_log_layer_parameter_exposure_for_internal_user(
1778        &self,
1779        user_internal: &StatsigUserInternal<'_, '_>,
1780        layer_name: &InternedString,
1781        parameter_name: InternedString,
1782    ) {
1783        self.manually_log_layer_parameter_exposure_for_internal_user_with_metadata(
1784            user_internal,
1785            layer_name,
1786            parameter_name,
1787            None,
1788        );
1789    }
1790
1791    #[doc(hidden)]
1792    pub fn manually_log_layer_parameter_exposure_for_internal_user_with_metadata(
1793        &self,
1794        user_internal: &StatsigUserInternal<'_, '_>,
1795        layer_name: &InternedString,
1796        parameter_name: InternedString,
1797        exposure_metadata: Option<HashMap<String, Value>>,
1798    ) {
1799        let (details, evaluation) =
1800            self.evaluate_spec_raw(user_internal, layer_name.as_str(), &SpecType::Layer, None);
1801
1802        self.event_logger.enqueue(
1803            EnqueueExposureOp::layer_param_exposure(
1804                user_internal,
1805                layer_name,
1806                parameter_name,
1807                ExposureTrigger::Manual,
1808                details,
1809                evaluation,
1810            )
1811            .with_extra_metadata(exposure_metadata),
1812        );
1813    }
1814
1815    pub fn get_fields_needed_for_layer(&self, layer_name: &str) -> Vec<String> {
1816        self.spec_store
1817            .get_fields_used_for_entity(layer_name, SpecType::Layer)
1818    }
1819}
1820
1821// ------------------------------------------------------------------------------- [ Feat: ffi-support ]
1822
1823#[cfg(feature = "ffi-support")]
1824impl Statsig {
1825    pub fn use_raw_feature_gate_with_delayed_exposure<T>(
1826        &self,
1827        user: &StatsigUser,
1828        gate_name: &str,
1829        callback: impl FnOnce(&FeatureGateRaw<'_>) -> T,
1830    ) -> (T, Option<String>) {
1831        self.use_raw_feature_gate_with_delayed_exposure_with_options(
1832            user, gate_name, true, callback,
1833        )
1834    }
1835
1836    pub fn use_raw_feature_gate_with_delayed_exposure_with_options<T>(
1837        &self,
1838        user: &StatsigUser,
1839        gate_name: &str,
1840        include_local_override: bool,
1841        callback: impl FnOnce(&FeatureGateRaw<'_>) -> T,
1842    ) -> (T, Option<String>) {
1843        let user_internal = self.internalize_user(user);
1844        self.use_raw_feature_gate_impl(
1845            &user_internal,
1846            gate_name,
1847            FeatureGateEvaluationOptions::default(),
1848            include_local_override,
1849            RawExposureLogging::Delayed,
1850            callback,
1851        )
1852    }
1853
1854    #[doc(hidden)]
1855    pub fn use_raw_feature_gate_with_delayed_exposure_with_options_for_internal_user<T>(
1856        &self,
1857        user_internal: &StatsigUserInternal<'_, '_>,
1858        gate_name: &str,
1859        include_local_override: bool,
1860        callback: impl FnOnce(&FeatureGateRaw<'_>) -> T,
1861    ) -> (T, Option<String>) {
1862        self.use_raw_feature_gate_impl(
1863            user_internal,
1864            gate_name,
1865            FeatureGateEvaluationOptions::default(),
1866            include_local_override,
1867            RawExposureLogging::Delayed,
1868            callback,
1869        )
1870    }
1871
1872    pub fn use_raw_feature_gate_with_options<T>(
1873        &self,
1874        user: &StatsigUser,
1875        gate_name: &str,
1876        options: FeatureGateEvaluationOptions,
1877        callback: impl FnOnce(&FeatureGateRaw<'_>) -> T,
1878    ) -> T {
1879        let user_internal = self.internalize_user(user);
1880        self.use_raw_feature_gate_with_options_for_internal_user(
1881            &user_internal,
1882            gate_name,
1883            options,
1884            callback,
1885        )
1886    }
1887
1888    #[doc(hidden)]
1889    pub fn use_raw_feature_gate_with_options_for_internal_user<T>(
1890        &self,
1891        user_internal: &StatsigUserInternal<'_, '_>,
1892        gate_name: &str,
1893        options: FeatureGateEvaluationOptions,
1894        callback: impl FnOnce(&FeatureGateRaw<'_>) -> T,
1895    ) -> T {
1896        self.use_raw_feature_gate_impl(
1897            user_internal,
1898            gate_name,
1899            options,
1900            true,
1901            RawExposureLogging::Immediate,
1902            callback,
1903        )
1904        .0
1905    }
1906
1907    fn use_raw_feature_gate_impl<T>(
1908        &self,
1909        user_internal: &StatsigUserInternal<'_, '_>,
1910        gate_name: &str,
1911        options: FeatureGateEvaluationOptions,
1912        include_local_override: bool,
1913        exposure_logging: RawExposureLogging,
1914        callback: impl FnOnce(&FeatureGateRaw<'_>) -> T,
1915    ) -> (T, Option<String>) {
1916        use crate::evaluation::evaluator_result::result_to_gate_raw;
1917
1918        let interned_gate_name = InternedString::from_str_ref(gate_name);
1919        let (details, evaluation) = self.evaluate_spec_raw_with_include_local_override(
1920            user_internal,
1921            gate_name,
1922            &SpecType::Gate,
1923            None,
1924            include_local_override,
1925        );
1926
1927        let raw = result_to_gate_raw(gate_name, &details, evaluation.as_ref());
1928        let result = callback(&raw);
1929
1930        self.emit_gate_evaluated_parts(gate_name, details.reason.as_str(), evaluation.as_ref());
1931
1932        let token = match exposure_logging {
1933            RawExposureLogging::Immediate => {
1934                if options.disable_exposure_logging {
1935                    log_d!(TAG, "Exposure logging is disabled for gate {}", gate_name);
1936                    self.event_logger.increment_non_exposure_checks(gate_name);
1937                    None
1938                } else {
1939                    self.event_logger.enqueue(EnqueueExposureOp::gate_exposure(
1940                        user_internal,
1941                        &interned_gate_name,
1942                        ExposureTrigger::Auto,
1943                        details,
1944                        evaluation,
1945                    ));
1946                    None
1947                }
1948            }
1949            RawExposureLogging::Delayed => self
1950                .event_logger
1951                .prepare_event(EnqueueExposureOp::gate_exposure(
1952                    user_internal,
1953                    &interned_gate_name,
1954                    ExposureTrigger::Auto,
1955                    details,
1956                    evaluation,
1957                ))
1958                .map(|prepared| self.delayed_exposure_store.insert_prepared(prepared)),
1959        };
1960
1961        (result, token)
1962    }
1963
1964    pub fn use_raw_dynamic_config_with_delayed_exposure<T>(
1965        &self,
1966        user: &StatsigUser,
1967        dynamic_config_name: &str,
1968        callback: impl FnOnce(&DynamicConfigRaw<'_>) -> T,
1969    ) -> (T, Option<String>) {
1970        self.use_raw_dynamic_config_with_delayed_exposure_with_options(
1971            user,
1972            dynamic_config_name,
1973            true,
1974            callback,
1975        )
1976    }
1977
1978    pub fn use_raw_dynamic_config_with_delayed_exposure_with_options<T>(
1979        &self,
1980        user: &StatsigUser,
1981        dynamic_config_name: &str,
1982        include_local_override: bool,
1983        callback: impl FnOnce(&DynamicConfigRaw<'_>) -> T,
1984    ) -> (T, Option<String>) {
1985        let user_internal = self.internalize_user(user);
1986        self.use_raw_dynamic_config_impl(
1987            &user_internal,
1988            dynamic_config_name,
1989            DynamicConfigEvaluationOptions::default(),
1990            include_local_override,
1991            RawExposureLogging::Delayed,
1992            callback,
1993        )
1994    }
1995
1996    #[doc(hidden)]
1997    pub fn use_raw_dynamic_config_with_delayed_exposure_with_options_for_internal_user<T>(
1998        &self,
1999        user_internal: &StatsigUserInternal<'_, '_>,
2000        dynamic_config_name: &str,
2001        include_local_override: bool,
2002        callback: impl FnOnce(&DynamicConfigRaw<'_>) -> T,
2003    ) -> (T, Option<String>) {
2004        self.use_raw_dynamic_config_impl(
2005            user_internal,
2006            dynamic_config_name,
2007            DynamicConfigEvaluationOptions::default(),
2008            include_local_override,
2009            RawExposureLogging::Delayed,
2010            callback,
2011        )
2012    }
2013
2014    pub fn use_raw_dynamic_config_with_options<T>(
2015        &self,
2016        user: &StatsigUser,
2017        dynamic_config_name: &str,
2018        options: DynamicConfigEvaluationOptions,
2019        callback: impl FnOnce(&DynamicConfigRaw<'_>) -> T,
2020    ) -> T {
2021        let user_internal = self.internalize_user(user);
2022        self.use_raw_dynamic_config_with_options_for_internal_user(
2023            &user_internal,
2024            dynamic_config_name,
2025            options,
2026            callback,
2027        )
2028    }
2029
2030    #[doc(hidden)]
2031    pub fn use_raw_dynamic_config_with_options_for_internal_user<T>(
2032        &self,
2033        user_internal: &StatsigUserInternal<'_, '_>,
2034        dynamic_config_name: &str,
2035        options: DynamicConfigEvaluationOptions,
2036        callback: impl FnOnce(&DynamicConfigRaw<'_>) -> T,
2037    ) -> T {
2038        self.use_raw_dynamic_config_impl(
2039            user_internal,
2040            dynamic_config_name,
2041            options,
2042            true,
2043            RawExposureLogging::Immediate,
2044            callback,
2045        )
2046        .0
2047    }
2048
2049    fn use_raw_dynamic_config_impl<T>(
2050        &self,
2051        user_internal: &StatsigUserInternal<'_, '_>,
2052        dynamic_config_name: &str,
2053        options: DynamicConfigEvaluationOptions,
2054        include_local_override: bool,
2055        exposure_logging: RawExposureLogging,
2056        callback: impl FnOnce(&DynamicConfigRaw<'_>) -> T,
2057    ) -> (T, Option<String>) {
2058        use crate::evaluation::evaluator_result::result_to_dynamic_config_raw;
2059
2060        let interned_dynamic_config_name = InternedString::from_str_ref(dynamic_config_name);
2061        let disable_exposure_logging: bool = options.disable_exposure_logging;
2062
2063        let (details, evaluation) = self.evaluate_spec_raw_with_include_local_override(
2064            user_internal,
2065            dynamic_config_name,
2066            &SpecType::DynamicConfig,
2067            Some(disable_exposure_logging),
2068            include_local_override,
2069        );
2070
2071        let raw = result_to_dynamic_config_raw(dynamic_config_name, &details, evaluation.as_ref());
2072        let result = callback(&raw);
2073
2074        self.emit_dynamic_config_evaluated_parts(
2075            dynamic_config_name,
2076            details.reason.as_str(),
2077            evaluation.as_ref(),
2078        );
2079
2080        let token = match exposure_logging {
2081            RawExposureLogging::Immediate => {
2082                if disable_exposure_logging {
2083                    log_d!(
2084                        TAG,
2085                        "Exposure logging is disabled for Dynamic Config {}",
2086                        dynamic_config_name
2087                    );
2088                    self.event_logger
2089                        .increment_non_exposure_checks(dynamic_config_name);
2090                    None
2091                } else {
2092                    self.event_logger
2093                        .enqueue(EnqueueExposureOp::dynamic_config_exposure(
2094                            user_internal,
2095                            &interned_dynamic_config_name,
2096                            ExposureTrigger::Auto,
2097                            details,
2098                            evaluation,
2099                        ));
2100                    None
2101                }
2102            }
2103            RawExposureLogging::Delayed => self
2104                .event_logger
2105                .prepare_event(EnqueueExposureOp::dynamic_config_exposure(
2106                    user_internal,
2107                    &interned_dynamic_config_name,
2108                    ExposureTrigger::Auto,
2109                    details,
2110                    evaluation,
2111                ))
2112                .map(|prepared| self.delayed_exposure_store.insert_prepared(prepared)),
2113        };
2114
2115        (result, token)
2116    }
2117
2118    pub fn get_raw_experiment_by_group_name(
2119        &self,
2120        experiment_name: &str,
2121        group_name: &str,
2122    ) -> String {
2123        self.get_experiment_by_group_name_impl(
2124            experiment_name,
2125            group_name,
2126            |spec, rule, details| {
2127                experiment_group_to_raw_json(experiment_name, spec, rule, details)
2128            },
2129        )
2130    }
2131
2132    pub fn get_raw_experiment_by_group_id_advanced(
2133        &self,
2134        experiment_name: &str,
2135        group_id: &str,
2136    ) -> String {
2137        self.get_experiment_by_group_id_advanced_impl(
2138            experiment_name,
2139            group_id,
2140            |spec, rule, details| {
2141                experiment_group_to_raw_json(experiment_name, spec, rule, details)
2142            },
2143        )
2144    }
2145
2146    pub fn use_raw_experiment_with_delayed_exposure<T>(
2147        &self,
2148        user: &StatsigUser,
2149        experiment_name: &str,
2150        options: ExperimentEvaluationOptions,
2151        callback: impl FnOnce(&ExperimentRaw<'_>) -> T,
2152    ) -> (T, Option<String>) {
2153        self.use_raw_experiment_with_delayed_exposure_with_options(
2154            user,
2155            experiment_name,
2156            options,
2157            true,
2158            callback,
2159        )
2160    }
2161
2162    pub fn use_raw_experiment_with_delayed_exposure_with_options<T>(
2163        &self,
2164        user: &StatsigUser,
2165        experiment_name: &str,
2166        options: ExperimentEvaluationOptions,
2167        include_local_override: bool,
2168        callback: impl FnOnce(&ExperimentRaw<'_>) -> T,
2169    ) -> (T, Option<String>) {
2170        let user_internal = self.internalize_user(user);
2171        self.use_raw_experiment_impl(
2172            &user_internal,
2173            experiment_name,
2174            options,
2175            RawExperimentExposureOptions {
2176                include_local_override,
2177                exposure_logging: RawExposureLogging::Delayed,
2178                exposure_metadata: None,
2179            },
2180            callback,
2181        )
2182    }
2183
2184    #[doc(hidden)]
2185    pub fn use_raw_experiment_with_delayed_exposure_with_options_for_internal_user<T>(
2186        &self,
2187        user_internal: &StatsigUserInternal<'_, '_>,
2188        experiment_name: &str,
2189        options: ExperimentEvaluationOptions,
2190        include_local_override: bool,
2191        callback: impl FnOnce(&ExperimentRaw<'_>) -> T,
2192    ) -> (T, Option<String>) {
2193        self.use_raw_experiment_impl(
2194            user_internal,
2195            experiment_name,
2196            options,
2197            RawExperimentExposureOptions {
2198                include_local_override,
2199                exposure_logging: RawExposureLogging::Delayed,
2200                exposure_metadata: None,
2201            },
2202            callback,
2203        )
2204    }
2205
2206    pub fn use_raw_experiment_with_options<T>(
2207        &self,
2208        user: &StatsigUser,
2209        experiment_name: &str,
2210        options: ExperimentEvaluationOptions,
2211        callback: impl FnOnce(&ExperimentRaw<'_>) -> T,
2212    ) -> T {
2213        let user_internal = self.internalize_user(user);
2214        self.use_raw_experiment_with_options_for_internal_user(
2215            &user_internal,
2216            experiment_name,
2217            options,
2218            callback,
2219        )
2220    }
2221
2222    #[doc(hidden)]
2223    pub fn use_raw_experiment_with_options_for_internal_user<T>(
2224        &self,
2225        user_internal: &StatsigUserInternal<'_, '_>,
2226        experiment_name: &str,
2227        options: ExperimentEvaluationOptions,
2228        callback: impl FnOnce(&ExperimentRaw<'_>) -> T,
2229    ) -> T {
2230        self.use_raw_experiment_with_options_for_internal_user_with_metadata(
2231            user_internal,
2232            experiment_name,
2233            options,
2234            None,
2235            callback,
2236        )
2237    }
2238
2239    #[doc(hidden)]
2240    pub fn use_raw_experiment_with_options_for_internal_user_with_metadata<T>(
2241        &self,
2242        user_internal: &StatsigUserInternal<'_, '_>,
2243        experiment_name: &str,
2244        options: ExperimentEvaluationOptions,
2245        exposure_metadata: Option<HashMap<String, Value>>,
2246        callback: impl FnOnce(&ExperimentRaw<'_>) -> T,
2247    ) -> T {
2248        self.use_raw_experiment_impl(
2249            user_internal,
2250            experiment_name,
2251            options,
2252            RawExperimentExposureOptions {
2253                include_local_override: true,
2254                exposure_logging: RawExposureLogging::Immediate,
2255                exposure_metadata,
2256            },
2257            callback,
2258        )
2259        .0
2260    }
2261
2262    fn use_raw_experiment_impl<T>(
2263        &self,
2264        user_internal: &StatsigUserInternal<'_, '_>,
2265        experiment_name: &str,
2266        options: ExperimentEvaluationOptions,
2267        exposure_options: RawExperimentExposureOptions,
2268        callback: impl FnOnce(&ExperimentRaw<'_>) -> T,
2269    ) -> (T, Option<String>) {
2270        use crate::evaluation::evaluator_result::result_to_experiment_raw;
2271
2272        let RawExperimentExposureOptions {
2273            include_local_override,
2274            exposure_logging,
2275            exposure_metadata,
2276        } = exposure_options;
2277        let interned_experiment_name = InternedString::from_str_ref(experiment_name);
2278        let disable_exposure_logging: bool = options.disable_exposure_logging;
2279
2280        let (details, evaluation) = self.evaluate_spec_raw_with_include_local_override(
2281            user_internal,
2282            experiment_name,
2283            &SpecType::Experiment,
2284            Some(disable_exposure_logging),
2285            include_local_override,
2286        );
2287
2288        let (evaluation, details) =
2289            PersistentValuesManager::try_apply_sticky_value_to_raw_experiment(
2290                &self.persistent_values_manager,
2291                user_internal,
2292                &options,
2293                details,
2294                evaluation,
2295            );
2296
2297        let raw = result_to_experiment_raw(experiment_name, &details, evaluation.as_ref());
2298        let result = callback(&raw);
2299
2300        self.emit_experiment_evaluated_parts(
2301            experiment_name,
2302            details.reason.as_str(),
2303            evaluation.as_ref(),
2304        );
2305
2306        let token = match exposure_logging {
2307            RawExposureLogging::Immediate => {
2308                if disable_exposure_logging {
2309                    log_d!(
2310                        TAG,
2311                        "Exposure logging is disabled for Experiment {}",
2312                        experiment_name
2313                    );
2314                    self.event_logger
2315                        .increment_non_exposure_checks(experiment_name);
2316                    None
2317                } else {
2318                    self.event_logger.enqueue(
2319                        EnqueueExposureOp::experiment_exposure(
2320                            user_internal,
2321                            &interned_experiment_name,
2322                            ExposureTrigger::Auto,
2323                            details,
2324                            evaluation,
2325                        )
2326                        .with_extra_metadata(exposure_metadata),
2327                    );
2328                    None
2329                }
2330            }
2331            RawExposureLogging::Delayed => self
2332                .event_logger
2333                .prepare_event(
2334                    EnqueueExposureOp::experiment_exposure(
2335                        user_internal,
2336                        &interned_experiment_name,
2337                        ExposureTrigger::Auto,
2338                        details,
2339                        evaluation,
2340                    )
2341                    .with_extra_metadata(exposure_metadata),
2342                )
2343                .map(|prepared| self.delayed_exposure_store.insert_prepared(prepared)),
2344        };
2345
2346        (result, token)
2347    }
2348
2349    pub fn use_raw_layer_with_delayed_exposure<T>(
2350        &self,
2351        user: &StatsigUser,
2352        layer_name: &str,
2353        options: LayerEvaluationOptions,
2354        callback: impl FnOnce(&LayerRaw<'_>) -> T,
2355    ) -> (T, Option<String>) {
2356        self.use_raw_layer_with_delayed_exposure_with_options(
2357            user, layer_name, options, true, callback,
2358        )
2359    }
2360
2361    pub fn use_raw_layer_with_delayed_exposure_with_options<T>(
2362        &self,
2363        user: &StatsigUser,
2364        layer_name: &str,
2365        options: LayerEvaluationOptions,
2366        include_local_override: bool,
2367        callback: impl FnOnce(&LayerRaw<'_>) -> T,
2368    ) -> (T, Option<String>) {
2369        let user_internal = self.internalize_user(user);
2370        self.use_raw_layer_impl(
2371            &user_internal,
2372            layer_name,
2373            options,
2374            include_local_override,
2375            RawExposureLogging::Delayed,
2376            callback,
2377        )
2378    }
2379
2380    #[doc(hidden)]
2381    pub fn use_raw_layer_with_delayed_exposure_with_options_for_internal_user<T>(
2382        &self,
2383        user_internal: &StatsigUserInternal<'_, '_>,
2384        layer_name: &str,
2385        options: LayerEvaluationOptions,
2386        include_local_override: bool,
2387        callback: impl FnOnce(&LayerRaw<'_>) -> T,
2388    ) -> (T, Option<String>) {
2389        self.use_raw_layer_impl(
2390            user_internal,
2391            layer_name,
2392            options,
2393            include_local_override,
2394            RawExposureLogging::Delayed,
2395            callback,
2396        )
2397    }
2398
2399    pub fn use_raw_layer_with_options<T>(
2400        &self,
2401        user: &StatsigUser,
2402        layer_name: &str,
2403        options: LayerEvaluationOptions,
2404        callback: impl FnOnce(&LayerRaw<'_>) -> T,
2405    ) -> T {
2406        let user_internal = self.internalize_user(user);
2407        self.use_raw_layer_with_options_for_internal_user(
2408            &user_internal,
2409            layer_name,
2410            options,
2411            callback,
2412        )
2413    }
2414
2415    #[doc(hidden)]
2416    pub fn use_raw_layer_with_options_for_internal_user<T>(
2417        &self,
2418        user_internal: &StatsigUserInternal<'_, '_>,
2419        layer_name: &str,
2420        options: LayerEvaluationOptions,
2421        callback: impl FnOnce(&LayerRaw<'_>) -> T,
2422    ) -> T {
2423        self.use_raw_layer_impl(
2424            user_internal,
2425            layer_name,
2426            options,
2427            true,
2428            RawExposureLogging::Immediate,
2429            callback,
2430        )
2431        .0
2432    }
2433
2434    fn use_raw_layer_impl<T>(
2435        &self,
2436        user_internal: &StatsigUserInternal<'_, '_>,
2437        layer_name: &str,
2438        options: LayerEvaluationOptions,
2439        include_local_override: bool,
2440        exposure_logging: RawExposureLogging,
2441        callback: impl FnOnce(&LayerRaw<'_>) -> T,
2442    ) -> (T, Option<String>) {
2443        use crate::evaluation::evaluator_result::result_to_layer_raw;
2444
2445        let disable_exposure_logging: bool = options.disable_exposure_logging;
2446
2447        let (details, evaluation) = self.evaluate_spec_raw_with_include_local_override(
2448            user_internal,
2449            layer_name,
2450            &SpecType::Layer,
2451            Some(disable_exposure_logging),
2452            include_local_override,
2453        );
2454
2455        let (evaluation, details) = PersistentValuesManager::try_apply_sticky_value_to_raw_layer(
2456            &self.persistent_values_manager,
2457            user_internal,
2458            &options,
2459            &self.spec_store,
2460            &self.ops_stats,
2461            details,
2462            evaluation,
2463        );
2464
2465        let raw = result_to_layer_raw(
2466            user_internal,
2467            layer_name,
2468            options,
2469            &details,
2470            evaluation.as_ref(),
2471        );
2472
2473        let token = match exposure_logging {
2474            RawExposureLogging::Immediate => {
2475                if disable_exposure_logging {
2476                    log_d!(TAG, "Exposure logging is disabled for Layer {}", layer_name);
2477                    self.event_logger.increment_non_exposure_checks(layer_name);
2478                }
2479                None
2480            }
2481            RawExposureLogging::Delayed => {
2482                if raw.disable_exposure || self.options.disable_all_logging == Some(true) {
2483                    None
2484                } else {
2485                    Some(
2486                        self.delayed_exposure_store
2487                            .insert_layer(crate::statsig_types_raw::PartialLayerRaw::from(&raw)),
2488                    )
2489                }
2490            }
2491        };
2492
2493        let result = callback(&raw);
2494
2495        self.emit_layer_evaluated_parts(layer_name, details.reason.as_str(), evaluation.as_ref());
2496
2497        (result, token)
2498    }
2499
2500    pub fn bulk_evaluate_with_delayed_exposures(
2501        &self,
2502        user: &StatsigUser,
2503        options: BulkEvaluationOptions,
2504    ) -> BulkEvaluationResponse {
2505        let mut response = BulkEvaluationResponse::default();
2506        let resolved = self.resolve_bulk_evaluation_options(options);
2507        let include_local_override = resolved.include_local_override;
2508
2509        macro_rules! insert_bulk_evaluations {
2510            ($destination:expr, $names:expr, |$name:ident| $evaluate:expr) => {
2511                for $name in $names {
2512                    let (mut evaluation, exposure_token) = $evaluate;
2513                    evaluation.common.exposure_token = exposure_token;
2514                    $destination.insert($name, evaluation);
2515                }
2516            };
2517        }
2518
2519        insert_bulk_evaluations!(response.feature_gates, resolved.feature_gates, |name| self
2520            .use_raw_feature_gate_with_delayed_exposure_with_options(
2521                user,
2522                &name,
2523                include_local_override,
2524                |raw| BulkFeatureGateEvaluation {
2525                    value: raw.value,
2526                    common: bulk_common_fields_from_raw_parts(
2527                        interned_rule_id_from_raw(&raw.rule_id),
2528                        raw.id_type.cloned(),
2529                        raw.details.clone(),
2530                    ),
2531                },
2532            ));
2533
2534        insert_bulk_evaluations!(response.dynamic_configs, resolved.dynamic_configs, |name| {
2535            self.use_raw_dynamic_config_with_delayed_exposure_with_options(
2536                user,
2537                &name,
2538                include_local_override,
2539                |raw| BulkDynamicConfigEvaluation {
2540                    value: raw
2541                        .value
2542                        .and_then(|value| value.get_json())
2543                        .unwrap_or_default(),
2544                    common: bulk_common_fields_from_raw_parts(
2545                        interned_rule_id_from_raw(&raw.rule_id),
2546                        raw.id_type.cloned(),
2547                        raw.details.clone(),
2548                    ),
2549                },
2550            )
2551        });
2552
2553        insert_bulk_evaluations!(response.experiments, resolved.experiments, |name| self
2554            .use_raw_experiment_with_delayed_exposure_with_options(
2555                user,
2556                &name,
2557                ExperimentEvaluationOptions::default(),
2558                include_local_override,
2559                |raw| BulkExperimentEvaluation {
2560                    value: raw
2561                        .value
2562                        .and_then(|value| value.get_json())
2563                        .unwrap_or_default(),
2564                    group_name: raw.group_name.map(|name| name.unperformant_to_string()),
2565                    common: bulk_common_fields_from_raw_parts(
2566                        interned_rule_id_from_raw(&raw.rule_id),
2567                        raw.id_type.cloned(),
2568                        raw.details.clone(),
2569                    ),
2570                },
2571            ));
2572
2573        insert_bulk_evaluations!(response.layer_configs, resolved.layers, |name| self
2574            .use_raw_layer_with_delayed_exposure_with_options(
2575                user,
2576                &name,
2577                LayerEvaluationOptions::default(),
2578                include_local_override,
2579                |raw| BulkLayerEvaluation {
2580                    value: raw
2581                        .value
2582                        .and_then(|value| value.get_json())
2583                        .unwrap_or_default(),
2584                    group_name: raw.group_name.map(|name| name.unperformant_to_string()),
2585                    allocated_experiment_name: raw
2586                        .allocated_experiment_name
2587                        .map(|name| name.unperformant_to_string()),
2588                    common: bulk_common_fields_from_raw_parts(
2589                        interned_rule_id_from_raw(&raw.rule_id),
2590                        raw.id_type.cloned(),
2591                        raw.details.clone(),
2592                    ),
2593                },
2594            ));
2595
2596        response
2597    }
2598
2599    pub fn resolve_bulk_evaluation_options(
2600        &self,
2601        options: BulkEvaluationOptions,
2602    ) -> ResolvedBulkEvaluationOptions {
2603        let feature_gates = options.feature_gate_filter.unwrap_or_else(|| {
2604            self.spec_store
2605                .unperformant_keys_entity_filter("feature_gates", "feature_gate")
2606        });
2607        let dynamic_configs = options.dynamic_config_filter.unwrap_or_else(|| {
2608            let mut dynamic_config_names = self
2609                .spec_store
2610                .unperformant_keys_entity_filter("dynamic_configs", "dynamic_config");
2611            dynamic_config_names.extend(
2612                self.spec_store
2613                    .unperformant_keys_entity_filter("dynamic_configs", "autotune"),
2614            );
2615            dynamic_config_names
2616        });
2617        let experiments = options.experiment_filter.unwrap_or_else(|| {
2618            self.spec_store
2619                .unperformant_keys_entity_filter("dynamic_configs", "experiment")
2620        });
2621        let layers = options.layer_filter.unwrap_or_else(|| {
2622            self.spec_store
2623                .unperformant_keys_entity_filter("layer_configs", "*")
2624        });
2625
2626        ResolvedBulkEvaluationOptions {
2627            feature_gates,
2628            dynamic_configs,
2629            experiments,
2630            layers,
2631            include_local_override: options.include_local_override,
2632        }
2633    }
2634
2635    pub fn log_delayed_exposure(&self, token: &str) -> bool {
2636        self.delayed_exposure_store
2637            .log_delayed_exposure(token, &self.event_logger)
2638    }
2639
2640    pub fn log_delayed_layer_parameter_exposure(&self, token: &str, parameter_name: &str) -> bool {
2641        self.delayed_exposure_store
2642            .log_delayed_layer_parameter_exposure(token, parameter_name, &self.event_logger)
2643    }
2644
2645    pub fn release_delayed_exposure(&self, token: &str) -> bool {
2646        self.delayed_exposure_store.release(token)
2647    }
2648
2649    pub fn release_delayed_exposures(&self, tokens: &[String]) -> usize {
2650        self.delayed_exposure_store.release_many(tokens)
2651    }
2652
2653    pub fn log_layer_param_exposure_from_raw(&self, raw: String, param_name: String) {
2654        use crate::statsig_types_raw::PartialLayerRaw;
2655
2656        let partial_raw = match serde_json::from_str::<PartialLayerRaw>(&raw) {
2657            Ok(partial_raw) => partial_raw,
2658            Err(e) => {
2659                log_e!(TAG, "Failed to parse partial layer raw: {}", e);
2660                return;
2661            }
2662        };
2663
2664        self.log_layer_param_exposure_from_partial_raw(partial_raw, param_name);
2665    }
2666
2667    pub fn log_layer_param_exposure_from_partial_raw(
2668        &self,
2669        partial_raw: crate::statsig_types_raw::PartialLayerRaw,
2670        param_name: String,
2671    ) {
2672        self.log_layer_param_exposure_from_partial_raw_with_metadata(partial_raw, param_name, None);
2673    }
2674
2675    pub fn log_layer_param_exposure_from_partial_raw_with_metadata(
2676        &self,
2677        partial_raw: crate::statsig_types_raw::PartialLayerRaw,
2678        param_name: String,
2679        exposure_metadata: Option<HashMap<String, Value>>,
2680    ) {
2681        if partial_raw.disable_exposure {
2682            self.event_logger
2683                .increment_non_exposure_checks(&partial_raw.name);
2684            return;
2685        }
2686
2687        let interned_parameter_name = InternedString::from_string(param_name);
2688
2689        self.event_logger.enqueue(
2690            EnqueueExposureOp::layer_param_exposure_from_partial_raw(
2691                interned_parameter_name,
2692                ExposureTrigger::Auto,
2693                partial_raw,
2694            )
2695            .with_extra_metadata(exposure_metadata),
2696        );
2697    }
2698}
2699
2700#[cfg(feature = "ffi-support")]
2701fn bulk_common_fields_from_raw_parts(
2702    rule_id: InternedString,
2703    id_type: Option<InternedString>,
2704    details: EvaluationDetails,
2705) -> BulkEvaluationCommonFields {
2706    BulkEvaluationCommonFields {
2707        rule_id,
2708        id_type,
2709        details,
2710        exposure_token: None,
2711    }
2712}
2713
2714#[cfg(feature = "ffi-support")]
2715fn interned_rule_id_from_raw(
2716    rule_id: &crate::statsig_types_raw::SuffixedRuleId<'_>,
2717) -> InternedString {
2718    match rule_id.try_as_unprefixed_str() {
2719        Some(_) => rule_id.rule_id.clone(),
2720        None => InternedString::from_string(rule_id.unperformant_to_string()),
2721    }
2722}
2723
2724// ------------------------------------------------------------------------------- [ Internal ]
2725
2726impl Statsig {
2727    pub(crate) fn get_from_statsig_env(&self, key: &str) -> Option<DynamicValue> {
2728        if let Some(env) = &self.statsig_environment {
2729            return env.get(key).cloned();
2730        }
2731
2732        if let Some(fallback_env) = self
2733            .fallback_environment
2734            .try_lock_for(Duration::from_secs(5))
2735        {
2736            if let Some(env) = &*fallback_env {
2737                return env.get(key).cloned();
2738            }
2739        }
2740
2741        None
2742    }
2743
2744    pub(crate) fn get_value_from_global_custom_fields(&self, key: &str) -> Option<&DynamicValue> {
2745        if let Some(env) = &self.options.global_custom_fields {
2746            return env.get(key);
2747        }
2748
2749        None
2750    }
2751
2752    pub(crate) fn use_global_custom_fields<T>(
2753        &self,
2754        f: impl FnOnce(Option<&HashMap<String, DynamicValue>>) -> T,
2755    ) -> T {
2756        f(self.options.global_custom_fields.as_ref())
2757    }
2758
2759    pub(crate) fn use_statsig_env<T>(
2760        &self,
2761        f: impl FnOnce(Option<&HashMap<String, DynamicValue>>) -> T,
2762    ) -> T {
2763        if let Some(env) = &self.statsig_environment {
2764            return f(Some(env));
2765        }
2766
2767        if let Some(fallback_env) = self
2768            .fallback_environment
2769            .try_lock_for(Duration::from_secs(5))
2770        {
2771            if let Some(env) = &*fallback_env {
2772                return f(Some(env));
2773            }
2774        }
2775
2776        f(None)
2777    }
2778}
2779
2780// ------------------------------------------------------------------------------- [ Private ]
2781
2782impl Statsig {
2783    async fn start_background_tasks(
2784        statsig_runtime: Arc<StatsigRuntime>,
2785        id_lists_adapter: Option<Arc<dyn IdListsAdapter>>,
2786        specs_adapter: Arc<dyn SpecsAdapter>,
2787        ops_stats: Arc<OpsStatsForInstance>,
2788        bg_tasks_started: Arc<AtomicBool>,
2789    ) -> bool {
2790        if bg_tasks_started.load(Ordering::SeqCst) {
2791            return true;
2792        }
2793
2794        let mut success = true;
2795
2796        if let Some(adapter) = &id_lists_adapter {
2797            if let Err(e) = adapter
2798                .clone()
2799                .schedule_background_sync(&statsig_runtime)
2800                .await
2801            {
2802                success = false;
2803                log_w!(TAG, "Failed to schedule idlist background job {}", e);
2804            }
2805        }
2806
2807        if let Err(e) = specs_adapter
2808            .clone()
2809            .schedule_background_sync(&statsig_runtime)
2810            .await
2811        {
2812            success = false;
2813            log_error_to_statsig_and_console!(
2814                ops_stats,
2815                TAG,
2816                StatsigErr::SpecsAdapterSkipPoll(format!(
2817                    "Failed to schedule specs adapter background job: {e}"
2818                ))
2819            );
2820        }
2821
2822        bg_tasks_started.store(true, Ordering::SeqCst);
2823
2824        success
2825    }
2826
2827    async fn apply_timeout_to_init(
2828        &self,
2829        timeout_ms: u64,
2830    ) -> Result<InitializeDetails, StatsigErr> {
2831        let timeout = Duration::from_millis(timeout_ms);
2832
2833        let init_future = self.initialize_impl_with_details();
2834        let timeout_future = sleep(timeout);
2835
2836        let statsig_runtime = self.statsig_runtime.clone();
2837        let id_lists_adapter = self.id_lists_adapter.inner.clone();
2838        let specs_adapter = self.specs_adapter.inner.clone();
2839        let ops_stats = self.ops_stats.clone();
2840        let background_tasks_started = self.background_tasks_started.clone();
2841        // Create another clone specifically for the closure
2842        let statsig_runtime_for_closure = statsig_runtime.clone();
2843
2844        tokio::select! {
2845            result = init_future => {
2846                result
2847            },
2848            _ = timeout_future => {
2849                statsig_runtime.spawn(
2850                    "start_background_tasks",
2851                    |_shutdown_notify| async move {
2852                        Self::start_background_tasks(
2853                            statsig_runtime_for_closure,
2854                            id_lists_adapter,
2855                            specs_adapter,
2856                            ops_stats,
2857                            background_tasks_started,
2858                        ).await;
2859                    }
2860                )?;
2861                Ok(InitializeDetails::from_timeout_failure(timeout_ms))
2862            },
2863        }
2864    }
2865
2866    async fn initialize_impl_with_details(&self) -> Result<InitializeDetails, StatsigErr> {
2867        let start_time = Instant::now();
2868        self.spec_store.set_source(SpecsSource::Loading);
2869        self.specs_adapter.inner.initialize(self.spec_store.clone());
2870        let use_third_party_ua_parser = self.should_user_third_party_parser();
2871
2872        let mut error_message = None;
2873        let mut id_list_ready = None;
2874
2875        let init_country_lookup = if !self.options.disable_country_lookup.unwrap_or_default() {
2876            Some(self.statsig_runtime.spawn(INIT_IP_TAG, |_| async {
2877                CountryLookup::load_country_lookup();
2878            }))
2879        } else {
2880            None
2881        };
2882
2883        let init_ua = if use_third_party_ua_parser {
2884            Some(self.statsig_runtime.spawn(INIT_UA_TAG, |_| async {
2885                UserAgentParser::load_parser();
2886            }))
2887        } else {
2888            None
2889        };
2890
2891        let init_res = match self
2892            .specs_adapter
2893            .inner
2894            .clone()
2895            .start(&self.statsig_runtime)
2896            .await
2897        {
2898            Ok(()) => Ok(()),
2899            Err(e) => {
2900                self.spec_store.set_source(SpecsSource::NoValues);
2901                error_message = Some(format!("Failed to start specs adapter: {e}"));
2902                Err(e)
2903            }
2904        };
2905
2906        if let Some(adapter) = &self.id_lists_adapter.inner {
2907            match adapter
2908                .clone()
2909                .start(&self.statsig_runtime, self.spec_store.clone())
2910                .await
2911            {
2912                Ok(()) => {
2913                    id_list_ready = Some(true);
2914                }
2915                Err(e) => {
2916                    id_list_ready = Some(false);
2917                    error_message.get_or_insert_with(|| format!("Failed to sync ID lists: {e}"));
2918                }
2919            }
2920        }
2921
2922        if let Err(e) = self
2923            .event_logging_adapter
2924            .clone()
2925            .start(&self.statsig_runtime)
2926            .await
2927        {
2928            log_error_to_statsig_and_console!(
2929                self.ops_stats.clone(),
2930                TAG,
2931                StatsigErr::UnstartedAdapter(format!("Failed to start event logging adapter: {e}"))
2932            );
2933        }
2934
2935        let spec_info = self.spec_store.get_current_specs_info();
2936        let duration = start_time.elapsed().as_millis() as u64;
2937
2938        self.set_default_environment_from_server();
2939
2940        if self.options.wait_for_country_lookup_init.unwrap_or(false) {
2941            match init_country_lookup {
2942                Some(Ok(task_id)) => {
2943                    let _ = self
2944                        .statsig_runtime
2945                        .await_join_handle(INIT_IP_TAG, &task_id)
2946                        .await;
2947                }
2948                Some(Err(e)) => {
2949                    log_error_to_statsig_and_console!(
2950                        self.ops_stats.clone(),
2951                        TAG,
2952                        StatsigErr::UnstartedAdapter(format!(
2953                            "Failed to spawn country lookup task: {e}"
2954                        ))
2955                    );
2956                }
2957                _ => {}
2958            }
2959        }
2960        if self.options.wait_for_user_agent_init.unwrap_or(false) {
2961            match init_ua {
2962                Some(Ok(task_id)) => {
2963                    let _ = self
2964                        .statsig_runtime
2965                        .await_join_handle(INIT_UA_TAG, &task_id)
2966                        .await;
2967                }
2968                Some(Err(e)) => {
2969                    log_error_to_statsig_and_console!(
2970                        self.ops_stats.clone(),
2971                        TAG,
2972                        StatsigErr::UnstartedAdapter(format!(
2973                            "Failed to spawn user agent parser task: {e}"
2974                        ))
2975                    );
2976                }
2977                _ => {}
2978            }
2979        }
2980
2981        let error = init_res.clone().err();
2982
2983        let success = Self::start_background_tasks(
2984            self.statsig_runtime.clone(),
2985            self.id_lists_adapter.inner.clone(),
2986            self.specs_adapter.inner.clone(),
2987            self.ops_stats.clone(),
2988            self.background_tasks_started.clone(),
2989        )
2990        .await;
2991
2992        Ok(InitializeDetails::new(
2993            success,
2994            duration,
2995            spec_info,
2996            id_list_ready,
2997            error,
2998        ))
2999    }
3000
3001    fn log_init_details(&self, init_details: &Result<InitializeDetails, StatsigErr>) {
3002        match init_details {
3003            Ok(details) => {
3004                self.log_init_finish(
3005                    details.init_success,
3006                    &None,
3007                    &details.duration_ms,
3008                    &self.spec_store.get_current_specs_info(),
3009                );
3010                if let Some(failure) = &details.failure_details {
3011                    log_error_to_statsig_and_console!(
3012                        self.ops_stats,
3013                        TAG,
3014                        StatsigErr::InitializationError(failure.reason.clone())
3015                    );
3016                }
3017            }
3018            Err(err) => {
3019                // we store errors on init details so we should never return error and thus do not need to log
3020                log_w!(TAG, "Initialization error: {:?}", err);
3021            }
3022        }
3023    }
3024
3025    fn create_standard_eval_context<'a>(
3026        &'a self,
3027        user_internal: &'a StatsigUserInternal,
3028        data: &'a SpecStoreData,
3029        app_id: Option<&'a DynamicValue>,
3030        override_adapter: Option<&'a Arc<dyn OverrideAdapter>>,
3031        disable_exposure_logging: bool,
3032    ) -> EvaluatorContext<'a> {
3033        EvaluatorContext::new(
3034            user_internal,
3035            &data.snapshot,
3036            IdListResolution::MapLookup(data.id_lists.as_ref()),
3037            &self.hashing,
3038            app_id,
3039            override_adapter,
3040            self.should_user_third_party_parser(),
3041            Some(self),
3042            disable_exposure_logging,
3043        )
3044    }
3045
3046    fn create_gcir_eval_context<'a>(
3047        &'a self,
3048        user_internal: &'a StatsigUserInternal,
3049        data: &'a SpecStoreData,
3050        options: &'a ClientInitResponseOptions,
3051    ) -> EvaluatorContext<'a> {
3052        let app_id = select_app_id_for_gcir(options, &data.snapshot, &self.hashing);
3053        let override_adapter = match options.include_local_overrides {
3054            Some(true) => self.override_adapter.as_ref(),
3055            _ => None,
3056        };
3057
3058        EvaluatorContext::new(
3059            user_internal,
3060            &data.snapshot,
3061            IdListResolution::MapLookup(data.id_lists.as_ref()),
3062            &self.hashing,
3063            app_id,
3064            override_adapter,
3065            self.should_user_third_party_parser(),
3066            None,
3067            true,
3068        )
3069    }
3070
3071    fn evaluate_spec_raw(
3072        &self,
3073        user_internal: &StatsigUserInternal,
3074        spec_name: &str,
3075        spec_type: &SpecType,
3076        disable_exposure_logging: Option<bool>,
3077    ) -> (EvaluationDetails, Option<EvaluatorResult>) {
3078        self.evaluate_spec_raw_with_include_local_override(
3079            user_internal,
3080            spec_name,
3081            spec_type,
3082            disable_exposure_logging,
3083            true,
3084        )
3085    }
3086
3087    fn evaluate_spec_raw_with_include_local_override(
3088        &self,
3089        user_internal: &StatsigUserInternal,
3090        spec_name: &str,
3091        spec_type: &SpecType,
3092        disable_exposure_logging: Option<bool>,
3093        include_local_override: bool,
3094    ) -> (EvaluationDetails, Option<EvaluatorResult>) {
3095        let data = self.spec_store.load_data();
3096
3097        let override_adapter = if include_local_override {
3098            self.override_adapter.as_ref()
3099        } else {
3100            None
3101        };
3102
3103        let mut context = self.create_standard_eval_context(
3104            user_internal,
3105            &data,
3106            data.snapshot.app_id.as_ref(),
3107            override_adapter,
3108            disable_exposure_logging.unwrap_or(false),
3109        );
3110
3111        match Self::evaluate_with_details(&mut context, &data, spec_name, spec_type) {
3112            Ok(eval_details) => (eval_details, Some(context.result)),
3113            Err(e) => {
3114                log_error_to_statsig_and_console!(
3115                    &self.ops_stats,
3116                    TAG,
3117                    StatsigErr::EvaluationError(e.to_string())
3118                );
3119                (EvaluationDetails::error(&e.to_string()), None)
3120            }
3121        }
3122    }
3123
3124    #[allow(clippy::too_many_arguments)]
3125    fn evaluate_spec<T>(
3126        &self,
3127        user_internal: &StatsigUserInternal,
3128        spec_name: &str,
3129        make_empty_result: impl FnOnce(EvaluationDetails) -> T,
3130        make_result: impl FnOnce(EvaluatorResult, EvaluationDetails) -> T,
3131        spec_type: &SpecType,
3132        disable_exposure_logging: Option<bool>,
3133    ) -> T {
3134        let data = self.spec_store.load_data();
3135
3136        let mut context = self.create_standard_eval_context(
3137            user_internal,
3138            &data,
3139            data.snapshot.app_id.as_ref(),
3140            self.override_adapter.as_ref(),
3141            disable_exposure_logging.unwrap_or(false),
3142        );
3143
3144        match Self::evaluate_with_details(&mut context, &data, spec_name, spec_type) {
3145            Ok(eval_details) => make_result(context.result, eval_details),
3146            Err(e) => {
3147                log_error_to_statsig_and_console!(
3148                    &self.ops_stats,
3149                    TAG,
3150                    StatsigErr::EvaluationError(e.to_string())
3151                );
3152                make_empty_result(EvaluationDetails::error(&e.to_string()))
3153            }
3154        }
3155    }
3156
3157    fn evaluate_with_details(
3158        ctx: &mut EvaluatorContext,
3159        spec_store_data: &SpecStoreData,
3160        spec_name: &str,
3161        spec_type: &SpecType,
3162    ) -> Result<EvaluationDetails, StatsigErr> {
3163        let recognition = Evaluator::evaluate(ctx, spec_name, spec_type)?;
3164
3165        if recognition == Recognition::Unrecognized {
3166            return Ok(EvaluationDetails::unrecognized(
3167                &spec_store_data.source,
3168                spec_store_data.lcut(),
3169                spec_store_data.time_received_at,
3170            ));
3171        }
3172
3173        if let Some(reason) = ctx.result.override_reason {
3174            return Ok(EvaluationDetails::recognized_but_overridden(
3175                spec_store_data.lcut(),
3176                spec_store_data.time_received_at,
3177                reason,
3178                ctx.result.version,
3179            ));
3180        }
3181
3182        Ok(EvaluationDetails::recognized(
3183            &spec_store_data.source,
3184            spec_store_data.lcut(),
3185            spec_store_data.time_received_at,
3186            &ctx.result,
3187        ))
3188    }
3189
3190    fn stringify_gcir_response<T: Serialize>(
3191        &self,
3192        input: Result<T, StatsigErr>,
3193        fallback: impl FnOnce() -> T,
3194    ) -> String {
3195        match input {
3196            Ok(value) => serde_json::to_string(&value).unwrap_or_default(),
3197            Err(e) => {
3198                log_error_to_statsig_and_console!(
3199                    &self.ops_stats,
3200                    TAG,
3201                    StatsigErr::GCIRError(e.to_string())
3202                );
3203                serde_json::to_string(&fallback()).unwrap_or_default()
3204            }
3205        }
3206    }
3207
3208    fn get_gate_evaluation(
3209        &self,
3210        user_internal: &StatsigUserInternal,
3211        gate_name: &str,
3212        disable_exposure_logging: Option<bool>,
3213    ) -> (EvaluationDetails, Option<GateEvaluation>) {
3214        self.evaluate_spec(
3215            user_internal,
3216            gate_name,
3217            |eval_details| (eval_details, None),
3218            |mut result, eval_details| {
3219                let evaluation = result_to_gate_eval(gate_name, &mut result);
3220                (eval_details, Some(evaluation))
3221            },
3222            &SpecType::Gate,
3223            disable_exposure_logging,
3224        )
3225    }
3226
3227    fn get_dynamic_config_impl(
3228        &self,
3229        user_internal: &StatsigUserInternal,
3230        config_name: &str,
3231        disable_exposure_logging: Option<bool>,
3232    ) -> DynamicConfig {
3233        self.evaluate_spec(
3234            user_internal,
3235            config_name,
3236            |eval_details| make_dynamic_config(config_name, None, eval_details),
3237            |mut result, eval_details| {
3238                let evaluation = result_to_dynamic_config_eval(config_name, &mut result);
3239                make_dynamic_config(config_name, Some(evaluation), eval_details)
3240            },
3241            &SpecType::DynamicConfig,
3242            disable_exposure_logging,
3243        )
3244    }
3245
3246    fn get_experiment_impl(
3247        &self,
3248        user_internal: &StatsigUserInternal,
3249        experiment_name: &str,
3250        disable_exposure_logging: Option<bool>,
3251    ) -> Experiment {
3252        self.evaluate_spec(
3253            user_internal,
3254            experiment_name,
3255            |eval_details| make_experiment(experiment_name, None, eval_details),
3256            |mut result, eval_details| {
3257                let evaluation = result_to_experiment_eval(experiment_name, None, &mut result);
3258                make_experiment(experiment_name, Some(evaluation), eval_details)
3259            },
3260            &SpecType::Experiment,
3261            disable_exposure_logging,
3262        )
3263    }
3264
3265    fn get_layer_impl(
3266        &self,
3267        user_internal: StatsigUserInternal,
3268        layer_name: &str,
3269        evaluation_options: LayerEvaluationOptions,
3270    ) -> Layer {
3271        let disable_exposure_logging = evaluation_options.disable_exposure_logging;
3272
3273        if disable_exposure_logging {
3274            self.event_logger.increment_non_exposure_checks(layer_name);
3275        }
3276
3277        let mut layer = self.evaluate_spec(
3278            &user_internal,
3279            layer_name,
3280            |eval_details| {
3281                make_layer(
3282                    user_internal.to_loggable(),
3283                    layer_name,
3284                    None,
3285                    eval_details,
3286                    None,
3287                    disable_exposure_logging,
3288                )
3289            },
3290            |mut result, eval_details| {
3291                let evaluation = result_to_layer_eval(layer_name, &mut result);
3292                let event_logger_ptr = Arc::downgrade(&self.event_logger);
3293
3294                make_layer(
3295                    user_internal.to_loggable(),
3296                    layer_name,
3297                    Some(evaluation),
3298                    eval_details,
3299                    Some(event_logger_ptr),
3300                    disable_exposure_logging,
3301                )
3302            },
3303            &SpecType::Layer,
3304            Some(evaluation_options.disable_exposure_logging),
3305        );
3306
3307        layer = PersistentValuesManager::try_apply_sticky_value_to_layer(
3308            &self.persistent_values_manager,
3309            &user_internal,
3310            &evaluation_options,
3311            &self.spec_store,
3312            &self.ops_stats,
3313            layer,
3314        );
3315
3316        self.emit_layer_evaluated(&layer);
3317
3318        layer
3319    }
3320
3321    fn internalize_user<'s, 'u>(&'s self, user: &'u StatsigUser) -> StatsigUserInternal<'s, 'u> {
3322        StatsigUserInternal::new(user, Some(self))
3323    }
3324
3325    fn set_default_environment_from_server(&self) {
3326        let data = self.spec_store.load_data();
3327
3328        if let Some(default_env) = data.snapshot.default_environment.as_ref() {
3329            let env_map = HashMap::from([("tier".to_string(), dyn_value!(default_env.as_str()))]);
3330
3331            match self
3332                .fallback_environment
3333                .try_lock_for(Duration::from_secs(5))
3334            {
3335                Some(mut fallback_env) => {
3336                    *fallback_env = Some(env_map);
3337                }
3338                None => {
3339                    log_e!(TAG, "Failed to lock fallback_environment");
3340                }
3341            }
3342        }
3343    }
3344
3345    fn log_init_finish(
3346        &self,
3347        success: bool,
3348        error_message: &Option<String>,
3349        duration_ms: &u64,
3350        specs_info: &SpecsInfo,
3351    ) {
3352        let is_store_populated = specs_info.source != SpecsSource::NoValues;
3353        let source_str = specs_info.source.to_string();
3354        let event = ObservabilityEvent::new_event(
3355            MetricType::Dist,
3356            "initialization".to_string(),
3357            *duration_ms as f64,
3358            Some(HashMap::from([
3359                ("init_success".to_owned(), success.to_string()),
3360                ("source".to_owned(), source_str.clone()),
3361                ("store_populated".to_owned(), is_store_populated.to_string()),
3362                (
3363                    "sdk_key".to_owned(),
3364                    get_loggable_sdk_key(self.sdk_key.as_str()),
3365                ),
3366                (
3367                    "init_source_api".to_owned(),
3368                    specs_info.source_api.clone().unwrap_or_default(),
3369                ),
3370            ])),
3371        );
3372
3373        self.ops_stats.log(event);
3374        self.ops_stats.add_marker(
3375            {
3376                let marker = Marker::new(KeyType::Overall, ActionType::End, None)
3377                    .with_is_success(success)
3378                    .with_config_spec_ready(specs_info.source != SpecsSource::NoValues)
3379                    .with_source(source_str);
3380
3381                if let Some(msg) = &error_message {
3382                    marker.with_message(msg.to_string())
3383                } else {
3384                    marker
3385                }
3386            },
3387            Some(ContextType::Initialize),
3388        );
3389        self.ops_stats
3390            .enqueue_diagnostics_event(None, Some(ContextType::Initialize));
3391    }
3392
3393    fn should_user_third_party_parser(&self) -> bool {
3394        self.options.use_third_party_ua_parser.unwrap_or(false)
3395    }
3396}
3397
3398fn initialize_event_logging_adapter(
3399    sdk_key: &str,
3400    options: &StatsigOptions,
3401) -> Arc<dyn EventLoggingAdapter> {
3402    options
3403        .event_logging_adapter
3404        .clone()
3405        .unwrap_or_else(|| Arc::new(StatsigHttpEventLoggingAdapter::new(sdk_key, Some(options))))
3406}
3407
3408fn initialize_specs_adapter(
3409    sdk_key: &str,
3410    data_store_key: &str,
3411    options: &StatsigOptions,
3412) -> SpecsAdapterHousing {
3413    if let Some(adapter) = options.specs_adapter.clone() {
3414        log_d!(TAG, "Using provided SpecsAdapter: {}", sdk_key);
3415        return SpecsAdapterHousing {
3416            inner: adapter,
3417            as_default_adapter: None,
3418        };
3419    }
3420
3421    if let Some(adapter_config) = options.spec_adapters_config.clone() {
3422        let adapter = Arc::new(StatsigCustomizedSpecsAdapter::new_from_config(
3423            sdk_key,
3424            data_store_key,
3425            adapter_config,
3426            options,
3427        ));
3428
3429        return SpecsAdapterHousing {
3430            inner: adapter,
3431            as_default_adapter: None,
3432        };
3433    }
3434
3435    if let Some(data_store) = options.data_store.clone() {
3436        let adapter = Arc::new(StatsigCustomizedSpecsAdapter::new_from_data_store(
3437            sdk_key,
3438            data_store_key,
3439            data_store,
3440            options,
3441        ));
3442
3443        return SpecsAdapterHousing {
3444            inner: adapter,
3445            as_default_adapter: None,
3446        };
3447    }
3448
3449    let adapter = Arc::new(StatsigHttpSpecsAdapter::new(sdk_key, Some(options), None));
3450
3451    SpecsAdapterHousing {
3452        inner: adapter.clone(),
3453        as_default_adapter: Some(adapter),
3454    }
3455}
3456
3457fn initialize_id_lists_adapter(sdk_key: &str, options: &StatsigOptions) -> IdListsAdapterHousing {
3458    if let Some(id_lists_adapter) = options.id_lists_adapter.clone() {
3459        return IdListsAdapterHousing {
3460            inner: Some(id_lists_adapter),
3461            as_default_adapter: None,
3462        };
3463    }
3464
3465    if options.enable_id_lists.unwrap_or(false) {
3466        let adapter = Arc::new(StatsigHttpIdListsAdapter::new(sdk_key, options));
3467
3468        return IdListsAdapterHousing {
3469            inner: Some(adapter.clone()),
3470            as_default_adapter: Some(adapter),
3471        };
3472    }
3473
3474    IdListsAdapterHousing {
3475        inner: None,
3476        as_default_adapter: None,
3477    }
3478}
3479
3480struct IdListsAdapterHousing {
3481    inner: Option<Arc<dyn IdListsAdapter>>,
3482    as_default_adapter: Option<Arc<StatsigHttpIdListsAdapter>>,
3483}
3484
3485struct SpecsAdapterHousing {
3486    inner: Arc<dyn SpecsAdapter>,
3487    as_default_adapter: Option<Arc<StatsigHttpSpecsAdapter>>,
3488}
3489
3490fn setup_ops_stats(
3491    sdk_instance_id: &str,
3492    statsig_runtime: Arc<StatsigRuntime>,
3493    error_observer: &Arc<dyn OpsStatsEventObserver>,
3494    diagnostics_observer: &Arc<dyn OpsStatsEventObserver>,
3495    console_capture_observer: &Arc<dyn OpsStatsEventObserver>,
3496    external_observer: &Option<Weak<dyn ObservabilityClient>>,
3497) -> Arc<OpsStatsForInstance> {
3498    let ops_stat = OPS_STATS.get_for_instance(sdk_instance_id);
3499    ops_stat.subscribe(statsig_runtime.clone(), Arc::downgrade(error_observer));
3500    ops_stat.subscribe(
3501        statsig_runtime.clone(),
3502        Arc::downgrade(diagnostics_observer),
3503    );
3504    ops_stat.subscribe(
3505        statsig_runtime.clone(),
3506        Arc::downgrade(console_capture_observer),
3507    );
3508    if let Some(ob_client) = external_observer {
3509        if let Some(client) = ob_client.upgrade() {
3510            client.init();
3511            let as_observer = client.to_ops_stats_event_observer();
3512            ops_stat.subscribe(statsig_runtime, Arc::downgrade(&as_observer));
3513        }
3514    }
3515
3516    ops_stat
3517}
3518
3519#[cfg(test)]
3520mod tests {
3521    use rusty_fork::rusty_fork_test;
3522
3523    use super::*;
3524    use crate::{
3525        interned_values::{
3526            interned_store::{preload_mmap_v2_for_test, write_mmap_v2_for_test},
3527            InternedStore,
3528        },
3529        networking::ResponseData,
3530        SpecsUpdate,
3531    };
3532
3533    const EVAL_PROJ_JSON: &[u8] = include_bytes!("../tests/data/eval_proj_dcs.json");
3534
3535    rusty_fork_test! {
3536        #[test]
3537        fn experiment_group_lookups_do_not_materialize_mmap_specs() {
3538            assert!(!InternedStore::has_preloaded_mmap_v2());
3539            let directory = tempfile::tempdir().unwrap();
3540            let path = directory.path().join("interned-store-v2-group-lookup.mmap");
3541            write_mmap_v2_for_test(EVAL_PROJ_JSON, &path).unwrap();
3542            preload_mmap_v2_for_test(&path).unwrap();
3543            assert!(InternedStore::has_preloaded_mmap_v2());
3544
3545            let statsig = Statsig::new("secret-key", None);
3546            statsig
3547                .spec_store
3548                .set_values(SpecsUpdate {
3549                    data: ResponseData::from_bytes(EVAL_PROJ_JSON.to_vec()),
3550                    source: SpecsSource::Network,
3551                    received_at: 2_000,
3552                    source_api: Some("mmap-group-test".to_string()),
3553                    has_updates: None,
3554                })
3555                .unwrap();
3556
3557            let experiment_name = "test_experiment_no_targeting";
3558            let group_name = "Control";
3559            let group_id = "54QJztEPRLXK7ZCvXeY9q4";
3560            assert_eq!(InternedStore::get_mmap_spec_materialization_len(), 0);
3561
3562            let by_name = statsig.get_experiment_by_group_name(experiment_name, group_name);
3563            let by_id = statsig.get_experiment_by_group_id_advanced(experiment_name, group_id);
3564            assert_eq!(by_name.rule_id, group_id);
3565            assert_eq!(by_id.group_name.as_deref(), Some(group_name));
3566
3567            #[cfg(feature = "ffi-support")]
3568            {
3569                for raw in [
3570                    statsig.get_raw_experiment_by_group_name(experiment_name, group_name),
3571                    statsig.get_raw_experiment_by_group_id_advanced(experiment_name, group_id),
3572                ] {
3573                    let raw: Value = serde_json::from_str(&raw).unwrap();
3574                    assert_eq!(raw["ruleID"], group_id);
3575                    assert_eq!(raw["groupName"], group_name);
3576                    assert_eq!(raw["value"]["value"], "control");
3577                }
3578            }
3579
3580            assert_eq!(InternedStore::get_mmap_spec_materialization_len(), 0);
3581        }
3582    }
3583}