1#[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#[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 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
82pub 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 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 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 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 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 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 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 pub fn get_global_user_participation(&self) -> Result<bool> {
303 self.get_experiment_participation()
305 }
306
307 pub fn set_global_user_participation(
310 &self,
311 user_participating: bool,
312 ) -> Result<Vec<EnrollmentChangeEvent>> {
313 let experiment_events = self.set_experiment_participation(user_participating)?;
315 let rollout_events = self.set_rollout_participation(user_participating)?;
316
317 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 fn update_ta_install_dates(
403 &self,
404 db: &Database,
405 writer: &mut Writer,
406 state: &mut MutexGuard<InternalMutableState>,
407 ) -> Result<()> {
408 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 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 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 self.evolve_experiments(db, &mut writer, &mut state, &new_experiments)?
488 }
489 None => vec![],
490 };
491
492 self.end_initialize(db, writer, &mut state)?;
494 Ok(res)
495 }
496
497 #[allow(deprecated)] fn get_installation_date(&self, db: &Database, writer: &mut Writer) -> Result<DateTime<Utc>> {
499 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 (Some(persisted), Some(current), Some(date)) if persisted == *current => date,
544 (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 (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 (_, _, Some(date)) => date,
560 _ => 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 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 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 let store = db.get_store(StoreId::Meta);
631 if store.get::<String, _>(&writer, DB_KEY_NIMBUS_ID)?.is_some() {
632 events = reset_telemetry_identifiers(db, &mut writer)?;
634
635 db.clear_event_count_data(&mut writer)?;
637
638 store.delete(&mut writer, DB_KEY_NIMBUS_ID)?;
641 self.end_initialize(db, writer, &mut state)?;
642 }
643
644 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 writer.commit()?;
661 Ok(uuid)
662 }
663
664 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 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 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 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 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 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 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 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 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 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 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");