/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use crate::enrollment::Participation; use crate::enrollment::{ EnrollmentChangeEvent, EnrollmentChangeEventType, EnrollmentsEvolver, ExperimentEnrollment, map_enrollments, }; use crate::error::{Result, debug, warn}; use crate::stateful::gecko_prefs::GeckoPrefStore; use crate::stateful::gecko_prefs::PrefUnenrollReason; use crate::stateful::persistence::{ DB_KEY_EXPERIMENT_PARTICIPATION, DB_KEY_ROLLOUT_PARTICIPATION, DEFAULT_EXPERIMENT_PARTICIPATION, DEFAULT_ROLLOUT_PARTICIPATION, }; use crate::stateful::persistence::{Database, Readable, StoreId, Writer}; use crate::{EnrolledExperiment, EnrollmentStatus, Experiment}; impl EnrollmentsEvolver<'_> { /// Convenient wrapper around `evolve_enrollments` that fetches the current state of experiments, /// enrollments and user participation from the database. pub(crate) fn evolve_enrollments_in_db( &mut self, db: &Database, writer: &mut Writer, next_experiments: &[Experiment], gecko_pref_store: Option<&GeckoPrefStore>, ) -> Result> { // Get separate participation states from the db let is_participating_in_experiments = get_experiment_participation(db, writer)?; let is_participating_in_rollouts = get_rollout_participation(db, writer)?; let participation = Participation { in_experiments: is_participating_in_experiments, in_rollouts: is_participating_in_rollouts, }; let experiments_store = db.get_store(StoreId::Experiments); let enrollments_store = db.get_store(StoreId::Enrollments); let prev_experiments: Vec = experiments_store.collect_all(writer)?; let prev_enrollments: Vec = enrollments_store.collect_all(writer)?; // Calculate the changes. let (next_enrollments, enrollments_change_events) = self.evolve_enrollments( participation, &prev_experiments, next_experiments, &prev_enrollments, gecko_pref_store, )?; let next_enrollments = map_enrollments(&next_enrollments); // Write the changes to the Database. enrollments_store.clear(writer)?; for enrollment in next_enrollments.values() { enrollments_store.put(writer, &enrollment.slug, *enrollment)?; } experiments_store.clear(writer)?; for experiment in next_experiments { // Sanity check. if !next_enrollments.contains_key(&experiment.slug) { error_support::report_error!( "nimbus-evolve-enrollments", "evolve_enrollments_in_db: experiment '{}' has no enrollment, dropping to keep database consistent", &experiment.slug ); continue; } experiments_store.put(writer, &experiment.slug, experiment)?; } Ok(enrollments_change_events) } } /// Return information about all enrolled experiments. /// Note this does not include rollouts pub fn get_enrollments<'r>( db: &Database, reader: &'r impl Readable<'r>, ) -> Result> { let enrollments: Vec = db.get_store(StoreId::Enrollments).collect_all(reader)?; let mut result = Vec::with_capacity(enrollments.len()); for enrollment in enrollments { debug!("Have enrollment: {:?}", enrollment); if let EnrollmentStatus::Enrolled { branch, .. } = &enrollment.status { match db .get_store(StoreId::Experiments) .get::(reader, &enrollment.slug)? { Some(experiment) => { result.push(EnrolledExperiment { feature_ids: experiment.get_feature_ids(), slug: experiment.slug, user_facing_name: experiment.user_facing_name, user_facing_description: experiment.user_facing_description, branch_slug: branch.to_string(), }); } _ => { warn!( "Have enrollment {:?} but no matching experiment!", enrollment ); } }; } } Ok(result) } pub fn opt_in_with_branch( db: &Database, writer: &mut Writer, experiment_slug: &str, branch: &str, ) -> Result> { let mut events = vec![]; if let Ok(Some(exp)) = db .get_store(StoreId::Experiments) .get::(writer, experiment_slug) { let enrollment = ExperimentEnrollment::from_explicit_opt_in(&exp, branch, &mut events); db.get_store(StoreId::Enrollments) .put(writer, experiment_slug, &enrollment.unwrap())?; } else { events.push(EnrollmentChangeEvent { experiment_slug: experiment_slug.to_string(), branch_slug: branch.to_string(), reason: Some("does-not-exist".to_string()), change: EnrollmentChangeEventType::EnrollFailed, }); } Ok(events) } pub fn opt_out( db: &Database, writer: &mut Writer, experiment_slug: &str, gecko_prefs: Option<&GeckoPrefStore>, ) -> Result> { let mut events = vec![]; let enr_store = db.get_store(StoreId::Enrollments); if let Ok(Some(existing_enrollment)) = enr_store.get::(writer, experiment_slug) { let updated_enrollment = &existing_enrollment.on_explicit_opt_out(&mut events, gecko_prefs); enr_store.put(writer, experiment_slug, updated_enrollment)?; } else { events.push(EnrollmentChangeEvent { experiment_slug: experiment_slug.to_string(), branch_slug: "N/A".to_string(), reason: Some("does-not-exist".to_string()), change: EnrollmentChangeEventType::UnenrollFailed, }); } Ok(events) } #[cfg(feature = "stateful")] pub fn unenroll_for_pref( db: &Database, writer: &mut Writer, experiment_slug: &str, unenroll_reason: PrefUnenrollReason, triggering_pref_name: &str, gecko_pref_store: Option<&GeckoPrefStore>, ) -> Result> { let mut events = vec![]; let enr_store = db.get_store(StoreId::Enrollments); if let Ok(Some(existing_enrollment)) = enr_store.get::(writer, experiment_slug) { #[cfg(feature = "stateful")] existing_enrollment .maybe_revert_unchanged_gecko_pref_states(triggering_pref_name, gecko_pref_store); let updated_enrollment = &existing_enrollment.on_pref_unenroll(unenroll_reason, &mut events); enr_store.put(writer, experiment_slug, updated_enrollment)?; } else { events.push(EnrollmentChangeEvent { experiment_slug: experiment_slug.to_string(), branch_slug: "N/A".to_string(), reason: Some("does-not-exist".to_string()), change: EnrollmentChangeEventType::UnenrollFailed, }); } Ok(events) } pub fn get_experiment_participation<'r>( db: &Database, reader: &'r impl Readable<'r>, ) -> Result { let store = db.get_store(StoreId::Meta); let opted_in = store.get::(reader, DB_KEY_EXPERIMENT_PARTICIPATION)?; if let Some(opted_in) = opted_in { Ok(opted_in) } else { Ok(DEFAULT_EXPERIMENT_PARTICIPATION) } } pub fn get_rollout_participation<'r>(db: &Database, reader: &'r impl Readable<'r>) -> Result { let store = db.get_store(StoreId::Meta); let opted_in = store.get::(reader, DB_KEY_ROLLOUT_PARTICIPATION)?; if let Some(opted_in) = opted_in { Ok(opted_in) } else { Ok(DEFAULT_ROLLOUT_PARTICIPATION) } } pub fn set_experiment_participation( db: &Database, writer: &mut Writer, opt_in: bool, ) -> Result<()> { let store = db.get_store(StoreId::Meta); store.put(writer, DB_KEY_EXPERIMENT_PARTICIPATION, &opt_in) } pub fn set_rollout_participation(db: &Database, writer: &mut Writer, opt_in: bool) -> Result<()> { let store = db.get_store(StoreId::Meta); store.put(writer, DB_KEY_ROLLOUT_PARTICIPATION, &opt_in) } /// Reset unique identifiers in response to application-level telemetry reset. /// pub fn reset_telemetry_identifiers( db: &Database, writer: &mut Writer, ) -> Result> { let mut events = vec![]; let store = db.get_store(StoreId::Enrollments); let enrollments: Vec = store.collect_all(writer)?; let updated_enrollments = enrollments .iter() .map(|enrollment| enrollment.reset_telemetry_identifiers(&mut events)); store.clear(writer)?; for enrollment in updated_enrollments { store.put(writer, &enrollment.slug, &enrollment)?; } Ok(events) }