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