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