зеркало из https://github.com/mozilla/glean.git
[UniFFI] Migrate datetime metric type implementation
This commit is contained in:
Родитель
807bd7c24a
Коммит
a807650951
|
@ -13,6 +13,11 @@ package mozilla.telemetry.glean.private
|
|||
*/
|
||||
typealias CommonMetricData = mozilla.telemetry.glean.internal.CommonMetricData
|
||||
|
||||
/**
|
||||
* Representation of a date, time and timezone.
|
||||
*/
|
||||
typealias Datetime = mozilla.telemetry.glean.internal.Datetime
|
||||
|
||||
/**
|
||||
* Enumeration of the different kinds of histograms supported by metrics based on histograms.
|
||||
*/
|
||||
|
|
|
@ -5,17 +5,10 @@
|
|||
package mozilla.telemetry.glean.private
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.sun.jna.StringArray
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit as AndroidTimeUnit
|
||||
import mozilla.telemetry.glean.Dispatchers
|
||||
import mozilla.telemetry.glean.rust.LibGleanFFI
|
||||
import mozilla.telemetry.glean.rust.getAndConsumeRustString
|
||||
import mozilla.telemetry.glean.rust.toBoolean
|
||||
import mozilla.telemetry.glean.rust.toByte
|
||||
import mozilla.telemetry.glean.testing.ErrorType
|
||||
import mozilla.telemetry.glean.utils.parseISOTimeString
|
||||
import mozilla.telemetry.glean.internal.DatetimeMetric
|
||||
|
||||
/**
|
||||
* This implements the developer facing API for recording datetime metrics.
|
||||
|
@ -23,73 +16,20 @@ import mozilla.telemetry.glean.utils.parseISOTimeString
|
|||
* Instances of this class type are automatically generated by the parsers at build time,
|
||||
* allowing developers to record values that were previously registered in the metrics.yaml file.
|
||||
*/
|
||||
class DatetimeMetricType internal constructor(
|
||||
private var handle: Long,
|
||||
private var disabled: Boolean,
|
||||
private val sendInPings: List<String>
|
||||
) {
|
||||
/**
|
||||
* The public constructor used by automatically generated metrics.
|
||||
*/
|
||||
constructor(
|
||||
disabled: Boolean,
|
||||
category: String,
|
||||
lifetime: Lifetime,
|
||||
name: String,
|
||||
sendInPings: List<String>,
|
||||
timeUnit: TimeUnit = TimeUnit.Minute
|
||||
) : this(handle = 0, disabled = disabled, sendInPings = sendInPings) {
|
||||
val ffiPingsList = StringArray(sendInPings.toTypedArray(), "utf-8")
|
||||
this.handle = LibGleanFFI.INSTANCE.glean_new_datetime_metric(
|
||||
category = category,
|
||||
name = name,
|
||||
send_in_pings = ffiPingsList,
|
||||
send_in_pings_len = sendInPings.size,
|
||||
lifetime = lifetime.ordinal,
|
||||
disabled = disabled.toByte(),
|
||||
time_unit = timeUnit.ordinal
|
||||
)
|
||||
}
|
||||
class DatetimeMetricType(meta: CommonMetricData, timeUnit: TimeUnit = TimeUnit.MINUTE) {
|
||||
val inner = DatetimeMetric(meta, timeUnit)
|
||||
|
||||
/**
|
||||
* Set a datetime value, truncating it to the metric's resolution.
|
||||
*
|
||||
* @param value The [Date] value to set. If not provided, will record the current time.
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun set(value: Date = Date()) {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.time = value
|
||||
set(cal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly set a value synchronously.
|
||||
*
|
||||
* This is only to be used for the glean-ac to glean-core data migration.
|
||||
*
|
||||
* @param cal The [Calendar] value to set.
|
||||
*/
|
||||
internal fun setSync(cal: Calendar) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
LibGleanFFI.INSTANCE.glean_datetime_set(
|
||||
this@DatetimeMetricType.handle,
|
||||
year = cal.get(Calendar.YEAR),
|
||||
month = cal.get(Calendar.MONTH) + 1,
|
||||
day = cal.get(Calendar.DAY_OF_MONTH),
|
||||
hour = cal.get(Calendar.HOUR_OF_DAY),
|
||||
minute = cal.get(Calendar.MINUTE),
|
||||
second = cal.get(Calendar.SECOND),
|
||||
nano = AndroidTimeUnit.MILLISECONDS.toNanos(cal.get(Calendar.MILLISECOND).toLong()),
|
||||
offset_seconds = AndroidTimeUnit.MILLISECONDS.toSeconds(
|
||||
cal.get(Calendar.ZONE_OFFSET).toLong() + cal.get(Calendar.DST_OFFSET)
|
||||
).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a datetime value, truncating it to the metric's resolution.
|
||||
*
|
||||
|
@ -99,109 +39,48 @@ class DatetimeMetricType internal constructor(
|
|||
*
|
||||
* @param value The [Calendar] value to set. If not provided, will record the current time.
|
||||
*/
|
||||
internal fun set(value: Calendar) {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
Dispatchers.API.launch {
|
||||
LibGleanFFI.INSTANCE.glean_datetime_set(
|
||||
this@DatetimeMetricType.handle,
|
||||
year = value.get(Calendar.YEAR),
|
||||
month = value.get(Calendar.MONTH) + 1,
|
||||
day = value.get(Calendar.DAY_OF_MONTH),
|
||||
hour = value.get(Calendar.HOUR_OF_DAY),
|
||||
minute = value.get(Calendar.MINUTE),
|
||||
second = value.get(Calendar.SECOND),
|
||||
nano = AndroidTimeUnit.MILLISECONDS.toNanos(value.get(Calendar.MILLISECOND).toLong()),
|
||||
offset_seconds = AndroidTimeUnit.MILLISECONDS.toSeconds(
|
||||
value.get(Calendar.ZONE_OFFSET).toLong() + value.get(Calendar.DST_OFFSET)
|
||||
).toInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether a value is stored for the metric for testing purposes only. This function will
|
||||
* attempt to await the last task (if any) writing to the the metric's storage engine before
|
||||
* returning a value.
|
||||
*
|
||||
* @param pingName represents the name of the ping to retrieve the metric for.
|
||||
* Defaults to the first value in `sendInPings`.
|
||||
* @return true if metric value exists, otherwise false
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
@JvmOverloads
|
||||
fun testHasValue(pingName: String = sendInPings.first()): Boolean {
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
Dispatchers.API.assertInTestingMode()
|
||||
|
||||
return LibGleanFFI
|
||||
.INSTANCE.glean_datetime_test_has_value(this.handle, pingName)
|
||||
.toBoolean()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string representation of the stored value for testing purposes only. This
|
||||
* function will attempt to await the last task (if any) writing to the the metric's storage
|
||||
* engine before returning a value.
|
||||
*
|
||||
* @param pingName represents the name of the ping to retrieve the metric for.
|
||||
* Defaults to the first value in `sendInPings`.
|
||||
* @return value of the stored metric
|
||||
* @throws [NullPointerException] if no value is stored
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
@JvmOverloads
|
||||
fun testGetValueAsString(pingName: String = sendInPings.first()): String {
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
Dispatchers.API.assertInTestingMode()
|
||||
|
||||
if (!testHasValue(pingName)) {
|
||||
throw NullPointerException("Metric has no value")
|
||||
}
|
||||
val ptr = LibGleanFFI
|
||||
.INSTANCE
|
||||
.glean_datetime_test_get_value_as_string(this.handle, pingName)!!
|
||||
return ptr.getAndConsumeRustString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored value for testing purposes only. This function will attempt to await the
|
||||
* last task (if any) writing to the the metric's storage engine before returning a value.
|
||||
*
|
||||
* [Date] objects are always in the user's local timezone offset. If you
|
||||
* care about checking that the timezone offset was set and sent correctly, use
|
||||
* [testGetValueAsString] and inspect the offset.
|
||||
*
|
||||
* @param pingName represents the name of the ping to retrieve the metric for.
|
||||
* Defaults to the first value in `sendInPings`.
|
||||
* @return value of the stored metric
|
||||
* @throws [NullPointerException] if no value is stored
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
@JvmOverloads
|
||||
fun testGetValue(pingName: String = sendInPings.first()): Date {
|
||||
return parseISOTimeString(testGetValueAsString(pingName))!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of errors recorded for the given metric.
|
||||
*
|
||||
* @param errorType The type of the error recorded.
|
||||
* @param pingName represents the name of the ping to retrieve the metric for.
|
||||
* Defaults to the first value in `sendInPings`.
|
||||
* @return the number of errors recorded for the metric.
|
||||
*/
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
@JvmOverloads
|
||||
fun testGetNumRecordedErrors(errorType: ErrorType, pingName: String = sendInPings.first()): Int {
|
||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||
Dispatchers.API.assertInTestingMode()
|
||||
|
||||
return LibGleanFFI.INSTANCE.glean_datetime_test_get_num_recorded_errors(
|
||||
this.handle, errorType.ordinal, pingName
|
||||
internal fun set(cal: Calendar) {
|
||||
val dt = Datetime(
|
||||
year = cal.get(Calendar.YEAR),
|
||||
month = (cal.get(Calendar.MONTH) + 1).toUInt(),
|
||||
day = cal.get(Calendar.DAY_OF_MONTH).toUInt(),
|
||||
hour = cal.get(Calendar.HOUR_OF_DAY).toUInt(),
|
||||
minute = cal.get(Calendar.MINUTE).toUInt(),
|
||||
second = cal.get(Calendar.SECOND).toUInt(),
|
||||
nanosecond = AndroidTimeUnit.MILLISECONDS.toNanos(cal.get(Calendar.MILLISECOND).toLong()).toUInt(),
|
||||
offsetSeconds = AndroidTimeUnit.MILLISECONDS.toSeconds(
|
||||
cal.get(Calendar.ZONE_OFFSET).toLong() + cal.get(Calendar.DST_OFFSET)
|
||||
).toInt()
|
||||
)
|
||||
inner.set(dt)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
@JvmOverloads
|
||||
fun testGetValue(pingName: String? = null): Date? {
|
||||
return inner.testGetValue(pingName)?.let { dt ->
|
||||
val cal = Calendar.getInstance()
|
||||
cal.set(Calendar.ZONE_OFFSET, AndroidTimeUnit.SECONDS.toMillis(dt.offsetSeconds.toLong()).toInt())
|
||||
cal.set(Calendar.YEAR, dt.year.toInt())
|
||||
cal.set(Calendar.MONTH, dt.month.toInt()-1) // java.util.calendar's month is 0-based for months
|
||||
cal.set(Calendar.DAY_OF_MONTH, dt.day.toInt())
|
||||
cal.set(Calendar.HOUR_OF_DAY, dt.hour.toInt())
|
||||
cal.set(Calendar.MINUTE, dt.minute.toInt())
|
||||
cal.set(Calendar.SECOND, dt.second.toInt())
|
||||
cal.set(Calendar.MILLISECOND, AndroidTimeUnit.NANOSECONDS.toMillis(dt.nanosecond.toLong()).toInt())
|
||||
cal.getTime()
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
@JvmOverloads
|
||||
fun testGetValueAsString(pingName: String? = null): String? {
|
||||
return inner.testGetValueAsString(pingName)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
@JvmOverloads
|
||||
fun testHasValue(pingName: String? = null): Boolean {
|
||||
return inner.testGetValue(pingName) != null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ package mozilla.telemetry.glean.private
|
|||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import mozilla.telemetry.glean.testing.ErrorType
|
||||
import mozilla.telemetry.glean.testing.GleanTestRule
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
|
@ -22,6 +23,7 @@ import org.junit.runner.RunWith
|
|||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
import java.util.concurrent.TimeUnit as AndroidTimeUnit
|
||||
|
||||
const val MILLIS_PER_SEC = 1000L
|
||||
private fun Date.asSeconds() = time / MILLIS_PER_SEC
|
||||
|
@ -35,13 +37,13 @@ class DatetimeMetricTypeTest {
|
|||
@Test
|
||||
fun `The API saves to its storage engine`() {
|
||||
// Define a 'datetimeMetric' datetime metric, which will be stored in "store1"
|
||||
val datetimeMetric = DatetimeMetricType(
|
||||
val datetimeMetric = DatetimeMetricType(CommonMetricData(
|
||||
disabled = false,
|
||||
category = "telemetry",
|
||||
lifetime = Lifetime.Application,
|
||||
lifetime = Lifetime.APPLICATION,
|
||||
name = "datetime_metric",
|
||||
sendInPings = listOf("store1")
|
||||
)
|
||||
))
|
||||
|
||||
val value = Calendar.getInstance()
|
||||
value.set(2004, 11, 9, 8, 3, 29)
|
||||
|
@ -68,27 +70,26 @@ class DatetimeMetricTypeTest {
|
|||
assertEquals("1969-08-20T20:17-12:00", datetimeMetric.testGetValueAsString())
|
||||
|
||||
// A date following 2038 (the extent of signed 32-bits after UNIX epoch)
|
||||
// This fails on some workers on Taskcluster. 32-bit platforms, perhaps?
|
||||
|
||||
// val value4 = Calendar.getInstance()
|
||||
// value4.set(2039, 7, 20, 20, 17, 3)
|
||||
// datetimeMetric.set(value4)
|
||||
// // Check that data was properly recorded.
|
||||
// assertTrue(datetimeMetric.testHasValue())
|
||||
// assertEquals("2039-08-20T20:17:03-04:00", datetimeMetric.testGetValueAsString())
|
||||
val value4 = Calendar.getInstance()
|
||||
value4.set(2039, 7, 20, 20, 17, 3)
|
||||
value4.timeZone = TimeZone.getTimeZone("GMT-4")
|
||||
datetimeMetric.set(value4)
|
||||
// Check that data was properly recorded.
|
||||
assertTrue(datetimeMetric.testHasValue())
|
||||
assertEquals("2039-08-20T20:17-04:00", datetimeMetric.testGetValueAsString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `disabled datetimes must not record data`() {
|
||||
// Define a 'datetimeMetric' datetime metric, which will be stored in "store1". It's disabled
|
||||
// so it should not record anything.
|
||||
val datetimeMetric = DatetimeMetricType(
|
||||
val datetimeMetric = DatetimeMetricType(CommonMetricData(
|
||||
disabled = true,
|
||||
category = "telemetry",
|
||||
lifetime = Lifetime.Application,
|
||||
lifetime = Lifetime.APPLICATION,
|
||||
name = "datetimeMetric",
|
||||
sendInPings = listOf("store1")
|
||||
)
|
||||
))
|
||||
|
||||
// Attempt to store the datetime.
|
||||
datetimeMetric.set()
|
||||
|
@ -100,14 +101,13 @@ class DatetimeMetricTypeTest {
|
|||
// This test is adopted from `SyncTelemetryTest.kt` in android-components.
|
||||
// Previously we failed to properly deal with DST when converting from `Calendar` into its pieces.
|
||||
|
||||
val datetimeMetric = DatetimeMetricType(
|
||||
val datetimeMetric = DatetimeMetricType(CommonMetricData(
|
||||
disabled = false,
|
||||
category = "telemetry",
|
||||
lifetime = Lifetime.Ping,
|
||||
lifetime = Lifetime.PING,
|
||||
name = "datetimeMetric",
|
||||
sendInPings = listOf("store1"),
|
||||
timeUnit = TimeUnit.Millisecond
|
||||
)
|
||||
), timeUnit = TimeUnit.MILLISECOND)
|
||||
|
||||
val nowDate = Date()
|
||||
val now = nowDate.asSeconds()
|
||||
|
@ -115,6 +115,6 @@ class DatetimeMetricTypeTest {
|
|||
|
||||
datetimeMetric.set(timestamp)
|
||||
|
||||
assertEquals(now, datetimeMetric.testGetValue().asSeconds())
|
||||
assertEquals(now, datetimeMetric.testGetValue()!!.asSeconds())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -309,8 +309,8 @@ impl Glean {
|
|||
.get_value(self, "glean_client_info")
|
||||
.is_none()
|
||||
{
|
||||
self.core_metrics.first_run_date.set(self, None);
|
||||
self.core_metrics.first_run_hour.set(self, None);
|
||||
self.core_metrics.first_run_date.set_sync(self, None);
|
||||
self.core_metrics.first_run_hour.set_sync(self, None);
|
||||
// The `first_run_date` field is generated on the very first run
|
||||
// and persisted across upload toggling. We can assume that, the only
|
||||
// time it is set, that's indeed our "first run".
|
||||
|
@ -483,14 +483,14 @@ impl Glean {
|
|||
if let Some(existing_first_run_date) = existing_first_run_date {
|
||||
self.core_metrics
|
||||
.first_run_date
|
||||
.set(self, Some(existing_first_run_date));
|
||||
.set_sync(self, Some(existing_first_run_date));
|
||||
}
|
||||
|
||||
// Restore the first_run_hour.
|
||||
if let Some(existing_first_run_hour) = existing_first_run_hour {
|
||||
self.core_metrics
|
||||
.first_run_hour
|
||||
.set(self, Some(existing_first_run_hour));
|
||||
.set_sync(self, Some(existing_first_run_hour));
|
||||
}
|
||||
|
||||
self.upload_enabled = false;
|
||||
|
|
|
@ -337,3 +337,27 @@ interface CustomDistributionMetric {
|
|||
|
||||
i32 test_get_num_recorded_errors(ErrorType error, optional string? ping_name = null);
|
||||
};
|
||||
|
||||
// Representation of a date, time and timezone.
|
||||
dictionary Datetime {
|
||||
i32 year;
|
||||
u32 month;
|
||||
u32 day;
|
||||
u32 hour;
|
||||
u32 minute;
|
||||
u32 second;
|
||||
u32 nanosecond;
|
||||
i32 offset_seconds;
|
||||
};
|
||||
|
||||
interface DatetimeMetric {
|
||||
constructor(CommonMetricData meta, TimeUnit time_unit);
|
||||
|
||||
void set(optional Datetime? value = null);
|
||||
|
||||
Datetime? test_get_value(optional string? ping_name = null);
|
||||
|
||||
string? test_get_value_as_string(optional string? ping_name = null);
|
||||
|
||||
i32 test_get_num_recorded_errors(ErrorType error, optional string? ping_name = null);
|
||||
};
|
||||
|
|
|
@ -51,10 +51,10 @@ pub use crate::error_recording::{test_get_num_recorded_errors, ErrorType};
|
|||
use crate::histogram::HistogramType;
|
||||
pub use crate::metrics::labeled::{LabeledBoolean, LabeledCounter, LabeledString};
|
||||
pub use crate::metrics::{
|
||||
BooleanMetric, CounterMetric, DistributionData, MemoryDistributionMetric, MemoryUnit, PingType,
|
||||
QuantityMetric, RecordedExperiment, StringListMetric, StringMetric, TimeUnit, TimespanMetric,
|
||||
UrlMetric, UuidMetric,
|
||||
CustomDistributionMetric,
|
||||
BooleanMetric, CounterMetric, CustomDistributionMetric, Datetime, DatetimeMetric,
|
||||
DistributionData, MemoryDistributionMetric, MemoryUnit, PingType, QuantityMetric,
|
||||
RecordedExperiment, StringListMetric, StringMetric, TimeUnit, TimespanMetric, UrlMetric,
|
||||
UuidMetric,
|
||||
};
|
||||
pub use crate::upload::{PingRequest, PingUploadTask, UploadResult};
|
||||
|
||||
|
|
|
@ -191,12 +191,12 @@ fn client_id_and_first_run_date_and_first_run_hour_must_be_regenerated() {
|
|||
assert!(glean
|
||||
.core_metrics
|
||||
.first_run_date
|
||||
.test_get_value_as_string(&glean, "glean_client_info")
|
||||
.get_value(&glean, "glean_client_info")
|
||||
.is_none());
|
||||
assert!(glean
|
||||
.core_metrics
|
||||
.first_run_hour
|
||||
.test_get_value_as_string(&glean, "metrics")
|
||||
.get_value(&glean, "metrics")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
|
@ -210,12 +210,12 @@ fn client_id_and_first_run_date_and_first_run_hour_must_be_regenerated() {
|
|||
assert!(glean
|
||||
.core_metrics
|
||||
.first_run_date
|
||||
.test_get_value_as_string(&glean, "glean_client_info")
|
||||
.get_value(&glean, "glean_client_info")
|
||||
.is_some());
|
||||
assert!(glean
|
||||
.core_metrics
|
||||
.first_run_hour
|
||||
.test_get_value_as_string(&glean, "metrics")
|
||||
.get_value(&glean, "metrics")
|
||||
.is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
// 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/.
|
||||
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::error_recording::{record_error, ErrorType};
|
||||
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;
|
||||
|
@ -13,20 +13,41 @@ use crate::util::{get_iso_time_string, local_now_with_offset_and_record};
|
|||
use crate::CommonMetricData;
|
||||
use crate::Glean;
|
||||
|
||||
use chrono::{DateTime, FixedOffset, TimeZone, Timelike};
|
||||
use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike};
|
||||
|
||||
/// A datetime type.
|
||||
///
|
||||
/// Used to feed data to the `DatetimeMetric`.
|
||||
pub type Datetime = DateTime<FixedOffset>;
|
||||
pub type ChronoDatetime = DateTime<FixedOffset>;
|
||||
|
||||
/// Representation of a date, time and timezone.
|
||||
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,
|
||||
}
|
||||
|
||||
/// A datetime metric.
|
||||
///
|
||||
/// Used to record an absolute date and time, such as the time the user first ran
|
||||
/// the application.
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DatetimeMetric {
|
||||
meta: CommonMetricData,
|
||||
meta: Arc<CommonMetricData>,
|
||||
time_unit: TimeUnit,
|
||||
}
|
||||
|
||||
|
@ -36,6 +57,24 @@ impl MetricType for DatetimeMetric {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<ChronoDatetime> 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
|
||||
|
@ -43,72 +82,63 @@ impl MetricType for DatetimeMetric {
|
|||
impl DatetimeMetric {
|
||||
/// Creates a new datetime metric.
|
||||
pub fn new(meta: CommonMetricData, time_unit: TimeUnit) -> Self {
|
||||
Self { meta, time_unit }
|
||||
Self {
|
||||
meta: Arc::new(meta),
|
||||
time_unit,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the metric to a date/time including the timezone offset.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `glean` - the Glean instance this metric belongs to.
|
||||
/// * `year` - the year to set the metric to.
|
||||
/// * `month` - the month to set the metric to (1-12).
|
||||
/// * `day` - the day to set the metric to (1-based).
|
||||
/// * `hour` - the hour to set the metric to.
|
||||
/// * `minute` - the minute to set the metric to.
|
||||
/// * `second` - the second to set the metric to.
|
||||
/// * `nano` - the nanosecond fraction to the last whole second.
|
||||
/// * `offset_seconds` - the timezone difference, in seconds, for the Eastern
|
||||
/// Hemisphere. Negative seconds mean Western Hemisphere.
|
||||
pub fn set_with_details(
|
||||
&self,
|
||||
glean: &Glean,
|
||||
year: i32,
|
||||
month: u32,
|
||||
day: u32,
|
||||
hour: u32,
|
||||
minute: u32,
|
||||
second: u32,
|
||||
nano: u32,
|
||||
offset_seconds: i32,
|
||||
) {
|
||||
if !self.should_record(glean) {
|
||||
return;
|
||||
}
|
||||
/// * `dt` - the optinal datetime to set this to. If missing the current date is used.
|
||||
pub fn set(&self, dt: Option<Datetime>) {
|
||||
let metric = self.clone();
|
||||
crate::launch_with_glean(move |glean| {
|
||||
if !metric.should_record(glean) {
|
||||
return;
|
||||
}
|
||||
|
||||
let timezone_offset = FixedOffset::east_opt(offset_seconds);
|
||||
if timezone_offset.is_none() {
|
||||
let msg = format!("Invalid timezone offset {}. Not recording.", offset_seconds);
|
||||
record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
|
||||
return;
|
||||
};
|
||||
if dt.is_none() {
|
||||
return metric.set_sync(glean, None);
|
||||
}
|
||||
|
||||
let datetime_obj = FixedOffset::east(offset_seconds)
|
||||
.ymd_opt(year, month, day)
|
||||
.and_hms_nano_opt(hour, minute, second, nano);
|
||||
let dt = dt.unwrap();
|
||||
|
||||
match datetime_obj.single() {
|
||||
Some(d) => self.set(glean, Some(d)),
|
||||
_ => {
|
||||
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, &metric.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() {
|
||||
metric.set_sync(glean, Some(dt))
|
||||
} else {
|
||||
record_error(
|
||||
glean,
|
||||
&self.meta,
|
||||
&metric.meta,
|
||||
ErrorType::InvalidValue,
|
||||
"Invalid input data. Not recording.",
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the metric to a date/time which including the timezone offset.
|
||||
/// Sets the metric to a date/time which including the timezone offset synchronously.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `glean` - the Glean instance this metric belongs to.
|
||||
/// * `value` - Some [`DateTime`] value, with offset, to set the metric to.
|
||||
/// If none, the current local time is used.
|
||||
pub fn set(&self, glean: &Glean, value: Option<Datetime>) {
|
||||
/// Use [`set`](Self::set) instead.
|
||||
#[doc(hidden)]
|
||||
pub fn set_sync(&self, glean: &Glean, value: Option<ChronoDatetime>) {
|
||||
if !self.should_record(glean) {
|
||||
return;
|
||||
}
|
||||
|
@ -119,23 +149,69 @@ impl DatetimeMetric {
|
|||
}
|
||||
|
||||
/// Gets the stored datetime value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `glean` - the Glean instance this metric belongs to.
|
||||
/// * `storage_name` - the storage name to look into.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The stored value or `None` if nothing stored.
|
||||
pub(crate) fn get_value(&self, glean: &Glean, storage_name: &str) -> Option<Datetime> {
|
||||
#[doc(hidden)]
|
||||
pub fn get_value<'a, S: Into<Option<&'a str>>>(
|
||||
&self,
|
||||
glean: &Glean,
|
||||
ping_name: S,
|
||||
) -> Option<ChronoDatetime> {
|
||||
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().send_in_pings[0]);
|
||||
|
||||
match StorageManager.snapshot_metric(
|
||||
glean.storage(),
|
||||
storage_name,
|
||||
queried_ping_name,
|
||||
&self.meta.identifier(glean),
|
||||
self.meta.lifetime,
|
||||
) {
|
||||
Some(Metric::Datetime(dt, _)) => Some(dt),
|
||||
Some(Metric::Datetime(d, tu)) => Some((d, tu)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -154,67 +230,61 @@ impl DatetimeMetric {
|
|||
/// # Returns
|
||||
///
|
||||
/// The stored value or `None` if nothing stored.
|
||||
pub fn test_get_value(&self, glean: &Glean, storage_name: &str) -> Option<Datetime> {
|
||||
match StorageManager.snapshot_metric_for_test(
|
||||
glean.storage(),
|
||||
storage_name,
|
||||
&self.meta.identifier(glean),
|
||||
self.meta.lifetime,
|
||||
) {
|
||||
Some(Metric::Datetime(d, tu)) => {
|
||||
// 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 => 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),
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
pub fn test_get_value(&self, ping_name: Option<String>) -> Option<Datetime> {
|
||||
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 currently stored value as a String.
|
||||
/// Gets the stored datetime value, formatted as an ISO8601 string.
|
||||
///
|
||||
/// The precision of this value is truncated to the `time_unit` precision.
|
||||
///
|
||||
/// This doesn't clear the stored value.
|
||||
pub fn test_get_value_as_string(&self, glean: &Glean, storage_name: &str) -> Option<String> {
|
||||
match StorageManager.snapshot_metric_for_test(
|
||||
glean.storage(),
|
||||
storage_name,
|
||||
&self.meta.identifier(glean),
|
||||
self.meta.lifetime,
|
||||
) {
|
||||
Some(Metric::Datetime(d, tu)) => Some(get_iso_time_string(d, tu)),
|
||||
_ => None,
|
||||
}
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `glean` - the Glean instance this metric belongs to.
|
||||
/// * `storage_name` - the storage name to look into.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The stored value or `None` if nothing stored.
|
||||
pub fn test_get_value_as_string(&self, ping_name: Option<String>) -> Option<String> {
|
||||
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<String>) -> Option<String> {
|
||||
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
|
||||
/// * `ping_name` - represents the optional name of the ping to retrieve the
|
||||
/// metric for. Defaults to the first value in `send_in_pings`.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The number of errors reported.
|
||||
pub fn test_get_num_recorded_errors(&self, error: ErrorType, ping_name: Option<String>) -> i32 {
|
||||
crate::block_on_dispatcher();
|
||||
|
||||
crate::core::with_glean(|glean| {
|
||||
test_get_num_recorded_errors(glean, self.meta(), error, ping_name.as_deref())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ impl PingMaker {
|
|||
let end_time_data = local_now_with_offset_and_record(glean);
|
||||
|
||||
// Update the start time with the current time.
|
||||
start_time.set(glean, Some(end_time_data));
|
||||
start_time.set_sync(glean, Some(end_time_data));
|
||||
|
||||
// Format the times.
|
||||
let start_time_data = get_iso_time_string(start_time_data, time_unit);
|
||||
|
|
|
@ -54,7 +54,7 @@ impl MetricsPingSubmitter for GleanMetricsPingSubmitter {
|
|||
fn submit_metrics_ping(&self, glean: &Glean, reason: Option<&str>, now: DateTime<FixedOffset>) {
|
||||
glean.submit_ping_by_name("metrics", reason);
|
||||
// Always update the collection date, irrespective of the ping being sent.
|
||||
get_last_sent_time_metric().set(glean, Some(now));
|
||||
get_last_sent_time_metric().set_sync(glean, Some(now));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -351,7 +351,7 @@ mod test {
|
|||
let (glean, _t) = new_glean(None);
|
||||
|
||||
let fake_now = FixedOffset::east(0).ymd(2021, 4, 30).and_hms(14, 36, 14);
|
||||
get_last_sent_time_metric().set(&glean, Some(fake_now));
|
||||
get_last_sent_time_metric().set_sync(&glean, Some(fake_now));
|
||||
|
||||
let (submitter, submitter_count, scheduler, scheduler_count) = new_proxies(
|
||||
|_, reason| panic!("Case #1 shouldn't submit a ping! reason: {:?}", reason),
|
||||
|
@ -372,7 +372,7 @@ mod test {
|
|||
let fake_yesterday = FixedOffset::east(0)
|
||||
.ymd(2021, 4, 29)
|
||||
.and_hms(SCHEDULED_HOUR, 0, 1);
|
||||
get_last_sent_time_metric().set(&glean, Some(fake_yesterday));
|
||||
get_last_sent_time_metric().set_sync(&glean, Some(fake_yesterday));
|
||||
let fake_now = fake_yesterday + Duration::days(1);
|
||||
|
||||
let (submitter, submitter_count, scheduler, scheduler_count) = new_proxies(
|
||||
|
@ -395,7 +395,7 @@ mod test {
|
|||
FixedOffset::east(0)
|
||||
.ymd(2021, 4, 29)
|
||||
.and_hms(SCHEDULED_HOUR - 1, 0, 1);
|
||||
get_last_sent_time_metric().set(&glean, Some(fake_yesterday));
|
||||
get_last_sent_time_metric().set_sync(&glean, Some(fake_yesterday));
|
||||
let fake_now = fake_yesterday + Duration::days(1);
|
||||
|
||||
let (submitter, submitter_count, scheduler, scheduler_count) = new_proxies(
|
||||
|
|
|
@ -42,7 +42,7 @@ fn datetime_serializer_should_correctly_serialize_datetime() {
|
|||
let dt = FixedOffset::east(0)
|
||||
.ymd(1983, 4, 13)
|
||||
.and_hms_milli(12, 9, 14, 274);
|
||||
metric.set(&glean, Some(dt));
|
||||
metric.set_sync(&glean, Some(dt));
|
||||
|
||||
let snapshot = StorageManager
|
||||
.snapshot_as_json(glean.storage(), "store1", true)
|
||||
|
@ -88,13 +88,13 @@ fn set_value_properly_sets_the_value_in_all_stores() {
|
|||
let dt = FixedOffset::east(0)
|
||||
.ymd(1983, 4, 13)
|
||||
.and_hms_nano(12, 9, 14, 1_560_274);
|
||||
metric.set(&glean, Some(dt));
|
||||
metric.set_sync(&glean, Some(dt));
|
||||
|
||||
for store_name in store_names {
|
||||
assert_eq!(
|
||||
"1983-04-13T12:09:14.001560274+00:00",
|
||||
metric
|
||||
.test_get_value_as_string(&glean, &store_name)
|
||||
.get_value_as_string(&glean, Some(store_name))
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
@ -175,11 +175,13 @@ fn test_that_truncation_works() {
|
|||
},
|
||||
t.desired_resolution,
|
||||
);
|
||||
metric.set(&glean, Some(high_res_datetime));
|
||||
metric.set_sync(&glean, Some(high_res_datetime));
|
||||
|
||||
assert_eq!(
|
||||
t.expected_result,
|
||||
metric.test_get_value_as_string(&glean, store_name).unwrap()
|
||||
metric
|
||||
.get_value_as_string(&glean, Some(store_name.into()))
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче