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