statsig_rust/
statsig.rs

1use crate::evaluation::cmab_evaluator::{get_cmab_ranked_list, CMABRankedGroup};
2use crate::evaluation::country_lookup::CountryLookup;
3use crate::evaluation::dynamic_value::DynamicValue;
4use crate::evaluation::evaluation_details::EvaluationDetails;
5use crate::evaluation::evaluation_types::GateEvaluation;
6use crate::evaluation::evaluator::{Evaluator, SpecType};
7use crate::evaluation::evaluator_context::EvaluatorContext;
8use crate::evaluation::evaluator_result::{
9    result_to_dynamic_config_eval, result_to_experiment_eval, result_to_gate_eval,
10    result_to_layer_eval, EvaluatorResult,
11};
12use crate::evaluation::user_agent_parsing::{ParsedUserAgentValue, UserAgentParser};
13use crate::event_logging::event_logger::{EventLogger, ExposureTrigger};
14use crate::event_logging::event_queue::queued_config_expo::EnqueueConfigExpoOp;
15use crate::event_logging::event_queue::queued_experiment_expo::EnqueueExperimentExpoOp;
16use crate::event_logging::event_queue::queued_gate_expo::EnqueueGateExpoOp;
17use crate::event_logging::event_queue::queued_layer_param_expo::EnqueueLayerParamExpoOp;
18use crate::event_logging::event_queue::queued_passthrough::EnqueuePassthroughOp;
19use crate::event_logging::statsig_event_internal::{StatsigEventInternal, StatsigLogLineLevel};
20use crate::event_logging_adapter::EventLoggingAdapter;
21use crate::event_logging_adapter::StatsigHttpEventLoggingAdapter;
22use crate::gcir::gcir_formatter::GCIRFormatter;
23use crate::hashing::HashUtil;
24use crate::initialize_response::InitializeResponse;
25use crate::networking::NetworkError;
26use crate::observability::diagnostics_observer::DiagnosticsObserver;
27use crate::observability::observability_client_adapter::{MetricType, ObservabilityEvent};
28use crate::observability::ops_stats::{OpsStatsForInstance, OPS_STATS};
29use crate::observability::sdk_errors_observer::{ErrorBoundaryEvent, SDKErrorsObserver};
30use crate::output_logger::{initialize_output_logger, shutdown_output_logger};
31use crate::persistent_storage::persistent_values_manager::PersistentValuesManager;
32use crate::sdk_diagnostics::diagnostics::{ContextType, Diagnostics};
33use crate::sdk_diagnostics::marker::{ActionType, KeyType, Marker};
34use crate::sdk_event_emitter::SdkEventEmitter;
35use crate::spec_store::SpecStore;
36use crate::specs_adapter::{StatsigCustomizedSpecsAdapter, StatsigHttpSpecsAdapter};
37use crate::statsig_err::StatsigErr;
38use crate::statsig_metadata::StatsigMetadata;
39use crate::statsig_options::StatsigOptions;
40use crate::statsig_runtime::StatsigRuntime;
41use crate::statsig_type_factories::{
42    make_dynamic_config, make_experiment, make_feature_gate, make_layer,
43};
44use crate::statsig_types::{DynamicConfig, Experiment, FeatureGate, Layer, ParameterStore};
45use crate::user::StatsigUserInternal;
46use crate::{
47    dyn_value, log_d, log_e, log_w, read_lock_or_else, ClientInitResponseOptions,
48    GCIRResponseFormat, IdListsAdapter, ObservabilityClient, OpsStatsEventObserver,
49    OverrideAdapter, SpecsAdapter, SpecsInfo, SpecsSource, SpecsUpdateListener,
50    StatsigHttpIdListsAdapter, StatsigLocalOverrideAdapter, StatsigUser,
51};
52use crate::{
53    log_error_to_statsig_and_console,
54    statsig_core_api_options::{
55        DynamicConfigEvaluationOptions, ExperimentEvaluationOptions, FeatureGateEvaluationOptions,
56        LayerEvaluationOptions, ParameterStoreEvaluationOptions,
57    },
58};
59use chrono::Utc;
60use parking_lot::Mutex;
61use serde::de::DeserializeOwned;
62use serde::Serialize;
63use serde_json::json;
64use serde_json::Value;
65use std::borrow::Cow;
66use std::collections::HashMap;
67use std::sync::atomic::{AtomicBool, Ordering};
68use std::sync::{Arc, Weak};
69use std::time::{Duration, Instant};
70use tokio::time::sleep;
71use tokio::try_join;
72
73const TAG: &str = stringify!(Statsig);
74const ERROR_SDK_KEY: &str = "__STATSIG_ERROR_SDK_KEY__";
75const INIT_IP_TAG: &str = "INIT_COUNTRY_LOOKUP";
76const INIT_UA_TAG: &str = "INIT_UA";
77
78lazy_static::lazy_static! {
79    static ref SHARED_INSTANCE: Mutex<Option<Arc<Statsig>>> = Mutex::new(None);
80}
81
82pub struct Statsig {
83    pub statsig_runtime: Arc<StatsigRuntime>,
84    pub options: Arc<StatsigOptions>,
85    pub event_emitter: SdkEventEmitter,
86
87    sdk_key: String,
88    event_logger: Arc<EventLogger>,
89    specs_adapter: SpecsAdapterHousing,
90    event_logging_adapter: Arc<dyn EventLoggingAdapter>,
91    id_lists_adapter: IdListsAdapterHousing,
92    override_adapter: Option<Arc<dyn OverrideAdapter>>,
93    spec_store: Arc<SpecStore>,
94    hashing: Arc<HashUtil>,
95    gcir_formatter: Arc<GCIRFormatter>,
96    statsig_environment: Option<HashMap<String, DynamicValue>>,
97    fallback_environment: Mutex<Option<HashMap<String, DynamicValue>>>,
98    ops_stats: Arc<OpsStatsForInstance>,
99    error_observer: Arc<dyn OpsStatsEventObserver>,
100    diagnostics_observer: Arc<dyn OpsStatsEventObserver>,
101    background_tasks_started: Arc<AtomicBool>,
102    persistent_values_manager: Option<Arc<PersistentValuesManager>>,
103    initialize_details: Mutex<InitializeDetails>,
104}
105
106pub struct StatsigContext {
107    pub sdk_key: String,
108    pub options: Arc<StatsigOptions>,
109    pub local_override_adapter: Option<Arc<dyn OverrideAdapter>>,
110    pub error_observer: Arc<dyn OpsStatsEventObserver>,
111    pub diagnostics_observer: Arc<dyn OpsStatsEventObserver>,
112    pub spec_store: Arc<SpecStore>,
113}
114
115#[derive(Debug, Clone, Serialize)]
116pub struct FailureDetails {
117    pub reason: String,
118    pub error: Option<StatsigErr>,
119}
120
121#[derive(Debug, Clone, Serialize)]
122pub struct InitializeDetails {
123    pub duration: f64,
124    pub init_success: bool,
125    pub is_config_spec_ready: bool,
126    pub is_id_list_ready: Option<bool>,
127    pub source: SpecsSource,
128    pub failure_details: Option<FailureDetails>,
129    pub spec_source_api: Option<String>,
130}
131
132impl Default for InitializeDetails {
133    fn default() -> Self {
134        InitializeDetails {
135            duration: 0.0,
136            init_success: false,
137            is_config_spec_ready: false,
138            is_id_list_ready: None,
139            source: SpecsSource::Uninitialized,
140            failure_details: None,
141            spec_source_api: None,
142        }
143    }
144}
145
146impl InitializeDetails {
147    pub fn from_error(reason: &str, error: Option<StatsigErr>) -> Self {
148        InitializeDetails {
149            duration: 0.0,
150            init_success: false,
151            is_config_spec_ready: false,
152            is_id_list_ready: None,
153            source: SpecsSource::Uninitialized,
154            failure_details: Some(FailureDetails {
155                reason: reason.to_string(),
156                error,
157            }),
158            spec_source_api: None,
159        }
160    }
161}
162
163impl Drop for Statsig {
164    fn drop(&mut self) {
165        self.event_logger.force_shutdown();
166
167        if let Some(adapter) = &self.id_lists_adapter.as_default_adapter {
168            adapter.force_shutdown();
169        }
170
171        if let Some(adapter) = &self.specs_adapter.as_default_adapter {
172            adapter.force_shutdown();
173        }
174
175        shutdown_output_logger();
176
177        log_d!(TAG, "Statsig instance dropped");
178    }
179}
180
181impl Statsig {
182    pub fn new(sdk_key: &str, options: Option<Arc<StatsigOptions>>) -> Self {
183        let statsig_runtime = StatsigRuntime::get_runtime();
184        let options = options.map(|o| o.validate_and_fix()).unwrap_or_default();
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
193        let specs_adapter = initialize_specs_adapter(sdk_key, &options, &hashing);
194        let id_lists_adapter = initialize_id_lists_adapter(sdk_key, &options);
195        let event_logging_adapter = initialize_event_logging_adapter(sdk_key, &options);
196        let override_adapter = match options.override_adapter.as_ref() {
197            Some(adapter) => Some(Arc::clone(adapter)),
198            None => Some(Arc::new(StatsigLocalOverrideAdapter::new()) as Arc<dyn OverrideAdapter>),
199        };
200
201        let event_logger =
202            EventLogger::new(sdk_key, &options, &event_logging_adapter, &statsig_runtime);
203
204        let diagnostics = Arc::new(Diagnostics::new(event_logger.clone(), sdk_key));
205        let diagnostics_observer: Arc<dyn OpsStatsEventObserver> =
206            Arc::new(DiagnosticsObserver::new(diagnostics));
207        let error_observer: Arc<dyn OpsStatsEventObserver> =
208            Arc::new(SDKErrorsObserver::new(sdk_key, &options));
209
210        let ops_stats = setup_ops_stats(
211            sdk_key,
212            statsig_runtime.clone(),
213            &error_observer,
214            &diagnostics_observer,
215            &options.observability_client,
216        );
217
218        let spec_store = Arc::new(SpecStore::new(
219            sdk_key,
220            hashing.sha256(sdk_key),
221            statsig_runtime.clone(),
222            options.data_store.clone(),
223        ));
224
225        let environment = options
226            .environment
227            .as_ref()
228            .map(|env| HashMap::from([("tier".into(), dyn_value!(env.as_str()))]));
229
230        let persistent_values_manager = options.persistent_storage.clone().map(|storage| {
231            Arc::new(PersistentValuesManager {
232                persistent_storage: storage,
233            })
234        });
235
236        StatsigMetadata::update_service_name(options.service_name.clone());
237
238        let use_experimental_ua_parser = options.__experimental_ua_parsing_enabled.unwrap_or(false);
239
240        Statsig {
241            sdk_key: sdk_key.to_string(),
242            options,
243            gcir_formatter: Arc::new(GCIRFormatter::new(
244                &spec_store,
245                &override_adapter,
246                &ops_stats,
247                use_experimental_ua_parser,
248            )),
249            hashing,
250            statsig_environment: environment,
251            fallback_environment: Mutex::new(None),
252            override_adapter,
253            spec_store,
254            specs_adapter,
255            event_logging_adapter,
256            event_logger,
257            id_lists_adapter,
258            statsig_runtime,
259            ops_stats,
260            error_observer,
261            diagnostics_observer,
262            background_tasks_started: Arc::new(AtomicBool::new(false)),
263            persistent_values_manager,
264            initialize_details: Mutex::new(InitializeDetails::default()),
265            event_emitter: SdkEventEmitter::default(),
266        }
267    }
268
269    /***
270     *  Initializes the Statsig client and returns an error if initialization fails.
271     *
272     *  This method performs the client initialization and returns `Ok(())` if successful.
273     *  If the initialization completes with failure details, it returns a [`StatsigErr`]
274     *  describing the failure.
275     *
276     *  For detailed information about the initialization process—regardless of success or failure—
277     *  use [`initialize_with_details`] instead.
278     *
279     *  # Errors
280     *
281     *  Returns a [`StatsigErr`] if the client fails to initialize successfully.
282     */
283    pub async fn initialize(&self) -> Result<(), StatsigErr> {
284        let details = self.initialize_with_details().await?;
285
286        if let Some(failure_details) = details.failure_details {
287            Err(failure_details
288                .error
289                .unwrap_or(StatsigErr::InitializationError(failure_details.reason)))
290        } else {
291            Ok(())
292        }
293    }
294
295    /***
296     *  Initializes the Statsig client and returns detailed information about the process.
297     *
298     *  This method returns a [`StatsigInitializeDetails`] struct, which includes metadata such as
299     *  the success status, initialization source, and any failure details. Even if initialization
300     *  fails, this method does not return an error; instead, the `init_success` field will be `false`
301     *  and `failure_details` may be populated.
302     ***/
303    pub async fn initialize_with_details(&self) -> Result<InitializeDetails, StatsigErr> {
304        self.ops_stats.add_marker(
305            Marker::new(KeyType::Overall, ActionType::Start, None),
306            Some(ContextType::Initialize),
307        );
308
309        let init_details = if let Some(timeout_ms) = self.options.init_timeout_ms {
310            self.apply_timeout_to_init(timeout_ms).await
311        } else {
312            self.initialize_impl_with_details().await
313        };
314        self.log_init_details(&init_details);
315        if let Ok(details) = &init_details {
316            match self.initialize_details.try_lock_for(Duration::from_secs(5)) {
317                Some(mut curr_init_details) => {
318                    *curr_init_details = details.clone();
319                }
320                None => {
321                    log_e!(TAG, "Failed to lock initialize_details");
322                }
323            }
324        }
325        init_details
326    }
327
328    pub fn get_initialize_details(&self) -> InitializeDetails {
329        match self.initialize_details.try_lock_for(Duration::from_secs(5)) {
330            Some(details) => details.clone(),
331            None => InitializeDetails::from_error(
332                "Failed to lock initialize_details",
333                Some(StatsigErr::LockFailure(
334                    "Failed to lock initialize_details".to_string(),
335                )),
336            ),
337        }
338    }
339
340    pub fn is_initialized(&self) -> bool {
341        match self.initialize_details.try_lock_for(Duration::from_secs(5)) {
342            Some(details) => details.init_success,
343            None => false,
344        }
345    }
346
347    pub async fn shutdown(&self) -> Result<(), StatsigErr> {
348        self.shutdown_with_timeout(Duration::from_secs(3)).await
349    }
350
351    pub async fn shutdown_with_timeout(&self, timeout: Duration) -> Result<(), StatsigErr> {
352        log_d!(
353            TAG,
354            "Shutting down Statsig with timeout {}ms",
355            timeout.as_millis()
356        );
357
358        let start = Instant::now();
359        let shutdown_result = tokio::select! {
360            () = tokio::time::sleep(timeout) => {
361                log_w!(TAG, "Statsig shutdown timed out. {}", start.elapsed().as_millis());
362                Err(StatsigErr::ShutdownFailure(
363                    "Shutdown timed out".to_string()
364                ))
365            }
366            sub_result = async {
367                let id_list_shutdown: Pin<Box<_>> = if let Some(adapter) = &self.id_lists_adapter.inner {
368                    adapter.shutdown(timeout)
369                } else {
370                    Box::pin(async { Ok(()) })
371                };
372
373                shutdown_output_logger();
374
375                try_join!(
376                    id_list_shutdown,
377                    self.event_logger.shutdown(),
378                    self.specs_adapter.inner.shutdown(timeout, &self.statsig_runtime),
379                )
380            } => {
381                match sub_result {
382                    Ok(_) => {
383                        log_d!(TAG, "All Statsig tasks shutdown successfully");
384                        Ok(())
385                    }
386                    Err(e) => {
387                        log_w!(TAG, "Error during shutdown: {:?}", e);
388                        Err(e)
389                    }
390                }
391            }
392        };
393
394        self.statsig_runtime.shutdown();
395        shutdown_result
396    }
397
398    async fn start_background_tasks(
399        statsig_runtime: Arc<StatsigRuntime>,
400        id_lists_adapter: Option<Arc<dyn IdListsAdapter>>,
401        specs_adapter: Arc<dyn SpecsAdapter>,
402        ops_stats: Arc<OpsStatsForInstance>,
403        bg_tasks_started: Arc<AtomicBool>,
404    ) -> bool {
405        if bg_tasks_started.load(Ordering::SeqCst) {
406            return true;
407        }
408
409        let mut success = true;
410
411        if let Some(adapter) = &id_lists_adapter {
412            if let Err(e) = adapter
413                .clone()
414                .schedule_background_sync(&statsig_runtime)
415                .await
416            {
417                success = false;
418                log_w!(TAG, "Failed to schedule idlist background job {}", e);
419            }
420        }
421
422        if let Err(e) = specs_adapter
423            .clone()
424            .schedule_background_sync(&statsig_runtime)
425            .await
426        {
427            success = false;
428            log_error_to_statsig_and_console!(
429                ops_stats,
430                TAG,
431                StatsigErr::SpecsAdapterSkipPoll(format!(
432                    "Failed to schedule specs adapter background job: {e}"
433                ))
434            );
435        }
436
437        bg_tasks_started.store(true, Ordering::SeqCst);
438
439        success
440    }
441
442    async fn apply_timeout_to_init(
443        &self,
444        timeout_ms: u64,
445    ) -> Result<InitializeDetails, StatsigErr> {
446        let timeout = Duration::from_millis(timeout_ms);
447
448        let init_future = self.initialize_impl_with_details();
449        let timeout_future = sleep(timeout);
450
451        let statsig_runtime = self.statsig_runtime.clone();
452        let id_lists_adapter = self.id_lists_adapter.inner.clone();
453        let specs_adapter = self.specs_adapter.inner.clone();
454        let ops_stats = self.ops_stats.clone();
455        let background_tasks_started = self.background_tasks_started.clone();
456        // Create another clone specifically for the closure
457        let statsig_runtime_for_closure = statsig_runtime.clone();
458
459        tokio::select! {
460            result = init_future => {
461                result
462            },
463            _ = timeout_future => {
464                statsig_runtime.spawn(
465                    "start_background_tasks",
466                    |_shutdown_notify| async move {
467                        Self::start_background_tasks(
468                            statsig_runtime_for_closure,
469                            id_lists_adapter,
470                            specs_adapter,
471                            ops_stats,
472                            background_tasks_started,
473                        ).await;
474                    }
475                )?;
476                Ok(self.timeout_failure(timeout_ms))
477            },
478        }
479    }
480
481    async fn initialize_impl_with_details(&self) -> Result<InitializeDetails, StatsigErr> {
482        let start_time = Instant::now();
483        self.spec_store.set_source(SpecsSource::Loading);
484        self.specs_adapter.inner.initialize(self.spec_store.clone());
485        let use_experimental_ua_parser = self.should_use_experimental_ua_parser();
486
487        let mut error_message = None;
488        let mut id_list_ready = None;
489
490        let init_country_lookup = if !self.options.disable_country_lookup.unwrap_or_default() {
491            Some(self.statsig_runtime.spawn(INIT_IP_TAG, |_| async {
492                CountryLookup::load_country_lookup();
493            }))
494        } else {
495            None
496        };
497
498        let init_ua = if !self.options.disable_user_agent_parsing.unwrap_or_default()
499            && !use_experimental_ua_parser
500        {
501            Some(self.statsig_runtime.spawn(INIT_UA_TAG, |_| async {
502                UserAgentParser::load_parser();
503            }))
504        } else {
505            None
506        };
507
508        let init_res = match self
509            .specs_adapter
510            .inner
511            .clone()
512            .start(&self.statsig_runtime)
513            .await
514        {
515            Ok(()) => Ok(()),
516            Err(e) => {
517                self.spec_store.set_source(SpecsSource::NoValues);
518                error_message = Some(format!("Failed to start specs adapter: {e}"));
519                Err(e)
520            }
521        };
522
523        if let Some(adapter) = &self.id_lists_adapter.inner {
524            match adapter
525                .clone()
526                .start(&self.statsig_runtime, self.spec_store.clone())
527                .await
528            {
529                Ok(()) => {
530                    id_list_ready = Some(true);
531                }
532                Err(e) => {
533                    id_list_ready = Some(false);
534                    error_message.get_or_insert_with(|| format!("Failed to sync ID lists: {e}"));
535                }
536            }
537            if let Err(e) = adapter
538                .clone()
539                .schedule_background_sync(&self.statsig_runtime)
540                .await
541            {
542                log_w!(TAG, "Failed to schedule id_list background job {}", e);
543            }
544        }
545
546        if let Err(e) = self
547            .event_logging_adapter
548            .clone()
549            .start(&self.statsig_runtime)
550            .await
551        {
552            log_error_to_statsig_and_console!(
553                self.ops_stats.clone(),
554                TAG,
555                StatsigErr::UnstartedAdapter(format!("Failed to start event logging adapter: {e}"))
556            );
557        }
558
559        let spec_info = self.spec_store.get_current_specs_info();
560        let duration = start_time.elapsed().as_millis() as f64;
561
562        self.set_default_environment_from_server();
563
564        if self.options.wait_for_country_lookup_init.unwrap_or(false) {
565            match init_country_lookup {
566                Some(Ok(task_id)) => {
567                    let _ = self
568                        .statsig_runtime
569                        .await_join_handle(INIT_IP_TAG, &task_id)
570                        .await;
571                }
572                Some(Err(e)) => {
573                    log_error_to_statsig_and_console!(
574                        self.ops_stats.clone(),
575                        TAG,
576                        StatsigErr::UnstartedAdapter(format!(
577                            "Failed to spawn country lookup task: {e}"
578                        ))
579                    );
580                }
581                _ => {}
582            }
583        }
584        if self.options.wait_for_user_agent_init.unwrap_or(false) {
585            match init_ua {
586                Some(Ok(task_id)) => {
587                    let _ = self
588                        .statsig_runtime
589                        .await_join_handle(INIT_UA_TAG, &task_id)
590                        .await;
591                }
592                Some(Err(e)) => {
593                    log_error_to_statsig_and_console!(
594                        self.ops_stats.clone(),
595                        TAG,
596                        StatsigErr::UnstartedAdapter(format!(
597                            "Failed to spawn user agent parser task: {e}"
598                        ))
599                    );
600                }
601                _ => {}
602            }
603        }
604
605        let error = init_res.clone().err();
606
607        let success = Self::start_background_tasks(
608            self.statsig_runtime.clone(),
609            self.id_lists_adapter.inner.clone(),
610            self.specs_adapter.inner.clone(),
611            self.ops_stats.clone(),
612            self.background_tasks_started.clone(),
613        )
614        .await;
615
616        Ok(self.construct_initialize_details(success, duration, spec_info, id_list_ready, error))
617    }
618
619    fn construct_initialize_details(
620        &self,
621        init_success: bool,
622        duration: f64,
623        specs_info: SpecsInfo,
624        is_id_list_ready: Option<bool>,
625        error: Option<StatsigErr>,
626    ) -> InitializeDetails {
627        let is_config_spec_ready = matches!(specs_info.lcut, Some(v) if v != 0);
628
629        let failure_details =
630            if let Some(StatsigErr::NetworkError(NetworkError::DisableNetworkOn(_))) = error {
631                None
632            } else {
633                error.as_ref().map(|e| FailureDetails {
634                    reason: e.to_string(),
635                    error: Some(e.clone()),
636                })
637            };
638
639        InitializeDetails {
640            init_success,
641            is_config_spec_ready,
642            is_id_list_ready,
643            source: specs_info.source.clone(),
644            failure_details,
645            duration,
646            spec_source_api: specs_info.source_api.clone(),
647        }
648    }
649
650    fn timeout_failure(&self, timeout_ms: u64) -> InitializeDetails {
651        InitializeDetails {
652            init_success: false,
653            is_config_spec_ready: false,
654            is_id_list_ready: None,
655            source: SpecsSource::Uninitialized,
656            failure_details: Some(FailureDetails {
657                reason: "Initialization timed out".to_string(),
658                error: None,
659            }),
660            duration: timeout_ms as f64,
661            spec_source_api: None,
662        }
663    }
664
665    fn log_init_details(&self, init_details: &Result<InitializeDetails, StatsigErr>) {
666        match init_details {
667            Ok(details) => {
668                self.log_init_finish(
669                    details.init_success,
670                    &None,
671                    &details.duration,
672                    &self.spec_store.get_current_specs_info(),
673                );
674                if let Some(failure) = &details.failure_details {
675                    log_error_to_statsig_and_console!(
676                        self.ops_stats,
677                        TAG,
678                        StatsigErr::InitializationError(failure.reason.clone())
679                    );
680                }
681            }
682            Err(err) => {
683                // we store errors on init details so we should never return error and thus do not need to log
684                log_w!(TAG, "Initialization error: {:?}", err);
685            }
686        }
687    }
688
689    pub fn get_context(&self) -> StatsigContext {
690        StatsigContext {
691            sdk_key: self.sdk_key.clone(),
692            options: self.options.clone(),
693            local_override_adapter: self.override_adapter.clone(),
694            error_observer: self.error_observer.clone(),
695            diagnostics_observer: self.diagnostics_observer.clone(),
696            spec_store: self.spec_store.clone(),
697        }
698    }
699
700    pub fn log_event(
701        &self,
702        user: &StatsigUser,
703        event_name: &str,
704        value: Option<String>,
705        metadata: Option<HashMap<String, String>>,
706    ) {
707        let user_internal = self.internalize_user(user);
708
709        self.event_logger.enqueue(EnqueuePassthroughOp {
710            event: StatsigEventInternal::new_custom_event(
711                user_internal.to_loggable(),
712                event_name.to_string(),
713                value.map(|v| json!(v)),
714                metadata,
715            ),
716        });
717    }
718
719    pub fn log_event_with_number(
720        &self,
721        user: &StatsigUser,
722        event_name: &str,
723        value: Option<f64>,
724        metadata: Option<HashMap<String, String>>,
725    ) {
726        let user_internal = self.internalize_user(user);
727        self.event_logger.enqueue(EnqueuePassthroughOp {
728            event: StatsigEventInternal::new_custom_event(
729                user_internal.to_loggable(),
730                event_name.to_string(),
731                value.map(|v| json!(v)),
732                metadata,
733            ),
734        });
735    }
736
737    pub fn forward_log_line_event(
738        &self,
739        user: &StatsigUser,
740        log_level: StatsigLogLineLevel,
741        value: Option<String>,
742        metadata: Option<HashMap<String, String>>,
743    ) {
744        let user_internal = self.internalize_user(user);
745        self.event_logger.enqueue(EnqueuePassthroughOp {
746            event: StatsigEventInternal::new_statsig_log_line_event(
747                user_internal.to_loggable(),
748                log_level,
749                value,
750                metadata,
751            ),
752        });
753    }
754
755    pub fn log_layer_param_exposure_with_layer_json(
756        &self,
757        layer_json: String,
758        parameter_name: String,
759    ) {
760        let layer = match serde_json::from_str::<Layer>(&layer_json) {
761            Ok(layer) => layer,
762            Err(e) => {
763                log_error_to_statsig_and_console!(
764                    self.ops_stats.clone(),
765                    TAG,
766                    StatsigErr::ShutdownFailure(e.to_string())
767                );
768                return;
769            }
770        };
771
772        self.log_layer_param_exposure_with_layer(layer, parameter_name);
773    }
774
775    pub fn log_layer_param_exposure_with_layer(&self, layer: Layer, parameter_name: String) {
776        if layer.__disable_exposure {
777            self.event_logger.increment_non_exposure_checks(&layer.name);
778            return;
779        }
780
781        self.event_logger
782            .enqueue(EnqueueLayerParamExpoOp::LayerOwned(
783                Utc::now().timestamp_millis() as u64,
784                Box::new(layer),
785                parameter_name,
786                ExposureTrigger::Auto,
787            ));
788    }
789
790    pub async fn flush_events(&self) {
791        let _ = self.event_logger.flush_all_pending_events().await;
792    }
793
794    pub fn get_client_init_response(&self, user: &StatsigUser) -> InitializeResponse {
795        self.get_client_init_response_with_options(user, self.gcir_formatter.get_default_options())
796    }
797
798    pub fn get_client_init_response_with_options(
799        &self,
800        user: &StatsigUser,
801        options: &ClientInitResponseOptions,
802    ) -> InitializeResponse {
803        let user_internal = self.internalize_user(user);
804        self.gcir_formatter
805            .get_as_v1_format(user_internal, &self.hashing, options)
806    }
807
808    pub fn get_client_init_response_as_string(&self, user: &StatsigUser) -> String {
809        serde_json::to_string(&self.get_client_init_response(user)).unwrap_or_default()
810    }
811
812    pub fn get_client_init_response_with_options_as_string(
813        &self,
814        user: &StatsigUser,
815        options: &ClientInitResponseOptions,
816    ) -> String {
817        let user_internal = self.internalize_user(user);
818        let response = match options.response_format {
819            Some(GCIRResponseFormat::InitializeWithSecondaryExposureMapping) => {
820                json!(self
821                    .gcir_formatter
822                    .get_as_v2_format(user_internal, &self.hashing, options))
823            }
824            _ => json!(self
825                .gcir_formatter
826                .get_as_v1_format(user_internal, &self.hashing, options)),
827        };
828
829        json!(response).to_string()
830    }
831
832    pub fn get_string_parameter_from_store(
833        &self,
834        user: &StatsigUser,
835        parameter_store_name: &str,
836        parameter_name: &str,
837        fallback: Option<String>,
838        options: Option<ParameterStoreEvaluationOptions>,
839    ) -> Option<String> {
840        self.get_parameter_from_store(
841            user,
842            parameter_store_name,
843            parameter_name,
844            fallback,
845            options,
846        )
847    }
848
849    pub fn get_boolean_parameter_from_store(
850        &self,
851        user: &StatsigUser,
852        parameter_store_name: &str,
853        parameter_name: &str,
854        fallback: Option<bool>,
855        options: Option<ParameterStoreEvaluationOptions>,
856    ) -> Option<bool> {
857        self.get_parameter_from_store(
858            user,
859            parameter_store_name,
860            parameter_name,
861            fallback,
862            options,
863        )
864    }
865
866    pub fn get_float_parameter_from_store(
867        &self,
868        user: &StatsigUser,
869        parameter_store_name: &str,
870        parameter_name: &str,
871        fallback: Option<f64>,
872        options: Option<ParameterStoreEvaluationOptions>,
873    ) -> Option<f64> {
874        self.get_parameter_from_store(
875            user,
876            parameter_store_name,
877            parameter_name,
878            fallback,
879            options,
880        )
881    }
882
883    pub fn get_integer_parameter_from_store(
884        &self,
885        user: &StatsigUser,
886        parameter_store_name: &str,
887        parameter_name: &str,
888        fallback: Option<i64>,
889        options: Option<ParameterStoreEvaluationOptions>,
890    ) -> Option<i64> {
891        self.get_parameter_from_store(
892            user,
893            parameter_store_name,
894            parameter_name,
895            fallback,
896            options,
897        )
898    }
899
900    pub fn get_array_parameter_from_store(
901        &self,
902        user: &StatsigUser,
903        parameter_store_name: &str,
904        parameter_name: &str,
905        fallback: Option<Vec<Value>>,
906        options: Option<ParameterStoreEvaluationOptions>,
907    ) -> Option<Vec<Value>> {
908        self.get_parameter_from_store(
909            user,
910            parameter_store_name,
911            parameter_name,
912            fallback,
913            options,
914        )
915    }
916
917    pub fn get_object_parameter_from_store(
918        &self,
919        user: &StatsigUser,
920        parameter_store_name: &str,
921        parameter_name: &str,
922        fallback: Option<HashMap<String, Value>>,
923        options: Option<ParameterStoreEvaluationOptions>,
924    ) -> Option<HashMap<String, Value>> {
925        self.get_parameter_from_store(
926            user,
927            parameter_store_name,
928            parameter_name,
929            fallback,
930            options,
931        )
932    }
933
934    pub fn get_parameter_from_store<T: DeserializeOwned>(
935        &self,
936        user: &StatsigUser,
937        parameter_store_name: &str,
938        parameter_name: &str,
939        fallback: Option<T>,
940        options: Option<ParameterStoreEvaluationOptions>,
941    ) -> Option<T> {
942        let store = self
943            .get_parameter_store_with_options(parameter_store_name, options.unwrap_or_default());
944        match fallback {
945            Some(fallback) => Some(store.get(user, parameter_name, fallback)),
946            None => store.get_opt(user, parameter_name),
947        }
948    }
949
950    pub fn get_parameter_store(&self, parameter_store_name: &str) -> ParameterStore<'_> {
951        self.get_parameter_store_with_options(
952            parameter_store_name,
953            ParameterStoreEvaluationOptions::default(),
954        )
955    }
956
957    pub fn get_parameter_store_with_options(
958        &self,
959        parameter_store_name: &str,
960        options: ParameterStoreEvaluationOptions,
961    ) -> ParameterStore<'_> {
962        self.event_logger
963            .increment_non_exposure_checks(parameter_store_name);
964
965        let data = read_lock_or_else!(self.spec_store.data, {
966            log_error_to_statsig_and_console!(
967                self.ops_stats.clone(),
968                TAG,
969                StatsigErr::LockFailure(
970                    "Failed to acquire read lock for spec store data".to_string()
971                )
972            );
973            return ParameterStore {
974                name: parameter_store_name.to_string(),
975                parameters: HashMap::new(),
976                details: EvaluationDetails::unrecognized_no_data(),
977                options,
978                _statsig_ref: self,
979            };
980        });
981
982        let stores = &data.values.param_stores;
983        let store = match stores {
984            Some(stores) => stores.get(parameter_store_name),
985            None => {
986                return ParameterStore {
987                    name: parameter_store_name.to_string(),
988                    parameters: HashMap::new(),
989                    details: EvaluationDetails::unrecognized(&data),
990                    options,
991                    _statsig_ref: self,
992                };
993            }
994        };
995        match store {
996            Some(store) => ParameterStore {
997                name: parameter_store_name.to_string(),
998                parameters: store.parameters.clone(),
999                details: EvaluationDetails::recognized(&data, &EvaluatorResult::default()),
1000                options,
1001                _statsig_ref: self,
1002            },
1003            None => ParameterStore {
1004                name: parameter_store_name.to_string(),
1005                parameters: HashMap::new(),
1006                details: EvaluationDetails::unrecognized(&data),
1007                options,
1008                _statsig_ref: self,
1009            },
1010        }
1011    }
1012}
1013
1014// -------------------------
1015//   User Store Functions
1016// -------------------------
1017
1018impl Statsig {
1019    pub fn identify(&self, user: &StatsigUser) {
1020        let user_internal = self.internalize_user(user);
1021
1022        self.event_logger.enqueue(EnqueuePassthroughOp {
1023            event: StatsigEventInternal::new_custom_event(
1024                user_internal.to_loggable(),
1025                "statsig::identify".to_string(),
1026                None,
1027                None,
1028            ),
1029        });
1030    }
1031}
1032
1033// -------------------------
1034//   CMAB Functions
1035// -------------------------
1036
1037impl Statsig {
1038    pub fn get_cmab_ranked_groups(
1039        &self,
1040        user: &StatsigUser,
1041        cmab_name: &str,
1042    ) -> Vec<CMABRankedGroup> {
1043        self.event_logger.increment_non_exposure_checks(cmab_name);
1044
1045        let data = read_lock_or_else!(self.spec_store.data, {
1046            log_error_to_statsig_and_console!(
1047                self.ops_stats.clone(),
1048                TAG,
1049                StatsigErr::LockFailure(
1050                    "Failed to acquire read lock for spec store data".to_string()
1051                )
1052            );
1053            return vec![];
1054        });
1055        let user_internal = self.internalize_user(user);
1056        get_cmab_ranked_list(
1057            &mut EvaluatorContext::new(
1058                &user_internal,
1059                &data,
1060                &self.hashing,
1061                data.values.app_id.as_ref(),
1062                self.override_adapter.as_ref(),
1063                self.should_use_experimental_ua_parser(),
1064            ),
1065            cmab_name,
1066        )
1067    }
1068
1069    pub fn log_cmab_exposure_for_group(
1070        &self,
1071        user: &StatsigUser,
1072        cmab_name: &str,
1073        group_id: String,
1074    ) {
1075        let user_internal = self.internalize_user(user);
1076
1077        let mut experiment = self.get_experiment_impl(&user_internal, cmab_name);
1078        experiment.rule_id = group_id;
1079
1080        self.event_logger.enqueue(EnqueueExperimentExpoOp {
1081            exposure_time: Utc::now().timestamp_millis() as u64,
1082            user: &user_internal,
1083            experiment: &experiment,
1084            trigger: ExposureTrigger::Manual,
1085        });
1086    }
1087}
1088
1089// -------------------------
1090//   Shared Instance Functions
1091// -------------------------
1092
1093impl Statsig {
1094    pub fn shared() -> Arc<Statsig> {
1095        let lock = match SHARED_INSTANCE.try_lock_for(Duration::from_secs(5)) {
1096            Some(lock) => lock,
1097            None => {
1098                log_e!(
1099                    TAG,
1100                    "Statsig::shared() mutex error: Failed to lock SHARED_INSTANCE"
1101                );
1102                return Arc::new(Statsig::new(ERROR_SDK_KEY, None));
1103            }
1104        };
1105
1106        match lock.as_ref() {
1107            Some(statsig) => statsig.clone(),
1108            None => {
1109                log_e!(
1110                    TAG,
1111                    "Statsig::shared() called, but no instance has been set with Statsig::new_shared(...)"
1112                );
1113                Arc::new(Statsig::new(ERROR_SDK_KEY, None))
1114            }
1115        }
1116    }
1117
1118    pub fn new_shared(
1119        sdk_key: &str,
1120        options: Option<Arc<StatsigOptions>>,
1121    ) -> Result<Arc<Statsig>, StatsigErr> {
1122        match SHARED_INSTANCE.try_lock_for(Duration::from_secs(5)) {
1123            Some(mut lock) => {
1124                if lock.is_some() {
1125                    let message = "Statsig shared instance already exists. Call Statsig::remove_shared() before creating a new instance.";
1126                    log_e!(TAG, "{}", message);
1127                    return Err(StatsigErr::SharedInstanceFailure(message.to_string()));
1128                }
1129
1130                let statsig = Arc::new(Statsig::new(sdk_key, options));
1131                *lock = Some(statsig.clone());
1132                Ok(statsig)
1133            }
1134            None => {
1135                let message = "Statsig::new_shared() mutex error: Failed to lock SHARED_INSTANCE";
1136                log_e!(TAG, "{}", message);
1137                Err(StatsigErr::SharedInstanceFailure(message.to_string()))
1138            }
1139        }
1140    }
1141
1142    pub fn remove_shared() {
1143        match SHARED_INSTANCE.try_lock_for(Duration::from_secs(5)) {
1144            Some(mut lock) => {
1145                *lock = None;
1146            }
1147            None => {
1148                log_e!(
1149                    TAG,
1150                    "Statsig::remove_shared() mutex error: Failed to lock SHARED_INSTANCE"
1151                );
1152            }
1153        }
1154    }
1155
1156    pub fn has_shared_instance() -> bool {
1157        match SHARED_INSTANCE.try_lock_for(Duration::from_secs(5)) {
1158            Some(lock) => lock.is_some(),
1159            None => false,
1160        }
1161    }
1162}
1163
1164// -------------------------
1165//   Feature Gate Functions
1166// -------------------------
1167
1168impl Statsig {
1169    pub fn check_gate(&self, user: &StatsigUser, gate_name: &str) -> bool {
1170        self.check_gate_with_options(user, gate_name, FeatureGateEvaluationOptions::default())
1171    }
1172
1173    pub fn check_gate_with_options(
1174        &self,
1175        user: &StatsigUser,
1176        gate_name: &str,
1177        options: FeatureGateEvaluationOptions,
1178    ) -> bool {
1179        let user_internal = self.internalize_user(user);
1180        let disable_exposure_logging = options.disable_exposure_logging;
1181        let (details, evaluation) = self.get_gate_evaluation(&user_internal, gate_name);
1182
1183        let value = evaluation.as_ref().map(|e| e.value).unwrap_or_default();
1184        let rule_id = evaluation
1185            .as_ref()
1186            .map(|e| e.base.rule_id.clone())
1187            .unwrap_or_default();
1188
1189        if disable_exposure_logging {
1190            log_d!(TAG, "Exposure logging is disabled for gate {}", gate_name);
1191            self.event_logger.increment_non_exposure_checks(gate_name);
1192        } else {
1193            self.event_logger.enqueue(EnqueueGateExpoOp {
1194                exposure_time: Utc::now().timestamp_millis() as u64,
1195                user: &user_internal,
1196                queried_gate_name: gate_name,
1197                evaluation: evaluation.map(Cow::Owned),
1198                details: details.clone(),
1199                trigger: ExposureTrigger::Auto,
1200            });
1201        }
1202
1203        self.emit_gate_evaluated(gate_name, rule_id.as_str(), value, &details.reason);
1204
1205        value
1206    }
1207
1208    pub fn get_feature_gate(&self, user: &StatsigUser, gate_name: &str) -> FeatureGate {
1209        self.get_feature_gate_with_options(user, gate_name, FeatureGateEvaluationOptions::default())
1210    }
1211
1212    pub fn get_feature_gate_with_options(
1213        &self,
1214        user: &StatsigUser,
1215        gate_name: &str,
1216        options: FeatureGateEvaluationOptions,
1217    ) -> FeatureGate {
1218        let user_internal = self.internalize_user(user);
1219        let disable_exposure_logging = options.disable_exposure_logging;
1220        let (details, evaluation) = self.get_gate_evaluation(&user_internal, gate_name);
1221
1222        if disable_exposure_logging {
1223            log_d!(TAG, "Exposure logging is disabled for gate {}", gate_name);
1224            self.event_logger.increment_non_exposure_checks(gate_name);
1225        } else {
1226            self.event_logger.enqueue(EnqueueGateExpoOp {
1227                exposure_time: Utc::now().timestamp_millis() as u64,
1228                user: &user_internal,
1229                queried_gate_name: gate_name,
1230                evaluation: evaluation.as_ref().map(Cow::Borrowed),
1231                details: details.clone(),
1232                trigger: ExposureTrigger::Auto,
1233            });
1234        }
1235
1236        let gate = make_feature_gate(gate_name, evaluation, details);
1237        self.emit_gate_evaluated(gate_name, &gate.rule_id, gate.value, &gate.details.reason);
1238        gate
1239    }
1240
1241    pub fn manually_log_gate_exposure(&self, user: &StatsigUser, gate_name: &str) {
1242        let user_internal = self.internalize_user(user);
1243        let (details, evaluation) = self.get_gate_evaluation(&user_internal, gate_name);
1244        self.event_logger.enqueue(EnqueueGateExpoOp {
1245            exposure_time: Utc::now().timestamp_millis() as u64,
1246            user: &user_internal,
1247            queried_gate_name: gate_name,
1248            evaluation: evaluation.map(Cow::Owned),
1249            details: details.clone(),
1250            trigger: ExposureTrigger::Manual,
1251        });
1252    }
1253
1254    pub fn get_fields_needed_for_gate(&self, gate_name: &str) -> Vec<String> {
1255        let data = read_lock_or_else!(self.spec_store.data, {
1256            log_error_to_statsig_and_console!(
1257                self.ops_stats.clone(),
1258                TAG,
1259                StatsigErr::LockFailure(
1260                    "Failed to acquire read lock for spec store data".to_string()
1261                )
1262            );
1263            return vec![];
1264        });
1265
1266        let gate = data.values.feature_gates.get(gate_name);
1267        match gate {
1268            Some(gate) => match &gate.spec.fields_used {
1269                Some(fields) => fields.iter().map(|f| f.unperformant_to_string()).collect(),
1270                None => vec![],
1271            },
1272            None => vec![],
1273        }
1274    }
1275}
1276
1277// -------------------------
1278//   Override Functions
1279// -------------------------
1280
1281impl Statsig {
1282    pub fn override_gate(&self, gate_name: &str, value: bool, id: Option<&str>) {
1283        if let Some(adapter) = &self.override_adapter {
1284            adapter.override_gate(gate_name, value, id);
1285        }
1286    }
1287
1288    pub fn override_dynamic_config(
1289        &self,
1290        config_name: &str,
1291        value: HashMap<String, serde_json::Value>,
1292        id: Option<&str>,
1293    ) {
1294        if let Some(adapter) = &self.override_adapter {
1295            adapter.override_dynamic_config(config_name, value, id);
1296        }
1297    }
1298
1299    pub fn override_layer(
1300        &self,
1301        layer_name: &str,
1302        value: HashMap<String, serde_json::Value>,
1303        id: Option<&str>,
1304    ) {
1305        if let Some(adapter) = &self.override_adapter {
1306            adapter.override_layer(layer_name, value, id);
1307        }
1308    }
1309
1310    pub fn override_experiment(
1311        &self,
1312        experiment_name: &str,
1313        value: HashMap<String, serde_json::Value>,
1314        id: Option<&str>,
1315    ) {
1316        if let Some(adapter) = &self.override_adapter {
1317            adapter.override_experiment(experiment_name, value, id);
1318        }
1319    }
1320
1321    pub fn override_experiment_by_group_name(
1322        &self,
1323        experiment_name: &str,
1324        group_name: &str,
1325        id: Option<&str>,
1326    ) {
1327        if let Some(adapter) = &self.override_adapter {
1328            adapter.override_experiment_by_group_name(experiment_name, group_name, id);
1329        }
1330    }
1331
1332    pub fn remove_gate_override(&self, gate_name: &str, id: Option<&str>) {
1333        if let Some(adapter) = &self.override_adapter {
1334            adapter.remove_gate_override(gate_name, id);
1335        }
1336    }
1337
1338    pub fn remove_dynamic_config_override(&self, config_name: &str, id: Option<&str>) {
1339        if let Some(adapter) = &self.override_adapter {
1340            adapter.remove_dynamic_config_override(config_name, id);
1341        }
1342    }
1343
1344    pub fn remove_experiment_override(&self, experiment_name: &str, id: Option<&str>) {
1345        if let Some(adapter) = &self.override_adapter {
1346            adapter.remove_experiment_override(experiment_name, id);
1347        }
1348    }
1349
1350    pub fn remove_layer_override(&self, layer_name: &str, id: Option<&str>) {
1351        if let Some(adapter) = &self.override_adapter {
1352            adapter.remove_layer_override(layer_name, id);
1353        }
1354    }
1355
1356    pub fn remove_all_overrides(&self) {
1357        if let Some(adapter) = &self.override_adapter {
1358            adapter.remove_all_overrides();
1359        }
1360    }
1361}
1362
1363// -------------------------
1364//   Debugging Functions
1365// -------------------------
1366
1367impl Statsig {
1368    pub fn get_feature_gate_list(&self) -> Vec<String> {
1369        let data = read_lock_or_else!(self.spec_store.data, {
1370            log_error_to_statsig_and_console!(
1371                &self.ops_stats,
1372                TAG,
1373                StatsigErr::LockFailure(
1374                    "Failed to acquire read lock for spec store data".to_string()
1375                )
1376            );
1377            return vec![];
1378        });
1379
1380        data.values.feature_gates.unperformant_keys()
1381    }
1382
1383    pub fn get_dynamic_config_list(&self) -> Vec<String> {
1384        let data = read_lock_or_else!(self.spec_store.data, {
1385            log_error_to_statsig_and_console!(
1386                &self.ops_stats,
1387                TAG,
1388                StatsigErr::LockFailure(
1389                    "Failed to acquire read lock for spec store data".to_string()
1390                )
1391            );
1392            return vec![];
1393        });
1394
1395        data.values
1396            .dynamic_configs
1397            .unperformant_keys_entity_filter("dynamic_config")
1398    }
1399
1400    pub fn get_experiment_list(&self) -> Vec<String> {
1401        let data = read_lock_or_else!(self.spec_store.data, {
1402            log_error_to_statsig_and_console!(
1403                &self.ops_stats,
1404                TAG,
1405                StatsigErr::LockFailure(
1406                    "Failed to acquire read lock for spec store data".to_string()
1407                )
1408            );
1409            return vec![];
1410        });
1411
1412        data.values
1413            .dynamic_configs
1414            .unperformant_keys_entity_filter("experiment")
1415    }
1416
1417    pub fn get_autotune_list(&self) -> Vec<String> {
1418        let data = read_lock_or_else!(self.spec_store.data, {
1419            log_error_to_statsig_and_console!(
1420                &self.ops_stats,
1421                TAG,
1422                StatsigErr::LockFailure(
1423                    "Failed to acquire read lock for spec store data".to_string()
1424                )
1425            );
1426            return vec![];
1427        });
1428
1429        data.values
1430            .dynamic_configs
1431            .unperformant_keys_entity_filter("autotune")
1432    }
1433
1434    pub fn get_parameter_store_list(&self) -> Vec<String> {
1435        let data = read_lock_or_else!(self.spec_store.data, {
1436            log_error_to_statsig_and_console!(
1437                &self.ops_stats,
1438                TAG,
1439                StatsigErr::LockFailure(
1440                    "Failed to acquire read lock for spec store data".to_string()
1441                )
1442            );
1443            return vec![];
1444        });
1445
1446        match &data.values.param_stores {
1447            Some(param_stores) => param_stores.keys().cloned().collect(),
1448            None => vec![],
1449        }
1450    }
1451
1452    pub fn get_layer_list(&self) -> Vec<String> {
1453        let data = read_lock_or_else!(self.spec_store.data, {
1454            log_error_to_statsig_and_console!(
1455                &self.ops_stats,
1456                TAG,
1457                StatsigErr::LockFailure(
1458                    "Failed to acquire read lock for spec store data".to_string()
1459                )
1460            );
1461            return vec![];
1462        });
1463
1464        data.values.layer_configs.unperformant_keys()
1465    }
1466
1467    pub fn __get_parsed_user_agent_value(
1468        &self,
1469        user: &StatsigUser,
1470    ) -> Option<ParsedUserAgentValue> {
1471        UserAgentParser::get_parsed_user_agent_value_for_user(user, &self.options)
1472    }
1473}
1474
1475// -------------------------
1476//   Dynamic Config Functions
1477// -------------------------
1478
1479impl Statsig {
1480    pub fn get_dynamic_config(
1481        &self,
1482        user: &StatsigUser,
1483        dynamic_config_name: &str,
1484    ) -> DynamicConfig {
1485        self.get_dynamic_config_with_options(
1486            user,
1487            dynamic_config_name,
1488            DynamicConfigEvaluationOptions::default(),
1489        )
1490    }
1491
1492    pub fn get_dynamic_config_with_options(
1493        &self,
1494        user: &StatsigUser,
1495        dynamic_config_name: &str,
1496        options: DynamicConfigEvaluationOptions,
1497    ) -> DynamicConfig {
1498        let user_internal = self.internalize_user(user);
1499        let disable_exposure_logging = options.disable_exposure_logging;
1500        let dynamic_config = self.get_dynamic_config_impl(&user_internal, dynamic_config_name);
1501
1502        if disable_exposure_logging {
1503            log_d!(
1504                TAG,
1505                "Exposure logging is disabled for Dynamic Config {}",
1506                dynamic_config_name
1507            );
1508            self.event_logger
1509                .increment_non_exposure_checks(dynamic_config_name);
1510        } else {
1511            self.event_logger.enqueue(EnqueueConfigExpoOp {
1512                exposure_time: Utc::now().timestamp_millis() as u64,
1513                user: &user_internal,
1514                config: &dynamic_config,
1515                trigger: ExposureTrigger::Auto,
1516            });
1517        }
1518
1519        self.emit_dynamic_config_evaluated(&dynamic_config);
1520
1521        dynamic_config
1522    }
1523
1524    pub fn manually_log_dynamic_config_exposure(
1525        &self,
1526        user: &StatsigUser,
1527        dynamic_config_name: &str,
1528    ) {
1529        let user_internal = self.internalize_user(user);
1530        let dynamic_config = self.get_dynamic_config_impl(&user_internal, dynamic_config_name);
1531        self.event_logger.enqueue(EnqueueConfigExpoOp {
1532            exposure_time: Utc::now().timestamp_millis() as u64,
1533            user: &user_internal,
1534            config: &dynamic_config,
1535            trigger: ExposureTrigger::Manual,
1536        });
1537    }
1538
1539    pub fn get_fields_needed_for_dynamic_config(&self, config_name: &str) -> Vec<String> {
1540        let data = read_lock_or_else!(self.spec_store.data, {
1541            log_error_to_statsig_and_console!(
1542                self.ops_stats.clone(),
1543                TAG,
1544                StatsigErr::LockFailure(
1545                    "Failed to acquire read lock for spec store data".to_string()
1546                )
1547            );
1548            return vec![];
1549        });
1550
1551        let config = data.values.dynamic_configs.get(config_name);
1552        match config {
1553            Some(config) => match &config.spec.fields_used {
1554                Some(fields) => fields.iter().map(|f| f.unperformant_to_string()).collect(),
1555                None => vec![],
1556            },
1557            None => vec![],
1558        }
1559    }
1560}
1561
1562// -------------------------
1563//   Experiment Functions
1564// -------------------------
1565
1566impl Statsig {
1567    pub fn get_experiment(&self, user: &StatsigUser, experiment_name: &str) -> Experiment {
1568        self.get_experiment_with_options(
1569            user,
1570            experiment_name,
1571            ExperimentEvaluationOptions::default(),
1572        )
1573    }
1574
1575    pub fn get_experiment_with_options(
1576        &self,
1577        user: &StatsigUser,
1578        experiment_name: &str,
1579        options: ExperimentEvaluationOptions,
1580    ) -> Experiment {
1581        let user_internal = self.internalize_user(user);
1582        let disable_exposure_logging = options.disable_exposure_logging;
1583        let mut experiment = self.get_experiment_impl(&user_internal, experiment_name);
1584        if let Some(persisted_experiment) = self.persistent_values_manager.as_ref().and_then(|m| {
1585            m.try_apply_sticky_value_to_experiment(&user_internal, &options, &experiment)
1586        }) {
1587            experiment = persisted_experiment
1588        }
1589
1590        if disable_exposure_logging {
1591            log_d!(
1592                TAG,
1593                "Exposure logging is disabled for experiment {}",
1594                experiment_name
1595            );
1596            self.event_logger
1597                .increment_non_exposure_checks(experiment_name);
1598        } else {
1599            self.event_logger.enqueue(EnqueueExperimentExpoOp {
1600                exposure_time: Utc::now().timestamp_millis() as u64,
1601                user: &user_internal,
1602                experiment: &experiment,
1603                trigger: ExposureTrigger::Auto,
1604            });
1605        }
1606
1607        self.emit_experiment_evaluated(&experiment);
1608
1609        experiment
1610    }
1611
1612    pub fn manually_log_experiment_exposure(&self, user: &StatsigUser, experiment_name: &str) {
1613        let user_internal = self.internalize_user(user);
1614        let experiment = self.get_experiment_impl(&user_internal, experiment_name);
1615        self.event_logger.enqueue(EnqueueExperimentExpoOp {
1616            exposure_time: Utc::now().timestamp_millis() as u64,
1617            user: &user_internal,
1618            experiment: &experiment,
1619            trigger: ExposureTrigger::Manual,
1620        });
1621    }
1622
1623    pub fn get_fields_needed_for_experiment(&self, experiment_name: &str) -> Vec<String> {
1624        let data = read_lock_or_else!(self.spec_store.data, {
1625            log_error_to_statsig_and_console!(
1626                self.ops_stats.clone(),
1627                TAG,
1628                StatsigErr::LockFailure(
1629                    "Failed to acquire read lock for spec store data".to_string()
1630                )
1631            );
1632            return vec![];
1633        });
1634
1635        let config = data.values.dynamic_configs.get(experiment_name);
1636        match config {
1637            Some(config) => match &config.spec.fields_used {
1638                Some(fields) => fields.iter().map(|f| f.unperformant_to_string()).collect(),
1639                None => vec![],
1640            },
1641            None => vec![],
1642        }
1643    }
1644
1645    pub fn get_experiment_by_group_name(
1646        &self,
1647        experiment_name: &str,
1648        group_name: &str,
1649    ) -> Experiment {
1650        let data = read_lock_or_else!(self.spec_store.data, {
1651            log_error_to_statsig_and_console!(
1652                self.ops_stats.clone(),
1653                TAG,
1654                StatsigErr::LockFailure(
1655                    "Failed to acquire read lock for spec store data".to_string()
1656                )
1657            );
1658            return make_experiment(
1659                experiment_name,
1660                None,
1661                EvaluationDetails::error("Failed to acquire read lock for spec store data"),
1662            );
1663        });
1664
1665        let Some(exp) = data.values.dynamic_configs.get(experiment_name) else {
1666            return make_experiment(
1667                experiment_name,
1668                None,
1669                EvaluationDetails::unrecognized(&data),
1670            );
1671        };
1672
1673        if let Some(rule) = exp
1674            .spec
1675            .rules
1676            .iter()
1677            .find(|rule| rule.group_name.as_deref() == Some(group_name))
1678        {
1679            let value = rule.return_value.get_json().unwrap_or_default();
1680            let rule_id = String::from(rule.id.as_str());
1681            let id_type = rule.id_type.value.unperformant_to_string();
1682            let group_name = rule.group_name.as_ref().map(|g| g.unperformant_to_string());
1683
1684            return Experiment {
1685                name: experiment_name.to_string(),
1686                value,
1687                rule_id,
1688                id_type,
1689                group_name,
1690                details: EvaluationDetails::recognized_without_eval_result(&data),
1691                is_experiment_active: exp.spec.is_active.unwrap_or(false),
1692                __evaluation: None,
1693            };
1694        }
1695
1696        make_experiment(
1697            experiment_name,
1698            None,
1699            EvaluationDetails::unrecognized(&data),
1700        )
1701    }
1702}
1703
1704// -------------------------
1705//   Layer Functions
1706// -------------------------
1707
1708impl Statsig {
1709    pub fn get_layer(&self, user: &StatsigUser, layer_name: &str) -> Layer {
1710        self.get_layer_with_options(user, layer_name, LayerEvaluationOptions::default())
1711    }
1712
1713    pub fn get_layer_with_options(
1714        &self,
1715        user: &StatsigUser,
1716        layer_name: &str,
1717        options: LayerEvaluationOptions,
1718    ) -> Layer {
1719        let user_internal = self.internalize_user(user);
1720        self.get_layer_impl(user_internal, layer_name, options)
1721    }
1722
1723    pub fn get_prompt(&self, user: &StatsigUser, prompt_name: &str) -> Layer {
1724        self.get_layer(user, prompt_name)
1725    }
1726
1727    pub fn get_prompt_with_options(
1728        &self,
1729        user: &StatsigUser,
1730        prompt_name: &str,
1731        options: LayerEvaluationOptions,
1732    ) -> Layer {
1733        self.get_layer_with_options(user, prompt_name, options)
1734    }
1735
1736    pub fn manually_log_layer_parameter_exposure(
1737        &self,
1738        user: &StatsigUser,
1739        layer_name: &str,
1740        parameter_name: String,
1741    ) {
1742        let user_internal = self.internalize_user(user);
1743        let layer =
1744            self.get_layer_impl(user_internal, layer_name, LayerEvaluationOptions::default());
1745
1746        self.event_logger
1747            .enqueue(EnqueueLayerParamExpoOp::LayerOwned(
1748                Utc::now().timestamp_millis() as u64,
1749                Box::new(layer),
1750                parameter_name,
1751                ExposureTrigger::Manual,
1752            ));
1753    }
1754
1755    pub fn get_fields_needed_for_layer(&self, layer_name: &str) -> Vec<String> {
1756        let data = read_lock_or_else!(self.spec_store.data, {
1757            log_error_to_statsig_and_console!(
1758                self.ops_stats.clone(),
1759                TAG,
1760                StatsigErr::LockFailure(
1761                    "Failed to acquire read lock for spec store data".to_string()
1762                )
1763            );
1764            return vec![];
1765        });
1766
1767        let layer = data.values.layer_configs.get(layer_name);
1768        match layer {
1769            Some(layer) => match &layer.spec.fields_used {
1770                Some(fields) => fields.iter().map(|f| f.unperformant_to_string()).collect(),
1771                None => vec![],
1772            },
1773            None => vec![],
1774        }
1775    }
1776}
1777
1778// -------------------------
1779//   Internal Functions
1780// -------------------------
1781
1782impl Statsig {
1783    pub(crate) fn get_from_statsig_env(&self, key: &str) -> Option<DynamicValue> {
1784        if let Some(env) = &self.statsig_environment {
1785            return env.get(key).cloned();
1786        }
1787
1788        if let Some(fallback_env) = self
1789            .fallback_environment
1790            .try_lock_for(Duration::from_secs(5))
1791        {
1792            if let Some(env) = &*fallback_env {
1793                return env.get(key).cloned();
1794            }
1795        }
1796
1797        None
1798    }
1799
1800    pub(crate) fn get_value_from_global_custom_fields(&self, key: &str) -> Option<&DynamicValue> {
1801        if let Some(env) = &self.options.global_custom_fields {
1802            return env.get(key);
1803        }
1804
1805        None
1806    }
1807
1808    pub(crate) fn use_global_custom_fields<T>(
1809        &self,
1810        f: impl FnOnce(Option<&HashMap<String, DynamicValue>>) -> T,
1811    ) -> T {
1812        f(self.options.global_custom_fields.as_ref())
1813    }
1814
1815    pub(crate) fn use_statsig_env<T>(
1816        &self,
1817        f: impl FnOnce(Option<&HashMap<String, DynamicValue>>) -> T,
1818    ) -> T {
1819        if let Some(env) = &self.statsig_environment {
1820            return f(Some(env));
1821        }
1822
1823        if let Some(fallback_env) = self
1824            .fallback_environment
1825            .try_lock_for(Duration::from_secs(5))
1826        {
1827            if let Some(env) = &*fallback_env {
1828                return f(Some(env));
1829            }
1830        }
1831
1832        f(None)
1833    }
1834}
1835
1836// -------------------------
1837//   Private Functions
1838// -------------------------
1839
1840impl Statsig {
1841    fn evaluate_spec<T>(
1842        &self,
1843        user_internal: &StatsigUserInternal,
1844        spec_name: &str,
1845        make_empty_result: impl FnOnce(EvaluationDetails) -> T,
1846        make_result: impl FnOnce(EvaluatorResult, EvaluationDetails) -> T,
1847        spec_type: &SpecType,
1848    ) -> T {
1849        let data = read_lock_or_else!(self.spec_store.data, {
1850            log_error_to_statsig_and_console!(
1851                &self.ops_stats,
1852                TAG,
1853                StatsigErr::LockFailure(
1854                    "Failed to acquire read lock for spec store data".to_string()
1855                )
1856            );
1857            return make_empty_result(EvaluationDetails::unrecognized_no_data());
1858        });
1859
1860        let app_id = data.values.app_id.as_ref();
1861        let mut context = EvaluatorContext::new(
1862            user_internal,
1863            &data,
1864            &self.hashing,
1865            app_id,
1866            self.override_adapter.as_ref(),
1867            self.should_use_experimental_ua_parser(),
1868        );
1869
1870        match Evaluator::evaluate_with_details(&mut context, spec_name, spec_type) {
1871            Ok(eval_details) => make_result(context.result, eval_details),
1872            Err(e) => {
1873                log_error_to_statsig_and_console!(
1874                    &self.ops_stats,
1875                    TAG,
1876                    StatsigErr::EvaluationError(e.to_string())
1877                );
1878                make_empty_result(EvaluationDetails::error(&e.to_string()))
1879            }
1880        }
1881    }
1882
1883    fn get_gate_evaluation(
1884        &self,
1885        user_internal: &StatsigUserInternal,
1886        gate_name: &str,
1887    ) -> (EvaluationDetails, Option<GateEvaluation>) {
1888        self.evaluate_spec(
1889            user_internal,
1890            gate_name,
1891            |eval_details| (eval_details, None),
1892            |mut result, eval_details| {
1893                let evaluation = result_to_gate_eval(gate_name, &mut result);
1894                (eval_details, Some(evaluation))
1895            },
1896            &SpecType::Gate,
1897        )
1898    }
1899
1900    fn get_dynamic_config_impl(
1901        &self,
1902        user_internal: &StatsigUserInternal,
1903        config_name: &str,
1904    ) -> DynamicConfig {
1905        self.evaluate_spec(
1906            user_internal,
1907            config_name,
1908            |eval_details| make_dynamic_config(config_name, None, eval_details),
1909            |mut result, eval_details| {
1910                let evaluation = result_to_dynamic_config_eval(config_name, &mut result);
1911                make_dynamic_config(config_name, Some(evaluation), eval_details)
1912            },
1913            &SpecType::DynamicConfig,
1914        )
1915    }
1916
1917    fn get_experiment_impl(
1918        &self,
1919        user_internal: &StatsigUserInternal,
1920        experiment_name: &str,
1921    ) -> Experiment {
1922        self.evaluate_spec(
1923            user_internal,
1924            experiment_name,
1925            |eval_details| make_experiment(experiment_name, None, eval_details),
1926            |mut result, eval_details| {
1927                let evaluation = result_to_experiment_eval(experiment_name, None, &mut result);
1928                make_experiment(experiment_name, Some(evaluation), eval_details)
1929            },
1930            &SpecType::Experiment,
1931        )
1932    }
1933
1934    fn get_layer_impl(
1935        &self,
1936        user_internal: StatsigUserInternal,
1937        layer_name: &str,
1938        evaluation_options: LayerEvaluationOptions,
1939    ) -> Layer {
1940        let disable_exposure_logging = evaluation_options.disable_exposure_logging;
1941
1942        if disable_exposure_logging {
1943            self.event_logger.increment_non_exposure_checks(layer_name);
1944        }
1945
1946        let mut layer = self.evaluate_spec(
1947            &user_internal,
1948            layer_name,
1949            |eval_details| {
1950                make_layer(
1951                    user_internal.to_loggable(),
1952                    layer_name,
1953                    None,
1954                    eval_details,
1955                    None,
1956                    disable_exposure_logging,
1957                )
1958            },
1959            |mut result, eval_details| {
1960                let evaluation = result_to_layer_eval(layer_name, &mut result);
1961                let event_logger_ptr = Arc::downgrade(&self.event_logger);
1962
1963                make_layer(
1964                    user_internal.to_loggable(),
1965                    layer_name,
1966                    Some(evaluation),
1967                    eval_details,
1968                    Some(event_logger_ptr),
1969                    disable_exposure_logging,
1970                )
1971            },
1972            &SpecType::Layer,
1973        );
1974
1975        let data = read_lock_or_else!(self.spec_store.data, {
1976            log_error_to_statsig_and_console!(
1977                &self.ops_stats,
1978                TAG,
1979                StatsigErr::LockFailure(
1980                    "Failed to acquire read lock for spec store data".to_string()
1981                )
1982            );
1983            return layer;
1984        });
1985        if let Some(persisted_layer) = self.persistent_values_manager.as_ref().and_then(|p| {
1986            let event_logger_ptr = Arc::downgrade(&self.event_logger);
1987            p.try_apply_sticky_value_to_layer(
1988                &user_internal,
1989                &evaluation_options,
1990                &layer,
1991                Some(event_logger_ptr),
1992                disable_exposure_logging,
1993                &data,
1994            )
1995        }) {
1996            layer = persisted_layer
1997        }
1998
1999        self.emit_layer_evaluated(&layer);
2000
2001        layer
2002    }
2003
2004    fn internalize_user<'s, 'u>(&'s self, user: &'u StatsigUser) -> StatsigUserInternal<'s, 'u> {
2005        StatsigUserInternal::new(user, Some(self))
2006    }
2007
2008    fn set_default_environment_from_server(&self) {
2009        let data = read_lock_or_else!(self.spec_store.data, {
2010            return;
2011        });
2012
2013        if let Some(default_env) = data.values.default_environment.as_ref() {
2014            let env_map = HashMap::from([("tier".to_string(), dyn_value!(default_env.as_str()))]);
2015
2016            match self
2017                .fallback_environment
2018                .try_lock_for(Duration::from_secs(5))
2019            {
2020                Some(mut fallback_env) => {
2021                    *fallback_env = Some(env_map);
2022                }
2023                None => {
2024                    log_e!(TAG, "Failed to lock fallback_environment");
2025                }
2026            }
2027        }
2028    }
2029
2030    fn log_init_finish(
2031        &self,
2032        success: bool,
2033        error_message: &Option<String>,
2034        duration: &f64,
2035        specs_info: &SpecsInfo,
2036    ) {
2037        let is_store_populated = specs_info.source != SpecsSource::NoValues;
2038        let source_str = specs_info.source.to_string();
2039
2040        let event = ObservabilityEvent::new_event(
2041            MetricType::Dist,
2042            "initialization".to_string(),
2043            *duration,
2044            Some(HashMap::from([
2045                ("success".to_owned(), success.to_string()),
2046                ("source".to_owned(), source_str.clone()),
2047                ("store_populated".to_owned(), is_store_populated.to_string()),
2048                (
2049                    "spec_source_api".to_owned(),
2050                    specs_info.source_api.clone().unwrap_or_default(),
2051                ),
2052            ])),
2053        );
2054
2055        self.ops_stats.log(event);
2056        self.ops_stats.add_marker(
2057            {
2058                let marker = Marker::new(KeyType::Overall, ActionType::End, None)
2059                    .with_is_success(success)
2060                    .with_config_spec_ready(specs_info.source != SpecsSource::NoValues)
2061                    .with_source(source_str);
2062
2063                if let Some(msg) = &error_message {
2064                    marker.with_message(msg.to_string())
2065                } else {
2066                    marker
2067                }
2068            },
2069            Some(ContextType::Initialize),
2070        );
2071        self.ops_stats
2072            .enqueue_diagnostics_event(None, Some(ContextType::Initialize));
2073    }
2074
2075    fn should_use_experimental_ua_parser(&self) -> bool {
2076        self.options
2077            .__experimental_ua_parsing_enabled
2078            .unwrap_or(false)
2079    }
2080}
2081
2082fn initialize_event_logging_adapter(
2083    sdk_key: &str,
2084    options: &StatsigOptions,
2085) -> Arc<dyn EventLoggingAdapter> {
2086    options
2087        .event_logging_adapter
2088        .clone()
2089        .unwrap_or_else(|| Arc::new(StatsigHttpEventLoggingAdapter::new(sdk_key, Some(options))))
2090}
2091
2092fn initialize_specs_adapter(
2093    sdk_key: &str,
2094    options: &StatsigOptions,
2095    hashing: &HashUtil,
2096) -> SpecsAdapterHousing {
2097    if let Some(adapter) = options.specs_adapter.clone() {
2098        log_d!(TAG, "Using provided SpecsAdapter: {}", sdk_key);
2099        return SpecsAdapterHousing {
2100            inner: adapter,
2101            as_default_adapter: None,
2102        };
2103    }
2104
2105    if let Some(adapter_config) = options.spec_adapters_config.clone() {
2106        let adapter = Arc::new(StatsigCustomizedSpecsAdapter::new_from_config(
2107            sdk_key,
2108            adapter_config,
2109            options,
2110            hashing,
2111        ));
2112
2113        return SpecsAdapterHousing {
2114            inner: adapter,
2115            as_default_adapter: None,
2116        };
2117    }
2118
2119    if let Some(data_adapter) = options.data_store.clone() {
2120        let adapter = Arc::new(StatsigCustomizedSpecsAdapter::new_from_data_store(
2121            sdk_key,
2122            data_adapter,
2123            options,
2124            hashing,
2125        ));
2126
2127        return SpecsAdapterHousing {
2128            inner: adapter,
2129            as_default_adapter: None,
2130        };
2131    }
2132
2133    let adapter = Arc::new(StatsigHttpSpecsAdapter::new(sdk_key, Some(options), None));
2134
2135    SpecsAdapterHousing {
2136        inner: adapter.clone(),
2137        as_default_adapter: Some(adapter),
2138    }
2139}
2140
2141fn initialize_id_lists_adapter(sdk_key: &str, options: &StatsigOptions) -> IdListsAdapterHousing {
2142    if let Some(id_lists_adapter) = options.id_lists_adapter.clone() {
2143        return IdListsAdapterHousing {
2144            inner: Some(id_lists_adapter),
2145            as_default_adapter: None,
2146        };
2147    }
2148
2149    if options.enable_id_lists.unwrap_or(false) {
2150        let adapter = Arc::new(StatsigHttpIdListsAdapter::new(sdk_key, options));
2151
2152        return IdListsAdapterHousing {
2153            inner: Some(adapter.clone()),
2154            as_default_adapter: Some(adapter),
2155        };
2156    }
2157
2158    IdListsAdapterHousing {
2159        inner: None,
2160        as_default_adapter: None,
2161    }
2162}
2163
2164struct IdListsAdapterHousing {
2165    inner: Option<Arc<dyn IdListsAdapter>>,
2166    as_default_adapter: Option<Arc<StatsigHttpIdListsAdapter>>,
2167}
2168
2169struct SpecsAdapterHousing {
2170    inner: Arc<dyn SpecsAdapter>,
2171    as_default_adapter: Option<Arc<StatsigHttpSpecsAdapter>>,
2172}
2173
2174fn setup_ops_stats(
2175    sdk_key: &str,
2176    statsig_runtime: Arc<StatsigRuntime>,
2177    error_observer: &Arc<dyn OpsStatsEventObserver>,
2178    diagnostics_observer: &Arc<dyn OpsStatsEventObserver>,
2179    external_observer: &Option<Weak<dyn ObservabilityClient>>,
2180) -> Arc<OpsStatsForInstance> {
2181    let ops_stat = OPS_STATS.get_for_instance(sdk_key);
2182    ops_stat.subscribe(statsig_runtime.clone(), Arc::downgrade(error_observer));
2183    ops_stat.subscribe(
2184        statsig_runtime.clone(),
2185        Arc::downgrade(diagnostics_observer),
2186    );
2187
2188    if let Some(ob_client) = external_observer {
2189        if let Some(client) = ob_client.upgrade() {
2190            client.init();
2191            let as_observer = client.to_ops_stats_event_observer();
2192            ops_stat.subscribe(statsig_runtime, Arc::downgrade(&as_observer));
2193        }
2194    }
2195
2196    ops_stat
2197}