nimbus/stateful/
evaluator.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use 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)] // Bug 1960256 - use of deprecated chrono functions.
127pub 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}