1#[cfg(test)]
6use crate::tests::helpers::{TestMetrics, TestRecordedContext};
7use crate::{
8 defaults::Defaults,
9 enrollment::{
10 EnrolledFeature, EnrollmentChangeEvent, EnrollmentChangeEventType, EnrollmentsEvolver,
11 ExperimentEnrollment,
12 },
13 error::{info, warn, BehaviorError},
14 evaluator::{
15 get_calculated_attributes, is_experiment_available, CalculatedAttributes,
16 TargetingAttributes,
17 },
18 json::JsonObject,
19 metrics::{
20 EnrollmentStatusExtraDef, FeatureExposureExtraDef, MalformedFeatureConfigExtraDef,
21 MetricsHandler,
22 },
23 schema::parse_experiments,
24 stateful::{
25 behavior::EventStore,
26 client::{create_client, SettingsClient},
27 dbcache::DatabaseCache,
28 enrollment::{
29 get_global_user_participation, opt_in_with_branch, opt_out,
30 reset_telemetry_identifiers, set_global_user_participation,
31 },
32 matcher::AppContext,
33 persistence::{Database, StoreId, Writer},
34 targeting::{validate_event_queries, RecordedContext},
35 updating::{read_and_remove_pending_experiments, write_pending_experiments},
36 },
37 strings::fmt_with_map,
38 AvailableExperiment, AvailableRandomizationUnits, EnrolledExperiment, Experiment,
39 ExperimentBranch, NimbusError, NimbusTargetingHelper, Result,
40};
41use chrono::{DateTime, NaiveDateTime, Utc};
42use once_cell::sync::OnceCell;
43use remote_settings::RemoteSettingsConfig;
44use serde_json::Value;
45use std::collections::HashSet;
46use std::fmt::Debug;
47use std::path::{Path, PathBuf};
48use std::sync::{Arc, Mutex, MutexGuard};
49use uuid::Uuid;
50
51const DB_KEY_NIMBUS_ID: &str = "nimbus-id";
52pub const DB_KEY_INSTALLATION_DATE: &str = "installation-date";
53pub const DB_KEY_UPDATE_DATE: &str = "update-date";
54pub const DB_KEY_APP_VERSION: &str = "app-version";
55pub const DB_KEY_FETCH_ENABLED: &str = "fetch-enabled";
56
57#[derive(Default)]
62pub struct InternalMutableState {
63 pub(crate) available_randomization_units: AvailableRandomizationUnits,
64 pub(crate) install_date: Option<DateTime<Utc>>,
65 pub(crate) update_date: Option<DateTime<Utc>>,
66 pub(crate) targeting_attributes: TargetingAttributes,
68}
69
70impl InternalMutableState {
71 pub(crate) fn update_time_to_now(&mut self, now: DateTime<Utc>) {
72 self.targeting_attributes
73 .update_time_to_now(now, &self.install_date, &self.update_date);
74 }
75}
76
77pub struct NimbusClient {
81 settings_client: Mutex<Box<dyn SettingsClient + Send>>,
82 pub(crate) mutable_state: Mutex<InternalMutableState>,
83 app_context: AppContext,
84 pub(crate) db: OnceCell<Database>,
85 database_cache: DatabaseCache,
88 db_path: PathBuf,
89 coenrolling_feature_ids: Vec<String>,
90 event_store: Arc<Mutex<EventStore>>,
91 recorded_context: Option<Arc<dyn RecordedContext>>,
92 metrics_handler: Arc<Box<dyn MetricsHandler>>,
93}
94
95impl NimbusClient {
96 pub fn new<P: Into<PathBuf>>(
99 app_context: AppContext,
100 recorded_context: Option<Arc<dyn RecordedContext>>,
101 coenrolling_feature_ids: Vec<String>,
102 db_path: P,
103 config: Option<RemoteSettingsConfig>,
104 metrics_handler: Box<dyn MetricsHandler>,
105 ) -> Result<Self> {
106 let settings_client = Mutex::new(create_client(config)?);
107
108 let targeting_attributes: TargetingAttributes = app_context.clone().into();
109 let mutable_state = Mutex::new(InternalMutableState {
110 available_randomization_units: Default::default(),
111 targeting_attributes,
112 install_date: Default::default(),
113 update_date: Default::default(),
114 });
115
116 Ok(Self {
117 settings_client,
118 mutable_state,
119 app_context,
120 database_cache: Default::default(),
121 db_path: db_path.into(),
122 coenrolling_feature_ids,
123 db: OnceCell::default(),
124 event_store: Arc::default(),
125 recorded_context,
126 metrics_handler: Arc::new(metrics_handler),
127 })
128 }
129
130 pub fn with_targeting_attributes(&mut self, targeting_attributes: TargetingAttributes) {
131 let mut state = self.mutable_state.lock().unwrap();
132 state.targeting_attributes = targeting_attributes;
133 }
134
135 pub fn get_targeting_attributes(&self) -> TargetingAttributes {
136 let mut state = self.mutable_state.lock().unwrap();
137 state.update_time_to_now(Utc::now());
138 state.targeting_attributes.clone()
139 }
140
141 pub fn initialize(&self) -> Result<()> {
142 let db = self.db()?;
143 let mut writer = db.write()?;
145
146 let mut state = self.mutable_state.lock().unwrap();
147 self.begin_initialize(db, &mut writer, &mut state)?;
148 self.end_initialize(db, writer, &mut state)?;
149
150 Ok(())
151 }
152
153 fn begin_initialize(
156 &self,
157 db: &Database,
158 writer: &mut Writer,
159 state: &mut MutexGuard<InternalMutableState>,
160 ) -> Result<()> {
161 self.read_or_create_nimbus_id(db, writer, state)?;
162 self.update_ta_install_dates(db, writer, state)?;
163 self.event_store
164 .lock()
165 .expect("unable to lock event_store mutex")
166 .read_from_db(db)?;
167
168 if let Some(recorded_context) = &self.recorded_context {
169 let targeting_helper = self.create_targeting_helper_with_context(match serde_json::to_value(
170 &state.targeting_attributes,
171 ) {
172 Ok(v) => v,
173 Err(e) => return Err(NimbusError::JSONError("targeting_helper = nimbus::stateful::nimbus_client::NimbusClient::begin_initialize::serde_json::to_value".into(), e.to_string()))
174 });
175 recorded_context.execute_queries(targeting_helper.as_ref())?;
176 state
177 .targeting_attributes
178 .set_recorded_context(recorded_context.to_json());
179 }
180
181 Ok(())
182 }
183
184 fn end_initialize(
187 &self,
188 db: &Database,
189 writer: Writer,
190 state: &mut MutexGuard<InternalMutableState>,
191 ) -> Result<()> {
192 self.update_ta_active_experiments(db, &writer, state)?;
193 let coenrolling_ids = self
194 .coenrolling_feature_ids
195 .iter()
196 .map(|s| s.as_str())
197 .collect();
198 self.database_cache
199 .commit_and_update(db, writer, &coenrolling_ids)?;
200 self.record_enrollment_status_telemetry(state)?;
201 Ok(())
202 }
203
204 pub fn get_enrollment_by_feature(&self, feature_id: String) -> Result<Option<EnrolledFeature>> {
205 self.database_cache.get_enrollment_by_feature(&feature_id)
206 }
207
208 pub fn get_experiment_branch(&self, slug: String) -> Result<Option<String>> {
210 self.database_cache.get_experiment_branch(&slug)
211 }
212
213 pub fn get_feature_config_variables(&self, feature_id: String) -> Result<Option<String>> {
214 Ok(
215 if let Some(s) = self
216 .database_cache
217 .get_feature_config_variables(&feature_id)?
218 {
219 self.record_feature_activation_if_needed(&feature_id);
220 Some(s)
221 } else {
222 None
223 },
224 )
225 }
226
227 pub fn get_experiment_branches(&self, slug: String) -> Result<Vec<ExperimentBranch>> {
228 self.get_all_experiments()?
229 .into_iter()
230 .find(|e| e.slug == slug)
231 .map(|e| e.branches.into_iter().map(|b| b.into()).collect())
232 .ok_or(NimbusError::NoSuchExperiment(slug))
233 }
234
235 pub fn get_global_user_participation(&self) -> Result<bool> {
236 let db = self.db()?;
237 let reader = db.read()?;
238 get_global_user_participation(db, &reader)
239 }
240
241 pub fn set_global_user_participation(
242 &self,
243 user_participating: bool,
244 ) -> Result<Vec<EnrollmentChangeEvent>> {
245 let db = self.db()?;
246 let mut writer = db.write()?;
247 let mut state = self.mutable_state.lock().unwrap();
248 set_global_user_participation(db, &mut writer, user_participating)?;
249
250 let existing_experiments: Vec<Experiment> =
251 db.get_store(StoreId::Experiments).collect_all(&writer)?;
252 let events = self.evolve_experiments(db, &mut writer, &mut state, &existing_experiments)?;
255 self.end_initialize(db, writer, &mut state)?;
256 Ok(events)
257 }
258
259 pub fn get_active_experiments(&self) -> Result<Vec<EnrolledExperiment>> {
260 self.database_cache.get_active_experiments()
261 }
262
263 pub fn get_all_experiments(&self) -> Result<Vec<Experiment>> {
264 let db = self.db()?;
265 let reader = db.read()?;
266 db.get_store(StoreId::Experiments)
267 .collect_all::<Experiment, _>(&reader)
268 }
269
270 pub fn get_available_experiments(&self) -> Result<Vec<AvailableExperiment>> {
271 let th = self.create_targeting_helper(None)?;
272 Ok(self
273 .get_all_experiments()?
274 .into_iter()
275 .filter(|exp| is_experiment_available(&th, exp, false))
276 .map(|exp| exp.into())
277 .collect())
278 }
279
280 pub fn opt_in_with_branch(
281 &self,
282 experiment_slug: String,
283 branch: String,
284 ) -> Result<Vec<EnrollmentChangeEvent>> {
285 let db = self.db()?;
286 let mut writer = db.write()?;
287 let result = opt_in_with_branch(db, &mut writer, &experiment_slug, &branch)?;
288 let mut state = self.mutable_state.lock().unwrap();
289 self.end_initialize(db, writer, &mut state)?;
290 Ok(result)
291 }
292
293 pub fn opt_out(&self, experiment_slug: String) -> Result<Vec<EnrollmentChangeEvent>> {
294 let db = self.db()?;
295 let mut writer = db.write()?;
296 let result = opt_out(db, &mut writer, &experiment_slug)?;
297 let mut state = self.mutable_state.lock().unwrap();
298 self.end_initialize(db, writer, &mut state)?;
299 Ok(result)
300 }
301
302 pub fn fetch_experiments(&self) -> Result<()> {
303 if !self.is_fetch_enabled()? {
304 return Ok(());
305 }
306 info!("fetching experiments");
307 let settings_client = self.settings_client.lock().unwrap();
308 let new_experiments = settings_client.fetch_experiments()?;
309 let db = self.db()?;
310 let mut writer = db.write()?;
311 write_pending_experiments(db, &mut writer, new_experiments)?;
312 writer.commit()?;
313 Ok(())
314 }
315
316 pub fn set_fetch_enabled(&self, allow: bool) -> Result<()> {
317 let db = self.db()?;
318 let mut writer = db.write()?;
319 db.get_store(StoreId::Meta)
320 .put(&mut writer, DB_KEY_FETCH_ENABLED, &allow)?;
321 writer.commit()?;
322 Ok(())
323 }
324
325 pub(crate) fn is_fetch_enabled(&self) -> Result<bool> {
326 let db = self.db()?;
327 let reader = db.read()?;
328 let enabled = db
329 .get_store(StoreId::Meta)
330 .get(&reader, DB_KEY_FETCH_ENABLED)?
331 .unwrap_or(true);
332 Ok(enabled)
333 }
334
335 fn update_ta_install_dates(
339 &self,
340 db: &Database,
341 writer: &mut Writer,
342 state: &mut MutexGuard<InternalMutableState>,
343 ) -> Result<()> {
344 if state.install_date.is_none() {
349 let installation_date = self.get_installation_date(db, writer)?;
350 state.install_date = Some(installation_date);
351 }
352 if state.update_date.is_none() {
353 let update_date = self.get_update_date(db, writer)?;
354 state.update_date = Some(update_date);
355 }
356 state.update_time_to_now(Utc::now());
357
358 Ok(())
359 }
360
361 fn update_ta_active_experiments(
365 &self,
366 db: &Database,
367 writer: &Writer,
368 state: &mut MutexGuard<InternalMutableState>,
369 ) -> Result<()> {
370 let enrollments_store = db.get_store(StoreId::Enrollments);
371 let prev_enrollments: Vec<ExperimentEnrollment> = enrollments_store.collect_all(writer)?;
372
373 state
374 .targeting_attributes
375 .update_enrollments(&prev_enrollments);
376
377 Ok(())
378 }
379
380 fn evolve_experiments(
381 &self,
382 db: &Database,
383 writer: &mut Writer,
384 state: &mut InternalMutableState,
385 experiments: &[Experiment],
386 ) -> Result<Vec<EnrollmentChangeEvent>> {
387 let mut targeting_helper = NimbusTargetingHelper::with_targeting_attributes(
388 &state.targeting_attributes,
389 self.event_store.clone(),
390 );
391 if let Some(ref recorded_context) = self.recorded_context {
392 recorded_context.record();
393 }
394 let coenrolling_feature_ids = self
395 .coenrolling_feature_ids
396 .iter()
397 .map(|s| s.as_str())
398 .collect();
399 let mut evolver = EnrollmentsEvolver::new(
400 &state.available_randomization_units,
401 &mut targeting_helper,
402 &coenrolling_feature_ids,
403 );
404 evolver.evolve_enrollments_in_db(db, writer, experiments)
405 }
406
407 pub fn apply_pending_experiments(&self) -> Result<Vec<EnrollmentChangeEvent>> {
408 info!("updating experiment list");
409 let db = self.db()?;
410 let mut writer = db.write()?;
411
412 let pending_updates = read_and_remove_pending_experiments(db, &mut writer)?;
415 let mut state = self.mutable_state.lock().unwrap();
416 self.begin_initialize(db, &mut writer, &mut state)?;
417
418 let res = match pending_updates {
419 Some(new_experiments) => {
420 self.update_ta_active_experiments(db, &writer, &mut state)?;
421 self.evolve_experiments(db, &mut writer, &mut state, &new_experiments)?
423 }
424 None => vec![],
425 };
426
427 self.end_initialize(db, writer, &mut state)?;
429 Ok(res)
430 }
431
432 #[allow(deprecated)] fn get_installation_date(&self, db: &Database, writer: &mut Writer) -> Result<DateTime<Utc>> {
434 if let Some(context_installation_date) = self.app_context.installation_date {
436 let res = DateTime::<Utc>::from_naive_utc_and_offset(
437 NaiveDateTime::from_timestamp_opt(context_installation_date / 1_000, 0).unwrap(),
438 Utc,
439 );
440 info!("[Nimbus] Retrieved date from Context: {}", res);
441 return Ok(res);
442 }
443 let store = db.get_store(StoreId::Meta);
444 let persisted_installation_date: Option<DateTime<Utc>> =
445 store.get(writer, DB_KEY_INSTALLATION_DATE)?;
446 Ok(
447 if let Some(installation_date) = persisted_installation_date {
448 installation_date
449 } else if let Some(home_directory) = &self.app_context.home_directory {
450 let installation_date = match self.get_creation_date_from_path(home_directory) {
451 Ok(installation_date) => installation_date,
452 Err(e) => {
453 warn!("[Nimbus] Unable to get installation date from path, defaulting to today: {:?}", e);
454 Utc::now()
455 }
456 };
457 let store = db.get_store(StoreId::Meta);
458 store.put(writer, DB_KEY_INSTALLATION_DATE, &installation_date)?;
459 installation_date
460 } else {
461 Utc::now()
462 },
463 )
464 }
465
466 fn get_update_date(&self, db: &Database, writer: &mut Writer) -> Result<DateTime<Utc>> {
467 let store = db.get_store(StoreId::Meta);
468
469 let persisted_app_version: Option<String> = store.get(writer, DB_KEY_APP_VERSION)?;
470 let update_date: Option<DateTime<Utc>> = store.get(writer, DB_KEY_UPDATE_DATE)?;
471 Ok(
472 match (
473 persisted_app_version,
474 &self.app_context.app_version,
475 update_date,
476 ) {
477 (Some(persisted), Some(current), Some(date)) if persisted == *current => date,
479 (Some(persisted), Some(current), _) if persisted != *current => {
481 let now = Utc::now();
482 store.put(writer, DB_KEY_APP_VERSION, current)?;
483 store.put(writer, DB_KEY_UPDATE_DATE, &now)?;
484 now
485 }
486 (None, Some(current), _) => {
488 let now = Utc::now();
489 store.put(writer, DB_KEY_APP_VERSION, current)?;
490 store.put(writer, DB_KEY_UPDATE_DATE, &now)?;
491 now
492 }
493 (_, _, Some(date)) => date,
495 _ => Utc::now(),
497 },
498 )
499 }
500
501 #[cfg(not(test))]
502 fn get_creation_date_from_path<P: AsRef<Path>>(&self, path: P) -> Result<DateTime<Utc>> {
503 info!("[Nimbus] Getting creation date from path");
504 let metadata = std::fs::metadata(path)?;
505 let system_time_created = metadata.created()?;
506 let date_time_created = DateTime::<Utc>::from(system_time_created);
507 info!(
508 "[Nimbus] Creation date retrieved form path successfully: {}",
509 date_time_created
510 );
511 Ok(date_time_created)
512 }
513
514 #[cfg(test)]
515 fn get_creation_date_from_path<P: AsRef<Path>>(&self, path: P) -> Result<DateTime<Utc>> {
516 use std::io::Read;
517 let test_path = path.as_ref().with_file_name("test.json");
518 let mut file = std::fs::File::open(test_path)?;
519 let mut buf = String::new();
520 file.read_to_string(&mut buf)?;
521
522 let res = match serde_json::from_str::<DateTime<Utc>>(&buf) {
523 Ok(v) => v,
524 Err(e) => return Err(NimbusError::JSONError("res = nimbus::stateful::nimbus_client::get_creation_date_from_path::serde_json::from_str".into(), e.to_string()))
525 };
526 Ok(res)
527 }
528
529 pub fn set_experiments_locally(&self, experiments_json: String) -> Result<()> {
530 let new_experiments = parse_experiments(&experiments_json)?;
531 let db = self.db()?;
532 let mut writer = db.write()?;
533 write_pending_experiments(db, &mut writer, new_experiments)?;
534 writer.commit()?;
535 Ok(())
536 }
537
538 pub fn reset_enrollments(&self) -> Result<()> {
542 let db = self.db()?;
543 let mut writer = db.write()?;
544 let mut state = self.mutable_state.lock().unwrap();
545 db.clear_experiments_and_enrollments(&mut writer)?;
546 self.end_initialize(db, writer, &mut state)?;
547 Ok(())
548 }
549
550 pub fn reset_telemetry_identifiers(&self) -> Result<Vec<EnrollmentChangeEvent>> {
559 let mut events = vec![];
560 let db = self.db()?;
561 let mut writer = db.write()?;
562 let mut state = self.mutable_state.lock().unwrap();
563 let store = db.get_store(StoreId::Meta);
566 if store.get::<String, _>(&writer, DB_KEY_NIMBUS_ID)?.is_some() {
567 events = reset_telemetry_identifiers(db, &mut writer)?;
569
570 db.clear_event_count_data(&mut writer)?;
572
573 store.delete(&mut writer, DB_KEY_NIMBUS_ID)?;
576 self.end_initialize(db, writer, &mut state)?;
577 }
578
579 state.available_randomization_units = Default::default();
581 state.targeting_attributes.nimbus_id = None;
582
583 Ok(events)
584 }
585
586 pub fn nimbus_id(&self) -> Result<Uuid> {
587 let db = self.db()?;
588 let mut writer = db.write()?;
589 let mut state = self.mutable_state.lock().unwrap();
590 let uuid = self.read_or_create_nimbus_id(db, &mut writer, &mut state)?;
591
592 writer.commit()?;
596 Ok(uuid)
597 }
598
599 fn read_or_create_nimbus_id(
604 &self,
605 db: &Database,
606 writer: &mut Writer,
607 state: &mut MutexGuard<'_, InternalMutableState>,
608 ) -> Result<Uuid> {
609 let store = db.get_store(StoreId::Meta);
610 let nimbus_id = match store.get(writer, DB_KEY_NIMBUS_ID)? {
611 Some(nimbus_id) => nimbus_id,
612 None => {
613 let nimbus_id = Uuid::new_v4();
614 store.put(writer, DB_KEY_NIMBUS_ID, &nimbus_id)?;
615 nimbus_id
616 }
617 };
618
619 state.available_randomization_units.nimbus_id = Some(nimbus_id.to_string());
620 state.targeting_attributes.nimbus_id = Some(nimbus_id.to_string());
621
622 Ok(nimbus_id)
623 }
624
625 pub fn set_nimbus_id(&self, uuid: &Uuid) -> Result<()> {
629 let db = self.db()?;
630 let mut writer = db.write()?;
631 db.get_store(StoreId::Meta)
632 .put(&mut writer, DB_KEY_NIMBUS_ID, uuid)?;
633 writer.commit()?;
634 Ok(())
635 }
636
637 pub(crate) fn db(&self) -> Result<&Database> {
638 self.db.get_or_try_init(|| Database::new(&self.db_path))
639 }
640
641 fn merge_additional_context(&self, context: Option<JsonObject>) -> Result<Value> {
642 let context = context.map(Value::Object);
643 let targeting = match serde_json::to_value(self.get_targeting_attributes()) {
644 Ok(v) => v,
645 Err(e) => return Err(NimbusError::JSONError("targeting = nimbus::stateful::nimbus_client::NimbusClient::merge_additional_context::serde_json::to_value".into(), e.to_string()))
646 };
647 let context = match context {
648 Some(v) => v.defaults(&targeting)?,
649 None => targeting,
650 };
651
652 Ok(context)
653 }
654
655 pub fn create_targeting_helper(
656 &self,
657 additional_context: Option<JsonObject>,
658 ) -> Result<Arc<NimbusTargetingHelper>> {
659 let context = self.merge_additional_context(additional_context)?;
660 let helper = NimbusTargetingHelper::new(context, self.event_store.clone());
661 Ok(Arc::new(helper))
662 }
663
664 pub fn create_targeting_helper_with_context(
665 &self,
666 context: Value,
667 ) -> Arc<NimbusTargetingHelper> {
668 Arc::new(NimbusTargetingHelper::new(
669 context,
670 self.event_store.clone(),
671 ))
672 }
673
674 pub fn create_string_helper(
675 &self,
676 additional_context: Option<JsonObject>,
677 ) -> Result<Arc<NimbusStringHelper>> {
678 let context = self.merge_additional_context(additional_context)?;
679 let helper = NimbusStringHelper::new(context.as_object().unwrap().to_owned());
680 Ok(Arc::new(helper))
681 }
682
683 pub fn record_event(&self, event_id: String, count: i64) -> Result<()> {
688 let mut event_store = self.event_store.lock().unwrap();
689 event_store.record_event(count as u64, &event_id, None)?;
690 event_store.persist_data(self.db()?)?;
691 Ok(())
692 }
693
694 pub fn record_past_event(&self, event_id: String, seconds_ago: i64, count: i64) -> Result<()> {
699 if seconds_ago < 0 {
700 return Err(NimbusError::BehaviorError(BehaviorError::InvalidDuration(
701 "Time duration in the past must be positive".to_string(),
702 )));
703 }
704 let mut event_store = self.event_store.lock().unwrap();
705 event_store.record_past_event(
706 count as u64,
707 &event_id,
708 None,
709 chrono::Duration::seconds(seconds_ago),
710 )?;
711 event_store.persist_data(self.db()?)?;
712 Ok(())
713 }
714
715 pub fn advance_event_time(&self, by_seconds: i64) -> Result<()> {
719 if by_seconds < 0 {
720 return Err(NimbusError::BehaviorError(BehaviorError::InvalidDuration(
721 "Time duration in the future must be positive".to_string(),
722 )));
723 }
724 let mut event_store = self.event_store.lock().unwrap();
725 event_store.advance_datum(chrono::Duration::seconds(by_seconds));
726 Ok(())
727 }
728
729 pub fn clear_events(&self) -> Result<()> {
733 let mut event_store = self.event_store.lock().unwrap();
734 event_store.clear(self.db()?)?;
735 Ok(())
736 }
737
738 pub fn event_store(&self) -> Arc<Mutex<EventStore>> {
739 self.event_store.clone()
740 }
741
742 pub fn dump_state_to_log(&self) -> Result<()> {
743 let experiments = self.get_active_experiments()?;
744 info!("{0: <65}| {1: <30}| {2}", "Slug", "Features", "Branch");
745 for exp in &experiments {
746 info!(
747 "{0: <65}| {1: <30}| {2}",
748 &exp.slug,
749 &exp.feature_ids.join(", "),
750 &exp.branch_slug
751 );
752 }
753 Ok(())
754 }
755
756 #[cfg(test)]
757 pub fn get_metrics_handler(&self) -> &&TestMetrics {
758 let metrics = &**self.metrics_handler;
759 unsafe { std::mem::transmute::<&&dyn MetricsHandler, &&TestMetrics>(&metrics) }
763 }
764
765 #[cfg(test)]
766 pub fn get_recorded_context(&self) -> &&TestRecordedContext {
767 self.recorded_context
768 .clone()
769 .map(|ref recorded_context|
770 unsafe {
775 std::mem::transmute::<&&dyn RecordedContext, &&TestRecordedContext>(
776 &&**recorded_context,
777 )
778 })
779 .expect("failed to unwrap RecordedContext object")
780 }
781}
782
783impl NimbusClient {
784 pub fn set_install_time(&mut self, then: DateTime<Utc>) {
785 let mut state = self.mutable_state.lock().unwrap();
786 state.install_date = Some(then);
787 state.update_time_to_now(Utc::now());
788 }
789
790 pub fn set_update_time(&mut self, then: DateTime<Utc>) {
791 let mut state = self.mutable_state.lock().unwrap();
792 state.update_date = Some(then);
793 state.update_time_to_now(Utc::now());
794 }
795}
796
797impl NimbusClient {
798 fn record_feature_activation_if_needed(&self, feature_id: &str) {
801 if let Ok(Some(f)) = self.database_cache.get_enrollment_by_feature(feature_id) {
802 if f.branch.is_some() && !self.coenrolling_feature_ids.contains(&f.feature_id) {
803 self.metrics_handler.record_feature_activation(f.into());
804 }
805 }
806 }
807
808 pub fn record_feature_exposure(&self, feature_id: String, slug: Option<String>) {
809 let event = if let Some(slug) = slug {
810 if let Ok(Some(branch)) = self.database_cache.get_experiment_branch(&slug) {
811 Some(FeatureExposureExtraDef {
812 feature_id,
813 branch: Some(branch),
814 slug,
815 })
816 } else {
817 None
818 }
819 } else if let Ok(Some(f)) = self.database_cache.get_enrollment_by_feature(&feature_id) {
820 if f.branch.is_some() {
821 Some(f.into())
822 } else {
823 None
824 }
825 } else {
826 None
827 };
828
829 if let Some(event) = event {
830 self.metrics_handler.record_feature_exposure(event);
831 }
832 }
833
834 pub fn record_malformed_feature_config(&self, feature_id: String, part_id: String) {
835 let event = if let Ok(Some(f)) = self.database_cache.get_enrollment_by_feature(&feature_id)
836 {
837 MalformedFeatureConfigExtraDef::from(f, part_id)
838 } else {
839 MalformedFeatureConfigExtraDef::new(feature_id, part_id)
840 };
841 self.metrics_handler.record_malformed_feature_config(event);
842 }
843
844 fn record_enrollment_status_telemetry(
845 &self,
846 state: &mut MutexGuard<InternalMutableState>,
847 ) -> Result<()> {
848 let targeting_helper = NimbusTargetingHelper::new(
849 state.targeting_attributes.clone(),
850 self.event_store.clone(),
851 );
852 let experiments = self
853 .database_cache
854 .get_experiments()?
855 .iter()
856 .filter_map(
857 |exp| match is_experiment_available(&targeting_helper, exp, true) {
858 true => Some(exp.slug.clone()),
859 false => None,
860 },
861 )
862 .collect::<HashSet<String>>();
863 self.metrics_handler.record_enrollment_statuses(
864 self.database_cache
865 .get_enrollments()?
866 .into_iter()
867 .filter_map(|e| match experiments.contains(&e.slug) {
868 true => Some(e.into()),
869 false => None,
870 })
871 .collect(),
872 );
873 Ok(())
874 }
875}
876
877pub struct NimbusStringHelper {
878 context: JsonObject,
879}
880
881impl NimbusStringHelper {
882 fn new(context: JsonObject) -> Self {
883 Self { context }
884 }
885
886 pub fn get_uuid(&self, template: String) -> Option<String> {
887 if template.contains("{uuid}") {
888 let uuid = Uuid::new_v4();
889 Some(uuid.to_string())
890 } else {
891 None
892 }
893 }
894
895 pub fn string_format(&self, template: String, uuid: Option<String>) -> String {
896 match uuid {
897 Some(uuid) => {
898 let mut map = self.context.clone();
899 map.insert("uuid".to_string(), Value::String(uuid));
900 fmt_with_map(&template, &map)
901 }
902 _ => fmt_with_map(&template, &self.context),
903 }
904 }
905}
906
907#[cfg(feature = "stateful-uniffi-bindings")]
908uniffi::custom_type!(JsonObject, String, {
909 remote,
910 try_lift: |val| {
911 let json: Value = serde_json::from_str(&val)?;
912
913 match json.as_object() {
914 Some(obj) => Ok(obj.clone()),
915 _ => Err(uniffi::deps::anyhow::anyhow!(
916 "Unexpected JSON-non-object in the bagging area"
917 )),
918 }
919 },
920 lower: |obj| serde_json::Value::Object(obj).to_string(),
921});
922
923#[cfg(feature = "stateful-uniffi-bindings")]
924uniffi::include_scaffolding!("nimbus");