Skip to main content

glean_core/metrics/
datetime.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 std::fmt;
6use std::sync::Arc;
7
8use crate::common_metric_data::CommonMetricDataInternal;
9use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
10use crate::metrics::time_unit::TimeUnit;
11use crate::metrics::Metric;
12use crate::metrics::MetricType;
13use crate::util::{get_iso_time_string, local_now_with_offset};
14use crate::Glean;
15use crate::{CommonMetricData, TestGetValue};
16
17use chrono::{DateTime, Datelike, FixedOffset, NaiveTime, TimeZone, Timelike};
18use malloc_size_of_derive::MallocSizeOf;
19
20/// A datetime type.
21///
22/// Used to feed data to the `DatetimeMetric`.
23pub type ChronoDatetime = DateTime<FixedOffset>;
24
25/// Representation of a date, time and timezone.
26#[derive(Clone, PartialEq, Eq, MallocSizeOf)]
27pub struct Datetime {
28    /// The year, e.g. 2021.
29    pub year: i32,
30    /// The month, 1=January.
31    pub month: u32,
32    /// The day of the month.
33    pub day: u32,
34    /// The hour. 0-23
35    pub hour: u32,
36    /// The minute. 0-59.
37    pub minute: u32,
38    /// The second. 0-60.
39    pub second: u32,
40    /// The nanosecond part of the time.
41    pub nanosecond: u32,
42    /// The timezone offset from UTC in seconds.
43    /// Negative for west, positive for east of UTC.
44    pub offset_seconds: i32,
45}
46
47impl fmt::Debug for Datetime {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        write!(
50            f,
51            "Datetime({:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}{}{:02}{:02})",
52            self.year,
53            self.month,
54            self.day,
55            self.hour,
56            self.minute,
57            self.second,
58            self.nanosecond,
59            if self.offset_seconds < 0 { "-" } else { "+" },
60            self.offset_seconds / 3600,        // hour part
61            (self.offset_seconds % 3600) / 60, // minute part
62        )
63    }
64}
65
66impl Default for Datetime {
67    fn default() -> Self {
68        Datetime {
69            year: 1970,
70            month: 1,
71            day: 1,
72            hour: 0,
73            minute: 0,
74            second: 0,
75            nanosecond: 0,
76            offset_seconds: 0,
77        }
78    }
79}
80
81/// A datetime metric.
82///
83/// Used to record an absolute date and time, such as the time the user first ran
84/// the application.
85#[derive(Clone, Debug)]
86pub struct DatetimeMetric {
87    meta: Arc<CommonMetricDataInternal>,
88    time_unit: TimeUnit,
89}
90
91impl MetricType for DatetimeMetric {
92    fn meta(&self) -> &CommonMetricDataInternal {
93        &self.meta
94    }
95}
96
97impl From<ChronoDatetime> for Datetime {
98    fn from(dt: ChronoDatetime) -> Self {
99        let date = dt.date_naive();
100        let time = dt.time();
101        let tz = dt.timezone();
102        Self {
103            year: date.year(),
104            month: date.month(),
105            day: date.day(),
106            hour: time.hour(),
107            minute: time.minute(),
108            second: time.second(),
109            nanosecond: time.nanosecond(),
110            offset_seconds: tz.local_minus_utc(),
111        }
112    }
113}
114
115// IMPORTANT:
116//
117// When changing this implementation, make sure all the operations are
118// also declared in the related trait in `../traits/`.
119impl DatetimeMetric {
120    /// Creates a new datetime metric.
121    pub fn new(meta: CommonMetricData, time_unit: TimeUnit) -> Self {
122        Self {
123            meta: Arc::new(meta.into()),
124            time_unit,
125        }
126    }
127
128    /// Sets the metric to a date/time including the timezone offset.
129    ///
130    /// # Arguments
131    ///
132    /// * `dt` - the optinal datetime to set this to. If missing the current date is used.
133    pub fn set(&self, dt: Option<Datetime>) {
134        let metric = self.clone();
135        crate::launch_with_glean(move |glean| {
136            metric.set_sync(glean, dt);
137        })
138    }
139
140    /// Sets the metric to a date/time which including the timezone offset synchronously.
141    ///
142    /// Use [`set`](Self::set) instead.
143    #[doc(hidden)]
144    pub fn set_sync(&self, glean: &Glean, value: Option<Datetime>) {
145        if !self.should_record(glean) {
146            return;
147        }
148
149        let value = match value {
150            None => local_now_with_offset(),
151            Some(dt) => {
152                let timezone_offset = FixedOffset::east_opt(dt.offset_seconds);
153                if timezone_offset.is_none() {
154                    let msg = format!(
155                        "Invalid timezone offset {}. Not recording.",
156                        dt.offset_seconds
157                    );
158                    record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
159                    return;
160                };
161
162                let datetime_obj = FixedOffset::east_opt(dt.offset_seconds)
163                    .unwrap()
164                    .with_ymd_and_hms(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
165                    .unwrap()
166                    .with_nanosecond(dt.nanosecond);
167
168                if let Some(dt) = datetime_obj {
169                    dt
170                } else {
171                    record_error(
172                        glean,
173                        &self.meta,
174                        ErrorType::InvalidValue,
175                        "Invalid input data. Not recording.",
176                        None,
177                    );
178                    return;
179                }
180            }
181        };
182
183        self.set_sync_chrono(glean, value);
184    }
185
186    pub(crate) fn set_sync_chrono(&self, glean: &Glean, value: ChronoDatetime) {
187        let value = Metric::Datetime(value, self.time_unit);
188        glean.storage().record(glean, &self.meta, &value)
189    }
190
191    /// Gets the stored datetime value.
192    #[doc(hidden)]
193    pub fn get_value<'a, S: Into<Option<&'a str>>>(
194        &self,
195        glean: &Glean,
196        ping_name: S,
197    ) -> Option<ChronoDatetime> {
198        let (d, tu) = self.get_value_inner(glean, ping_name.into())?;
199
200        let time = d.time();
201        let time = match tu {
202            TimeUnit::Nanosecond => time,
203            TimeUnit::Microsecond => time.with_nanosecond(time.nanosecond() / 1000)?,
204            TimeUnit::Millisecond => time.with_nanosecond(time.nanosecond() / 1000000)?,
205            TimeUnit::Second => time.with_nanosecond(0)?,
206            TimeUnit::Minute => NaiveTime::from_hms_opt(time.hour(), time.minute(), 0)?,
207            TimeUnit::Hour => NaiveTime::from_hms_opt(time.hour(), 0, 0)?,
208            TimeUnit::Day => NaiveTime::MIN,
209        };
210        d.with_time(time).single()
211    }
212
213    fn get_value_inner(
214        &self,
215        glean: &Glean,
216        ping_name: Option<&str>,
217    ) -> Option<(ChronoDatetime, TimeUnit)> {
218        let queried_ping_name = ping_name.unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
219
220        match glean.storage().get_metric(self.meta(), queried_ping_name) {
221            Some(Metric::Datetime(d, tu)) => Some((d, tu)),
222            _ => None,
223        }
224    }
225
226    /// **Test-only API (exported for FFI purposes).**
227    ///
228    /// Gets the stored datetime value, formatted as an ISO8601 string.
229    ///
230    /// The precision of this value is truncated to the `time_unit` precision.
231    ///
232    /// # Arguments
233    ///
234    /// * `ping_name` - the optional name of the ping to retrieve the metric
235    ///                 for. Defaults to the first value in `send_in_pings`.
236    ///
237    /// # Returns
238    ///
239    /// The stored value or `None` if nothing stored.
240    pub fn test_get_value_as_string(&self, ping_name: Option<String>) -> Option<String> {
241        crate::block_on_dispatcher();
242        crate::core::with_glean(|glean| self.get_value_as_string(glean, ping_name))
243    }
244
245    /// **Test-only API**
246    ///
247    /// Gets the stored datetime value, formatted as an ISO8601 string.
248    #[doc(hidden)]
249    pub fn get_value_as_string(&self, glean: &Glean, ping_name: Option<String>) -> Option<String> {
250        let value = self.get_value_inner(glean, ping_name.as_deref());
251        value.map(|(dt, tu)| get_iso_time_string(dt, tu))
252    }
253
254    /// **Exported for test purposes.**
255    ///
256    /// Gets the number of recorded errors for the given metric and error type.
257    ///
258    /// # Arguments
259    ///
260    /// * `error` - The type of error
261    ///
262    /// # Returns
263    ///
264    /// The number of errors reported.
265    pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
266        crate::block_on_dispatcher();
267
268        crate::core::with_glean(|glean| {
269            test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
270        })
271    }
272}
273
274impl TestGetValue for DatetimeMetric {
275    type Output = Datetime;
276
277    /// **Test-only API (exported for FFI purposes).**
278    ///
279    /// Gets the stored datetime value.
280    ///
281    /// The precision of this value is truncated to the `time_unit` precision.
282    ///
283    /// # Arguments
284    ///
285    /// * `ping_name` - the optional name of the ping to retrieve the metric
286    ///                 for. Defaults to the first value in `send_in_pings`.
287    ///
288    /// # Returns
289    ///
290    /// The stored value or `None` if nothing stored.
291    fn test_get_value(&self, ping_name: Option<String>) -> Option<Datetime> {
292        crate::block_on_dispatcher();
293        crate::core::with_glean(|glean| {
294            let dt = self.get_value(glean, ping_name.as_deref());
295            dt.map(Datetime::from)
296        })
297    }
298}