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