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