// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::fmt; use std::sync::Arc; use crate::common_metric_data::CommonMetricDataInternal; use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; use crate::metrics::time_unit::TimeUnit; use crate::metrics::Metric; use crate::metrics::MetricType; use crate::storage::StorageManager; use crate::util::{get_iso_time_string, local_now_with_offset}; use crate::CommonMetricData; use crate::Glean; use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike}; /// A datetime type. /// /// Used to feed data to the `DatetimeMetric`. pub type ChronoDatetime = DateTime; /// Representation of a date, time and timezone. #[derive(Clone, PartialEq, Eq)] pub struct Datetime { /// The year, e.g. 2021. pub year: i32, /// The month, 1=January. pub month: u32, /// The day of the month. pub day: u32, /// The hour. 0-23 pub hour: u32, /// The minute. 0-59. pub minute: u32, /// The second. 0-60. pub second: u32, /// The nanosecond part of the time. pub nanosecond: u32, /// The timezone offset from UTC in seconds. /// Negative for west, positive for east of UTC. pub offset_seconds: i32, } impl fmt::Debug for Datetime { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "Datetime({:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}{}{:02}{:02})", self.year, self.month, self.day, self.hour, self.minute, self.second, self.nanosecond, if self.offset_seconds < 0 { "-" } else { "+" }, self.offset_seconds / 3600, // hour part (self.offset_seconds % 3600) / 60, // minute part ) } } impl Default for Datetime { fn default() -> Self { Datetime { year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0, nanosecond: 0, offset_seconds: 0, } } } /// A datetime metric. /// /// Used to record an absolute date and time, such as the time the user first ran /// the application. #[derive(Clone, Debug)] pub struct DatetimeMetric { meta: Arc, time_unit: TimeUnit, } impl MetricType for DatetimeMetric { fn meta(&self) -> &CommonMetricDataInternal { &self.meta } } impl From for Datetime { fn from(dt: ChronoDatetime) -> Self { let date = dt.date(); let time = dt.time(); let tz = dt.timezone(); Self { year: date.year(), month: date.month(), day: date.day(), hour: time.hour(), minute: time.minute(), second: time.second(), nanosecond: time.nanosecond(), offset_seconds: tz.local_minus_utc(), } } } // IMPORTANT: // // When changing this implementation, make sure all the operations are // also declared in the related trait in `../traits/`. impl DatetimeMetric { /// Creates a new datetime metric. pub fn new(meta: CommonMetricData, time_unit: TimeUnit) -> Self { Self { meta: Arc::new(meta.into()), time_unit, } } /// Sets the metric to a date/time including the timezone offset. /// /// # Arguments /// /// * `dt` - the optinal datetime to set this to. If missing the current date is used. pub fn set(&self, dt: Option) { let metric = self.clone(); crate::launch_with_glean(move |glean| { metric.set_sync(glean, dt); }) } /// Sets the metric to a date/time which including the timezone offset synchronously. /// /// Use [`set`](Self::set) instead. #[doc(hidden)] pub fn set_sync(&self, glean: &Glean, value: Option) { if !self.should_record(glean) { return; } let value = match value { None => local_now_with_offset(), Some(dt) => { let timezone_offset = FixedOffset::east_opt(dt.offset_seconds); if timezone_offset.is_none() { let msg = format!( "Invalid timezone offset {}. Not recording.", dt.offset_seconds ); record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None); return; }; let datetime_obj = FixedOffset::east(dt.offset_seconds) .ymd_opt(dt.year, dt.month, dt.day) .and_hms_nano_opt(dt.hour, dt.minute, dt.second, dt.nanosecond); if let Some(dt) = datetime_obj.single() { dt } else { record_error( glean, &self.meta, ErrorType::InvalidValue, "Invalid input data. Not recording.", None, ); return; } } }; self.set_sync_chrono(glean, value); } pub(crate) fn set_sync_chrono(&self, glean: &Glean, value: ChronoDatetime) { let value = Metric::Datetime(value, self.time_unit); glean.storage().record(glean, &self.meta, &value) } /// Gets the stored datetime value. #[doc(hidden)] pub fn get_value<'a, S: Into>>( &self, glean: &Glean, ping_name: S, ) -> Option { let (d, tu) = self.get_value_inner(glean, ping_name.into())?; // The string version of the test function truncates using string // parsing. Unfortunately `parse_from_str` errors with `NotEnough` if we // try to truncate with `get_iso_time_string` and then parse it back // in a `Datetime`. So we need to truncate manually. let time = d.time(); match tu { TimeUnit::Nanosecond => d.date().and_hms_nano_opt( time.hour(), time.minute(), time.second(), time.nanosecond(), ), TimeUnit::Microsecond => { eprintln!( "microseconds. nanoseconds={}, nanoseconds/1000={}", time.nanosecond(), time.nanosecond() / 1000 ); d.date().and_hms_nano_opt( time.hour(), time.minute(), time.second(), time.nanosecond() / 1000, ) } TimeUnit::Millisecond => d.date().and_hms_nano_opt( time.hour(), time.minute(), time.second(), time.nanosecond() / 1000000, ), TimeUnit::Second => { d.date() .and_hms_nano_opt(time.hour(), time.minute(), time.second(), 0) } TimeUnit::Minute => d.date().and_hms_nano_opt(time.hour(), time.minute(), 0, 0), TimeUnit::Hour => d.date().and_hms_nano_opt(time.hour(), 0, 0, 0), TimeUnit::Day => d.date().and_hms_nano_opt(0, 0, 0, 0), } } fn get_value_inner( &self, glean: &Glean, ping_name: Option<&str>, ) -> Option<(ChronoDatetime, TimeUnit)> { let queried_ping_name = ping_name.unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); match StorageManager.snapshot_metric( glean.storage(), queried_ping_name, &self.meta.identifier(glean), self.meta.inner.lifetime, ) { Some(Metric::Datetime(d, tu)) => Some((d, tu)), _ => None, } } /// **Test-only API (exported for FFI purposes).** /// /// Gets the stored datetime value. /// /// The precision of this value is truncated to the `time_unit` precision. /// /// # Arguments /// /// * `ping_name` - the optional name of the ping to retrieve the metric /// for. Defaults to the first value in `send_in_pings`. /// /// # Returns /// /// The stored value or `None` if nothing stored. pub fn test_get_value(&self, ping_name: Option) -> Option { crate::block_on_dispatcher(); crate::core::with_glean(|glean| { let dt = self.get_value(glean, ping_name.as_deref()); dt.map(Datetime::from) }) } /// **Test-only API (exported for FFI purposes).** /// /// Gets the stored datetime value, formatted as an ISO8601 string. /// /// The precision of this value is truncated to the `time_unit` precision. /// /// # Arguments /// /// * `ping_name` - the optional name of the ping to retrieve the metric /// for. Defaults to the first value in `send_in_pings`. /// /// # Returns /// /// The stored value or `None` if nothing stored. pub fn test_get_value_as_string(&self, ping_name: Option) -> Option { crate::block_on_dispatcher(); crate::core::with_glean(|glean| self.get_value_as_string(glean, ping_name)) } /// **Test-only API** /// /// Gets the stored datetime value, formatted as an ISO8601 string. #[doc(hidden)] pub fn get_value_as_string(&self, glean: &Glean, ping_name: Option) -> Option { let value = self.get_value_inner(glean, ping_name.as_deref()); value.map(|(dt, tu)| get_iso_time_string(dt, tu)) } /// **Exported for test purposes.** /// /// Gets the number of recorded errors for the given metric and error type. /// /// # Arguments /// /// * `error` - The type of error /// /// # Returns /// /// The number of errors reported. pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 { crate::block_on_dispatcher(); crate::core::with_glean(|glean| { test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0) }) } }