nimbus/stateful/
evaluator.rs
1use crate::stateful::persistence::{Database, StoreId};
6use crate::{
7 enrollment::{EnrollmentStatus, ExperimentEnrollment},
8 error::{warn, Result},
9 evaluator::split_locale,
10 json::JsonObject,
11 stateful::matcher::AppContext,
12 DB_KEY_UPDATE_DATE,
13};
14use chrono::{DateTime, NaiveDateTime, Utc};
15use serde_derive::*;
16use std::collections::{HashMap, HashSet};
17
18#[derive(Serialize, Deserialize, Debug, Clone, Default)]
19pub struct TargetingAttributes {
20 #[serde(flatten)]
21 pub app_context: AppContext,
22 pub language: Option<String>,
23 pub region: Option<String>,
24 #[serde(flatten)]
25 pub recorded_context: Option<JsonObject>,
26 pub is_already_enrolled: bool,
27 pub days_since_install: Option<i32>,
28 pub days_since_update: Option<i32>,
29 pub active_experiments: HashSet<String>,
30 pub enrollments: HashSet<String>,
31 pub enrollments_map: HashMap<String, String>,
32 #[serde(with = "chrono::serde::ts_seconds")]
33 pub current_date: DateTime<Utc>,
34 pub nimbus_id: Option<String>,
35}
36
37impl From<AppContext> for TargetingAttributes {
38 fn from(app_context: AppContext) -> Self {
39 let (language, region) = app_context
40 .locale
41 .clone()
42 .map(split_locale)
43 .unwrap_or_else(|| (None, None));
44
45 Self {
46 app_context,
47 language,
48 region,
49 ..Default::default()
50 }
51 }
52}
53
54impl TargetingAttributes {
55 pub(crate) fn set_recorded_context(&mut self, recorded_context: JsonObject) {
56 self.recorded_context = Some(recorded_context);
57 }
58
59 pub(crate) fn update_time_to_now(
60 &mut self,
61 now: DateTime<Utc>,
62 install_date: &Option<DateTime<Utc>>,
63 update_date: &Option<DateTime<Utc>>,
64 ) {
65 self.days_since_install = install_date.map(|then| (now - then).num_days() as i32);
66 self.days_since_update = update_date.map(|then| (now - then).num_days() as i32);
67 self.current_date = now;
68 }
69
70 pub(crate) fn update_enrollments(&mut self, enrollments: &[ExperimentEnrollment]) -> u32 {
71 let mut modified_count = 0;
72 for experiment_enrollment in enrollments {
73 if self.update_enrollment(experiment_enrollment) {
74 modified_count += 1;
75 }
76 }
77 modified_count
78 }
79
80 pub(crate) fn update_enrollment(&mut self, enrollment: &ExperimentEnrollment) -> bool {
81 match &enrollment.status {
82 EnrollmentStatus::Enrolled { branch, .. } => {
83 let inserted_active = self.active_experiments.insert(enrollment.slug.clone());
84 let inserted_enrollment = self.enrollments.insert(enrollment.slug.clone());
85 let updated_enrollment_map = self
86 .enrollments_map
87 .insert(enrollment.slug.clone(), branch.clone());
88
89 inserted_active
90 || inserted_enrollment
91 || (updated_enrollment_map.is_some()
92 && &updated_enrollment_map.unwrap() != branch)
93 }
94 EnrollmentStatus::WasEnrolled { branch, .. }
95 | EnrollmentStatus::Disqualified { branch, .. } => {
96 let removed_active = self.active_experiments.remove(&enrollment.slug);
97 let inserted_enrollment = self.enrollments.insert(enrollment.slug.clone());
98 let updated_enrollments_map = self
99 .enrollments_map
100 .insert(enrollment.slug.clone(), branch.clone());
101
102 removed_active
103 || inserted_enrollment
104 || (updated_enrollments_map.is_some()
105 && &updated_enrollments_map.unwrap() != branch)
106 }
107 EnrollmentStatus::NotEnrolled { .. } | EnrollmentStatus::Error { .. } => {
108 let removed_active = self.active_experiments.remove(&enrollment.slug);
109 let removed_enrollment = self.enrollments.remove(&enrollment.slug);
110 let removed_from_enrollments_map = self.enrollments_map.remove(&enrollment.slug);
111
112 removed_active || removed_enrollment || removed_from_enrollments_map.is_some()
113 }
114 }
115 }
116}
117
118#[derive(Serialize, Deserialize, Debug, Clone, Default)]
119pub struct CalculatedAttributes {
120 pub days_since_install: Option<i32>,
121 pub days_since_update: Option<i32>,
122 pub language: Option<String>,
123 pub region: Option<String>,
124}
125
126#[allow(deprecated)] pub fn get_calculated_attributes(
128 installation_date: Option<i64>,
129 db_path: String,
130 locale: String,
131) -> Result<CalculatedAttributes> {
132 let mut days_since_update: Option<i32> = None;
133 let now = Utc::now();
134 let days_since_install: Option<i32> = installation_date.map(|installation_date| {
135 let installation_date = DateTime::<Utc>::from_naive_utc_and_offset(
136 NaiveDateTime::from_timestamp_opt(installation_date / 1_000, 0).unwrap(),
137 Utc,
138 );
139 (now - installation_date).num_days() as i32
140 });
141 match Database::open_single(db_path, StoreId::Meta) {
142 Ok(single_store) => match single_store.read() {
143 Ok(reader) => {
144 let update_date: DateTime<Utc> = single_store
145 .get(&reader, DB_KEY_UPDATE_DATE)?
146 .unwrap_or_else(Utc::now);
147 days_since_update = Some((now - update_date).num_days() as i32);
148 }
149 Err(e) => {
150 warn!("{}", e);
151 }
152 },
153 Err(e) => {
154 warn!("{}", e);
155 }
156 }
157
158 let (language, region) = split_locale(locale);
159
160 Ok(CalculatedAttributes {
161 days_since_install,
162 days_since_update,
163 language,
164 region,
165 })
166}