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