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, BehaviorError},
14 evaluator::{
15 get_calculated_attributes, is_experiment_available, CalculatedAttributes,
16 ExperimentAvailable, TargetingAttributes,
17 },
18 json::{JsonObject, PrefValue},
19 metrics::{
20 EnrollmentStatusExtraDef, FeatureExposureExtraDef, MalformedFeatureConfigExtraDef,
21 MetricsHandler,
22 },
23 schema::parse_experiments,
24 stateful::{
25 behavior::EventStore,
26 client::{create_client, SettingsClient},
27 dbcache::DatabaseCache,
28 enrollment::{
29 get_experiment_participation, get_rollout_participation, opt_in_with_branch, opt_out,
30 reset_telemetry_identifiers, set_experiment_participation, set_rollout_participation,
31 unenroll_for_pref,
32 },
33 gecko_prefs::{
34 GeckoPref, GeckoPrefHandler, GeckoPrefState, GeckoPrefStore, PrefBranch,
35 PrefEnrollmentData, PrefUnenrollReason,
36 },
37 matcher::AppContext,
38 persistence::{Database, StoreId, Writer},
39 targeting::{validate_event_queries, RecordedContext},
40 updating::{read_and_remove_pending_experiments, write_pending_experiments},
41 },
42 strings::fmt_with_map,
43 AvailableExperiment, AvailableRandomizationUnits, EnrolledExperiment, Experiment,
44 ExperimentBranch, NimbusError, NimbusTargetingHelper, Result,
45};
46use chrono::{DateTime, NaiveDateTime, Utc};
47use once_cell::sync::OnceCell;
48use remote_settings::RemoteSettingsService;
49use serde_json::Value;
50use std::collections::HashSet;
51use std::fmt::Debug;
52use std::path::PathBuf;
53use std::sync::{Arc, Mutex, MutexGuard};
54use uuid::Uuid;
55
56const DB_KEY_NIMBUS_ID: &str = "nimbus-id";
57pub const DB_KEY_INSTALLATION_DATE: &str = "installation-date";
58pub const DB_KEY_UPDATE_DATE: &str = "update-date";
59pub const DB_KEY_APP_VERSION: &str = "app-version";
60pub const DB_KEY_FETCH_ENABLED: &str = "fetch-enabled";
61
62#[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 #[allow(clippy::too_many_arguments)]
105 pub fn new<P: Into<PathBuf>>(
106 app_context: AppContext,
107 recorded_context: Option<Arc<dyn RecordedContext>>,
108 coenrolling_feature_ids: Vec<String>,
109 db_path: P,
110 metrics_handler: Box<dyn MetricsHandler>,
111 gecko_pref_handler: Option<Box<dyn GeckoPrefHandler>>,
112 remote_settings_service: Option<Arc<RemoteSettingsService>>,
113 collection_name: Option<String>,
114 ) -> Result<Self> {
115 let settings_client = Mutex::new(create_client(remote_settings_service, collection_name)?);
116
117 let targeting_attributes: TargetingAttributes = app_context.clone().into();
118 let mutable_state = Mutex::new(InternalMutableState {
119 available_randomization_units: Default::default(),
120 targeting_attributes,
121 install_date: Default::default(),
122 update_date: Default::default(),
123 });
124
125 let mut prefs = None;
126 if let Some(handler) = gecko_pref_handler {
127 prefs = Some(Arc::new(GeckoPrefStore::new(Arc::new(handler))));
128 }
129
130 info!(
131 "Initialized NimbusClient with: app_context = {:?}; recorded_context = {:?}",
132 app_context,
133 recorded_context
134 .as_ref()
135 .map(|rc| serde_json::Value::Object(rc.to_json()))
136 .unwrap_or(serde_json::Value::Null)
137 );
138
139 Ok(Self {
140 settings_client,
141 mutable_state,
142 app_context,
143 database_cache: Default::default(),
144 db_path: db_path.into(),
145 coenrolling_feature_ids,
146 db: OnceCell::default(),
147 event_store: Arc::default(),
148 recorded_context,
149 gecko_prefs: prefs,
150 metrics_handler: Arc::new(metrics_handler),
151 })
152 }
153
154 pub fn with_targeting_attributes(&mut self, targeting_attributes: TargetingAttributes) {
155 let mut state = self.mutable_state.lock().unwrap();
156 state.targeting_attributes = targeting_attributes;
157 }
158
159 pub fn get_targeting_attributes(&self) -> TargetingAttributes {
160 let mut state = self.mutable_state.lock().unwrap();
161 state.update_time_to_now(Utc::now());
162 state.targeting_attributes.clone()
163 }
164
165 pub fn initialize(&self) -> Result<()> {
166 let db = self.db()?;
167 let mut writer = db.write()?;
169
170 let mut state = self.mutable_state.lock().unwrap();
171 self.begin_initialize(db, &mut writer, &mut state)?;
172 self.end_initialize(db, writer, &mut state)?;
173
174 Ok(())
175 }
176
177 fn begin_initialize(
180 &self,
181 db: &Database,
182 writer: &mut Writer,
183 state: &mut MutexGuard<InternalMutableState>,
184 ) -> Result<()> {
185 self.read_or_create_nimbus_id(db, writer, state)?;
186 self.update_ta_install_dates(db, writer, state)?;
187 self.event_store
188 .lock()
189 .expect("unable to lock event_store mutex")
190 .read_from_db(db)?;
191
192 if let Some(recorded_context) = &self.recorded_context {
193 let targeting_helper = self.create_targeting_helper_with_context(match serde_json::to_value(
194 &state.targeting_attributes,
195 ) {
196 Ok(v) => v,
197 Err(e) => return Err(NimbusError::JSONError("targeting_helper = nimbus::stateful::nimbus_client::NimbusClient::begin_initialize::serde_json::to_value".into(), e.to_string()))
198 });
199 recorded_context.execute_queries(targeting_helper.as_ref())?;
200 state
201 .targeting_attributes
202 .set_recorded_context(recorded_context.to_json());
203 }
204
205 if let Some(gecko_prefs) = &self.gecko_prefs {
206 gecko_prefs.initialize()?;
207 }
208
209 Ok(())
210 }
211
212 fn end_initialize(
215 &self,
216 db: &Database,
217 writer: Writer,
218 state: &mut MutexGuard<InternalMutableState>,
219 ) -> Result<()> {
220 self.update_ta_active_experiments(db, &writer, state)?;
221 let coenrolling_ids = self
222 .coenrolling_feature_ids
223 .iter()
224 .map(|s| s.as_str())
225 .collect();
226 self.database_cache.commit_and_update(
227 db,
228 writer,
229 &coenrolling_ids,
230 self.gecko_prefs.clone(),
231 )?;
232 self.record_enrollment_status_telemetry(state)?;
233 Ok(())
234 }
235
236 pub fn get_enrollment_by_feature(&self, feature_id: String) -> Result<Option<EnrolledFeature>> {
237 self.database_cache.get_enrollment_by_feature(&feature_id)
238 }
239
240 pub fn get_experiment_branch(&self, slug: String) -> Result<Option<String>> {
242 self.database_cache.get_experiment_branch(&slug)
243 }
244
245 pub fn get_feature_config_variables(&self, feature_id: String) -> Result<Option<String>> {
246 Ok(
247 if let Some(s) = self
248 .database_cache
249 .get_feature_config_variables(&feature_id)?
250 {
251 self.record_feature_activation_if_needed(&feature_id);
252 Some(s)
253 } else {
254 None
255 },
256 )
257 }
258
259 pub fn get_experiment_branches(&self, slug: String) -> Result<Vec<ExperimentBranch>> {
260 self.get_all_experiments()?
261 .into_iter()
262 .find(|e| e.slug == slug)
263 .map(|e| e.branches.into_iter().map(|b| b.into()).collect())
264 .ok_or(NimbusError::NoSuchExperiment(slug))
265 }
266
267 pub fn get_experiment_participation(&self) -> Result<bool> {
268 let db = self.db()?;
269 let reader = db.read()?;
270 get_experiment_participation(db, &reader)
271 }
272
273 pub fn get_rollout_participation(&self) -> Result<bool> {
274 let db = self.db()?;
275 let reader = db.read()?;
276 get_rollout_participation(db, &reader)
277 }
278
279 pub fn set_experiment_participation(
280 &self,
281 user_participating: bool,
282 ) -> Result<Vec<EnrollmentChangeEvent>> {
283 let db = self.db()?;
284 let mut writer = db.write()?;
285 let mut state = self.mutable_state.lock().unwrap();
286 set_experiment_participation(db, &mut writer, user_participating)?;
287
288 let existing_experiments: Vec<Experiment> =
289 db.get_store(StoreId::Experiments).collect_all(&writer)?;
290 let events = self.evolve_experiments(db, &mut writer, &mut state, &existing_experiments)?;
291 self.end_initialize(db, writer, &mut state)?;
292 Ok(events)
293 }
294
295 pub fn set_rollout_participation(
296 &self,
297 user_participating: bool,
298 ) -> Result<Vec<EnrollmentChangeEvent>> {
299 let db = self.db()?;
300 let mut writer = db.write()?;
301 let mut state = self.mutable_state.lock().unwrap();
302 set_rollout_participation(db, &mut writer, user_participating)?;
303
304 let existing_experiments: Vec<Experiment> =
305 db.get_store(StoreId::Experiments).collect_all(&writer)?;
306 let events = self.evolve_experiments(db, &mut writer, &mut state, &existing_experiments)?;
307 self.end_initialize(db, writer, &mut state)?;
308 Ok(events)
309 }
310
311 pub fn get_active_experiments(&self) -> Result<Vec<EnrolledExperiment>> {
312 self.database_cache.get_active_experiments()
313 }
314
315 pub fn get_all_experiments(&self) -> Result<Vec<Experiment>> {
316 let db = self.db()?;
317 let reader = db.read()?;
318 db.get_store(StoreId::Experiments)
319 .collect_all::<Experiment, _>(&reader)
320 }
321
322 pub fn get_available_experiments(&self) -> Result<Vec<AvailableExperiment>> {
323 let th = self.create_targeting_helper(None)?;
324 Ok(self
325 .get_all_experiments()?
326 .into_iter()
327 .filter(|exp| {
328 is_experiment_available(&th, exp, false) == ExperimentAvailable::Available
329 })
330 .map(|exp| exp.into())
331 .collect())
332 }
333
334 pub fn opt_in_with_branch(
335 &self,
336 experiment_slug: String,
337 branch: String,
338 ) -> Result<Vec<EnrollmentChangeEvent>> {
339 let db = self.db()?;
340 let mut writer = db.write()?;
341 let result = opt_in_with_branch(db, &mut writer, &experiment_slug, &branch)?;
342 let mut state = self.mutable_state.lock().unwrap();
343 self.end_initialize(db, writer, &mut state)?;
344 Ok(result)
345 }
346
347 pub fn opt_out(&self, experiment_slug: String) -> Result<Vec<EnrollmentChangeEvent>> {
348 let db = self.db()?;
349 let mut writer = db.write()?;
350 let result = opt_out(db, &mut writer, &experiment_slug)?;
351 let mut state = self.mutable_state.lock().unwrap();
352 self.end_initialize(db, writer, &mut state)?;
353 Ok(result)
354 }
355
356 pub fn fetch_experiments(&self) -> Result<()> {
357 if !self.is_fetch_enabled()? {
358 return Ok(());
359 }
360 info!("fetching experiments");
361 let settings_client = self.settings_client.lock().unwrap();
362 let new_experiments = settings_client.fetch_experiments()?;
363 let db = self.db()?;
364 let mut writer = db.write()?;
365 write_pending_experiments(db, &mut writer, new_experiments)?;
366 writer.commit()?;
367 Ok(())
368 }
369
370 pub fn set_fetch_enabled(&self, allow: bool) -> Result<()> {
371 let db = self.db()?;
372 let mut writer = db.write()?;
373 db.get_store(StoreId::Meta)
374 .put(&mut writer, DB_KEY_FETCH_ENABLED, &allow)?;
375 writer.commit()?;
376 Ok(())
377 }
378
379 pub(crate) fn is_fetch_enabled(&self) -> Result<bool> {
380 let db = self.db()?;
381 let reader = db.read()?;
382 let enabled = db
383 .get_store(StoreId::Meta)
384 .get(&reader, DB_KEY_FETCH_ENABLED)?
385 .unwrap_or(true);
386 Ok(enabled)
387 }
388
389 fn update_ta_install_dates(
393 &self,
394 db: &Database,
395 writer: &mut Writer,
396 state: &mut MutexGuard<InternalMutableState>,
397 ) -> Result<()> {
398 if state.install_date.is_none() {
403 let installation_date = self.get_installation_date(db, writer)?;
404 state.install_date = Some(installation_date);
405 }
406 if state.update_date.is_none() {
407 let update_date = self.get_update_date(db, writer)?;
408 state.update_date = Some(update_date);
409 }
410 state.update_time_to_now(Utc::now());
411
412 Ok(())
413 }
414
415 fn update_ta_active_experiments(
419 &self,
420 db: &Database,
421 writer: &Writer,
422 state: &mut MutexGuard<InternalMutableState>,
423 ) -> Result<()> {
424 let enrollments_store = db.get_store(StoreId::Enrollments);
425 let prev_enrollments: Vec<ExperimentEnrollment> = enrollments_store.collect_all(writer)?;
426
427 state
428 .targeting_attributes
429 .update_enrollments(&prev_enrollments);
430
431 Ok(())
432 }
433
434 fn evolve_experiments(
435 &self,
436 db: &Database,
437 writer: &mut Writer,
438 state: &mut InternalMutableState,
439 experiments: &[Experiment],
440 ) -> Result<Vec<EnrollmentChangeEvent>> {
441 let mut targeting_helper = NimbusTargetingHelper::with_targeting_attributes(
442 &state.targeting_attributes,
443 self.event_store.clone(),
444 self.gecko_prefs.clone(),
445 );
446 if let Some(ref recorded_context) = self.recorded_context {
447 recorded_context.record();
448 }
449 let coenrolling_feature_ids = self
450 .coenrolling_feature_ids
451 .iter()
452 .map(|s| s.as_str())
453 .collect();
454 let mut evolver = EnrollmentsEvolver::new(
455 &state.available_randomization_units,
456 &mut targeting_helper,
457 &coenrolling_feature_ids,
458 );
459 evolver.evolve_enrollments_in_db(db, writer, experiments)
460 }
461
462 pub fn apply_pending_experiments(&self) -> Result<Vec<EnrollmentChangeEvent>> {
463 info!("updating experiment list");
464 let db = self.db()?;
465 let mut writer = db.write()?;
466
467 let pending_updates = read_and_remove_pending_experiments(db, &mut writer)?;
470 let mut state = self.mutable_state.lock().unwrap();
471 self.begin_initialize(db, &mut writer, &mut state)?;
472
473 let res = match pending_updates {
474 Some(new_experiments) => {
475 self.update_ta_active_experiments(db, &writer, &mut state)?;
476 self.evolve_experiments(db, &mut writer, &mut state, &new_experiments)?
478 }
479 None => vec![],
480 };
481
482 self.end_initialize(db, writer, &mut state)?;
484 Ok(res)
485 }
486
487 #[allow(deprecated)] fn get_installation_date(&self, db: &Database, writer: &mut Writer) -> Result<DateTime<Utc>> {
489 if let Some(context_installation_date) = self.app_context.installation_date {
491 let res = DateTime::<Utc>::from_naive_utc_and_offset(
492 NaiveDateTime::from_timestamp_opt(context_installation_date / 1_000, 0).unwrap(),
493 Utc,
494 );
495 info!("[Nimbus] Retrieved date from Context: {}", res);
496 return Ok(res);
497 }
498 let store = db.get_store(StoreId::Meta);
499 let persisted_installation_date: Option<DateTime<Utc>> =
500 store.get(writer, DB_KEY_INSTALLATION_DATE)?;
501 Ok(
502 if let Some(installation_date) = persisted_installation_date {
503 installation_date
504 } else {
505 Utc::now()
506 },
507 )
508 }
509
510 fn get_update_date(&self, db: &Database, writer: &mut Writer) -> Result<DateTime<Utc>> {
511 let store = db.get_store(StoreId::Meta);
512
513 let persisted_app_version: Option<String> = store.get(writer, DB_KEY_APP_VERSION)?;
514 let update_date: Option<DateTime<Utc>> = store.get(writer, DB_KEY_UPDATE_DATE)?;
515 Ok(
516 match (
517 persisted_app_version,
518 &self.app_context.app_version,
519 update_date,
520 ) {
521 (Some(persisted), Some(current), Some(date)) if persisted == *current => date,
523 (Some(persisted), Some(current), _) if persisted != *current => {
525 let now = Utc::now();
526 store.put(writer, DB_KEY_APP_VERSION, current)?;
527 store.put(writer, DB_KEY_UPDATE_DATE, &now)?;
528 now
529 }
530 (None, Some(current), _) => {
532 let now = Utc::now();
533 store.put(writer, DB_KEY_APP_VERSION, current)?;
534 store.put(writer, DB_KEY_UPDATE_DATE, &now)?;
535 now
536 }
537 (_, _, Some(date)) => date,
539 _ => Utc::now(),
541 },
542 )
543 }
544
545 pub fn set_experiments_locally(&self, experiments_json: String) -> Result<()> {
546 let new_experiments = parse_experiments(&experiments_json)?;
547 let db = self.db()?;
548 let mut writer = db.write()?;
549 write_pending_experiments(db, &mut writer, new_experiments)?;
550 writer.commit()?;
551 Ok(())
552 }
553
554 pub fn reset_enrollments(&self) -> Result<()> {
558 let db = self.db()?;
559 let mut writer = db.write()?;
560 let mut state = self.mutable_state.lock().unwrap();
561 db.clear_experiments_and_enrollments(&mut writer)?;
562 self.end_initialize(db, writer, &mut state)?;
563 Ok(())
564 }
565
566 pub fn reset_telemetry_identifiers(&self) -> Result<Vec<EnrollmentChangeEvent>> {
575 let mut events = vec![];
576 let db = self.db()?;
577 let mut writer = db.write()?;
578 let mut state = self.mutable_state.lock().unwrap();
579 let store = db.get_store(StoreId::Meta);
582 if store.get::<String, _>(&writer, DB_KEY_NIMBUS_ID)?.is_some() {
583 events = reset_telemetry_identifiers(db, &mut writer)?;
585
586 db.clear_event_count_data(&mut writer)?;
588
589 store.delete(&mut writer, DB_KEY_NIMBUS_ID)?;
592 self.end_initialize(db, writer, &mut state)?;
593 }
594
595 state.available_randomization_units = Default::default();
597 state.targeting_attributes.nimbus_id = None;
598
599 Ok(events)
600 }
601
602 pub fn nimbus_id(&self) -> Result<Uuid> {
603 let db = self.db()?;
604 let mut writer = db.write()?;
605 let mut state = self.mutable_state.lock().unwrap();
606 let uuid = self.read_or_create_nimbus_id(db, &mut writer, &mut state)?;
607
608 writer.commit()?;
612 Ok(uuid)
613 }
614
615 fn read_or_create_nimbus_id(
620 &self,
621 db: &Database,
622 writer: &mut Writer,
623 state: &mut MutexGuard<'_, InternalMutableState>,
624 ) -> Result<Uuid> {
625 let store = db.get_store(StoreId::Meta);
626 let nimbus_id = match store.get(writer, DB_KEY_NIMBUS_ID)? {
627 Some(nimbus_id) => nimbus_id,
628 None => {
629 let nimbus_id = Uuid::new_v4();
630 store.put(writer, DB_KEY_NIMBUS_ID, &nimbus_id)?;
631 nimbus_id
632 }
633 };
634
635 state.available_randomization_units.nimbus_id = Some(nimbus_id.to_string());
636 state.targeting_attributes.nimbus_id = Some(nimbus_id.to_string());
637
638 Ok(nimbus_id)
639 }
640
641 pub fn set_nimbus_id(&self, uuid: &Uuid) -> Result<()> {
645 let db = self.db()?;
646 let mut writer = db.write()?;
647 db.get_store(StoreId::Meta)
648 .put(&mut writer, DB_KEY_NIMBUS_ID, uuid)?;
649 writer.commit()?;
650 Ok(())
651 }
652
653 pub(crate) fn db(&self) -> Result<&Database> {
654 self.db.get_or_try_init(|| Database::new(&self.db_path))
655 }
656
657 fn merge_additional_context(&self, context: Option<JsonObject>) -> Result<Value> {
658 let context = context.map(Value::Object);
659 let targeting = match serde_json::to_value(self.get_targeting_attributes()) {
660 Ok(v) => v,
661 Err(e) => return Err(NimbusError::JSONError("targeting = nimbus::stateful::nimbus_client::NimbusClient::merge_additional_context::serde_json::to_value".into(), e.to_string()))
662 };
663 let context = match context {
664 Some(v) => v.defaults(&targeting)?,
665 None => targeting,
666 };
667
668 Ok(context)
669 }
670
671 pub fn create_targeting_helper(
672 &self,
673 additional_context: Option<JsonObject>,
674 ) -> Result<Arc<NimbusTargetingHelper>> {
675 let context = self.merge_additional_context(additional_context)?;
676 let helper =
677 NimbusTargetingHelper::new(context, self.event_store.clone(), self.gecko_prefs.clone());
678 Ok(Arc::new(helper))
679 }
680
681 pub fn create_targeting_helper_with_context(
682 &self,
683 context: Value,
684 ) -> Arc<NimbusTargetingHelper> {
685 Arc::new(NimbusTargetingHelper::new(
686 context,
687 self.event_store.clone(),
688 self.gecko_prefs.clone(),
689 ))
690 }
691
692 pub fn create_string_helper(
693 &self,
694 additional_context: Option<JsonObject>,
695 ) -> Result<Arc<NimbusStringHelper>> {
696 let context = self.merge_additional_context(additional_context)?;
697 let helper = NimbusStringHelper::new(context.as_object().unwrap().to_owned());
698 Ok(Arc::new(helper))
699 }
700
701 pub fn record_event(&self, event_id: String, count: i64) -> Result<()> {
706 let mut event_store = self.event_store.lock().unwrap();
707 event_store.record_event(count as u64, &event_id, None)?;
708 event_store.persist_data(self.db()?)?;
709 Ok(())
710 }
711
712 pub fn record_past_event(&self, event_id: String, seconds_ago: i64, count: i64) -> Result<()> {
717 if seconds_ago < 0 {
718 return Err(NimbusError::BehaviorError(BehaviorError::InvalidDuration(
719 "Time duration in the past must be positive".to_string(),
720 )));
721 }
722 let mut event_store = self.event_store.lock().unwrap();
723 event_store.record_past_event(
724 count as u64,
725 &event_id,
726 None,
727 chrono::Duration::seconds(seconds_ago),
728 )?;
729 event_store.persist_data(self.db()?)?;
730 Ok(())
731 }
732
733 pub fn advance_event_time(&self, by_seconds: i64) -> Result<()> {
737 if by_seconds < 0 {
738 return Err(NimbusError::BehaviorError(BehaviorError::InvalidDuration(
739 "Time duration in the future must be positive".to_string(),
740 )));
741 }
742 let mut event_store = self.event_store.lock().unwrap();
743 event_store.advance_datum(chrono::Duration::seconds(by_seconds));
744 Ok(())
745 }
746
747 pub fn clear_events(&self) -> Result<()> {
751 let mut event_store = self.event_store.lock().unwrap();
752 event_store.clear(self.db()?)?;
753 Ok(())
754 }
755
756 pub fn event_store(&self) -> Arc<Mutex<EventStore>> {
757 self.event_store.clone()
758 }
759
760 pub fn dump_state_to_log(&self) -> Result<()> {
761 let experiments = self.get_active_experiments()?;
762 info!("{0: <65}| {1: <30}| {2}", "Slug", "Features", "Branch");
763 for exp in &experiments {
764 info!(
765 "{0: <65}| {1: <30}| {2}",
766 &exp.slug,
767 &exp.feature_ids.join(", "),
768 &exp.branch_slug
769 );
770 }
771 Ok(())
772 }
773
774 pub fn unenroll_for_gecko_pref(
776 &self,
777 pref_state: GeckoPrefState,
778 pref_unenroll_reason: PrefUnenrollReason,
779 ) -> Result<Vec<EnrollmentChangeEvent>> {
780 if let Some(prefs) = self.gecko_prefs.clone() {
781 {
782 let mut pref_store_state = prefs.get_mutable_pref_state();
783 pref_store_state.update_pref_state(&pref_state);
784 }
785 let enrollments = self
786 .database_cache
787 .get_enrollments_for_pref(&pref_state.gecko_pref.pref)?;
788
789 let db = self.db()?;
790 let mut writer = db.write()?;
791
792 let mut results = Vec::new();
793 for experiment_slug in enrollments.unwrap() {
794 let result =
795 unenroll_for_pref(db, &mut writer, &experiment_slug, pref_unenroll_reason)?;
796 results.push(result);
797 }
798
799 let mut state = self.mutable_state.lock().unwrap();
800 self.end_initialize(db, writer, &mut state)?;
801 return Ok(results.concat());
802 }
803 Ok(Vec::new())
804 }
805
806 #[cfg(test)]
807 pub fn get_metrics_handler(&self) -> &&TestMetrics {
808 let metrics = &**self.metrics_handler;
809 unsafe { std::mem::transmute::<&&dyn MetricsHandler, &&TestMetrics>(&metrics) }
813 }
814
815 #[cfg(test)]
816 pub fn get_recorded_context(&self) -> &&TestRecordedContext {
817 self.recorded_context
818 .clone()
819 .map(|ref recorded_context|
820 unsafe {
825 std::mem::transmute::<&&dyn RecordedContext, &&TestRecordedContext>(
826 &&**recorded_context,
827 )
828 })
829 .expect("failed to unwrap RecordedContext object")
830 }
831
832 #[cfg(test)]
833 pub fn get_gecko_pref_store(&self) -> Arc<Box<TestGeckoPrefHandler>> {
834 self.gecko_prefs.clone()
835 .clone()
836 .map(|ref pref_store|
837 unsafe {
842 std::mem::transmute::<Arc<Box<dyn GeckoPrefHandler>>, Arc<Box<TestGeckoPrefHandler>>>(
843 pref_store.clone().handler.clone(),
844 )
845 })
846 .expect("failed to unwrap GeckoPrefHandler object")
847 }
848}
849
850impl NimbusClient {
851 pub fn set_install_time(&mut self, then: DateTime<Utc>) {
852 let mut state = self.mutable_state.lock().unwrap();
853 state.install_date = Some(then);
854 state.update_time_to_now(Utc::now());
855 }
856
857 pub fn set_update_time(&mut self, then: DateTime<Utc>) {
858 let mut state = self.mutable_state.lock().unwrap();
859 state.update_date = Some(then);
860 state.update_time_to_now(Utc::now());
861 }
862}
863
864impl NimbusClient {
865 fn record_feature_activation_if_needed(&self, feature_id: &str) {
868 if let Ok(Some(f)) = self.database_cache.get_enrollment_by_feature(feature_id) {
869 if f.branch.is_some() && !self.coenrolling_feature_ids.contains(&f.feature_id) {
870 self.metrics_handler.record_feature_activation(f.into());
871 }
872 }
873 }
874
875 pub fn record_feature_exposure(&self, feature_id: String, slug: Option<String>) {
876 let event = if let Some(slug) = slug {
877 if let Ok(Some(branch)) = self.database_cache.get_experiment_branch(&slug) {
878 Some(FeatureExposureExtraDef {
879 feature_id,
880 branch: Some(branch),
881 slug,
882 })
883 } else {
884 None
885 }
886 } else if let Ok(Some(f)) = self.database_cache.get_enrollment_by_feature(&feature_id) {
887 if f.branch.is_some() {
888 Some(f.into())
889 } else {
890 None
891 }
892 } else {
893 None
894 };
895
896 if let Some(event) = event {
897 self.metrics_handler.record_feature_exposure(event);
898 }
899 }
900
901 pub fn record_malformed_feature_config(&self, feature_id: String, part_id: String) {
902 let event = if let Ok(Some(f)) = self.database_cache.get_enrollment_by_feature(&feature_id)
903 {
904 MalformedFeatureConfigExtraDef::from(f, part_id)
905 } else {
906 MalformedFeatureConfigExtraDef::new(feature_id, part_id)
907 };
908 self.metrics_handler.record_malformed_feature_config(event);
909 }
910
911 fn record_enrollment_status_telemetry(
912 &self,
913 state: &mut MutexGuard<InternalMutableState>,
914 ) -> Result<()> {
915 let targeting_helper = NimbusTargetingHelper::new(
916 state.targeting_attributes.clone(),
917 self.event_store.clone(),
918 self.gecko_prefs.clone(),
919 );
920 let experiments = self.database_cache.get_experiments()?;
921 let experiments = experiments
922 .iter()
923 .filter(|exp| {
924 is_experiment_available(&targeting_helper, exp, true)
925 == ExperimentAvailable::Available
926 })
927 .map(|exp| &*exp.slug)
928 .collect::<HashSet<&str>>();
929 self.metrics_handler.record_enrollment_statuses(
930 self.database_cache
931 .get_enrollments()?
932 .into_iter()
933 .filter_map(|e| match experiments.contains(&*e.slug) {
934 true => Some(e.into()),
935 false => None,
936 })
937 .collect(),
938 );
939 Ok(())
940 }
941}
942
943pub struct NimbusStringHelper {
944 context: JsonObject,
945}
946
947impl NimbusStringHelper {
948 fn new(context: JsonObject) -> Self {
949 Self { context }
950 }
951
952 pub fn get_uuid(&self, template: String) -> Option<String> {
953 if template.contains("{uuid}") {
954 let uuid = Uuid::new_v4();
955 Some(uuid.to_string())
956 } else {
957 None
958 }
959 }
960
961 pub fn string_format(&self, template: String, uuid: Option<String>) -> String {
962 match uuid {
963 Some(uuid) => {
964 let mut map = self.context.clone();
965 map.insert("uuid".to_string(), Value::String(uuid));
966 fmt_with_map(&template, &map)
967 }
968 _ => fmt_with_map(&template, &self.context),
969 }
970 }
971}
972
973#[cfg(feature = "stateful-uniffi-bindings")]
974uniffi::custom_type!(JsonObject, String, {
975 remote,
976 try_lift: |val| {
977 let json: Value = serde_json::from_str(&val)?;
978
979 match json.as_object() {
980 Some(obj) => Ok(obj.clone()),
981 _ => Err(uniffi::deps::anyhow::anyhow!(
982 "Unexpected JSON-non-object in the bagging area"
983 )),
984 }
985 },
986 lower: |obj| serde_json::Value::Object(obj).to_string(),
987});
988
989#[cfg(feature = "stateful-uniffi-bindings")]
990uniffi::custom_type!(PrefValue, String, {
991 remote,
992 try_lift: |val| {
993 let json: Value = serde_json::from_str(&val)?;
994 if json.is_string() || json.is_boolean() || (json.is_number() && !json.is_f64()) || json.is_null() {
995 Ok(json)
996 } else {
997 Err(anyhow::anyhow!(format!("Value {} is not a string, boolean, number, or null, or is a float", json)))
998 }
999 },
1000 lower: |val| {
1001 val.to_string()
1002 }
1003});
1004
1005#[cfg(feature = "stateful-uniffi-bindings")]
1006uniffi::include_scaffolding!("nimbus");