nimbus/stateful/
nimbus_client.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5#[cfg(test)]
6use crate::tests::helpers::{TestGeckoPrefHandler, TestMetrics, TestRecordedContext};
7use crate::{
8    defaults::Defaults,
9    enrollment::{
10        EnrolledFeature, EnrollmentChangeEvent, EnrollmentChangeEventType, EnrollmentsEvolver,
11        ExperimentEnrollment,
12    },
13    error::{info, warn, BehaviorError},
14    evaluator::{
15        get_calculated_attributes, is_experiment_available, CalculatedAttributes,
16        TargetingAttributes,
17    },
18    json::{JsonObject, PrefValue},
19    metrics::{
20        EnrollmentStatusExtraDef, FeatureExposureExtraDef, MalformedFeatureConfigExtraDef,
21        MetricsHandler,
22    },
23    schema::parse_experiments,
24    stateful::{
25        behavior::EventStore,
26        client::{create_client, SettingsClient},
27        dbcache::DatabaseCache,
28        enrollment::{
29            get_global_user_participation, opt_in_with_branch, opt_out,
30            reset_telemetry_identifiers, set_global_user_participation, unenroll_for_pref,
31        },
32        gecko_prefs::{
33            GeckoPref, GeckoPrefHandler, GeckoPrefState, GeckoPrefStore, PrefBranch,
34            PrefEnrollmentData, PrefUnenrollReason,
35        },
36        matcher::AppContext,
37        persistence::{Database, StoreId, Writer},
38        targeting::{validate_event_queries, RecordedContext},
39        updating::{read_and_remove_pending_experiments, write_pending_experiments},
40    },
41    strings::fmt_with_map,
42    AvailableExperiment, AvailableRandomizationUnits, EnrolledExperiment, Experiment,
43    ExperimentBranch, NimbusError, NimbusTargetingHelper, Result,
44};
45use chrono::{DateTime, NaiveDateTime, Utc};
46use once_cell::sync::OnceCell;
47use remote_settings::RemoteSettingsConfig;
48use serde_json::Value;
49use std::collections::HashSet;
50use std::fmt::Debug;
51use std::path::{Path, PathBuf};
52use std::sync::{Arc, Mutex, MutexGuard};
53use uuid::Uuid;
54
55const DB_KEY_NIMBUS_ID: &str = "nimbus-id";
56pub const DB_KEY_INSTALLATION_DATE: &str = "installation-date";
57pub const DB_KEY_UPDATE_DATE: &str = "update-date";
58pub const DB_KEY_APP_VERSION: &str = "app-version";
59pub const DB_KEY_FETCH_ENABLED: &str = "fetch-enabled";
60
61// The main `NimbusClient` struct must not expose any methods that make an `&mut self`,
62// in order to be compatible with the uniffi's requirements on objects. This is a helper
63// struct to contain the bits that do actually need to be mutable, so they can be
64// protected by a Mutex.
65#[derive(Default)]
66pub struct InternalMutableState {
67    pub(crate) available_randomization_units: AvailableRandomizationUnits,
68    pub(crate) install_date: Option<DateTime<Utc>>,
69    pub(crate) update_date: Option<DateTime<Utc>>,
70    // Application level targeting attributes
71    pub(crate) targeting_attributes: TargetingAttributes,
72}
73
74impl InternalMutableState {
75    pub(crate) fn update_time_to_now(&mut self, now: DateTime<Utc>) {
76        self.targeting_attributes
77            .update_time_to_now(now, &self.install_date, &self.update_date);
78    }
79}
80
81/// Nimbus is the main struct representing the experiments state
82/// It should hold all the information needed to communicate a specific user's
83/// experimentation status
84pub struct NimbusClient {
85    settings_client: Mutex<Box<dyn SettingsClient + Send>>,
86    pub(crate) mutable_state: Mutex<InternalMutableState>,
87    app_context: AppContext,
88    pub(crate) db: OnceCell<Database>,
89    // Manages an in-memory cache so that we can answer certain requests
90    // without doing (or waiting for) IO.
91    database_cache: DatabaseCache,
92    db_path: PathBuf,
93    coenrolling_feature_ids: Vec<String>,
94    event_store: Arc<Mutex<EventStore>>,
95    recorded_context: Option<Arc<dyn RecordedContext>>,
96    pub(crate) gecko_prefs: Option<Arc<GeckoPrefStore>>,
97    metrics_handler: Arc<Box<dyn MetricsHandler>>,
98}
99
100impl NimbusClient {
101    // This constructor *must* not do any kind of I/O since it might be called on the main
102    // thread in the gecko Javascript stack, hence the use of OnceCell for the db.
103    pub fn new<P: Into<PathBuf>>(
104        app_context: AppContext,
105        recorded_context: Option<Arc<dyn RecordedContext>>,
106        coenrolling_feature_ids: Vec<String>,
107        db_path: P,
108        config: Option<RemoteSettingsConfig>,
109        metrics_handler: Box<dyn MetricsHandler>,
110        gecko_pref_handler: Option<Box<dyn GeckoPrefHandler>>,
111    ) -> Result<Self> {
112        let settings_client = Mutex::new(create_client(config)?);
113
114        let targeting_attributes: TargetingAttributes = app_context.clone().into();
115        let mutable_state = Mutex::new(InternalMutableState {
116            available_randomization_units: Default::default(),
117            targeting_attributes,
118            install_date: Default::default(),
119            update_date: Default::default(),
120        });
121
122        let mut prefs = None;
123        if let Some(handler) = gecko_pref_handler {
124            prefs = Some(Arc::new(GeckoPrefStore::new(Arc::new(handler))));
125        }
126
127        Ok(Self {
128            settings_client,
129            mutable_state,
130            app_context,
131            database_cache: Default::default(),
132            db_path: db_path.into(),
133            coenrolling_feature_ids,
134            db: OnceCell::default(),
135            event_store: Arc::default(),
136            recorded_context,
137            gecko_prefs: prefs,
138            metrics_handler: Arc::new(metrics_handler),
139        })
140    }
141
142    pub fn with_targeting_attributes(&mut self, targeting_attributes: TargetingAttributes) {
143        let mut state = self.mutable_state.lock().unwrap();
144        state.targeting_attributes = targeting_attributes;
145    }
146
147    pub fn get_targeting_attributes(&self) -> TargetingAttributes {
148        let mut state = self.mutable_state.lock().unwrap();
149        state.update_time_to_now(Utc::now());
150        state.targeting_attributes.clone()
151    }
152
153    pub fn initialize(&self) -> Result<()> {
154        let db = self.db()?;
155        // We're not actually going to write, we just want to exclude concurrent writers.
156        let mut writer = db.write()?;
157
158        let mut state = self.mutable_state.lock().unwrap();
159        self.begin_initialize(db, &mut writer, &mut state)?;
160        self.end_initialize(db, writer, &mut state)?;
161
162        Ok(())
163    }
164
165    // These are tasks which should be in the initialize and apply_pending_experiments
166    // but should happen before the enrollment calculations are done.
167    fn begin_initialize(
168        &self,
169        db: &Database,
170        writer: &mut Writer,
171        state: &mut MutexGuard<InternalMutableState>,
172    ) -> Result<()> {
173        self.read_or_create_nimbus_id(db, writer, state)?;
174        self.update_ta_install_dates(db, writer, state)?;
175        self.event_store
176            .lock()
177            .expect("unable to lock event_store mutex")
178            .read_from_db(db)?;
179
180        if let Some(recorded_context) = &self.recorded_context {
181            let targeting_helper = self.create_targeting_helper_with_context(match serde_json::to_value(
182                &state.targeting_attributes,
183            ) {
184                Ok(v) => v,
185                Err(e) => return Err(NimbusError::JSONError("targeting_helper = nimbus::stateful::nimbus_client::NimbusClient::begin_initialize::serde_json::to_value".into(), e.to_string()))
186            });
187            recorded_context.execute_queries(targeting_helper.as_ref())?;
188            state
189                .targeting_attributes
190                .set_recorded_context(recorded_context.to_json());
191        }
192
193        if let Some(gecko_prefs) = &self.gecko_prefs {
194            gecko_prefs.initialize()?;
195        }
196
197        Ok(())
198    }
199
200    // These are tasks which should be in the initialize and apply_pending_experiments
201    // but should happen after the enrollment calculations are done.
202    fn end_initialize(
203        &self,
204        db: &Database,
205        writer: Writer,
206        state: &mut MutexGuard<InternalMutableState>,
207    ) -> Result<()> {
208        self.update_ta_active_experiments(db, &writer, state)?;
209        let coenrolling_ids = self
210            .coenrolling_feature_ids
211            .iter()
212            .map(|s| s.as_str())
213            .collect();
214        self.database_cache.commit_and_update(
215            db,
216            writer,
217            &coenrolling_ids,
218            self.gecko_prefs.clone(),
219        )?;
220        self.record_enrollment_status_telemetry(state)?;
221        Ok(())
222    }
223
224    pub fn get_enrollment_by_feature(&self, feature_id: String) -> Result<Option<EnrolledFeature>> {
225        self.database_cache.get_enrollment_by_feature(&feature_id)
226    }
227
228    // Note: the contract for this function is that it never blocks on IO.
229    pub fn get_experiment_branch(&self, slug: String) -> Result<Option<String>> {
230        self.database_cache.get_experiment_branch(&slug)
231    }
232
233    pub fn get_feature_config_variables(&self, feature_id: String) -> Result<Option<String>> {
234        Ok(
235            if let Some(s) = self
236                .database_cache
237                .get_feature_config_variables(&feature_id)?
238            {
239                self.record_feature_activation_if_needed(&feature_id);
240                Some(s)
241            } else {
242                None
243            },
244        )
245    }
246
247    pub fn get_experiment_branches(&self, slug: String) -> Result<Vec<ExperimentBranch>> {
248        self.get_all_experiments()?
249            .into_iter()
250            .find(|e| e.slug == slug)
251            .map(|e| e.branches.into_iter().map(|b| b.into()).collect())
252            .ok_or(NimbusError::NoSuchExperiment(slug))
253    }
254
255    pub fn get_global_user_participation(&self) -> Result<bool> {
256        let db = self.db()?;
257        let reader = db.read()?;
258        get_global_user_participation(db, &reader)
259    }
260
261    pub fn set_global_user_participation(
262        &self,
263        user_participating: bool,
264    ) -> Result<Vec<EnrollmentChangeEvent>> {
265        let db = self.db()?;
266        let mut writer = db.write()?;
267        let mut state = self.mutable_state.lock().unwrap();
268        set_global_user_participation(db, &mut writer, user_participating)?;
269
270        let existing_experiments: Vec<Experiment> =
271            db.get_store(StoreId::Experiments).collect_all(&writer)?;
272        // We pass the existing experiments as "updated experiments"
273        // to the evolver.
274        let events = self.evolve_experiments(db, &mut writer, &mut state, &existing_experiments)?;
275        self.end_initialize(db, writer, &mut state)?;
276        Ok(events)
277    }
278
279    pub fn get_active_experiments(&self) -> Result<Vec<EnrolledExperiment>> {
280        self.database_cache.get_active_experiments()
281    }
282
283    pub fn get_all_experiments(&self) -> Result<Vec<Experiment>> {
284        let db = self.db()?;
285        let reader = db.read()?;
286        db.get_store(StoreId::Experiments)
287            .collect_all::<Experiment, _>(&reader)
288    }
289
290    pub fn get_available_experiments(&self) -> Result<Vec<AvailableExperiment>> {
291        let th = self.create_targeting_helper(None)?;
292        Ok(self
293            .get_all_experiments()?
294            .into_iter()
295            .filter(|exp| is_experiment_available(&th, exp, false))
296            .map(|exp| exp.into())
297            .collect())
298    }
299
300    pub fn opt_in_with_branch(
301        &self,
302        experiment_slug: String,
303        branch: String,
304    ) -> Result<Vec<EnrollmentChangeEvent>> {
305        let db = self.db()?;
306        let mut writer = db.write()?;
307        let result = opt_in_with_branch(db, &mut writer, &experiment_slug, &branch)?;
308        let mut state = self.mutable_state.lock().unwrap();
309        self.end_initialize(db, writer, &mut state)?;
310        Ok(result)
311    }
312
313    pub fn opt_out(&self, experiment_slug: String) -> Result<Vec<EnrollmentChangeEvent>> {
314        let db = self.db()?;
315        let mut writer = db.write()?;
316        let result = opt_out(db, &mut writer, &experiment_slug)?;
317        let mut state = self.mutable_state.lock().unwrap();
318        self.end_initialize(db, writer, &mut state)?;
319        Ok(result)
320    }
321
322    pub fn fetch_experiments(&self) -> Result<()> {
323        if !self.is_fetch_enabled()? {
324            return Ok(());
325        }
326        info!("fetching experiments");
327        let settings_client = self.settings_client.lock().unwrap();
328        let new_experiments = settings_client.fetch_experiments()?;
329        let db = self.db()?;
330        let mut writer = db.write()?;
331        write_pending_experiments(db, &mut writer, new_experiments)?;
332        writer.commit()?;
333        Ok(())
334    }
335
336    pub fn set_fetch_enabled(&self, allow: bool) -> Result<()> {
337        let db = self.db()?;
338        let mut writer = db.write()?;
339        db.get_store(StoreId::Meta)
340            .put(&mut writer, DB_KEY_FETCH_ENABLED, &allow)?;
341        writer.commit()?;
342        Ok(())
343    }
344
345    pub(crate) fn is_fetch_enabled(&self) -> Result<bool> {
346        let db = self.db()?;
347        let reader = db.read()?;
348        let enabled = db
349            .get_store(StoreId::Meta)
350            .get(&reader, DB_KEY_FETCH_ENABLED)?
351            .unwrap_or(true);
352        Ok(enabled)
353    }
354
355    /**
356     * Calculate the days since install and days since update on the targeting_attributes.
357     */
358    fn update_ta_install_dates(
359        &self,
360        db: &Database,
361        writer: &mut Writer,
362        state: &mut MutexGuard<InternalMutableState>,
363    ) -> Result<()> {
364        // Only set install_date and update_date with this method if it hasn't been set already.
365        // This cuts down on deriving the dates at runtime, but also allows us to use
366        // the test methods set_install_date() and set_update_date() to set up
367        // scenarios for test.
368        if state.install_date.is_none() {
369            let installation_date = self.get_installation_date(db, writer)?;
370            state.install_date = Some(installation_date);
371        }
372        if state.update_date.is_none() {
373            let update_date = self.get_update_date(db, writer)?;
374            state.update_date = Some(update_date);
375        }
376        state.update_time_to_now(Utc::now());
377
378        Ok(())
379    }
380
381    /**
382     * Calculates the active_experiments based on current enrollments for the targeting attributes.
383     */
384    fn update_ta_active_experiments(
385        &self,
386        db: &Database,
387        writer: &Writer,
388        state: &mut MutexGuard<InternalMutableState>,
389    ) -> Result<()> {
390        let enrollments_store = db.get_store(StoreId::Enrollments);
391        let prev_enrollments: Vec<ExperimentEnrollment> = enrollments_store.collect_all(writer)?;
392
393        state
394            .targeting_attributes
395            .update_enrollments(&prev_enrollments);
396
397        Ok(())
398    }
399
400    fn evolve_experiments(
401        &self,
402        db: &Database,
403        writer: &mut Writer,
404        state: &mut InternalMutableState,
405        experiments: &[Experiment],
406    ) -> Result<Vec<EnrollmentChangeEvent>> {
407        let mut targeting_helper = NimbusTargetingHelper::with_targeting_attributes(
408            &state.targeting_attributes,
409            self.event_store.clone(),
410            self.gecko_prefs.clone(),
411        );
412        if let Some(ref recorded_context) = self.recorded_context {
413            recorded_context.record();
414        }
415        let coenrolling_feature_ids = self
416            .coenrolling_feature_ids
417            .iter()
418            .map(|s| s.as_str())
419            .collect();
420        let mut evolver = EnrollmentsEvolver::new(
421            &state.available_randomization_units,
422            &mut targeting_helper,
423            &coenrolling_feature_ids,
424        );
425        evolver.evolve_enrollments_in_db(db, writer, experiments)
426    }
427
428    pub fn apply_pending_experiments(&self) -> Result<Vec<EnrollmentChangeEvent>> {
429        info!("updating experiment list");
430        let db = self.db()?;
431        let mut writer = db.write()?;
432
433        // We'll get the pending experiments which were stored for us, either by fetch_experiments
434        // or by set_experiments_locally.
435        let pending_updates = read_and_remove_pending_experiments(db, &mut writer)?;
436        let mut state = self.mutable_state.lock().unwrap();
437        self.begin_initialize(db, &mut writer, &mut state)?;
438
439        let res = match pending_updates {
440            Some(new_experiments) => {
441                self.update_ta_active_experiments(db, &writer, &mut state)?;
442                // Perform the enrollment calculations if there are pending experiments.
443                self.evolve_experiments(db, &mut writer, &mut state, &new_experiments)?
444            }
445            None => vec![],
446        };
447
448        // Finish up any cleanup, e.g. copying from database in to memory.
449        self.end_initialize(db, writer, &mut state)?;
450        Ok(res)
451    }
452
453    #[allow(deprecated)] // Bug 1960256 - use of deprecated chrono functions.
454    fn get_installation_date(&self, db: &Database, writer: &mut Writer) -> Result<DateTime<Utc>> {
455        // we first check our context
456        if let Some(context_installation_date) = self.app_context.installation_date {
457            let res = DateTime::<Utc>::from_naive_utc_and_offset(
458                NaiveDateTime::from_timestamp_opt(context_installation_date / 1_000, 0).unwrap(),
459                Utc,
460            );
461            info!("[Nimbus] Retrieved date from Context: {}", res);
462            return Ok(res);
463        }
464        let store = db.get_store(StoreId::Meta);
465        let persisted_installation_date: Option<DateTime<Utc>> =
466            store.get(writer, DB_KEY_INSTALLATION_DATE)?;
467        Ok(
468            if let Some(installation_date) = persisted_installation_date {
469                installation_date
470            } else if let Some(home_directory) = &self.app_context.home_directory {
471                let installation_date = match self.get_creation_date_from_path(home_directory) {
472                    Ok(installation_date) => installation_date,
473                    Err(e) => {
474                        warn!("[Nimbus] Unable to get installation date from path, defaulting to today: {:?}", e);
475                        Utc::now()
476                    }
477                };
478                let store = db.get_store(StoreId::Meta);
479                store.put(writer, DB_KEY_INSTALLATION_DATE, &installation_date)?;
480                installation_date
481            } else {
482                Utc::now()
483            },
484        )
485    }
486
487    fn get_update_date(&self, db: &Database, writer: &mut Writer) -> Result<DateTime<Utc>> {
488        let store = db.get_store(StoreId::Meta);
489
490        let persisted_app_version: Option<String> = store.get(writer, DB_KEY_APP_VERSION)?;
491        let update_date: Option<DateTime<Utc>> = store.get(writer, DB_KEY_UPDATE_DATE)?;
492        Ok(
493            match (
494                persisted_app_version,
495                &self.app_context.app_version,
496                update_date,
497            ) {
498                // The app been run before, but has not just been updated.
499                (Some(persisted), Some(current), Some(date)) if persisted == *current => date,
500                // The app has been run before, and just been updated.
501                (Some(persisted), Some(current), _) if persisted != *current => {
502                    let now = Utc::now();
503                    store.put(writer, DB_KEY_APP_VERSION, current)?;
504                    store.put(writer, DB_KEY_UPDATE_DATE, &now)?;
505                    now
506                }
507                // The app has just been installed
508                (None, Some(current), _) => {
509                    let now = Utc::now();
510                    store.put(writer, DB_KEY_APP_VERSION, current)?;
511                    store.put(writer, DB_KEY_UPDATE_DATE, &now)?;
512                    now
513                }
514                // The current version is not available, or the persisted date is not available.
515                (_, _, Some(date)) => date,
516                // Either way, this doesn't appear to be a good production environment.
517                _ => Utc::now(),
518            },
519        )
520    }
521
522    #[cfg(not(test))]
523    fn get_creation_date_from_path<P: AsRef<Path>>(&self, path: P) -> Result<DateTime<Utc>> {
524        info!("[Nimbus] Getting creation date from path");
525        let metadata = std::fs::metadata(path)?;
526        let system_time_created = metadata.created()?;
527        let date_time_created = DateTime::<Utc>::from(system_time_created);
528        info!(
529            "[Nimbus] Creation date retrieved form path successfully: {}",
530            date_time_created
531        );
532        Ok(date_time_created)
533    }
534
535    #[cfg(test)]
536    fn get_creation_date_from_path<P: AsRef<Path>>(&self, path: P) -> Result<DateTime<Utc>> {
537        use std::io::Read;
538        let test_path = path.as_ref().with_file_name("test.json");
539        let mut file = std::fs::File::open(test_path)?;
540        let mut buf = String::new();
541        file.read_to_string(&mut buf)?;
542
543        let res = match serde_json::from_str::<DateTime<Utc>>(&buf) {
544            Ok(v) => v,
545            Err(e) => return Err(NimbusError::JSONError("res = nimbus::stateful::nimbus_client::get_creation_date_from_path::serde_json::from_str".into(), e.to_string()))
546        };
547        Ok(res)
548    }
549
550    pub fn set_experiments_locally(&self, experiments_json: String) -> Result<()> {
551        let new_experiments = parse_experiments(&experiments_json)?;
552        let db = self.db()?;
553        let mut writer = db.write()?;
554        write_pending_experiments(db, &mut writer, new_experiments)?;
555        writer.commit()?;
556        Ok(())
557    }
558
559    /// Reset all enrollments and experiments in the database.
560    ///
561    /// This should only be used in testing.
562    pub fn reset_enrollments(&self) -> Result<()> {
563        let db = self.db()?;
564        let mut writer = db.write()?;
565        let mut state = self.mutable_state.lock().unwrap();
566        db.clear_experiments_and_enrollments(&mut writer)?;
567        self.end_initialize(db, writer, &mut state)?;
568        Ok(())
569    }
570
571    /// Reset internal state in response to application-level telemetry reset.
572    ///
573    /// When the user resets their telemetry state in the consuming application, we need learn
574    /// the new values of any external randomization units, and we need to reset any unique
575    /// identifiers used internally by the SDK. If we don't then we risk accidentally tracking
576    /// across the telemetry reset, since we could use Nimbus metrics to link their pings from
577    /// before and after the reset.
578    ///
579    pub fn reset_telemetry_identifiers(&self) -> Result<Vec<EnrollmentChangeEvent>> {
580        let mut events = vec![];
581        let db = self.db()?;
582        let mut writer = db.write()?;
583        let mut state = self.mutable_state.lock().unwrap();
584        // If we have no `nimbus_id` when we can safely assume that there's
585        // no other experiment state that needs to be reset.
586        let store = db.get_store(StoreId::Meta);
587        if store.get::<String, _>(&writer, DB_KEY_NIMBUS_ID)?.is_some() {
588            // Each enrollment state now opts out because we don't want to leak information between resets.
589            events = reset_telemetry_identifiers(db, &mut writer)?;
590
591            // Remove any stored event counts
592            db.clear_event_count_data(&mut writer)?;
593
594            // The `nimbus_id` itself is a unique identifier.
595            // N.B. we do this last, as a signal that all data has been reset.
596            store.delete(&mut writer, DB_KEY_NIMBUS_ID)?;
597            self.end_initialize(db, writer, &mut state)?;
598        }
599
600        // (No need to commit `writer` if the above check was false, since we didn't change anything)
601        state.available_randomization_units = Default::default();
602        state.targeting_attributes.nimbus_id = None;
603
604        Ok(events)
605    }
606
607    pub fn nimbus_id(&self) -> Result<Uuid> {
608        let db = self.db()?;
609        let mut writer = db.write()?;
610        let mut state = self.mutable_state.lock().unwrap();
611        let uuid = self.read_or_create_nimbus_id(db, &mut writer, &mut state)?;
612
613        // We don't know whether we needed to generate and save the uuid, so
614        // we commit just in case - this is hopefully close to a noop in that
615        // case!
616        writer.commit()?;
617        Ok(uuid)
618    }
619
620    /// Return the nimbus ID from the database, or create a new one and write it
621    /// to the database.
622    ///
623    /// The internal state will be updated with the nimbus ID.
624    fn read_or_create_nimbus_id(
625        &self,
626        db: &Database,
627        writer: &mut Writer,
628        state: &mut MutexGuard<'_, InternalMutableState>,
629    ) -> Result<Uuid> {
630        let store = db.get_store(StoreId::Meta);
631        let nimbus_id = match store.get(writer, DB_KEY_NIMBUS_ID)? {
632            Some(nimbus_id) => nimbus_id,
633            None => {
634                let nimbus_id = Uuid::new_v4();
635                store.put(writer, DB_KEY_NIMBUS_ID, &nimbus_id)?;
636                nimbus_id
637            }
638        };
639
640        state.available_randomization_units.nimbus_id = Some(nimbus_id.to_string());
641        state.targeting_attributes.nimbus_id = Some(nimbus_id.to_string());
642
643        Ok(nimbus_id)
644    }
645
646    // Sets the nimbus ID - TEST ONLY - should not be exposed to real clients.
647    // (Useful for testing so you can have some control over what experiments
648    // are enrolled)
649    pub fn set_nimbus_id(&self, uuid: &Uuid) -> Result<()> {
650        let db = self.db()?;
651        let mut writer = db.write()?;
652        db.get_store(StoreId::Meta)
653            .put(&mut writer, DB_KEY_NIMBUS_ID, uuid)?;
654        writer.commit()?;
655        Ok(())
656    }
657
658    pub(crate) fn db(&self) -> Result<&Database> {
659        self.db.get_or_try_init(|| Database::new(&self.db_path))
660    }
661
662    fn merge_additional_context(&self, context: Option<JsonObject>) -> Result<Value> {
663        let context = context.map(Value::Object);
664        let targeting = match serde_json::to_value(self.get_targeting_attributes()) {
665            Ok(v) => v,
666            Err(e) => return Err(NimbusError::JSONError("targeting = nimbus::stateful::nimbus_client::NimbusClient::merge_additional_context::serde_json::to_value".into(), e.to_string()))
667        };
668        let context = match context {
669            Some(v) => v.defaults(&targeting)?,
670            None => targeting,
671        };
672
673        Ok(context)
674    }
675
676    pub fn create_targeting_helper(
677        &self,
678        additional_context: Option<JsonObject>,
679    ) -> Result<Arc<NimbusTargetingHelper>> {
680        let context = self.merge_additional_context(additional_context)?;
681        let helper =
682            NimbusTargetingHelper::new(context, self.event_store.clone(), self.gecko_prefs.clone());
683        Ok(Arc::new(helper))
684    }
685
686    pub fn create_targeting_helper_with_context(
687        &self,
688        context: Value,
689    ) -> Arc<NimbusTargetingHelper> {
690        Arc::new(NimbusTargetingHelper::new(
691            context,
692            self.event_store.clone(),
693            self.gecko_prefs.clone(),
694        ))
695    }
696
697    pub fn create_string_helper(
698        &self,
699        additional_context: Option<JsonObject>,
700    ) -> Result<Arc<NimbusStringHelper>> {
701        let context = self.merge_additional_context(additional_context)?;
702        let helper = NimbusStringHelper::new(context.as_object().unwrap().to_owned());
703        Ok(Arc::new(helper))
704    }
705
706    /// Records an event for the purposes of behavioral targeting.
707    ///
708    /// This function is used to record and persist data used for the behavioral
709    /// targeting such as "core-active" user targeting.
710    pub fn record_event(&self, event_id: String, count: i64) -> Result<()> {
711        let mut event_store = self.event_store.lock().unwrap();
712        event_store.record_event(count as u64, &event_id, None)?;
713        event_store.persist_data(self.db()?)?;
714        Ok(())
715    }
716
717    /// Records an event for the purposes of behavioral targeting.
718    ///
719    /// This differs from the `record_event` method in that the event is recorded as if it were
720    /// recorded `seconds_ago` in the past. This makes it very useful for testing.
721    pub fn record_past_event(&self, event_id: String, seconds_ago: i64, count: i64) -> Result<()> {
722        if seconds_ago < 0 {
723            return Err(NimbusError::BehaviorError(BehaviorError::InvalidDuration(
724                "Time duration in the past must be positive".to_string(),
725            )));
726        }
727        let mut event_store = self.event_store.lock().unwrap();
728        event_store.record_past_event(
729            count as u64,
730            &event_id,
731            None,
732            chrono::Duration::seconds(seconds_ago),
733        )?;
734        event_store.persist_data(self.db()?)?;
735        Ok(())
736    }
737
738    /// Advances the event store's concept of `now` artificially.
739    ///
740    /// This works alongside `record_event` and `record_past_event` for testing purposes.
741    pub fn advance_event_time(&self, by_seconds: i64) -> Result<()> {
742        if by_seconds < 0 {
743            return Err(NimbusError::BehaviorError(BehaviorError::InvalidDuration(
744                "Time duration in the future must be positive".to_string(),
745            )));
746        }
747        let mut event_store = self.event_store.lock().unwrap();
748        event_store.advance_datum(chrono::Duration::seconds(by_seconds));
749        Ok(())
750    }
751
752    /// Clear all events in the Nimbus event store.
753    ///
754    /// This should only be used in testing or cases where the previous event store is no longer viable.
755    pub fn clear_events(&self) -> Result<()> {
756        let mut event_store = self.event_store.lock().unwrap();
757        event_store.clear(self.db()?)?;
758        Ok(())
759    }
760
761    pub fn event_store(&self) -> Arc<Mutex<EventStore>> {
762        self.event_store.clone()
763    }
764
765    pub fn dump_state_to_log(&self) -> Result<()> {
766        let experiments = self.get_active_experiments()?;
767        info!("{0: <65}| {1: <30}| {2}", "Slug", "Features", "Branch");
768        for exp in &experiments {
769            info!(
770                "{0: <65}| {1: <30}| {2}",
771                &exp.slug,
772                &exp.feature_ids.join(", "),
773                &exp.branch_slug
774            );
775        }
776        Ok(())
777    }
778
779    /// Given a Gecko pref state and a pref unenroll reason, unenroll from an experiment
780    pub fn unenroll_for_gecko_pref(
781        &self,
782        pref_state: GeckoPrefState,
783        pref_unenroll_reason: PrefUnenrollReason,
784    ) -> Result<Vec<EnrollmentChangeEvent>> {
785        if let Some(prefs) = self.gecko_prefs.clone() {
786            {
787                let mut pref_store_state = prefs.get_mutable_pref_state();
788                pref_store_state.update_pref_state(&pref_state);
789            }
790            let enrollments = self
791                .database_cache
792                .get_enrollments_for_pref(&pref_state.gecko_pref.pref)?;
793
794            let db = self.db()?;
795            let mut writer = db.write()?;
796
797            let mut results = Vec::new();
798            for experiment_slug in enrollments.unwrap() {
799                let result =
800                    unenroll_for_pref(db, &mut writer, &experiment_slug, pref_unenroll_reason)?;
801                results.push(result);
802            }
803
804            let mut state = self.mutable_state.lock().unwrap();
805            self.end_initialize(db, writer, &mut state)?;
806            return Ok(results.concat());
807        }
808        Ok(Vec::new())
809    }
810
811    #[cfg(test)]
812    pub fn get_metrics_handler(&self) -> &&TestMetrics {
813        let metrics = &**self.metrics_handler;
814        // SAFETY: The cast to TestMetrics is safe because the Rust instance is guaranteed to be
815        // a TestMetrics instance. TestMetrics is the only Rust-implemented version of
816        // MetricsHandler, and, like this method, is only used in tests.
817        unsafe { std::mem::transmute::<&&dyn MetricsHandler, &&TestMetrics>(&metrics) }
818    }
819
820    #[cfg(test)]
821    pub fn get_recorded_context(&self) -> &&TestRecordedContext {
822        self.recorded_context
823            .clone()
824            .map(|ref recorded_context|
825                // SAFETY: The cast to TestRecordedContext is safe because the Rust instance is
826                // guaranteed to be a TestRecordedContext instance. TestRecordedContext is the only
827                // Rust-implemented version of RecordedContext, and, like this method,  is only
828                // used in tests.
829                unsafe {
830                    std::mem::transmute::<&&dyn RecordedContext, &&TestRecordedContext>(
831                        &&**recorded_context,
832                    )
833                })
834            .expect("failed to unwrap RecordedContext object")
835    }
836
837    #[cfg(test)]
838    pub fn get_gecko_pref_store(&self) -> Arc<Box<TestGeckoPrefHandler>> {
839        self.gecko_prefs.clone()
840            .clone()
841            .map(|ref pref_store|
842                // SAFETY: The cast to TestGeckoPrefHandler is safe because the Rust instance is
843                // guaranteed to be a TestGeckoPrefHandler instance. TestGeckoPrefHandler is the only
844                // Rust-implemented version of GeckoPrefHandler, and, like this method,  is only
845                // used in tests.
846                unsafe {
847                    std::mem::transmute::<Arc<Box<dyn GeckoPrefHandler>>, Arc<Box<TestGeckoPrefHandler>>>(
848                        pref_store.clone().handler.clone(),
849                    )
850                })
851            .expect("failed to unwrap GeckoPrefHandler object")
852    }
853}
854
855impl NimbusClient {
856    pub fn set_install_time(&mut self, then: DateTime<Utc>) {
857        let mut state = self.mutable_state.lock().unwrap();
858        state.install_date = Some(then);
859        state.update_time_to_now(Utc::now());
860    }
861
862    pub fn set_update_time(&mut self, then: DateTime<Utc>) {
863        let mut state = self.mutable_state.lock().unwrap();
864        state.update_date = Some(then);
865        state.update_time_to_now(Utc::now());
866    }
867}
868
869impl NimbusClient {
870    /// This is only called from `get_feature_config_variables` which is itself is cached with
871    /// thread safety in the FeatureHolder.kt and FeatureHolder.swift
872    fn record_feature_activation_if_needed(&self, feature_id: &str) {
873        if let Ok(Some(f)) = self.database_cache.get_enrollment_by_feature(feature_id) {
874            if f.branch.is_some() && !self.coenrolling_feature_ids.contains(&f.feature_id) {
875                self.metrics_handler.record_feature_activation(f.into());
876            }
877        }
878    }
879
880    pub fn record_feature_exposure(&self, feature_id: String, slug: Option<String>) {
881        let event = if let Some(slug) = slug {
882            if let Ok(Some(branch)) = self.database_cache.get_experiment_branch(&slug) {
883                Some(FeatureExposureExtraDef {
884                    feature_id,
885                    branch: Some(branch),
886                    slug,
887                })
888            } else {
889                None
890            }
891        } else if let Ok(Some(f)) = self.database_cache.get_enrollment_by_feature(&feature_id) {
892            if f.branch.is_some() {
893                Some(f.into())
894            } else {
895                None
896            }
897        } else {
898            None
899        };
900
901        if let Some(event) = event {
902            self.metrics_handler.record_feature_exposure(event);
903        }
904    }
905
906    pub fn record_malformed_feature_config(&self, feature_id: String, part_id: String) {
907        let event = if let Ok(Some(f)) = self.database_cache.get_enrollment_by_feature(&feature_id)
908        {
909            MalformedFeatureConfigExtraDef::from(f, part_id)
910        } else {
911            MalformedFeatureConfigExtraDef::new(feature_id, part_id)
912        };
913        self.metrics_handler.record_malformed_feature_config(event);
914    }
915
916    fn record_enrollment_status_telemetry(
917        &self,
918        state: &mut MutexGuard<InternalMutableState>,
919    ) -> Result<()> {
920        let targeting_helper = NimbusTargetingHelper::new(
921            state.targeting_attributes.clone(),
922            self.event_store.clone(),
923            self.gecko_prefs.clone(),
924        );
925        let experiments = self
926            .database_cache
927            .get_experiments()?
928            .iter()
929            .filter_map(
930                |exp| match is_experiment_available(&targeting_helper, exp, true) {
931                    true => Some(exp.slug.clone()),
932                    false => None,
933                },
934            )
935            .collect::<HashSet<String>>();
936        self.metrics_handler.record_enrollment_statuses(
937            self.database_cache
938                .get_enrollments()?
939                .into_iter()
940                .filter_map(|e| match experiments.contains(&e.slug) {
941                    true => Some(e.into()),
942                    false => None,
943                })
944                .collect(),
945        );
946        Ok(())
947    }
948}
949
950pub struct NimbusStringHelper {
951    context: JsonObject,
952}
953
954impl NimbusStringHelper {
955    fn new(context: JsonObject) -> Self {
956        Self { context }
957    }
958
959    pub fn get_uuid(&self, template: String) -> Option<String> {
960        if template.contains("{uuid}") {
961            let uuid = Uuid::new_v4();
962            Some(uuid.to_string())
963        } else {
964            None
965        }
966    }
967
968    pub fn string_format(&self, template: String, uuid: Option<String>) -> String {
969        match uuid {
970            Some(uuid) => {
971                let mut map = self.context.clone();
972                map.insert("uuid".to_string(), Value::String(uuid));
973                fmt_with_map(&template, &map)
974            }
975            _ => fmt_with_map(&template, &self.context),
976        }
977    }
978}
979
980#[cfg(feature = "stateful-uniffi-bindings")]
981uniffi::custom_type!(JsonObject, String, {
982    remote,
983    try_lift: |val| {
984        let json: Value = serde_json::from_str(&val)?;
985
986        match json.as_object() {
987            Some(obj) => Ok(obj.clone()),
988            _ => Err(uniffi::deps::anyhow::anyhow!(
989                "Unexpected JSON-non-object in the bagging area"
990            )),
991        }
992    },
993    lower: |obj| serde_json::Value::Object(obj).to_string(),
994});
995
996#[cfg(feature = "stateful-uniffi-bindings")]
997uniffi::custom_type!(PrefValue, String, {
998    remote,
999    try_lift: |val| {
1000        let json: Value = serde_json::from_str(&val)?;
1001        if json.is_string() || json.is_boolean() || (json.is_number() && !json.is_f64()) || json.is_null() {
1002            Ok(json)
1003        } else {
1004            Err(anyhow::anyhow!(format!("Value {} is not a string, boolean, number, or null, or is a float", json)))
1005        }
1006    },
1007    lower: |val| {
1008        val.to_string()
1009    }
1010});
1011
1012#[cfg(feature = "stateful-uniffi-bindings")]
1013uniffi::include_scaffolding!("nimbus");