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};
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)]
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();
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(dt.offset_seconds)
163                    .ymd_opt(dt.year, dt.month, dt.day)
164                    .and_hms_nano_opt(dt.hour, dt.minute, dt.second, dt.nanosecond);
165
166                if let Some(dt) = datetime_obj.single() {
167                    dt
168                } else {
169                    record_error(
170                        glean,
171                        &self.meta,
172                        ErrorType::InvalidValue,
173                        "Invalid input data. Not recording.",
174                        None,
175                    );
176                    return;
177                }
178            }
179        };
180
181        self.set_sync_chrono(glean, value);
182    }
183
184    pub(crate) fn set_sync_chrono(&self, glean: &Glean, value: ChronoDatetime) {
185        let value = Metric::Datetime(value, self.time_unit);
186        glean.storage().record(glean, &self.meta, &value)
187    }
188
189    /// Gets the stored datetime value.
190    #[doc(hidden)]
191    pub fn get_value<'a, S: Into<Option<&'a str>>>(
192        &self,
193        glean: &Glean,
194        ping_name: S,
195    ) -> Option<ChronoDatetime> {
196        let (d, tu) = self.get_value_inner(glean, ping_name.into())?;
197
198        // The string version of the test function truncates using string
199        // parsing. Unfortunately `parse_from_str` errors with `NotEnough` if we
200        // try to truncate with `get_iso_time_string` and then parse it back
201        // in a `Datetime`. So we need to truncate manually.
202        let time = d.time();
203        match tu {
204            TimeUnit::Nanosecond => d.date().and_hms_nano_opt(
205                time.hour(),
206                time.minute(),
207                time.second(),
208                time.nanosecond(),
209            ),
210            TimeUnit::Microsecond => {
211                eprintln!(
212                    "microseconds. nanoseconds={}, nanoseconds/1000={}",
213                    time.nanosecond(),
214                    time.nanosecond() / 1000
215                );
216                d.date().and_hms_nano_opt(
217                    time.hour(),
218                    time.minute(),
219                    time.second(),
220                    time.nanosecond() / 1000,
221                )
222            }
223            TimeUnit::Millisecond => d.date().and_hms_nano_opt(
224                time.hour(),
225                time.minute(),
226                time.second(),
227                time.nanosecond() / 1000000,
228            ),
229            TimeUnit::Second => {
230                d.date()
231                    .and_hms_nano_opt(time.hour(), time.minute(), time.second(), 0)
232            }
233            TimeUnit::Minute => d.date().and_hms_nano_opt(time.hour(), time.minute(), 0, 0),
234            TimeUnit::Hour => d.date().and_hms_nano_opt(time.hour(), 0, 0, 0),
235            TimeUnit::Day => d.date().and_hms_nano_opt(0, 0, 0, 0),
236        }
237    }
238
239    fn get_value_inner(
240        &self,
241        glean: &Glean,
242        ping_name: Option<&str>,
243    ) -> Option<(ChronoDatetime, TimeUnit)> {
244        let queried_ping_name = ping_name.unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
245
246        match StorageManager.snapshot_metric(
247            glean.storage(),
248            queried_ping_name,
249            &self.meta.identifier(glean),
250            self.meta.inner.lifetime,
251        ) {
252            Some(Metric::Datetime(d, tu)) => Some((d, tu)),
253            _ => None,
254        }
255    }
256
257    /// **Test-only API (exported for FFI purposes).**
258    ///
259    /// Gets the stored datetime value.
260    ///
261    /// The precision of this value is truncated to the `time_unit` precision.
262    ///
263    /// # Arguments
264    ///
265    /// * `ping_name` - the optional name of the ping to retrieve the metric
266    ///                 for. Defaults to the first value in `send_in_pings`.
267    ///
268    /// # Returns
269    ///
270    /// The stored value or `None` if nothing stored.
271    pub fn test_get_value(&self, ping_name: Option<String>) -> Option<Datetime> {
272        crate::block_on_dispatcher();
273        crate::core::with_glean(|glean| {
274            let dt = self.get_value(glean, ping_name.as_deref());
275            dt.map(Datetime::from)
276        })
277    }
278
279    /// **Test-only API (exported for FFI purposes).**
280    ///
281    /// Gets the stored datetime value, formatted as an ISO8601 string.
282    ///
283    /// The precision of this value is truncated to the `time_unit` precision.
284    ///
285    /// # Arguments
286    ///
287    /// * `ping_name` - the optional name of the ping to retrieve the metric
288    ///                 for. Defaults to the first value in `send_in_pings`.
289    ///
290    /// # Returns
291    ///
292    /// The stored value or `None` if nothing stored.
293    pub fn test_get_value_as_string(&self, ping_name: Option<String>) -> Option<String> {
294        crate::block_on_dispatcher();
295        crate::core::with_glean(|glean| self.get_value_as_string(glean, ping_name))
296    }
297
298    /// **Test-only API**
299    ///
300    /// Gets the stored datetime value, formatted as an ISO8601 string.
301    #[doc(hidden)]
302    pub fn get_value_as_string(&self, glean: &Glean, ping_name: Option<String>) -> Option<String> {
303        let value = self.get_value_inner(glean, ping_name.as_deref());
304        value.map(|(dt, tu)| get_iso_time_string(dt, tu))
305    }
306
307    /// **Exported for test purposes.**
308    ///
309    /// Gets the number of recorded errors for the given metric and error type.
310    ///
311    /// # Arguments
312    ///
313    /// * `error` - The type of error
314    ///
315    /// # Returns
316    ///
317    /// The number of errors reported.
318    pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
319        crate::block_on_dispatcher();
320
321        crate::core::with_glean(|glean| {
322            test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
323        })
324    }
325}