summaryrefslogtreecommitdiffstats
path: root/third_party/rust/glean-core/src/metrics
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /third_party/rust/glean-core/src/metrics
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/glean-core/src/metrics')
-rw-r--r--third_party/rust/glean-core/src/metrics/boolean.rs134
-rw-r--r--third_party/rust/glean-core/src/metrics/counter.rs171
-rw-r--r--third_party/rust/glean-core/src/metrics/custom_distribution.rs222
-rw-r--r--third_party/rust/glean-core/src/metrics/datetime.rs327
-rw-r--r--third_party/rust/glean-core/src/metrics/denominator.rs140
-rw-r--r--third_party/rust/glean-core/src/metrics/event.rs213
-rw-r--r--third_party/rust/glean-core/src/metrics/experiment.rs266
-rw-r--r--third_party/rust/glean-core/src/metrics/labeled.rs294
-rw-r--r--third_party/rust/glean-core/src/metrics/memory_distribution.rs282
-rw-r--r--third_party/rust/glean-core/src/metrics/memory_unit.rs64
-rw-r--r--third_party/rust/glean-core/src/metrics/metrics_enabled_config.rs46
-rw-r--r--third_party/rust/glean-core/src/metrics/mod.rs285
-rw-r--r--third_party/rust/glean-core/src/metrics/numerator.rs94
-rw-r--r--third_party/rust/glean-core/src/metrics/ping.rs210
-rw-r--r--third_party/rust/glean-core/src/metrics/quantity.rs126
-rw-r--r--third_party/rust/glean-core/src/metrics/rate.rs191
-rw-r--r--third_party/rust/glean-core/src/metrics/recorded_experiment.rs35
-rw-r--r--third_party/rust/glean-core/src/metrics/string.rs176
-rw-r--r--third_party/rust/glean-core/src/metrics/string_list.rs199
-rw-r--r--third_party/rust/glean-core/src/metrics/text.rs180
-rw-r--r--third_party/rust/glean-core/src/metrics/time_unit.rs117
-rw-r--r--third_party/rust/glean-core/src/metrics/timespan.rs308
-rw-r--r--third_party/rust/glean-core/src/metrics/timing_distribution.rs557
-rw-r--r--third_party/rust/glean-core/src/metrics/url.rs312
-rw-r--r--third_party/rust/glean-core/src/metrics/uuid.rs159
25 files changed, 5108 insertions, 0 deletions
diff --git a/third_party/rust/glean-core/src/metrics/boolean.rs b/third_party/rust/glean-core/src/metrics/boolean.rs
new file mode 100644
index 0000000000..71ed2372c2
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/boolean.rs
@@ -0,0 +1,134 @@
+// 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::sync::Arc;
+
+use crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{test_get_num_recorded_errors, ErrorType};
+use crate::metrics::Metric;
+use crate::metrics::MetricType;
+use crate::storage::StorageManager;
+use crate::CommonMetricData;
+use crate::Glean;
+
+/// A boolean metric.
+///
+/// Records a simple flag.
+#[derive(Clone, Debug)]
+pub struct BooleanMetric {
+ meta: Arc<CommonMetricDataInternal>,
+}
+
+impl MetricType for BooleanMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+
+ fn with_name(&self, name: String) -> Self {
+ let mut meta = (*self.meta).clone();
+ meta.inner.name = name;
+ Self {
+ meta: Arc::new(meta),
+ }
+ }
+
+ fn with_dynamic_label(&self, label: String) -> Self {
+ let mut meta = (*self.meta).clone();
+ meta.inner.dynamic_label = Some(label);
+ Self {
+ meta: Arc::new(meta),
+ }
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl BooleanMetric {
+ /// Creates a new boolean metric.
+ pub fn new(meta: CommonMetricData) -> Self {
+ Self {
+ meta: Arc::new(meta.into()),
+ }
+ }
+
+ /// Sets to the specified boolean value.
+ ///
+ /// # Arguments
+ ///
+ /// * `glean` - the Glean instance this metric belongs to.
+ /// * `value` - the value to set.
+ #[doc(hidden)]
+ pub fn set_sync(&self, glean: &Glean, value: bool) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let value = Metric::Boolean(value);
+ glean.storage().record(glean, &self.meta, &value)
+ }
+
+ /// Sets to the specified boolean value.
+ ///
+ /// # Arguments
+ ///
+ /// * `glean` - the Glean instance this metric belongs to.
+ /// * `value` - the value to set.
+ pub fn set(&self, value: bool) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.set_sync(glean, value))
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as a boolean.
+ ///
+ /// This doesn't clear the stored value.
+ #[doc(hidden)]
+ pub fn get_value(&self, glean: &Glean, ping_name: Option<&str>) -> Option<bool> {
+ let queried_ping_name = ping_name.unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::Boolean(b)) => Some(b),
+ _ => None,
+ }
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as an integer.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<bool> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ /// **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. inner to the first value in `send_in_pings`.
+ ///
+ /// # 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)
+ })
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/counter.rs b/third_party/rust/glean-core/src/metrics/counter.rs
new file mode 100644
index 0000000000..8f0a01cc3e
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/counter.rs
@@ -0,0 +1,171 @@
+// 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::cmp::Ordering;
+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::Metric;
+use crate::metrics::MetricType;
+use crate::storage::StorageManager;
+use crate::CommonMetricData;
+use crate::Glean;
+
+/// A counter metric.
+///
+/// Used to count things.
+/// The value can only be incremented, not decremented.
+#[derive(Clone, Debug)]
+pub struct CounterMetric {
+ meta: Arc<CommonMetricDataInternal>,
+}
+
+impl MetricType for CounterMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+
+ fn with_name(&self, name: String) -> Self {
+ let mut meta = (*self.meta).clone();
+ meta.inner.name = name;
+ Self {
+ meta: Arc::new(meta),
+ }
+ }
+
+ fn with_dynamic_label(&self, label: String) -> Self {
+ let mut meta = (*self.meta).clone();
+ meta.inner.dynamic_label = Some(label);
+ Self {
+ meta: Arc::new(meta),
+ }
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl CounterMetric {
+ /// Creates a new counter metric.
+ pub fn new(meta: CommonMetricData) -> Self {
+ Self {
+ meta: Arc::new(meta.into()),
+ }
+ }
+
+ /// Increases the counter by `amount` synchronously.
+ #[doc(hidden)]
+ pub fn add_sync(&self, glean: &Glean, amount: i32) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ match amount.cmp(&0) {
+ Ordering::Less => {
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidValue,
+ format!("Added negative value {}", amount),
+ None,
+ );
+ return;
+ }
+ Ordering::Equal => {
+ // Silently ignore.
+ return;
+ }
+ Ordering::Greater => (),
+ };
+
+ // Let's be defensive here:
+ // The uploader tries to store a counter metric,
+ // but in tests that storage might be gone already.
+ // Let's just ignore those.
+ // This should never happen in real app usage.
+ if let Some(storage) = glean.storage_opt() {
+ storage.record_with(glean, &self.meta, |old_value| match old_value {
+ Some(Metric::Counter(old_value)) => {
+ Metric::Counter(old_value.saturating_add(amount))
+ }
+ _ => Metric::Counter(amount),
+ })
+ } else {
+ log::warn!(
+ "Couldn't get storage. Can't record counter '{}'.",
+ self.meta.base_identifier()
+ );
+ }
+ }
+
+ /// Increases the counter by `amount`.
+ ///
+ /// # Arguments
+ ///
+ /// * `glean` - The Glean instance this metric belongs to.
+ /// * `amount` - The amount to increase by. Should be positive.
+ ///
+ /// ## Notes
+ ///
+ /// Logs an error if the `amount` is 0 or negative.
+ pub fn add(&self, amount: i32) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.add_sync(glean, amount))
+ }
+
+ /// Get current value
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<i32> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::Counter(i)) => Some(i),
+ _ => None,
+ }
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as an integer.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<i32> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ /// **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. inner to the first value in `send_in_pings`.
+ ///
+ /// # 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)
+ })
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/custom_distribution.rs b/third_party/rust/glean-core/src/metrics/custom_distribution.rs
new file mode 100644
index 0000000000..929e4863ec
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/custom_distribution.rs
@@ -0,0 +1,222 @@
+// 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::sync::Arc;
+
+use crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
+use crate::histogram::{Bucketing, Histogram, HistogramType};
+use crate::metrics::{DistributionData, Metric, MetricType};
+use crate::storage::StorageManager;
+use crate::CommonMetricData;
+use crate::Glean;
+
+/// A custom distribution metric.
+///
+/// Memory distributions are used to accumulate and store memory sizes.
+#[derive(Clone, Debug)]
+pub struct CustomDistributionMetric {
+ meta: Arc<CommonMetricDataInternal>,
+ range_min: u64,
+ range_max: u64,
+ bucket_count: u64,
+ histogram_type: HistogramType,
+}
+
+/// Create a snapshot of the histogram.
+///
+/// The snapshot can be serialized into the payload format.
+pub(crate) fn snapshot<B: Bucketing>(hist: &Histogram<B>) -> DistributionData {
+ DistributionData {
+ values: hist
+ .snapshot_values()
+ .into_iter()
+ .map(|(k, v)| (k as i64, v as i64))
+ .collect(),
+ sum: hist.sum() as i64,
+ count: hist.count() as i64,
+ }
+}
+
+impl MetricType for CustomDistributionMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl CustomDistributionMetric {
+ /// Creates a new memory distribution metric.
+ pub fn new(
+ meta: CommonMetricData,
+ range_min: i64,
+ range_max: i64,
+ bucket_count: i64,
+ histogram_type: HistogramType,
+ ) -> Self {
+ Self {
+ meta: Arc::new(meta.into()),
+ range_min: range_min as u64,
+ range_max: range_max as u64,
+ bucket_count: bucket_count as u64,
+ histogram_type,
+ }
+ }
+
+ /// Accumulates the provided signed samples in the metric.
+ ///
+ /// This is required so that the platform-specific code can provide us with
+ /// 64 bit signed integers if no `u64` comparable type is available. This
+ /// will take care of filtering and reporting errors for any provided negative
+ /// sample.
+ ///
+ /// # Arguments
+ ///
+ /// - `samples` - The vector holding the samples to be recorded by the metric.
+ ///
+ /// ## Notes
+ ///
+ /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`]
+ /// for each of them.
+ pub fn accumulate_samples(&self, samples: Vec<i64>) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.accumulate_samples_sync(glean, samples))
+ }
+
+ /// Accumulates the provided sample in the metric synchronously.
+ ///
+ /// See [`accumulate_samples`](Self::accumulate_samples) for details.
+ #[doc(hidden)]
+ pub fn accumulate_samples_sync(&self, glean: &Glean, samples: Vec<i64>) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let mut num_negative_samples = 0;
+
+ // Generic accumulation function to handle the different histogram types and count negative
+ // samples.
+ fn accumulate<B: Bucketing, F>(
+ samples: &[i64],
+ mut hist: Histogram<B>,
+ metric: F,
+ ) -> (i32, Metric)
+ where
+ F: Fn(Histogram<B>) -> Metric,
+ {
+ let mut num_negative_samples = 0;
+ for &sample in samples.iter() {
+ if sample < 0 {
+ num_negative_samples += 1;
+ } else {
+ let sample = sample as u64;
+ hist.accumulate(sample);
+ }
+ }
+ (num_negative_samples, metric(hist))
+ }
+
+ glean.storage().record_with(glean, &self.meta, |old_value| {
+ let (num_negative, hist) = match self.histogram_type {
+ HistogramType::Linear => {
+ let hist = if let Some(Metric::CustomDistributionLinear(hist)) = old_value {
+ hist
+ } else {
+ Histogram::linear(
+ self.range_min,
+ self.range_max,
+ self.bucket_count as usize,
+ )
+ };
+ accumulate(&samples, hist, Metric::CustomDistributionLinear)
+ }
+ HistogramType::Exponential => {
+ let hist = if let Some(Metric::CustomDistributionExponential(hist)) = old_value
+ {
+ hist
+ } else {
+ Histogram::exponential(
+ self.range_min,
+ self.range_max,
+ self.bucket_count as usize,
+ )
+ };
+ accumulate(&samples, hist, Metric::CustomDistributionExponential)
+ }
+ };
+
+ num_negative_samples = num_negative;
+ hist
+ });
+
+ if num_negative_samples > 0 {
+ let msg = format!("Accumulated {} negative samples", num_negative_samples);
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidValue,
+ msg,
+ num_negative_samples,
+ );
+ }
+ }
+
+ /// Gets the currently stored histogram.
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<DistributionData> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ // Boxing the value, in order to return either of the possible buckets
+ Some(Metric::CustomDistributionExponential(hist)) => Some(snapshot(&hist)),
+ Some(Metric::CustomDistributionLinear(hist)) => Some(snapshot(&hist)),
+ _ => None,
+ }
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as an integer.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<DistributionData> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ /// **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. inner to the first value in `send_in_pings`.
+ ///
+ /// # 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)
+ })
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/datetime.rs b/third_party/rust/glean-core/src/metrics/datetime.rs
new file mode 100644
index 0000000000..3ef846a32c
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/datetime.rs
@@ -0,0 +1,327 @@
+// 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<FixedOffset>;
+
+/// 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<CommonMetricDataInternal>,
+ time_unit: TimeUnit,
+}
+
+impl MetricType for DatetimeMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+}
+
+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
+// 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<Datetime>) {
+ 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<Datetime>) {
+ 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<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().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
+ ///
+ /// * `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(&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 stored datetime value, formatted as an ISO8601 string.
+ ///
+ /// The precision of this value is truncated to the `time_unit` precision.
+ ///
+ /// # 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. inner to the first value in `send_in_pings`.
+ ///
+ /// # 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)
+ })
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/denominator.rs b/third_party/rust/glean-core/src/metrics/denominator.rs
new file mode 100644
index 0000000000..fb80874924
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/denominator.rs
@@ -0,0 +1,140 @@
+// 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 crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
+use crate::metrics::CounterMetric;
+use crate::metrics::Metric;
+use crate::metrics::MetricType;
+use crate::metrics::RateMetric;
+use crate::storage::StorageManager;
+use crate::CommonMetricData;
+use crate::Glean;
+
+/// A Denominator metric (a kind of count shared among Rate metrics).
+///
+/// Used to count things.
+/// The value can only be incremented, not decremented.
+// This is essentially a counter metric,
+// which additionally forwards increments to the denominator to a list of associated rates.
+// The numerator is incremented through the corresponding `NumeratorMetric`.
+#[derive(Clone, Debug)]
+pub struct DenominatorMetric {
+ counter: CounterMetric,
+ numerators: Vec<RateMetric>,
+}
+
+impl MetricType for DenominatorMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ self.counter.meta()
+ }
+}
+
+impl DenominatorMetric {
+ /// Creates a new denominator metric.
+ pub fn new(meta: CommonMetricData, numerators: Vec<CommonMetricData>) -> Self {
+ Self {
+ counter: CounterMetric::new(meta),
+ numerators: numerators.into_iter().map(RateMetric::new).collect(),
+ }
+ }
+
+ /// Increases the denominator by `amount`.
+ ///
+ /// # Arguments
+ ///
+ /// * `glean` - The Glean instance this metric belongs to.
+ /// * `amount` - The amount to increase by. Should be positive.
+ ///
+ /// ## Notes
+ ///
+ /// Logs an error if the `amount` is 0 or negative.
+ pub fn add(&self, amount: i32) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.add_sync(glean, amount))
+ }
+
+ #[doc(hidden)]
+ pub fn add_sync(&self, glean: &Glean, amount: i32) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ if amount <= 0 {
+ record_error(
+ glean,
+ self.meta(),
+ ErrorType::InvalidValue,
+ format!("Added negative or zero value {}", amount),
+ None,
+ );
+ return;
+ }
+
+ for num in &self.numerators {
+ num.add_to_denominator_sync(glean, amount);
+ }
+
+ glean
+ .storage()
+ .record_with(glean, self.counter.meta(), |old_value| match old_value {
+ Some(Metric::Counter(old_value)) => {
+ Metric::Counter(old_value.saturating_add(amount))
+ }
+ _ => Metric::Counter(amount),
+ })
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as an integer.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<i32> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<i32> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta().identifier(glean),
+ self.meta().inner.lifetime,
+ ) {
+ Some(Metric::Counter(i)) => Some(i),
+ _ => None,
+ }
+ }
+
+ /// **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` - the optional name of the ping to retrieve the metric
+ /// for. inner to the first value in `send_in_pings`.
+ ///
+ /// # 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)
+ })
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/event.rs b/third_party/rust/glean-core/src/metrics/event.rs
new file mode 100644
index 0000000000..5ad6e6d50c
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/event.rs
@@ -0,0 +1,213 @@
+// 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::collections::HashMap;
+
+use crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
+use crate::event_database::RecordedEvent;
+use crate::metrics::MetricType;
+use crate::util::truncate_string_at_boundary_with_error;
+use crate::CommonMetricData;
+use crate::Glean;
+
+use chrono::Utc;
+
+const MAX_LENGTH_EXTRA_KEY_VALUE: usize = 500;
+
+/// An event metric.
+///
+/// Events allow recording of e.g. individual occurences of user actions, say
+/// every time a view was open and from where. Each time you record an event, it
+/// records a timestamp, the event's name and a set of custom values.
+#[derive(Clone, Debug)]
+pub struct EventMetric {
+ meta: CommonMetricDataInternal,
+ allowed_extra_keys: Vec<String>,
+}
+
+impl MetricType for EventMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl EventMetric {
+ /// Creates a new event metric.
+ pub fn new(meta: CommonMetricData, allowed_extra_keys: Vec<String>) -> Self {
+ Self {
+ meta: meta.into(),
+ allowed_extra_keys,
+ }
+ }
+
+ /// Records an event.
+ ///
+ /// # Arguments
+ ///
+ /// * `extra` - A [`HashMap`] of `(key, value)` pairs.
+ /// Keys must be one of the allowed extra keys.
+ /// If any key is not allowed, an error is reported and no event is recorded.
+ pub fn record(&self, extra: HashMap<String, String>) {
+ let timestamp = crate::get_timestamp_ms();
+ self.record_with_time(timestamp, extra);
+ }
+
+ /// Record a new event with a provided timestamp.
+ ///
+ /// It's the caller's responsibility to ensure the timestamp comes from the same clock source.
+ ///
+ /// # Arguments
+ ///
+ /// * `timestamp` - The event timestamp, in milliseconds.
+ /// * `extra` - A [`HashMap`] of `(key, value)` pairs.
+ /// Keys must be one of the allowed extra keys.
+ /// If any key is not allowed, an error is reported and no event is recorded.
+ pub fn record_with_time(&self, timestamp: u64, extra: HashMap<String, String>) {
+ let metric = self.clone();
+
+ // Precise timestamp based on wallclock. Will be used if `enable_event_timestamps` is true.
+ let now = Utc::now();
+ let precise_timestamp = now.timestamp_millis() as u64;
+
+ crate::launch_with_glean(move |glean| {
+ let sent = metric.record_sync(glean, timestamp, extra, precise_timestamp);
+ if sent {
+ let state = crate::global_state().lock().unwrap();
+ if let Err(e) = state.callbacks.trigger_upload() {
+ log::error!("Triggering upload failed. Error: {}", e);
+ }
+ }
+ });
+
+ let id = self.meta().base_identifier();
+ crate::launch_with_glean(move |_| {
+ let event_listeners = crate::event_listeners().lock().unwrap();
+ event_listeners
+ .iter()
+ .for_each(|(_, listener)| listener.on_event_recorded(id.clone()));
+ });
+ }
+
+ /// Validate that extras are empty or all extra keys are allowed.
+ ///
+ /// If at least one key is not allowed, record an error and fail.
+ fn validate_extra(
+ &self,
+ glean: &Glean,
+ extra: HashMap<String, String>,
+ ) -> Result<Option<HashMap<String, String>>, ()> {
+ if extra.is_empty() {
+ return Ok(None);
+ }
+
+ let mut extra_strings = HashMap::new();
+ for (k, v) in extra.into_iter() {
+ if !self.allowed_extra_keys.contains(&k) {
+ let msg = format!("Invalid key index {}", k);
+ record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
+ return Err(());
+ }
+
+ let value = truncate_string_at_boundary_with_error(
+ glean,
+ &self.meta,
+ v,
+ MAX_LENGTH_EXTRA_KEY_VALUE,
+ );
+ extra_strings.insert(k, value);
+ }
+
+ Ok(Some(extra_strings))
+ }
+
+ /// Records an event.
+ ///
+ /// ## Returns
+ ///
+ /// `true` if a ping was submitted and should be uploaded.
+ /// `false` otherwise.
+ #[doc(hidden)]
+ pub fn record_sync(
+ &self,
+ glean: &Glean,
+ timestamp: u64,
+ extra: HashMap<String, String>,
+ precise_timestamp: u64,
+ ) -> bool {
+ if !self.should_record(glean) {
+ return false;
+ }
+
+ let mut extra_strings = match self.validate_extra(glean, extra) {
+ Ok(extra) => extra,
+ Err(()) => return false,
+ };
+
+ if glean.with_timestamps() {
+ if extra_strings.is_none() {
+ extra_strings.replace(Default::default());
+ }
+ let map = extra_strings.get_or_insert(Default::default());
+ map.insert("glean_timestamp".to_string(), precise_timestamp.to_string());
+ }
+
+ glean
+ .event_storage()
+ .record(glean, &self.meta, timestamp, extra_strings)
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Get the vector of currently stored events for this event metric.
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<Vec<RecordedEvent>> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ glean
+ .event_storage()
+ .test_get_value(&self.meta, queried_ping_name)
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Get the vector of currently stored events for this event metric.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<Vec<RecordedEvent>> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ /// **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. inner to the first value in `send_in_pings`.
+ ///
+ /// # 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)
+ })
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/experiment.rs b/third_party/rust/glean-core/src/metrics/experiment.rs
new file mode 100644
index 0000000000..23e6c41ce2
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/experiment.rs
@@ -0,0 +1,266 @@
+// 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::cmp;
+use std::collections::HashMap;
+use std::sync::atomic::AtomicU8;
+
+use crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{record_error, ErrorType};
+use crate::metrics::{Metric, MetricType, RecordedExperiment};
+use crate::storage::{StorageManager, INTERNAL_STORAGE};
+use crate::util::{truncate_string_at_boundary, truncate_string_at_boundary_with_error};
+use crate::Lifetime;
+use crate::{CommonMetricData, Glean};
+
+/// The maximum length of the experiment id, the branch id, and the keys of the
+/// `extra` map. Identifiers longer than this number of characters are truncated.
+const MAX_EXPERIMENTS_IDS_LEN: usize = 100;
+/// The maximum length of the experiment `extra` values. Values longer than this
+/// limit will be truncated.
+const MAX_EXPERIMENT_VALUE_LEN: usize = MAX_EXPERIMENTS_IDS_LEN;
+/// The maximum number of extras allowed in the `extra` hash map. Any items added
+/// beyond this limit will be dropped. Note that truncation of a hash map is
+/// nondeterministic in which items are truncated.
+const MAX_EXPERIMENTS_EXTRAS_SIZE: usize = 20;
+
+/// An experiment metric.
+///
+/// Used to store active experiments.
+/// This is used through the `set_experiment_active`/`set_experiment_inactive` Glean SDK API.
+#[derive(Clone, Debug)]
+pub struct ExperimentMetric {
+ meta: CommonMetricDataInternal,
+}
+
+impl MetricType for ExperimentMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+}
+
+impl ExperimentMetric {
+ /// Creates a new experiment metric.
+ ///
+ /// # Arguments
+ ///
+ /// * `id` - the id of the experiment. Please note that this will be
+ /// truncated to `MAX_EXPERIMENTS_IDS_LEN`, if needed.
+ ///
+ /// # Implementation note
+ ///
+ /// This runs synchronously and queries the database to record potential errors.
+ pub fn new(glean: &Glean, id: String) -> Self {
+ let mut error = None;
+
+ // Make sure that experiment id is within the expected limit.
+ let truncated_id = if id.len() > MAX_EXPERIMENTS_IDS_LEN {
+ let msg = format!(
+ "Value length {} for experiment id exceeds maximum of {}",
+ id.len(),
+ MAX_EXPERIMENTS_IDS_LEN
+ );
+ error = Some(msg);
+ truncate_string_at_boundary(id, MAX_EXPERIMENTS_IDS_LEN)
+ } else {
+ id
+ };
+
+ let new_experiment = Self {
+ meta: CommonMetricDataInternal {
+ inner: CommonMetricData {
+ name: format!("{}#experiment", truncated_id),
+ // We don't need a category, the name is already unique
+ category: "".into(),
+ send_in_pings: vec![INTERNAL_STORAGE.into()],
+ lifetime: Lifetime::Application,
+ ..Default::default()
+ },
+ disabled: AtomicU8::new(0),
+ },
+ };
+
+ // Check for a truncation error to record
+ if let Some(msg) = error {
+ record_error(
+ glean,
+ &new_experiment.meta,
+ ErrorType::InvalidValue,
+ msg,
+ None,
+ );
+ }
+
+ new_experiment
+ }
+
+ /// Records an experiment as active.
+ ///
+ /// # Arguments
+ ///
+ /// * `glean` - The Glean instance this metric belongs to.
+ /// * `branch` - the active branch of the experiment. Please note that this will be
+ /// truncated to `MAX_EXPERIMENTS_IDS_LEN`, if needed.
+ /// * `extra` - an optional, user defined String to String map used to provide richer
+ /// experiment context if needed.
+ pub fn set_active_sync(&self, glean: &Glean, branch: String, extra: HashMap<String, String>) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ // Make sure that branch id is within the expected limit.
+ let truncated_branch = if branch.len() > MAX_EXPERIMENTS_IDS_LEN {
+ truncate_string_at_boundary_with_error(
+ glean,
+ &self.meta,
+ branch,
+ MAX_EXPERIMENTS_IDS_LEN,
+ )
+ } else {
+ branch
+ };
+
+ // Apply limits to extras
+ if extra.len() > MAX_EXPERIMENTS_EXTRAS_SIZE {
+ let msg = format!(
+ "Extra hash map length {} exceeds maximum of {}",
+ extra.len(),
+ MAX_EXPERIMENTS_EXTRAS_SIZE
+ );
+ record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
+ }
+
+ let mut truncated_extras =
+ HashMap::with_capacity(cmp::min(extra.len(), MAX_EXPERIMENTS_EXTRAS_SIZE));
+ for (key, value) in extra.into_iter().take(MAX_EXPERIMENTS_EXTRAS_SIZE) {
+ let truncated_key = if key.len() > MAX_EXPERIMENTS_IDS_LEN {
+ truncate_string_at_boundary_with_error(
+ glean,
+ &self.meta,
+ key,
+ MAX_EXPERIMENTS_IDS_LEN,
+ )
+ } else {
+ key
+ };
+ let truncated_value = if value.len() > MAX_EXPERIMENT_VALUE_LEN {
+ truncate_string_at_boundary_with_error(
+ glean,
+ &self.meta,
+ value,
+ MAX_EXPERIMENT_VALUE_LEN,
+ )
+ } else {
+ value
+ };
+
+ truncated_extras.insert(truncated_key, truncated_value);
+ }
+ let truncated_extras = if truncated_extras.is_empty() {
+ None
+ } else {
+ Some(truncated_extras)
+ };
+
+ let value = Metric::Experiment(RecordedExperiment {
+ branch: truncated_branch,
+ extra: truncated_extras,
+ });
+ glean.storage().record(glean, &self.meta, &value)
+ }
+
+ /// Records an experiment as inactive.
+ ///
+ /// # Arguments
+ ///
+ /// * `glean` - The Glean instance this metric belongs to.
+ pub fn set_inactive_sync(&self, glean: &Glean) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ if let Err(e) = glean.storage().remove_single_metric(
+ Lifetime::Application,
+ INTERNAL_STORAGE,
+ &self.meta.inner.name,
+ ) {
+ log::error!("Failed to set experiment as inactive: {:?}", e);
+ }
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored experiment data as a JSON representation of
+ /// the RecordedExperiment.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, glean: &Glean) -> Option<RecordedExperiment> {
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ INTERNAL_STORAGE,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::Experiment(e)) => Some(e),
+ _ => None,
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn stable_serialization() {
+ let experiment_empty = RecordedExperiment {
+ branch: "branch".into(),
+ extra: Default::default(),
+ };
+
+ let mut data = HashMap::new();
+ data.insert("a key".to_string(), "a value".to_string());
+ let experiment_data = RecordedExperiment {
+ branch: "branch".into(),
+ extra: Some(data),
+ };
+
+ let experiment_empty_bin = bincode::serialize(&experiment_empty).unwrap();
+ let experiment_data_bin = bincode::serialize(&experiment_data).unwrap();
+
+ assert_eq!(
+ experiment_empty,
+ bincode::deserialize(&experiment_empty_bin).unwrap()
+ );
+ assert_eq!(
+ experiment_data,
+ bincode::deserialize(&experiment_data_bin).unwrap()
+ );
+ }
+
+ #[test]
+ #[rustfmt::skip] // Let's not add newlines unnecessary
+ fn deserialize_old_encoding() {
+ // generated by `bincode::serialize` as of Glean commit ac27fceb7c0d5a7288d7d569e8c5c5399a53afb2
+ // empty was generated from: `RecordedExperiment { branch: "branch".into(), extra: None, }`
+ let empty_bin = vec![6, 0, 0, 0, 0, 0, 0, 0, 98, 114, 97, 110, 99, 104];
+ // data was generated from: RecordedExperiment { branch: "branch".into(), extra: Some({"a key": "a value"}), };
+ let data_bin = vec![6, 0, 0, 0, 0, 0, 0, 0, 98, 114, 97, 110, 99, 104,
+ 1, 1, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0,
+ 97, 32, 107, 101, 121, 7, 0, 0, 0, 0, 0, 0, 0, 97,
+ 32, 118, 97, 108, 117, 101];
+
+
+ let mut data = HashMap::new();
+ data.insert("a key".to_string(), "a value".to_string());
+ let experiment_data = RecordedExperiment { branch: "branch".into(), extra: Some(data), };
+
+ // We can't actually decode old experiment data.
+ // Luckily Glean did store experiments in the database before commit ac27fceb7c0d5a7288d7d569e8c5c5399a53afb2.
+ let experiment_empty: Result<RecordedExperiment, _> = bincode::deserialize(&empty_bin);
+ assert!(experiment_empty.is_err());
+
+ assert_eq!(experiment_data, bincode::deserialize(&data_bin).unwrap());
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/labeled.rs b/third_party/rust/glean-core/src/metrics/labeled.rs
new file mode 100644
index 0000000000..fa3e6a6a75
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/labeled.rs
@@ -0,0 +1,294 @@
+// 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::borrow::Cow;
+use std::collections::{hash_map::Entry, HashMap};
+use std::sync::{Arc, Mutex};
+
+use crate::common_metric_data::{CommonMetricData, CommonMetricDataInternal};
+use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
+use crate::metrics::{BooleanMetric, CounterMetric, Metric, MetricType, StringMetric};
+use crate::Glean;
+
+const MAX_LABELS: usize = 16;
+const OTHER_LABEL: &str = "__other__";
+const MAX_LABEL_LENGTH: usize = 71;
+
+/// A labeled counter.
+pub type LabeledCounter = LabeledMetric<CounterMetric>;
+
+/// A labeled boolean.
+pub type LabeledBoolean = LabeledMetric<BooleanMetric>;
+
+/// A labeled string.
+pub type LabeledString = LabeledMetric<StringMetric>;
+
+/// A labeled metric.
+///
+/// Labeled metrics allow to record multiple sub-metrics of the same type under different string labels.
+#[derive(Debug)]
+pub struct LabeledMetric<T> {
+ labels: Option<Vec<Cow<'static, str>>>,
+ /// Type of the underlying metric
+ /// We hold on to an instance of it, which is cloned to create new modified instances.
+ submetric: T,
+
+ /// A map from a unique ID for the labeled submetric to a handle of an instantiated
+ /// metric type.
+ label_map: Mutex<HashMap<String, Arc<T>>>,
+}
+
+/// Sealed traits protect against downstream implementations.
+///
+/// We wrap it in a private module that is inaccessible outside of this module.
+mod private {
+ use crate::{
+ metrics::BooleanMetric, metrics::CounterMetric, metrics::StringMetric, CommonMetricData,
+ };
+
+ /// The sealed labeled trait.
+ ///
+ /// This also allows us to hide methods, that are only used internally
+ /// and should not be visible to users of the object implementing the
+ /// `Labeled<T>` trait.
+ pub trait Sealed {
+ /// Create a new `glean_core` metric from the metadata.
+ fn new_inner(meta: crate::CommonMetricData) -> Self;
+ }
+
+ impl Sealed for CounterMetric {
+ fn new_inner(meta: CommonMetricData) -> Self {
+ Self::new(meta)
+ }
+ }
+
+ impl Sealed for BooleanMetric {
+ fn new_inner(meta: CommonMetricData) -> Self {
+ Self::new(meta)
+ }
+ }
+
+ impl Sealed for StringMetric {
+ fn new_inner(meta: CommonMetricData) -> Self {
+ Self::new(meta)
+ }
+ }
+}
+
+/// Trait for metrics that can be nested inside a labeled metric.
+pub trait AllowLabeled: MetricType {
+ /// Create a new labeled metric.
+ fn new_labeled(meta: CommonMetricData) -> Self;
+}
+
+// Implement the trait for everything we marked as allowed.
+impl<T> AllowLabeled for T
+where
+ T: MetricType,
+ T: private::Sealed,
+{
+ fn new_labeled(meta: CommonMetricData) -> Self {
+ T::new_inner(meta)
+ }
+}
+
+impl<T> LabeledMetric<T>
+where
+ T: AllowLabeled + Clone,
+{
+ /// Creates a new labeled metric from the given metric instance and optional list of labels.
+ ///
+ /// See [`get`](LabeledMetric::get) for information on how static or dynamic labels are handled.
+ pub fn new(meta: CommonMetricData, labels: Option<Vec<Cow<'static, str>>>) -> LabeledMetric<T> {
+ let submetric = T::new_labeled(meta);
+ LabeledMetric::new_inner(submetric, labels)
+ }
+
+ fn new_inner(submetric: T, labels: Option<Vec<Cow<'static, str>>>) -> LabeledMetric<T> {
+ let label_map = Default::default();
+ LabeledMetric {
+ labels,
+ submetric,
+ label_map,
+ }
+ }
+
+ /// Creates a new metric with a specific label.
+ ///
+ /// This is used for static labels where we can just set the name to be `name/label`.
+ fn new_metric_with_name(&self, name: String) -> T {
+ self.submetric.with_name(name)
+ }
+
+ /// Creates a new metric with a specific label.
+ ///
+ /// This is used for dynamic labels where we have to actually validate and correct the
+ /// label later when we have a Glean object.
+ fn new_metric_with_dynamic_label(&self, label: String) -> T {
+ self.submetric.with_dynamic_label(label)
+ }
+
+ /// Creates a static label.
+ ///
+ /// # Safety
+ ///
+ /// Should only be called when static labels are available on this metric.
+ ///
+ /// # Arguments
+ ///
+ /// * `label` - The requested label
+ ///
+ /// # Returns
+ ///
+ /// The requested label if it is in the list of allowed labels.
+ /// Otherwise `OTHER_LABEL` is returned.
+ fn static_label<'a>(&self, label: &'a str) -> &'a str {
+ debug_assert!(self.labels.is_some());
+ let labels = self.labels.as_ref().unwrap();
+ if labels.iter().any(|l| l == label) {
+ label
+ } else {
+ OTHER_LABEL
+ }
+ }
+
+ /// Gets a specific metric for a given label.
+ ///
+ /// If a set of acceptable labels were specified in the `metrics.yaml` file,
+ /// and the given label is not in the set, it will be recorded under the special `OTHER_LABEL` label.
+ ///
+ /// If a set of acceptable labels was not specified in the `metrics.yaml` file,
+ /// only the first 16 unique labels will be used.
+ /// After that, any additional labels will be recorded under the special `OTHER_LABEL` label.
+ ///
+ /// Labels must be `snake_case` and less than 30 characters.
+ /// If an invalid label is used, the metric will be recorded in the special `OTHER_LABEL` label.
+ pub fn get<S: AsRef<str>>(&self, label: S) -> Arc<T> {
+ let label = label.as_ref();
+
+ // The handle is a unique number per metric.
+ // The label identifies the submetric.
+ let id = format!("{}/{}", self.submetric.meta().base_identifier(), label);
+
+ let mut map = self.label_map.lock().unwrap();
+ match map.entry(id) {
+ Entry::Occupied(entry) => Arc::clone(entry.get()),
+ Entry::Vacant(entry) => {
+ // We have 2 scenarios to consider:
+ // * Static labels. No database access needed. We just look at what is in memory.
+ // * Dynamic labels. We look up in the database all previously stored
+ // labels in order to keep a maximum of allowed labels. This is done later
+ // when the specific metric is actually recorded, when we are guaranteed to have
+ // an initialized Glean object.
+ let metric = match self.labels {
+ Some(_) => {
+ let label = self.static_label(label);
+ self.new_metric_with_name(combine_base_identifier_and_label(
+ &self.submetric.meta().inner.name,
+ label,
+ ))
+ }
+ None => self.new_metric_with_dynamic_label(label.to_string()),
+ };
+ let metric = Arc::new(metric);
+ entry.insert(Arc::clone(&metric));
+ metric
+ }
+ }
+ }
+
+ /// **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) -> i32 {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| {
+ test_get_num_recorded_errors(glean, self.submetric.meta(), error).unwrap_or(0)
+ })
+ }
+}
+
+/// Combines a metric's base identifier and label
+pub fn combine_base_identifier_and_label(base_identifer: &str, label: &str) -> String {
+ format!("{}/{}", base_identifer, label)
+}
+
+/// Strips the label off of a complete identifier
+pub fn strip_label(identifier: &str) -> &str {
+ identifier.split_once('/').map_or(identifier, |s| s.0)
+}
+
+/// Validates a dynamic label, changing it to `OTHER_LABEL` if it's invalid.
+///
+/// Checks the requested label against limitations, such as the label length and allowed
+/// characters.
+///
+/// # Arguments
+///
+/// * `label` - The requested label
+///
+/// # Returns
+///
+/// The entire identifier for the metric, including the base identifier and the corrected label.
+/// The errors are logged.
+pub fn validate_dynamic_label(
+ glean: &Glean,
+ meta: &CommonMetricDataInternal,
+ base_identifier: &str,
+ label: &str,
+) -> String {
+ let key = combine_base_identifier_and_label(base_identifier, label);
+ for store in &meta.inner.send_in_pings {
+ if glean.storage().has_metric(meta.inner.lifetime, store, &key) {
+ return key;
+ }
+ }
+
+ let mut label_count = 0;
+ let prefix = &key[..=base_identifier.len()];
+ let mut snapshotter = |_: &[u8], _: &Metric| {
+ label_count += 1;
+ };
+
+ let lifetime = meta.inner.lifetime;
+ for store in &meta.inner.send_in_pings {
+ glean
+ .storage()
+ .iter_store_from(lifetime, store, Some(prefix), &mut snapshotter);
+ }
+
+ let error = if label_count >= MAX_LABELS {
+ true
+ } else if label.len() > MAX_LABEL_LENGTH {
+ let msg = format!(
+ "label length {} exceeds maximum of {}",
+ label.len(),
+ MAX_LABEL_LENGTH
+ );
+ record_error(glean, meta, ErrorType::InvalidLabel, msg, None);
+ true
+ } else if label.chars().any(|c| !c.is_ascii() || c.is_ascii_control()) {
+ let msg = format!("label must be printable ascii, got '{}'", label);
+ record_error(glean, meta, ErrorType::InvalidLabel, msg, None);
+ true
+ } else {
+ false
+ };
+
+ if error {
+ combine_base_identifier_and_label(base_identifier, OTHER_LABEL)
+ } else {
+ key
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/memory_distribution.rs b/third_party/rust/glean-core/src/metrics/memory_distribution.rs
new file mode 100644
index 0000000000..ac9eda1a90
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/memory_distribution.rs
@@ -0,0 +1,282 @@
+// 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::sync::Arc;
+
+use crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
+use crate::histogram::{Functional, Histogram};
+use crate::metrics::memory_unit::MemoryUnit;
+use crate::metrics::{DistributionData, Metric, MetricType};
+use crate::storage::StorageManager;
+use crate::CommonMetricData;
+use crate::Glean;
+
+// The base of the logarithm used to determine bucketing
+const LOG_BASE: f64 = 2.0;
+
+// The buckets per each order of magnitude of the logarithm.
+const BUCKETS_PER_MAGNITUDE: f64 = 16.0;
+
+// Set a maximum recordable value of 1 terabyte so the buckets aren't
+// completely unbounded.
+const MAX_BYTES: u64 = 1 << 40;
+
+/// A memory distribution metric.
+///
+/// Memory distributions are used to accumulate and store memory sizes.
+#[derive(Clone, Debug)]
+pub struct MemoryDistributionMetric {
+ meta: Arc<CommonMetricDataInternal>,
+ memory_unit: MemoryUnit,
+}
+
+/// Create a snapshot of the histogram.
+///
+/// The snapshot can be serialized into the payload format.
+pub(crate) fn snapshot(hist: &Histogram<Functional>) -> DistributionData {
+ DistributionData {
+ // **Caution**: This cannot use `Histogram::snapshot_values` and needs to use the more
+ // specialized snapshot function.
+ values: hist
+ .snapshot()
+ .into_iter()
+ .map(|(k, v)| (k as i64, v as i64))
+ .collect(),
+ sum: hist.sum() as i64,
+ count: hist.count() as i64,
+ }
+}
+
+impl MetricType for MemoryDistributionMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl MemoryDistributionMetric {
+ /// Creates a new memory distribution metric.
+ pub fn new(meta: CommonMetricData, memory_unit: MemoryUnit) -> Self {
+ Self {
+ meta: Arc::new(meta.into()),
+ memory_unit,
+ }
+ }
+
+ /// Accumulates the provided sample in the metric.
+ ///
+ /// # Arguments
+ ///
+ /// * `sample` - The sample to be recorded by the metric. The sample is assumed to be in the
+ /// configured memory unit of the metric.
+ ///
+ /// ## Notes
+ ///
+ /// Values bigger than 1 Terabyte (2<sup>40</sup> bytes) are truncated
+ /// and an [`ErrorType::InvalidValue`] error is recorded.
+ pub fn accumulate(&self, sample: i64) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.accumulate_sync(glean, sample))
+ }
+
+ /// Accumulates the provided sample in the metric synchronously.
+ ///
+ /// See [`accumulate`](Self::accumulate) for details.
+ #[doc(hidden)]
+ pub fn accumulate_sync(&self, glean: &Glean, sample: i64) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ if sample < 0 {
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidValue,
+ "Accumulated a negative sample",
+ None,
+ );
+ return;
+ }
+
+ let mut sample = self.memory_unit.as_bytes(sample as u64);
+
+ if sample > MAX_BYTES {
+ let msg = "Sample is bigger than 1 terabyte";
+ record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
+ sample = MAX_BYTES;
+ }
+
+ // Let's be defensive here:
+ // The uploader tries to store some memory distribution metrics,
+ // but in tests that storage might be gone already.
+ // Let's just ignore those.
+ // We do the same for counters and timing distributions.
+ // This should never happen in real app usage.
+ if let Some(storage) = glean.storage_opt() {
+ storage.record_with(glean, &self.meta, |old_value| match old_value {
+ Some(Metric::MemoryDistribution(mut hist)) => {
+ hist.accumulate(sample);
+ Metric::MemoryDistribution(hist)
+ }
+ _ => {
+ let mut hist = Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE);
+ hist.accumulate(sample);
+ Metric::MemoryDistribution(hist)
+ }
+ });
+ } else {
+ log::warn!(
+ "Couldn't get storage. Can't record memory distribution '{}'.",
+ self.meta.base_identifier()
+ );
+ }
+ }
+
+ /// Accumulates the provided signed samples in the metric.
+ ///
+ /// This is required so that the platform-specific code can provide us with
+ /// 64 bit signed integers if no `u64` comparable type is available. This
+ /// will take care of filtering and reporting errors for any provided negative
+ /// sample.
+ ///
+ /// Please note that this assumes that the provided samples are already in
+ /// the "unit" declared by the instance of the metric type (e.g. if the the
+ /// instance this method was called on is using [`MemoryUnit::Kilobyte`], then
+ /// `samples` are assumed to be in that unit).
+ ///
+ /// # Arguments
+ ///
+ /// * `samples` - The vector holding the samples to be recorded by the metric.
+ ///
+ /// ## Notes
+ ///
+ /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`]
+ /// for each of them.
+ ///
+ /// Values bigger than 1 Terabyte (2<sup>40</sup> bytes) are truncated
+ /// and an [`ErrorType::InvalidValue`] error is recorded.
+ pub fn accumulate_samples(&self, samples: Vec<i64>) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.accumulate_samples_sync(glean, samples))
+ }
+
+ /// Accumulates the provided signed samples in the metric synchronously.
+ ///
+ /// See [`accumulate_samples`](Self::accumulate_samples) for details.
+ #[doc(hidden)]
+ pub fn accumulate_samples_sync(&self, glean: &Glean, samples: Vec<i64>) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let mut num_negative_samples = 0;
+ let mut num_too_log_samples = 0;
+
+ glean.storage().record_with(glean, &self.meta, |old_value| {
+ let mut hist = match old_value {
+ Some(Metric::MemoryDistribution(hist)) => hist,
+ _ => Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE),
+ };
+
+ for &sample in samples.iter() {
+ if sample < 0 {
+ num_negative_samples += 1;
+ } else {
+ let sample = sample as u64;
+ let mut sample = self.memory_unit.as_bytes(sample);
+ if sample > MAX_BYTES {
+ num_too_log_samples += 1;
+ sample = MAX_BYTES;
+ }
+
+ hist.accumulate(sample);
+ }
+ }
+ Metric::MemoryDistribution(hist)
+ });
+
+ if num_negative_samples > 0 {
+ let msg = format!("Accumulated {} negative samples", num_negative_samples);
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidValue,
+ msg,
+ num_negative_samples,
+ );
+ }
+
+ if num_too_log_samples > 0 {
+ let msg = format!(
+ "Accumulated {} samples larger than 1TB",
+ num_too_log_samples
+ );
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidValue,
+ msg,
+ num_too_log_samples,
+ );
+ }
+ }
+
+ /// Gets the currently stored value synchronously.
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<DistributionData> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::MemoryDistribution(hist)) => Some(snapshot(&hist)),
+ _ => None,
+ }
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<DistributionData> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ /// **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) -> i32 {
+ crate::block_on_dispatcher();
+
+ crate::core::with_glean(|glean| {
+ test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
+ })
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/memory_unit.rs b/third_party/rust/glean-core/src/metrics/memory_unit.rs
new file mode 100644
index 0000000000..ce51b975fa
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/memory_unit.rs
@@ -0,0 +1,64 @@
+// 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::convert::TryFrom;
+
+use serde::{Deserialize, Serialize};
+
+use crate::error::{Error, ErrorKind};
+
+/// Different resolutions supported by the memory related metric types (e.g.
+/// MemoryDistributionMetric).
+#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+#[repr(i32)] // use i32 to be compatible with our JNA definition
+pub enum MemoryUnit {
+ /// 1 byte
+ Byte,
+ /// 2^10 bytes
+ Kilobyte,
+ /// 2^20 bytes
+ Megabyte,
+ /// 2^30 bytes
+ Gigabyte,
+}
+
+impl MemoryUnit {
+ /// Converts a value in the given unit to bytes.
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - the value to convert.
+ ///
+ /// # Returns
+ ///
+ /// The integer representation of the byte value.
+ pub fn as_bytes(self, value: u64) -> u64 {
+ use MemoryUnit::*;
+ match self {
+ Byte => value,
+ Kilobyte => value << 10,
+ Megabyte => value << 20,
+ Gigabyte => value << 30,
+ }
+ }
+}
+
+/// Trait implementation for converting an integer value
+/// to a [`MemoryUnit`]. This is used in the FFI code. Please
+/// note that values should match the ordering of the platform
+/// specific side of things (e.g. Kotlin implementation).
+impl TryFrom<i32> for MemoryUnit {
+ type Error = Error;
+
+ fn try_from(value: i32) -> Result<MemoryUnit, Self::Error> {
+ match value {
+ 0 => Ok(MemoryUnit::Byte),
+ 1 => Ok(MemoryUnit::Kilobyte),
+ 2 => Ok(MemoryUnit::Megabyte),
+ 3 => Ok(MemoryUnit::Gigabyte),
+ e => Err(ErrorKind::MemoryUnit(e).into()),
+ }
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/metrics_enabled_config.rs b/third_party/rust/glean-core/src/metrics/metrics_enabled_config.rs
new file mode 100644
index 0000000000..26d0deff31
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/metrics_enabled_config.rs
@@ -0,0 +1,46 @@
+// 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::{collections::HashMap, convert::TryFrom};
+
+use serde::{Deserialize, Serialize};
+
+/// Represents a list of metrics and an associated boolean property
+/// indicating if the metric is enabledfrom the remote-settings
+/// configuration store. The expected format of this data is stringified JSON
+/// in the following format:
+/// ```json
+/// {
+/// "category.metric_name": true
+/// }
+/// ```
+#[derive(Serialize, Deserialize, Debug, Clone, Default)]
+pub struct MetricsEnabledConfig {
+ /// This is a `HashMap` consisting of base_identifiers as keys
+ /// and bool values representing an override for the `disabled`
+ /// property of the metric, only inverted to reduce confusion.
+ /// If a particular metric has a value of `true` here, it means
+ /// the default of the metric will be overriden and set to the
+ /// enabled state.
+ #[serde(flatten)]
+ pub metrics_enabled: HashMap<String, bool>,
+}
+
+impl MetricsEnabledConfig {
+ /// Creates a new MetricsEnabledConfig
+ pub fn new() -> Self {
+ Default::default()
+ }
+}
+
+impl TryFrom<String> for MetricsEnabledConfig {
+ type Error = crate::ErrorKind;
+
+ fn try_from(json: String) -> Result<Self, Self::Error> {
+ match serde_json::from_str(json.as_str()) {
+ Ok(config) => Ok(config),
+ Err(e) => Err(crate::ErrorKind::Json(e)),
+ }
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/mod.rs b/third_party/rust/glean-core/src/metrics/mod.rs
new file mode 100644
index 0000000000..43253b9aa7
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/mod.rs
@@ -0,0 +1,285 @@
+// 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/.
+
+//! The different metric types supported by the Glean SDK to handle data.
+
+use std::collections::HashMap;
+use std::sync::atomic::Ordering;
+
+use chrono::{DateTime, FixedOffset};
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value as JsonValue};
+
+mod boolean;
+mod counter;
+mod custom_distribution;
+mod datetime;
+mod denominator;
+mod event;
+mod experiment;
+pub(crate) mod labeled;
+mod memory_distribution;
+mod memory_unit;
+mod metrics_enabled_config;
+mod numerator;
+mod ping;
+mod quantity;
+mod rate;
+mod recorded_experiment;
+mod string;
+mod string_list;
+mod text;
+mod time_unit;
+mod timespan;
+mod timing_distribution;
+mod url;
+mod uuid;
+
+use crate::common_metric_data::CommonMetricDataInternal;
+pub use crate::event_database::RecordedEvent;
+use crate::histogram::{Functional, Histogram, PrecomputedExponential, PrecomputedLinear};
+pub use crate::metrics::datetime::Datetime;
+use crate::util::get_iso_time_string;
+use crate::Glean;
+
+pub use self::boolean::BooleanMetric;
+pub use self::counter::CounterMetric;
+pub use self::custom_distribution::CustomDistributionMetric;
+pub use self::datetime::DatetimeMetric;
+pub use self::denominator::DenominatorMetric;
+pub use self::event::EventMetric;
+pub(crate) use self::experiment::ExperimentMetric;
+pub use self::labeled::{LabeledBoolean, LabeledCounter, LabeledMetric, LabeledString};
+pub use self::memory_distribution::MemoryDistributionMetric;
+pub use self::memory_unit::MemoryUnit;
+pub use self::numerator::NumeratorMetric;
+pub use self::ping::PingType;
+pub use self::quantity::QuantityMetric;
+pub use self::rate::{Rate, RateMetric};
+pub use self::string::StringMetric;
+pub use self::string_list::StringListMetric;
+pub use self::text::TextMetric;
+pub use self::time_unit::TimeUnit;
+pub use self::timespan::TimespanMetric;
+pub use self::timing_distribution::TimerId;
+pub use self::timing_distribution::TimingDistributionMetric;
+pub use self::url::UrlMetric;
+pub use self::uuid::UuidMetric;
+pub use crate::histogram::HistogramType;
+pub use recorded_experiment::RecordedExperiment;
+
+pub use self::metrics_enabled_config::MetricsEnabledConfig;
+
+/// A snapshot of all buckets and the accumulated sum of a distribution.
+//
+// Note: Be careful when changing this structure.
+// The serialized form ends up in the ping payload.
+// New fields might require to be skipped on serialization.
+#[derive(Debug, Serialize)]
+pub struct DistributionData {
+ /// A map containig the bucket index mapped to the accumulated count.
+ ///
+ /// This can contain buckets with a count of `0`.
+ pub values: HashMap<i64, i64>,
+
+ /// The accumulated sum of all the samples in the distribution.
+ pub sum: i64,
+
+ /// The total number of entries in the distribution.
+ #[serde(skip)]
+ pub count: i64,
+}
+
+/// The available metrics.
+///
+/// This is the in-memory and persisted layout of a metric.
+///
+/// ## Note
+///
+/// The order of metrics in this enum is important, as it is used for serialization.
+/// Do not reorder the variants.
+///
+/// **Any new metric must be added at the end.**
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+pub enum Metric {
+ /// A boolean metric. See [`BooleanMetric`] for more information.
+ Boolean(bool),
+ /// A counter metric. See [`CounterMetric`] for more information.
+ Counter(i32),
+ /// A custom distribution with precomputed exponential bucketing.
+ /// See [`CustomDistributionMetric`] for more information.
+ CustomDistributionExponential(Histogram<PrecomputedExponential>),
+ /// A custom distribution with precomputed linear bucketing.
+ /// See [`CustomDistributionMetric`] for more information.
+ CustomDistributionLinear(Histogram<PrecomputedLinear>),
+ /// A datetime metric. See [`DatetimeMetric`] for more information.
+ Datetime(DateTime<FixedOffset>, TimeUnit),
+ /// An experiment metric. See `ExperimentMetric` for more information.
+ Experiment(recorded_experiment::RecordedExperiment),
+ /// A quantity metric. See [`QuantityMetric`] for more information.
+ Quantity(i64),
+ /// A string metric. See [`StringMetric`] for more information.
+ String(String),
+ /// A string list metric. See [`StringListMetric`] for more information.
+ StringList(Vec<String>),
+ /// A UUID metric. See [`UuidMetric`] for more information.
+ Uuid(String),
+ /// A timespan metric. See [`TimespanMetric`] for more information.
+ Timespan(std::time::Duration, TimeUnit),
+ /// A timing distribution. See [`TimingDistributionMetric`] for more information.
+ TimingDistribution(Histogram<Functional>),
+ /// A memory distribution. See [`MemoryDistributionMetric`] for more information.
+ MemoryDistribution(Histogram<Functional>),
+ /// **DEPRECATED**: A JWE metric..
+ /// Note: This variant MUST NOT be removed to avoid backwards-incompatible changes to the
+ /// serialization. This type has no underlying implementation anymore.
+ Jwe(String),
+ /// A rate metric. See [`RateMetric`] for more information.
+ Rate(i32, i32),
+ /// A URL metric. See [`UrlMetric`] for more information.
+ Url(String),
+ /// A Text metric. See [`TextMetric`] for more information.
+ Text(String),
+}
+
+/// A [`MetricType`] describes common behavior across all metrics.
+pub trait MetricType {
+ /// Access the stored metadata
+ fn meta(&self) -> &CommonMetricDataInternal;
+
+ /// Create a new metric from this with a new name.
+ fn with_name(&self, _name: String) -> Self
+ where
+ Self: Sized,
+ {
+ unimplemented!()
+ }
+
+ /// Create a new metric from this with a specific label.
+ fn with_dynamic_label(&self, _label: String) -> Self
+ where
+ Self: Sized,
+ {
+ unimplemented!()
+ }
+
+ /// Whether this metric should currently be recorded
+ ///
+ /// This depends on the metrics own state, as determined by its metadata,
+ /// and whether upload is enabled on the Glean object.
+ fn should_record(&self, glean: &Glean) -> bool {
+ if !glean.is_upload_enabled() {
+ return false;
+ }
+
+ // Technically nothing prevents multiple calls to should_record() to run in parallel,
+ // meaning both are reading self.meta().disabled and later writing it. In between it can
+ // also read remote_settings_metrics_config, which also could be modified in between those 2 reads.
+ // This means we could write the wrong remote_settings_epoch | current_disabled value. All in all
+ // at worst we would see that metric enabled/disabled wrongly once.
+ // But since everything is tunneled through the dispatcher, this should never ever happen.
+
+ // Get the current disabled field from the metric metadata, including
+ // the encoded remote_settings epoch
+ let disabled_field = self.meta().disabled.load(Ordering::Relaxed);
+ // Grab the epoch from the upper nibble
+ let epoch = disabled_field >> 4;
+ // Get the disabled flag from the lower nibble
+ let disabled = disabled_field & 0xF;
+ // Get the current remote_settings epoch to see if we need to bother with the
+ // more expensive HashMap lookup
+ let remote_settings_epoch = glean.remote_settings_epoch.load(Ordering::Acquire);
+ if epoch == remote_settings_epoch {
+ return disabled == 0;
+ }
+ // The epoch's didn't match so we need to look up the disabled flag
+ // by the base_identifier from the in-memory HashMap
+ let metrics_enabled = &glean
+ .remote_settings_metrics_config
+ .lock()
+ .unwrap()
+ .metrics_enabled;
+ // Get the value from the remote configuration if it is there, otherwise return the default value.
+ let current_disabled = {
+ let base_id = self.meta().base_identifier();
+ let identifier = base_id
+ .split_once('/')
+ .map(|split| split.0)
+ .unwrap_or(&base_id);
+ // NOTE: The `!` preceding the `*is_enabled` is important for inverting the logic since the
+ // underlying property in the metrics.yaml is `disabled` and the outward API is treating it as
+ // if it were `enabled` to make it easier to understand.
+ if let Some(is_enabled) = metrics_enabled.get(identifier) {
+ u8::from(!*is_enabled)
+ } else {
+ u8::from(self.meta().inner.disabled)
+ }
+ };
+
+ // Re-encode the epoch and enabled status and update the metadata
+ let new_disabled = (remote_settings_epoch << 4) | (current_disabled & 0xF);
+ self.meta().disabled.store(new_disabled, Ordering::Relaxed);
+
+ // Return a boolean indicating whether or not the metric should be recorded
+ current_disabled == 0
+ }
+}
+
+impl Metric {
+ /// Gets the ping section the metric fits into.
+ ///
+ /// This determines the section of the ping to place the metric data in when
+ /// assembling the ping payload.
+ pub fn ping_section(&self) -> &'static str {
+ match self {
+ Metric::Boolean(_) => "boolean",
+ Metric::Counter(_) => "counter",
+ // Custom distributions are in the same section, no matter what bucketing.
+ Metric::CustomDistributionExponential(_) => "custom_distribution",
+ Metric::CustomDistributionLinear(_) => "custom_distribution",
+ Metric::Datetime(_, _) => "datetime",
+ Metric::Experiment(_) => panic!("Experiments should not be serialized through this"),
+ Metric::Quantity(_) => "quantity",
+ Metric::Rate(..) => "rate",
+ Metric::String(_) => "string",
+ Metric::StringList(_) => "string_list",
+ Metric::Timespan(..) => "timespan",
+ Metric::TimingDistribution(_) => "timing_distribution",
+ Metric::Url(_) => "url",
+ Metric::Uuid(_) => "uuid",
+ Metric::MemoryDistribution(_) => "memory_distribution",
+ Metric::Jwe(_) => "jwe",
+ Metric::Text(_) => "text",
+ }
+ }
+
+ /// The JSON representation of the metric's data
+ pub fn as_json(&self) -> JsonValue {
+ match self {
+ Metric::Boolean(b) => json!(b),
+ Metric::Counter(c) => json!(c),
+ Metric::CustomDistributionExponential(hist) => {
+ json!(custom_distribution::snapshot(hist))
+ }
+ Metric::CustomDistributionLinear(hist) => json!(custom_distribution::snapshot(hist)),
+ Metric::Datetime(d, time_unit) => json!(get_iso_time_string(*d, *time_unit)),
+ Metric::Experiment(e) => e.as_json(),
+ Metric::Quantity(q) => json!(q),
+ Metric::Rate(num, den) => {
+ json!({"numerator": num, "denominator": den})
+ }
+ Metric::String(s) => json!(s),
+ Metric::StringList(v) => json!(v),
+ Metric::Timespan(time, time_unit) => {
+ json!({"value": time_unit.duration_convert(*time), "time_unit": time_unit})
+ }
+ Metric::TimingDistribution(hist) => json!(timing_distribution::snapshot(hist)),
+ Metric::Url(s) => json!(s),
+ Metric::Uuid(s) => json!(s),
+ Metric::MemoryDistribution(hist) => json!(memory_distribution::snapshot(hist)),
+ Metric::Jwe(s) => json!(s),
+ Metric::Text(s) => json!(s),
+ }
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/numerator.rs b/third_party/rust/glean-core/src/metrics/numerator.rs
new file mode 100644
index 0000000000..3c340cab1d
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/numerator.rs
@@ -0,0 +1,94 @@
+// 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::sync::Arc;
+
+use crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::ErrorType;
+use crate::metrics::MetricType;
+use crate::metrics::Rate;
+use crate::metrics::RateMetric;
+use crate::CommonMetricData;
+use crate::Glean;
+
+/// Developer-facing API for recording rate metrics with external denominators.
+///
+/// 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.
+#[derive(Clone)]
+pub struct NumeratorMetric(pub(crate) Arc<RateMetric>);
+
+impl MetricType for NumeratorMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ self.0.meta()
+ }
+}
+
+impl NumeratorMetric {
+ /// The public constructor used by automatically generated metrics.
+ pub fn new(meta: CommonMetricData) -> Self {
+ Self(Arc::new(RateMetric::new(meta)))
+ }
+
+ /// Increases the numerator by `amount`.
+ ///
+ /// # Arguments
+ ///
+ /// * `amount` - The amount to increase by. Should be non-negative.
+ ///
+ /// ## Notes
+ ///
+ /// Logs an error if the `amount` is negative.
+ pub fn add_to_numerator(&self, amount: i32) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.add_to_numerator_sync(glean, amount));
+ }
+
+ #[doc(hidden)]
+ pub fn add_to_numerator_sync(&self, glean: &Glean, amount: i32) {
+ self.0.add_to_numerator_sync(glean, amount)
+ }
+
+ /// **Exported for test purposes.**
+ ///
+ /// Gets the currently stored value as a pair of integers.
+ ///
+ /// # Arguments
+ ///
+ /// * `ping_name` - the optional name of the ping to retrieve the metric
+ /// for. Defaults to the first value in `send_in_pings`.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<Rate> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<Rate> {
+ self.0.get_value(glean, ping_name)
+ }
+
+ /// **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` - 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) -> i32 {
+ self.0.test_get_num_recorded_errors(error)
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/ping.rs b/third_party/rust/glean-core/src/metrics/ping.rs
new file mode 100644
index 0000000000..dc37d76a45
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/ping.rs
@@ -0,0 +1,210 @@
+// 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::ping::PingMaker;
+use crate::Glean;
+
+use uuid::Uuid;
+
+/// Stores information about a ping.
+///
+/// This is required so that given metric data queued on disk we can send
+/// pings with the correct settings, e.g. whether it has a client_id.
+#[derive(Clone)]
+pub struct PingType(Arc<InnerPing>);
+
+struct InnerPing {
+ /// The name of the ping.
+ pub name: String,
+ /// Whether the ping should include the client ID.
+ pub include_client_id: bool,
+ /// Whether the ping should be sent if it is empty
+ pub send_if_empty: bool,
+ /// Whether to use millisecond-precise start/end times.
+ pub precise_timestamps: bool,
+ /// The "reason" codes that this ping can send
+ pub reason_codes: Vec<String>,
+}
+
+impl fmt::Debug for PingType {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("PingType")
+ .field("name", &self.0.name)
+ .field("include_client_id", &self.0.include_client_id)
+ .field("send_if_empty", &self.0.send_if_empty)
+ .field("precise_timestamps", &self.0.precise_timestamps)
+ .field("reason_codes", &self.0.reason_codes)
+ .finish()
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl PingType {
+ /// Creates a new ping type for the given name, whether to include the client ID and whether to
+ /// send this ping empty.
+ ///
+ /// # Arguments
+ ///
+ /// * `name` - The name of the ping.
+ /// * `include_client_id` - Whether to include the client ID in the assembled ping when submitting.
+ /// * `send_if_empty` - Whether the ping should be sent empty or not.
+ /// * `reason_codes` - The valid reason codes for this ping.
+ pub fn new<A: Into<String>>(
+ name: A,
+ include_client_id: bool,
+ send_if_empty: bool,
+ precise_timestamps: bool,
+ reason_codes: Vec<String>,
+ ) -> Self {
+ let this = Self(Arc::new(InnerPing {
+ name: name.into(),
+ include_client_id,
+ send_if_empty,
+ precise_timestamps,
+ reason_codes,
+ }));
+
+ // Register this ping.
+ // That will happen asynchronously and not block operation.
+ crate::register_ping_type(&this);
+
+ this
+ }
+
+ pub(crate) fn name(&self) -> &str {
+ &self.0.name
+ }
+
+ pub(crate) fn include_client_id(&self) -> bool {
+ self.0.include_client_id
+ }
+
+ pub(crate) fn send_if_empty(&self) -> bool {
+ self.0.send_if_empty
+ }
+
+ pub(crate) fn precise_timestamps(&self) -> bool {
+ self.0.precise_timestamps
+ }
+
+ /// Submits the ping for eventual uploading.
+ ///
+ /// The ping content is assembled as soon as possible, but upload is not
+ /// guaranteed to happen immediately, as that depends on the upload policies.
+ ///
+ /// If the ping currently contains no content, it will not be sent,
+ /// unless it is configured to be sent if empty.
+ ///
+ /// # Arguments
+ ///
+ /// * `reason` - the reason the ping was triggered. Included in the
+ /// `ping_info.reason` part of the payload.
+ pub fn submit(&self, reason: Option<String>) {
+ let ping = PingType(Arc::clone(&self.0));
+
+ // Need to separate access to the Glean object from access to global state.
+ // `trigger_upload` itself might lock the Glean object and we need to avoid that deadlock.
+ crate::dispatcher::launch(|| {
+ let sent =
+ crate::core::with_glean(move |glean| ping.submit_sync(glean, reason.as_deref()));
+ if sent {
+ let state = crate::global_state().lock().unwrap();
+ if let Err(e) = state.callbacks.trigger_upload() {
+ log::error!("Triggering upload failed. Error: {}", e);
+ }
+ }
+ })
+ }
+
+ /// Collects and submits a ping for eventual uploading.
+ ///
+ /// # Returns
+ ///
+ /// Whether the ping was succesfully assembled and queued.
+ #[doc(hidden)]
+ pub fn submit_sync(&self, glean: &Glean, reason: Option<&str>) -> bool {
+ if !glean.is_upload_enabled() {
+ log::info!("Glean disabled: not submitting any pings.");
+ return false;
+ }
+
+ let ping = &self.0;
+
+ // Allowing `clippy::manual_filter`.
+ // This causes a false positive.
+ // We have a side-effect in the `else` branch,
+ // so shouldn't delete it.
+ #[allow(unknown_lints)]
+ #[allow(clippy::manual_filter)]
+ let corrected_reason = match reason {
+ Some(reason) => {
+ if ping.reason_codes.contains(&reason.to_string()) {
+ Some(reason)
+ } else {
+ log::error!("Invalid reason code {} for ping {}", reason, ping.name);
+ None
+ }
+ }
+ None => None,
+ };
+
+ let ping_maker = PingMaker::new();
+ let doc_id = Uuid::new_v4().to_string();
+ let url_path = glean.make_path(&ping.name, &doc_id);
+ match ping_maker.collect(glean, self, corrected_reason, &doc_id, &url_path) {
+ None => {
+ log::info!(
+ "No content for ping '{}', therefore no ping queued.",
+ ping.name
+ );
+ false
+ }
+ Some(ping) => {
+ // This metric is recorded *after* the ping is collected (since
+ // that is the only way to know *if* it will be submitted). The
+ // implication of this is that the count for a metrics ping will
+ // be included in the *next* metrics ping.
+ glean
+ .additional_metrics
+ .pings_submitted
+ .get(ping.name)
+ .add_sync(glean, 1);
+
+ if let Err(e) = ping_maker.store_ping(glean.get_data_path(), &ping) {
+ log::warn!("IO error while writing ping to file: {}. Enqueuing upload of what we have in memory.", e);
+ glean.additional_metrics.io_errors.add_sync(glean, 1);
+ // `serde_json::to_string` only fails if serialization of the content
+ // fails or it contains maps with non-string keys.
+ // However `ping.content` is already a `JsonValue`,
+ // so both scenarios should be impossible.
+ let content =
+ ::serde_json::to_string(&ping.content).expect("ping serialization failed");
+ glean.upload_manager.enqueue_ping(
+ glean,
+ ping.doc_id,
+ ping.url_path,
+ &content,
+ Some(ping.headers),
+ );
+ return true;
+ }
+
+ glean.upload_manager.enqueue_ping_from_file(glean, &doc_id);
+
+ log::info!(
+ "The ping '{}' was submitted and will be sent as soon as possible",
+ ping.name
+ );
+
+ true
+ }
+ }
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/quantity.rs b/third_party/rust/glean-core/src/metrics/quantity.rs
new file mode 100644
index 0000000000..c59d3a4a21
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/quantity.rs
@@ -0,0 +1,126 @@
+// 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 crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
+use crate::metrics::Metric;
+use crate::metrics::MetricType;
+use crate::storage::StorageManager;
+use crate::CommonMetricData;
+use crate::Glean;
+
+/// A quantity metric.
+///
+/// Used to store explicit non-negative integers.
+#[derive(Clone, Debug)]
+pub struct QuantityMetric {
+ meta: CommonMetricDataInternal,
+}
+
+impl MetricType for QuantityMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl QuantityMetric {
+ /// Creates a new quantity metric.
+ pub fn new(meta: CommonMetricData) -> Self {
+ Self { meta: meta.into() }
+ }
+
+ /// Sets the value. Must be non-negative.
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - The value. Must be non-negative.
+ ///
+ /// ## Notes
+ ///
+ /// Logs an error if the `value` is negative.
+ pub fn set(&self, value: i64) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.set_sync(glean, value))
+ }
+
+ /// Sets the value synchronously. Must be non-negative.
+ #[doc(hidden)]
+ pub fn set_sync(&self, glean: &Glean, value: i64) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ if value < 0 {
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidValue,
+ format!("Set negative value {}", value),
+ None,
+ );
+ return;
+ }
+
+ glean
+ .storage()
+ .record(glean, &self.meta, &Metric::Quantity(value))
+ }
+
+ /// Get current value.
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<i64> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::Quantity(i)) => Some(i),
+ _ => None,
+ }
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as an integer.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<i64> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ /// **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) -> i32 {
+ crate::block_on_dispatcher();
+
+ crate::core::with_glean(|glean| {
+ test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
+ })
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/rate.rs b/third_party/rust/glean-core/src/metrics/rate.rs
new file mode 100644
index 0000000000..ba7f085b55
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/rate.rs
@@ -0,0 +1,191 @@
+// 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 crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
+use crate::metrics::Metric;
+use crate::metrics::MetricType;
+use crate::storage::StorageManager;
+use crate::CommonMetricData;
+use crate::Glean;
+
+/// A rate value as given by its numerator and denominator.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Rate {
+ /// A rate's numerator
+ pub numerator: i32,
+ /// A rate's denominator
+ pub denominator: i32,
+}
+
+impl From<(i32, i32)> for Rate {
+ fn from((num, den): (i32, i32)) -> Self {
+ Self {
+ numerator: num,
+ denominator: den,
+ }
+ }
+}
+
+/// A rate metric.
+///
+/// Used to determine the proportion of things via two counts:
+/// * A numerator defining the amount of times something happened,
+/// * A denominator counting the amount of times someting could have happened.
+///
+/// Both numerator and denominator can only be incremented, not decremented.
+#[derive(Clone, Debug)]
+pub struct RateMetric {
+ meta: CommonMetricDataInternal,
+}
+
+impl MetricType for RateMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl RateMetric {
+ /// Creates a new rate metric.
+ pub fn new(meta: CommonMetricData) -> Self {
+ Self { meta: meta.into() }
+ }
+
+ /// Increases the numerator by `amount`.
+ ///
+ /// # Arguments
+ ///
+ /// * `glean` - The Glean instance this metric belongs to.
+ /// * `amount` - The amount to increase by. Should be non-negative.
+ ///
+ /// ## Notes
+ ///
+ /// Logs an error if the `amount` is negative.
+ pub fn add_to_numerator(&self, amount: i32) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.add_to_numerator_sync(glean, amount))
+ }
+
+ #[doc(hidden)]
+ pub fn add_to_numerator_sync(&self, glean: &Glean, amount: i32) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ if amount < 0 {
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidValue,
+ format!("Added negative value {} to numerator", amount),
+ None,
+ );
+ return;
+ }
+
+ glean
+ .storage()
+ .record_with(glean, &self.meta, |old_value| match old_value {
+ Some(Metric::Rate(num, den)) => Metric::Rate(num.saturating_add(amount), den),
+ _ => Metric::Rate(amount, 0), // Denominator will show up eventually. Probably.
+ });
+ }
+
+ /// Increases the denominator by `amount`.
+ ///
+ /// # Arguments
+ ///
+ /// * `glean` - The Glean instance this metric belongs to.
+ /// * `amount` - The amount to increase by. Should be non-negative.
+ ///
+ /// ## Notes
+ ///
+ /// Logs an error if the `amount` is negative.
+ pub fn add_to_denominator(&self, amount: i32) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.add_to_denominator_sync(glean, amount))
+ }
+
+ #[doc(hidden)]
+ pub fn add_to_denominator_sync(&self, glean: &Glean, amount: i32) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ if amount < 0 {
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidValue,
+ format!("Added negative value {} to denominator", amount),
+ None,
+ );
+ return;
+ }
+
+ glean
+ .storage()
+ .record_with(glean, &self.meta, |old_value| match old_value {
+ Some(Metric::Rate(num, den)) => Metric::Rate(num, den.saturating_add(amount)),
+ _ => Metric::Rate(0, amount),
+ });
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as a pair of integers.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<Rate> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ /// Get current value
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<Rate> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::Rate(n, d)) => Some((n, d).into()),
+ _ => None,
+ }
+ }
+
+ /// **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) -> i32 {
+ crate::block_on_dispatcher();
+
+ crate::core::with_glean(|glean| {
+ test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
+ })
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/recorded_experiment.rs b/third_party/rust/glean-core/src/metrics/recorded_experiment.rs
new file mode 100644
index 0000000000..8b9dc35d98
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/recorded_experiment.rs
@@ -0,0 +1,35 @@
+// 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::collections::HashMap;
+
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Map as JsonMap, Value as JsonValue};
+
+/// Deserialized experiment data.
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
+pub struct RecordedExperiment {
+ /// The experiment's branch as set through [`set_experiment_active`](crate::glean_set_experiment_active).
+ pub branch: String,
+ /// Any extra data associated with this experiment through [`set_experiment_active`](crate::glean_set_experiment_active).
+ /// Note: `Option` required to keep backwards-compatibility.
+ pub extra: Option<HashMap<String, String>>,
+}
+
+impl RecordedExperiment {
+ /// Gets the recorded experiment data as a JSON value.
+ ///
+ /// For JSON, we don't want to include `{"extra": null}` -- we just want to skip
+ /// `extra` entirely. Unfortunately, we can't use a serde field annotation for this,
+ /// since that would break bincode serialization, which doesn't support skipping
+ /// fields. Therefore, we use a custom serialization function just for JSON here.
+ pub fn as_json(&self) -> JsonValue {
+ let mut value = JsonMap::new();
+ value.insert("branch".to_string(), json!(self.branch));
+ if self.extra.is_some() {
+ value.insert("extra".to_string(), json!(self.extra));
+ }
+ JsonValue::Object(value)
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/string.rs b/third_party/rust/glean-core/src/metrics/string.rs
new file mode 100644
index 0000000000..5ed7b2c7f1
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/string.rs
@@ -0,0 +1,176 @@
+// 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::sync::Arc;
+
+use crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{test_get_num_recorded_errors, ErrorType};
+use crate::metrics::Metric;
+use crate::metrics::MetricType;
+use crate::storage::StorageManager;
+use crate::util::truncate_string_at_boundary_with_error;
+use crate::CommonMetricData;
+use crate::Glean;
+
+const MAX_LENGTH_VALUE: usize = 100;
+
+/// A string metric.
+///
+/// Record an Unicode string value with arbitrary content.
+/// Strings are length-limited to `MAX_LENGTH_VALUE` bytes.
+#[derive(Clone, Debug)]
+pub struct StringMetric {
+ meta: Arc<CommonMetricDataInternal>,
+}
+
+impl MetricType for StringMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+
+ fn with_name(&self, name: String) -> Self {
+ let mut meta = (*self.meta).clone();
+ meta.inner.name = name;
+ Self {
+ meta: Arc::new(meta),
+ }
+ }
+
+ fn with_dynamic_label(&self, label: String) -> Self {
+ let mut meta = (*self.meta).clone();
+ meta.inner.dynamic_label = Some(label);
+ Self {
+ meta: Arc::new(meta),
+ }
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl StringMetric {
+ /// Creates a new string metric.
+ pub fn new(meta: CommonMetricData) -> Self {
+ Self {
+ meta: Arc::new(meta.into()),
+ }
+ }
+
+ /// Sets to the specified value.
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - The string to set the metric to.
+ ///
+ /// ## Notes
+ ///
+ /// Truncates the value if it is longer than `MAX_LENGTH_VALUE` bytes and logs an error.
+ pub fn set(&self, value: String) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.set_sync(glean, &value))
+ }
+
+ /// Sets to the specified value synchronously.
+ #[doc(hidden)]
+ pub fn set_sync<S: Into<String>>(&self, glean: &Glean, value: S) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let s = truncate_string_at_boundary_with_error(glean, &self.meta, value, MAX_LENGTH_VALUE);
+
+ let value = Metric::String(s);
+ glean.storage().record(glean, &self.meta, &value)
+ }
+
+ /// Gets the current-stored value as a string, or None if there is no value.
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<String> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::String(s)) => Some(s),
+ _ => None,
+ }
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as a string.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<String> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ /// **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) -> i32 {
+ crate::block_on_dispatcher();
+
+ crate::core::with_glean(|glean| {
+ test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::test_get_num_recorded_errors;
+ use crate::tests::new_glean;
+ use crate::util::truncate_string_at_boundary;
+ use crate::ErrorType;
+ use crate::Lifetime;
+
+ #[test]
+ fn setting_a_long_string_records_an_error() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = StringMetric::new(CommonMetricData {
+ name: "string_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["store1".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ dynamic_label: None,
+ });
+
+ let sample_string = "0123456789".repeat(11);
+ metric.set_sync(&glean, sample_string.clone());
+
+ let truncated = truncate_string_at_boundary(sample_string, MAX_LENGTH_VALUE);
+ assert_eq!(truncated, metric.get_value(&glean, "store1").unwrap());
+
+ assert_eq!(
+ 1,
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow)
+ .unwrap()
+ );
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/string_list.rs b/third_party/rust/glean-core/src/metrics/string_list.rs
new file mode 100644
index 0000000000..75b2df7f80
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/string_list.rs
@@ -0,0 +1,199 @@
+// 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::sync::Arc;
+
+use crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
+use crate::metrics::Metric;
+use crate::metrics::MetricType;
+use crate::storage::StorageManager;
+use crate::util::truncate_string_at_boundary_with_error;
+use crate::CommonMetricData;
+use crate::Glean;
+
+// Maximum length of any list
+const MAX_LIST_LENGTH: usize = 100;
+// Maximum length of any string in the list
+const MAX_STRING_LENGTH: usize = 100;
+
+/// A string list metric.
+///
+/// This allows appending a string value with arbitrary content to a list.
+#[derive(Clone, Debug)]
+pub struct StringListMetric {
+ meta: Arc<CommonMetricDataInternal>,
+}
+
+impl MetricType for StringListMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl StringListMetric {
+ /// Creates a new string list metric.
+ pub fn new(meta: CommonMetricData) -> Self {
+ Self {
+ meta: Arc::new(meta.into()),
+ }
+ }
+
+ /// Adds a new string to the list.
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - The string to add.
+ ///
+ /// ## Notes
+ ///
+ /// Truncates the value if it is longer than `MAX_STRING_LENGTH` bytes and logs an error.
+ pub fn add(&self, value: String) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.add_sync(glean, value))
+ }
+
+ /// Adds a new string to the list synchronously
+ #[doc(hidden)]
+ pub fn add_sync<S: Into<String>>(&self, glean: &Glean, value: S) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let value =
+ truncate_string_at_boundary_with_error(glean, &self.meta, value, MAX_STRING_LENGTH);
+ let mut error = None;
+ glean
+ .storage()
+ .record_with(glean, &self.meta, |old_value| match old_value {
+ Some(Metric::StringList(mut old_value)) => {
+ if old_value.len() == MAX_LIST_LENGTH {
+ let msg = format!(
+ "String list length of {} exceeds maximum of {}",
+ old_value.len() + 1,
+ MAX_LIST_LENGTH
+ );
+ error = Some(msg);
+ } else {
+ old_value.push(value.clone());
+ }
+ Metric::StringList(old_value)
+ }
+ _ => Metric::StringList(vec![value.clone()]),
+ });
+
+ if let Some(msg) = error {
+ record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
+ }
+ }
+
+ /// Sets to a specific list of strings.
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - The list of string to set the metric to.
+ ///
+ /// ## Notes
+ ///
+ /// If passed an empty list, records an error and returns.
+ ///
+ /// Truncates the list if it is longer than `MAX_LIST_LENGTH` and logs an error.
+ ///
+ /// Truncates any value in the list if it is longer than `MAX_STRING_LENGTH` and logs an error.
+ pub fn set(&self, values: Vec<String>) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.set_sync(glean, values))
+ }
+
+ /// Sets to a specific list of strings synchronously.
+ #[doc(hidden)]
+ pub fn set_sync(&self, glean: &Glean, value: Vec<String>) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let value = if value.len() > MAX_LIST_LENGTH {
+ let msg = format!(
+ "StringList length {} exceeds maximum of {}",
+ value.len(),
+ MAX_LIST_LENGTH
+ );
+ record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
+ value[0..MAX_LIST_LENGTH].to_vec()
+ } else {
+ value
+ };
+
+ let value = value
+ .into_iter()
+ .map(|elem| {
+ truncate_string_at_boundary_with_error(glean, &self.meta, elem, MAX_STRING_LENGTH)
+ })
+ .collect();
+
+ let value = Metric::StringList(value);
+ glean.storage().record(glean, &self.meta, &value);
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently-stored values.
+ ///
+ /// This doesn't clear the stored value.
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<Vec<String>> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::StringList(values)) => Some(values),
+ _ => None,
+ }
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently-stored values.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<Vec<String>> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ /// **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) -> i32 {
+ crate::block_on_dispatcher();
+
+ crate::core::with_glean(|glean| {
+ test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
+ })
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/text.rs b/third_party/rust/glean-core/src/metrics/text.rs
new file mode 100644
index 0000000000..06ad5c0d78
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/text.rs
@@ -0,0 +1,180 @@
+// 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::sync::Arc;
+
+use crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{test_get_num_recorded_errors, ErrorType};
+use crate::metrics::Metric;
+use crate::metrics::MetricType;
+use crate::storage::StorageManager;
+use crate::util::truncate_string_at_boundary_with_error;
+use crate::CommonMetricData;
+use crate::Glean;
+
+// The maximum number of characters for text.
+const MAX_LENGTH_VALUE: usize = 200 * 1024;
+
+/// A text metric.
+///
+/// Records a single long Unicode text,
+/// used when the limits on `String` are too low.
+/// Text is length-limited to `MAX_LENGTH_VALUE` bytes.
+#[derive(Clone, Debug)]
+pub struct TextMetric {
+ meta: Arc<CommonMetricDataInternal>,
+}
+
+impl MetricType for TextMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+
+ fn with_name(&self, name: String) -> Self {
+ let mut meta = (*self.meta).clone();
+ meta.inner.name = name;
+ Self {
+ meta: Arc::new(meta),
+ }
+ }
+
+ fn with_dynamic_label(&self, label: String) -> Self {
+ let mut meta = (*self.meta).clone();
+ meta.inner.dynamic_label = Some(label);
+ Self {
+ meta: Arc::new(meta),
+ }
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl TextMetric {
+ /// Creates a new text metric.
+ pub fn new(meta: CommonMetricData) -> Self {
+ Self {
+ meta: Arc::new(meta.into()),
+ }
+ }
+
+ /// Sets to the specified value.
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - The text to set the metric to.
+ ///
+ /// ## Notes
+ ///
+ /// Truncates the value (at codepoint boundaries) if it is longer than `MAX_LENGTH_VALUE` bytes
+ /// and logs an error.
+ pub fn set(&self, value: String) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.set_sync(glean, &value))
+ }
+
+ /// Sets to the specified value synchronously,
+ /// truncating and recording an error if longer than `MAX_LENGTH_VALUE`.
+ #[doc(hidden)]
+ pub fn set_sync<S: Into<String>>(&self, glean: &Glean, value: S) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let s = truncate_string_at_boundary_with_error(glean, &self.meta, value, MAX_LENGTH_VALUE);
+
+ let value = Metric::Text(s);
+ glean.storage().record(glean, &self.meta, &value)
+ }
+
+ /// Gets the currently-stored value as a string, or None if there is no value.
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<String> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::Text(s)) => Some(s),
+ _ => None,
+ }
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as a string.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<String> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ /// **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) -> i32 {
+ crate::block_on_dispatcher();
+
+ crate::core::with_glean(|glean| {
+ test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::test_get_num_recorded_errors;
+ use crate::tests::new_glean;
+ use crate::util::truncate_string_at_boundary;
+ use crate::ErrorType;
+ use crate::Lifetime;
+
+ #[test]
+ fn setting_a_long_string_records_an_error() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = TextMetric::new(CommonMetricData {
+ name: "text_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["store1".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ dynamic_label: None,
+ });
+
+ let sample_string = "0123456789".repeat(200 * 1024);
+ metric.set_sync(&glean, sample_string.clone());
+
+ let truncated = truncate_string_at_boundary(sample_string, MAX_LENGTH_VALUE);
+ assert_eq!(truncated, metric.get_value(&glean, "store1").unwrap());
+
+ assert_eq!(
+ 1,
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow)
+ .unwrap()
+ );
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/time_unit.rs b/third_party/rust/glean-core/src/metrics/time_unit.rs
new file mode 100644
index 0000000000..6d61a8a242
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/time_unit.rs
@@ -0,0 +1,117 @@
+// 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::convert::TryFrom;
+use std::time::Duration;
+
+use serde::{Deserialize, Serialize};
+
+use crate::error::{Error, ErrorKind};
+
+/// Different resolutions supported by the time related
+/// metric types (e.g. DatetimeMetric).
+#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(rename_all = "lowercase")]
+#[repr(i32)] // use i32 to be compatible with our JNA definition
+pub enum TimeUnit {
+ /// Truncate to nanosecond precision.
+ Nanosecond,
+ /// Truncate to microsecond precision.
+ Microsecond,
+ /// Truncate to millisecond precision.
+ Millisecond,
+ /// Truncate to second precision.
+ Second,
+ /// Truncate to minute precision.
+ Minute,
+ /// Truncate to hour precision.
+ Hour,
+ /// Truncate to day precision.
+ Day,
+}
+
+impl TimeUnit {
+ /// Formats the given time unit, truncating the time if needed.
+ pub fn format_pattern(self) -> &'static str {
+ use TimeUnit::*;
+ match self {
+ Nanosecond => "%Y-%m-%dT%H:%M:%S%.f%:z",
+ Microsecond => "%Y-%m-%dT%H:%M:%S%.6f%:z",
+ Millisecond => "%Y-%m-%dT%H:%M:%S%.3f%:z",
+ Second => "%Y-%m-%dT%H:%M:%S%:z",
+ Minute => "%Y-%m-%dT%H:%M%:z",
+ Hour => "%Y-%m-%dT%H%:z",
+ Day => "%Y-%m-%d%:z",
+ }
+ }
+
+ /// Converts a duration to the requested time unit.
+ ///
+ /// # Arguments
+ ///
+ /// * `duration` - the duration to convert.
+ ///
+ /// # Returns
+ ///
+ /// The integer representation of the converted duration.
+ pub fn duration_convert(self, duration: Duration) -> u64 {
+ use TimeUnit::*;
+ match self {
+ Nanosecond => duration.as_nanos() as u64,
+ Microsecond => duration.as_micros() as u64,
+ Millisecond => duration.as_millis() as u64,
+ Second => duration.as_secs(),
+ Minute => duration.as_secs() / 60,
+ Hour => duration.as_secs() / 60 / 60,
+ Day => duration.as_secs() / 60 / 60 / 24,
+ }
+ }
+
+ /// Converts a duration in the given unit to nanoseconds.
+ ///
+ /// # Arguments
+ ///
+ /// * `duration` - the duration to convert.
+ ///
+ /// # Returns
+ ///
+ /// The integer representation of the nanosecond duration.
+ pub fn as_nanos(self, duration: u64) -> u64 {
+ use TimeUnit::*;
+ let duration = match self {
+ Nanosecond => Duration::from_nanos(duration),
+ Microsecond => Duration::from_micros(duration),
+ Millisecond => Duration::from_millis(duration),
+ Second => Duration::from_secs(duration),
+ Minute => Duration::from_secs(duration * 60),
+ Hour => Duration::from_secs(duration * 60 * 60),
+ Day => Duration::from_secs(duration * 60 * 60 * 24),
+ };
+
+ duration.as_nanos() as u64
+ }
+}
+
+/// Trait implementation for converting an integer value to a TimeUnit.
+///
+/// This is used in the FFI code.
+///
+/// Please note that values should match the ordering of the
+/// platform specific side of things (e.g. Kotlin implementation).
+impl TryFrom<i32> for TimeUnit {
+ type Error = Error;
+
+ fn try_from(value: i32) -> Result<TimeUnit, Self::Error> {
+ match value {
+ 0 => Ok(TimeUnit::Nanosecond),
+ 1 => Ok(TimeUnit::Microsecond),
+ 2 => Ok(TimeUnit::Millisecond),
+ 3 => Ok(TimeUnit::Second),
+ 4 => Ok(TimeUnit::Minute),
+ 5 => Ok(TimeUnit::Hour),
+ 6 => Ok(TimeUnit::Day),
+ e => Err(ErrorKind::TimeUnit(e).into()),
+ }
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/timespan.rs b/third_party/rust/glean-core/src/metrics/timespan.rs
new file mode 100644
index 0000000000..b4d3bd5902
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/timespan.rs
@@ -0,0 +1,308 @@
+// 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::convert::TryInto;
+use std::sync::{Arc, RwLock};
+use std::time::Duration;
+
+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::CommonMetricData;
+use crate::Glean;
+
+/// A timespan metric.
+///
+/// Timespans are used to make a measurement of how much time is spent in a particular task.
+///
+// Implementation note:
+// Because we dispatch this, we handle this with interior mutability.
+// The whole struct is clonable, but that's comparable cheap, as it does not clone the data.
+// Cloning `CommonMetricData` is not free, as it contains strings, so we also wrap that in an Arc.
+#[derive(Clone, Debug)]
+pub struct TimespanMetric {
+ meta: Arc<CommonMetricDataInternal>,
+ time_unit: TimeUnit,
+ start_time: Arc<RwLock<Option<u64>>>,
+}
+
+impl MetricType for TimespanMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl TimespanMetric {
+ /// Creates a new timespan metric.
+ pub fn new(meta: CommonMetricData, time_unit: TimeUnit) -> Self {
+ Self {
+ meta: Arc::new(meta.into()),
+ time_unit,
+ start_time: Arc::new(RwLock::new(None)),
+ }
+ }
+
+ /// Starts tracking time for the provided metric.
+ ///
+ /// This records an error if it's already tracking time (i.e. start was
+ /// already called with no corresponding
+ /// [`set_stop`](TimespanMetric::set_stop)): in that case the original start
+ /// time will be preserved.
+ pub fn start(&self) {
+ let start_time = time::precise_time_ns();
+
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.set_start(glean, start_time));
+ }
+
+ /// Set start time synchronously.
+ #[doc(hidden)]
+ pub fn set_start(&self, glean: &Glean, start_time: u64) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let mut lock = self
+ .start_time
+ .write()
+ .expect("Lock poisoned for timespan metric on start.");
+
+ if lock.is_some() {
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidState,
+ "Timespan already started",
+ None,
+ );
+ return;
+ }
+
+ *lock = Some(start_time);
+ }
+
+ /// Stops tracking time for the provided metric. Sets the metric to the elapsed time.
+ ///
+ /// This will record an error if no [`set_start`](TimespanMetric::set_start) was called.
+ pub fn stop(&self) {
+ let stop_time = time::precise_time_ns();
+
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.set_stop(glean, stop_time));
+ }
+
+ /// Set stop time synchronously.
+ #[doc(hidden)]
+ pub fn set_stop(&self, glean: &Glean, stop_time: u64) {
+ // Need to write in either case, so get the lock first.
+ let mut lock = self
+ .start_time
+ .write()
+ .expect("Lock poisoned for timespan metric on stop.");
+
+ if !self.should_record(glean) {
+ // Reset timer when disabled, so that we don't record timespans across
+ // disabled/enabled toggling.
+ *lock = None;
+ return;
+ }
+
+ if lock.is_none() {
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidState,
+ "Timespan not running",
+ None,
+ );
+ return;
+ }
+
+ let start_time = lock.take().unwrap();
+ let duration = match stop_time.checked_sub(start_time) {
+ Some(duration) => duration,
+ None => {
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidValue,
+ "Timespan was negative",
+ None,
+ );
+ return;
+ }
+ };
+ let duration = Duration::from_nanos(duration);
+ self.set_raw_inner(glean, duration);
+ }
+
+ /// Aborts a previous [`set_start`](TimespanMetric::set_start) call. No
+ /// error is recorded if no [`set_start`](TimespanMetric::set_start) was
+ /// called.
+ pub fn cancel(&self) {
+ let metric = self.clone();
+ crate::dispatcher::launch(move || {
+ let mut lock = metric
+ .start_time
+ .write()
+ .expect("Lock poisoned for timespan metric on cancel.");
+ *lock = None;
+ });
+ }
+
+ /// Explicitly sets the timespan value.
+ ///
+ /// This API should only be used if your library or application requires
+ /// recording times in a way that can not make use of
+ /// [`set_start`](TimespanMetric::set_start)/[`set_stop`](TimespanMetric::set_stop)/[`cancel`](TimespanMetric::cancel).
+ ///
+ /// Care should be taken using this if the ping lifetime might contain more
+ /// than one timespan measurement. To be safe,
+ /// [`set_raw`](TimespanMetric::set_raw) should generally be followed by
+ /// sending a custom ping containing the timespan.
+ ///
+ /// # Arguments
+ ///
+ /// * `elapsed` - The elapsed time to record.
+ pub fn set_raw(&self, elapsed: Duration) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.set_raw_sync(glean, elapsed));
+ }
+
+ /// Explicitly sets the timespan value in nanoseconds.
+ ///
+ /// This API should only be used if your library or application requires
+ /// recording times in a way that can not make use of
+ /// [`set_start`](TimespanMetric::set_start)/[`set_stop`](TimespanMetric::set_stop)/[`cancel`](TimespanMetric::cancel).
+ ///
+ /// Care should be taken using this if the ping lifetime might contain more
+ /// than one timespan measurement. To be safe,
+ /// [`set_raw`](TimespanMetric::set_raw) should generally be followed by
+ /// sending a custom ping containing the timespan.
+ ///
+ /// # Arguments
+ ///
+ /// * `elapsed_nanos` - The elapsed time to record, in nanoseconds.
+ pub fn set_raw_nanos(&self, elapsed_nanos: i64) {
+ let elapsed = Duration::from_nanos(elapsed_nanos.try_into().unwrap_or(0));
+ self.set_raw(elapsed)
+ }
+
+ /// Explicitly sets the timespan value synchronously.
+ #[doc(hidden)]
+ pub fn set_raw_sync(&self, glean: &Glean, elapsed: Duration) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let lock = self
+ .start_time
+ .read()
+ .expect("Lock poisoned for timespan metric on set_raw.");
+
+ if lock.is_some() {
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidState,
+ "Timespan already running. Raw value not recorded.",
+ None,
+ );
+ return;
+ }
+
+ self.set_raw_inner(glean, elapsed);
+ }
+
+ fn set_raw_inner(&self, glean: &Glean, elapsed: Duration) {
+ let mut report_value_exists: bool = false;
+ glean.storage().record_with(glean, &self.meta, |old_value| {
+ match old_value {
+ Some(old @ Metric::Timespan(..)) => {
+ // If some value already exists, report an error.
+ // We do this out of the storage since recording an
+ // error accesses the storage as well.
+ report_value_exists = true;
+ old
+ }
+ _ => Metric::Timespan(elapsed, self.time_unit),
+ }
+ });
+
+ if report_value_exists {
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidState,
+ "Timespan value already recorded. New value discarded.",
+ None,
+ );
+ };
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as an integer.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<i64> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| {
+ self.get_value(glean, ping_name.as_deref()).map(|val| {
+ val.try_into()
+ .expect("Timespan can't be represented as i64")
+ })
+ })
+ }
+
+ /// Get the current value
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<u64> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::Timespan(time, time_unit)) => Some(time_unit.duration_convert(time)),
+ _ => None,
+ }
+ }
+
+ /// **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) -> i32 {
+ crate::block_on_dispatcher();
+
+ crate::core::with_glean(|glean| {
+ test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
+ })
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/timing_distribution.rs b/third_party/rust/glean-core/src/metrics/timing_distribution.rs
new file mode 100644
index 0000000000..e339ef8882
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/timing_distribution.rs
@@ -0,0 +1,557 @@
+// 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::collections::HashMap;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::{Arc, Mutex};
+
+use crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
+use crate::histogram::{Functional, Histogram};
+use crate::metrics::time_unit::TimeUnit;
+use crate::metrics::{DistributionData, Metric, MetricType};
+use crate::storage::StorageManager;
+use crate::CommonMetricData;
+use crate::Glean;
+
+// The base of the logarithm used to determine bucketing
+const LOG_BASE: f64 = 2.0;
+
+// The buckets per each order of magnitude of the logarithm.
+const BUCKETS_PER_MAGNITUDE: f64 = 8.0;
+
+// Maximum time, which means we retain a maximum of 316 buckets.
+// It is automatically adjusted based on the `time_unit` parameter
+// so that:
+//
+// - `nanosecond` - 10 minutes
+// - `microsecond` - ~6.94 days
+// - `millisecond` - ~19 years
+const MAX_SAMPLE_TIME: u64 = 1000 * 1000 * 1000 * 60 * 10;
+
+/// Identifier for a running timer.
+///
+/// Its internals are considered private,
+/// but due to UniFFI's behavior we expose its field for now.
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
+pub struct TimerId {
+ /// This timer's id.
+ pub id: u64,
+}
+
+impl From<u64> for TimerId {
+ fn from(val: u64) -> TimerId {
+ TimerId { id: val }
+ }
+}
+
+impl From<usize> for TimerId {
+ fn from(val: usize) -> TimerId {
+ TimerId { id: val as u64 }
+ }
+}
+
+/// A timing distribution metric.
+///
+/// Timing distributions are used to accumulate and store time measurement, for analyzing distributions of the timing data.
+#[derive(Clone, Debug)]
+pub struct TimingDistributionMetric {
+ meta: Arc<CommonMetricDataInternal>,
+ time_unit: TimeUnit,
+ next_id: Arc<AtomicUsize>,
+ start_times: Arc<Mutex<HashMap<TimerId, u64>>>,
+}
+
+/// Create a snapshot of the histogram with a time unit.
+///
+/// The snapshot can be serialized into the payload format.
+pub(crate) fn snapshot(hist: &Histogram<Functional>) -> DistributionData {
+ DistributionData {
+ // **Caution**: This cannot use `Histogram::snapshot_values` and needs to use the more
+ // specialized snapshot function.
+ values: hist
+ .snapshot()
+ .into_iter()
+ .map(|(k, v)| (k as i64, v as i64))
+ .collect(),
+ sum: hist.sum() as i64,
+ count: hist.count() as i64,
+ }
+}
+
+impl MetricType for TimingDistributionMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl TimingDistributionMetric {
+ /// Creates a new timing distribution metric.
+ pub fn new(meta: CommonMetricData, time_unit: TimeUnit) -> Self {
+ Self {
+ meta: Arc::new(meta.into()),
+ time_unit,
+ next_id: Arc::new(AtomicUsize::new(0)),
+ start_times: Arc::new(Mutex::new(Default::default())),
+ }
+ }
+
+ /// Starts tracking time for the provided metric.
+ ///
+ /// This records an error if it’s already tracking time (i.e.
+ /// [`set_start`](TimingDistributionMetric::set_start) was already called with no
+ /// corresponding [`set_stop_and_accumulate`](TimingDistributionMetric::set_stop_and_accumulate)): in
+ /// that case the original start time will be preserved.
+ ///
+ /// # Arguments
+ ///
+ /// * `start_time` - Timestamp in nanoseconds.
+ ///
+ /// # Returns
+ ///
+ /// A unique [`TimerId`] for the new timer.
+ pub fn start(&self) -> TimerId {
+ let start_time = time::precise_time_ns();
+ let id = self.next_id.fetch_add(1, Ordering::SeqCst).into();
+ let metric = self.clone();
+ crate::launch_with_glean(move |_glean| metric.set_start(id, start_time));
+ id
+ }
+
+ pub(crate) fn start_sync(&self) -> TimerId {
+ let start_time = time::precise_time_ns();
+ let id = self.next_id.fetch_add(1, Ordering::SeqCst).into();
+ let metric = self.clone();
+ metric.set_start(id, start_time);
+ id
+ }
+
+ /// **Test-only API (exported for testing purposes).**
+ ///
+ /// Set start time for this metric synchronously.
+ ///
+ /// Use [`start`](Self::start) instead.
+ #[doc(hidden)]
+ pub fn set_start(&self, id: TimerId, start_time: u64) {
+ let mut map = self.start_times.lock().expect("can't lock timings map");
+ map.insert(id, start_time);
+ }
+
+ /// Stops tracking time for the provided metric and associated timer id.
+ ///
+ /// Adds a count to the corresponding bucket in the timing distribution.
+ /// This will record an error if no
+ /// [`set_start`](TimingDistributionMetric::set_start) was called.
+ ///
+ /// # Arguments
+ ///
+ /// * `id` - The [`TimerId`] to associate with this timing. This allows
+ /// for concurrent timing of events associated with different ids to the
+ /// same timespan metric.
+ /// * `stop_time` - Timestamp in nanoseconds.
+ pub fn stop_and_accumulate(&self, id: TimerId) {
+ let stop_time = time::precise_time_ns();
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.set_stop_and_accumulate(glean, id, stop_time));
+ }
+
+ fn set_stop(&self, id: TimerId, stop_time: u64) -> Result<u64, (ErrorType, &str)> {
+ let mut start_times = self.start_times.lock().expect("can't lock timings map");
+ let start_time = match start_times.remove(&id) {
+ Some(start_time) => start_time,
+ None => return Err((ErrorType::InvalidState, "Timing not running")),
+ };
+
+ let duration = match stop_time.checked_sub(start_time) {
+ Some(duration) => duration,
+ None => {
+ return Err((
+ ErrorType::InvalidValue,
+ "Timer stopped with negative duration",
+ ))
+ }
+ };
+
+ Ok(duration)
+ }
+
+ /// **Test-only API (exported for testing purposes).**
+ ///
+ /// Set stop time for this metric synchronously.
+ ///
+ /// Use [`stop_and_accumulate`](Self::stop_and_accumulate) instead.
+ #[doc(hidden)]
+ pub fn set_stop_and_accumulate(&self, glean: &Glean, id: TimerId, stop_time: u64) {
+ if !self.should_record(glean) {
+ let mut start_times = self.start_times.lock().expect("can't lock timings map");
+ start_times.remove(&id);
+ return;
+ }
+
+ // Duration is in nanoseconds.
+ let mut duration = match self.set_stop(id, stop_time) {
+ Err((err_type, err_msg)) => {
+ record_error(glean, &self.meta, err_type, err_msg, None);
+ return;
+ }
+ Ok(duration) => duration,
+ };
+
+ let min_sample_time = self.time_unit.as_nanos(1);
+ let max_sample_time = self.time_unit.as_nanos(MAX_SAMPLE_TIME);
+
+ duration = if duration < min_sample_time {
+ // If measurement is less than the minimum, just truncate. This is
+ // not recorded as an error.
+ min_sample_time
+ } else if duration > max_sample_time {
+ let msg = format!(
+ "Sample is longer than the max for a time_unit of {:?} ({} ns)",
+ self.time_unit, max_sample_time
+ );
+ record_error(glean, &self.meta, ErrorType::InvalidOverflow, msg, None);
+ max_sample_time
+ } else {
+ duration
+ };
+
+ if !self.should_record(glean) {
+ return;
+ }
+
+ // Let's be defensive here:
+ // The uploader tries to store some timing distribution metrics,
+ // but in tests that storage might be gone already.
+ // Let's just ignore those.
+ // We do the same for counters.
+ // This should never happen in real app usage.
+ if let Some(storage) = glean.storage_opt() {
+ storage.record_with(glean, &self.meta, |old_value| match old_value {
+ Some(Metric::TimingDistribution(mut hist)) => {
+ hist.accumulate(duration);
+ Metric::TimingDistribution(hist)
+ }
+ _ => {
+ let mut hist = Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE);
+ hist.accumulate(duration);
+ Metric::TimingDistribution(hist)
+ }
+ });
+ } else {
+ log::warn!(
+ "Couldn't get storage. Can't record timing distribution '{}'.",
+ self.meta.base_identifier()
+ );
+ }
+ }
+
+ /// Aborts a previous [`start`](Self::start) call.
+ ///
+ /// No error is recorded if no [`start`](Self::start) was called.
+ ///
+ /// # Arguments
+ ///
+ /// * `id` - The [`TimerId`] to associate with this timing. This allows
+ /// for concurrent timing of events associated with different ids to the
+ /// same timing distribution metric.
+ pub fn cancel(&self, id: TimerId) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |_glean| metric.cancel_sync(id));
+ }
+
+ /// Aborts a previous [`start`](Self::start) call synchronously.
+ pub(crate) fn cancel_sync(&self, id: TimerId) {
+ let mut map = self.start_times.lock().expect("can't lock timings map");
+ map.remove(&id);
+ }
+
+ /// Accumulates the provided signed samples in the metric.
+ ///
+ /// This is required so that the platform-specific code can provide us with
+ /// 64 bit signed integers if no `u64` comparable type is available. This
+ /// will take care of filtering and reporting errors for any provided negative
+ /// sample.
+ ///
+ /// Please note that this assumes that the provided samples are already in
+ /// the "unit" declared by the instance of the metric type (e.g. if the
+ /// instance this method was called on is using [`TimeUnit::Second`], then
+ /// `samples` are assumed to be in that unit).
+ ///
+ /// # Arguments
+ ///
+ /// * `samples` - The vector holding the samples to be recorded by the metric.
+ ///
+ /// ## Notes
+ ///
+ /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`]
+ /// for each of them. Reports an [`ErrorType::InvalidOverflow`] error for samples that
+ /// are longer than `MAX_SAMPLE_TIME`.
+ pub fn accumulate_samples(&self, samples: Vec<i64>) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.accumulate_samples_sync(glean, samples))
+ }
+
+ /// **Test-only API (exported for testing purposes).**
+ /// Accumulates the provided signed samples in the metric.
+ ///
+ /// Use [`accumulate_samples`](Self::accumulate_samples)
+ #[doc(hidden)]
+ pub fn accumulate_samples_sync(&self, glean: &Glean, samples: Vec<i64>) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let mut num_negative_samples = 0;
+ let mut num_too_long_samples = 0;
+ let max_sample_time = self.time_unit.as_nanos(MAX_SAMPLE_TIME);
+
+ glean.storage().record_with(glean, &self.meta, |old_value| {
+ let mut hist = match old_value {
+ Some(Metric::TimingDistribution(hist)) => hist,
+ _ => Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE),
+ };
+
+ for &sample in samples.iter() {
+ if sample < 0 {
+ num_negative_samples += 1;
+ } else {
+ let mut sample = sample as u64;
+
+ // Check the range prior to converting the incoming unit to
+ // nanoseconds, so we can compare against the constant
+ // MAX_SAMPLE_TIME.
+ if sample == 0 {
+ sample = 1;
+ } else if sample > MAX_SAMPLE_TIME {
+ num_too_long_samples += 1;
+ sample = MAX_SAMPLE_TIME;
+ }
+
+ sample = self.time_unit.as_nanos(sample);
+
+ hist.accumulate(sample);
+ }
+ }
+
+ Metric::TimingDistribution(hist)
+ });
+
+ if num_negative_samples > 0 {
+ let msg = format!("Accumulated {} negative samples", num_negative_samples);
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidValue,
+ msg,
+ num_negative_samples,
+ );
+ }
+
+ if num_too_long_samples > 0 {
+ let msg = format!(
+ "{} samples are longer than the maximum of {}",
+ num_too_long_samples, max_sample_time
+ );
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidOverflow,
+ msg,
+ num_too_long_samples,
+ );
+ }
+ }
+
+ /// Accumulates the provided samples in the metric.
+ ///
+ /// # Arguments
+ ///
+ /// * `samples` - A list of samples recorded by the metric.
+ /// Samples must be in nanoseconds.
+ /// ## Notes
+ ///
+ /// Reports an [`ErrorType::InvalidOverflow`] error for samples that
+ /// are longer than `MAX_SAMPLE_TIME`.
+ pub fn accumulate_raw_samples_nanos(&self, samples: Vec<u64>) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| {
+ metric.accumulate_raw_samples_nanos_sync(glean, &samples)
+ })
+ }
+
+ /// **Test-only API (exported for testing purposes).**
+ ///
+ /// Accumulates the provided samples in the metric.
+ ///
+ /// Use [`accumulate_raw_samples_nanos`](Self::accumulate_raw_samples_nanos) instead.
+ #[doc(hidden)]
+ pub fn accumulate_raw_samples_nanos_sync(&self, glean: &Glean, samples: &[u64]) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let mut num_too_long_samples = 0;
+ let min_sample_time = self.time_unit.as_nanos(1);
+ let max_sample_time = self.time_unit.as_nanos(MAX_SAMPLE_TIME);
+
+ glean.storage().record_with(glean, &self.meta, |old_value| {
+ let mut hist = match old_value {
+ Some(Metric::TimingDistribution(hist)) => hist,
+ _ => Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE),
+ };
+
+ for &sample in samples.iter() {
+ let mut sample = sample;
+
+ if sample < min_sample_time {
+ sample = min_sample_time;
+ } else if sample > max_sample_time {
+ num_too_long_samples += 1;
+ sample = max_sample_time;
+ }
+
+ // `sample` is in nanoseconds.
+ hist.accumulate(sample);
+ }
+
+ Metric::TimingDistribution(hist)
+ });
+
+ if num_too_long_samples > 0 {
+ let msg = format!(
+ "{} samples are longer than the maximum of {}",
+ num_too_long_samples, max_sample_time
+ );
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidOverflow,
+ msg,
+ num_too_long_samples,
+ );
+ }
+ }
+
+ /// Gets the currently stored value as an integer.
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<DistributionData> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::TimingDistribution(hist)) => Some(snapshot(&hist)),
+ _ => None,
+ }
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as an integer.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<DistributionData> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ /// **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) -> i32 {
+ crate::block_on_dispatcher();
+
+ crate::core::with_glean(|glean| {
+ test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn can_snapshot() {
+ use serde_json::json;
+
+ let mut hist = Histogram::functional(2.0, 8.0);
+
+ for i in 1..=10 {
+ hist.accumulate(i);
+ }
+
+ let snap = snapshot(&hist);
+
+ let expected_json = json!({
+ "sum": 55,
+ "values": {
+ "1": 1,
+ "2": 1,
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 1,
+ "8": 1,
+ "9": 1,
+ "10": 1,
+ "11": 0,
+ },
+ });
+
+ assert_eq!(expected_json, json!(snap));
+ }
+
+ #[test]
+ fn can_snapshot_sparse() {
+ use serde_json::json;
+
+ let mut hist = Histogram::functional(2.0, 8.0);
+
+ hist.accumulate(1024);
+ hist.accumulate(1024);
+ hist.accumulate(1116);
+ hist.accumulate(1448);
+
+ let snap = snapshot(&hist);
+
+ let expected_json = json!({
+ "sum": 4612,
+ "values": {
+ "1024": 2,
+ "1116": 1,
+ "1217": 0,
+ "1327": 0,
+ "1448": 1,
+ "1579": 0,
+ },
+ });
+
+ assert_eq!(expected_json, json!(snap));
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/url.rs b/third_party/rust/glean-core/src/metrics/url.rs
new file mode 100644
index 0000000000..c9eb824a3e
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/url.rs
@@ -0,0 +1,312 @@
+// 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::sync::Arc;
+
+use crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
+use crate::metrics::Metric;
+use crate::metrics::MetricType;
+use crate::storage::StorageManager;
+use crate::util::truncate_string_at_boundary_with_error;
+use crate::CommonMetricData;
+use crate::Glean;
+
+// The maximum number of characters a URL Metric may have, before encoding.
+const MAX_URL_LENGTH: usize = 8192;
+
+/// A URL metric.
+///
+/// Record an Unicode string value a URL content.
+/// The URL is length-limited to `MAX_URL_LENGTH` bytes.
+#[derive(Clone, Debug)]
+pub struct UrlMetric {
+ meta: Arc<CommonMetricDataInternal>,
+}
+
+impl MetricType for UrlMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl UrlMetric {
+ /// Creates a new string metric.
+ pub fn new(meta: CommonMetricData) -> Self {
+ Self {
+ meta: Arc::new(meta.into()),
+ }
+ }
+
+ fn is_valid_url_scheme(&self, value: String) -> bool {
+ let mut splits = value.split(':');
+ if let Some(scheme) = splits.next() {
+ if scheme.is_empty() {
+ return false;
+ }
+ let mut chars = scheme.chars();
+ // The list of characters allowed in the scheme is on
+ // the spec here: https://url.spec.whatwg.org/#url-scheme-string
+ return chars.next().unwrap().is_ascii_alphabetic()
+ && chars.all(|c| c.is_ascii_alphanumeric() || ['+', '-', '.'].contains(&c));
+ }
+
+ // No ':' found, this is not valid :)
+ false
+ }
+
+ /// Sets to the specified stringified URL.
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - The stringified URL to set the metric to.
+ ///
+ /// ## Notes
+ ///
+ /// Truncates the value if it is longer than `MAX_URL_LENGTH` bytes and logs an error.
+ pub fn set<S: Into<String>>(&self, value: S) {
+ let value = value.into();
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.set_sync(glean, value))
+ }
+
+ /// Sets to the specified stringified URL synchronously.
+ #[doc(hidden)]
+ pub fn set_sync<S: Into<String>>(&self, glean: &Glean, value: S) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let s = truncate_string_at_boundary_with_error(glean, &self.meta, value, MAX_URL_LENGTH);
+
+ if s.starts_with("data:") {
+ record_error(
+ glean,
+ &self.meta,
+ ErrorType::InvalidValue,
+ "URL metric does not support data URLs.",
+ None,
+ );
+ return;
+ }
+
+ if !self.is_valid_url_scheme(s.clone()) {
+ let msg = format!("\"{}\" does not start with a valid URL scheme.", s);
+ record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
+ return;
+ }
+
+ let value = Metric::Url(s);
+ glean.storage().record(glean, &self.meta, &value)
+ }
+
+ #[doc(hidden)]
+ pub(crate) fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<String> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::Url(s)) => Some(s),
+ _ => None,
+ }
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as a string.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<String> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
+ }
+
+ /// **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) -> i32 {
+ crate::block_on_dispatcher();
+
+ crate::core::with_glean(|glean| {
+ test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::test_get_num_recorded_errors;
+ use crate::tests::new_glean;
+ use crate::ErrorType;
+ use crate::Lifetime;
+
+ #[test]
+ fn payload_is_correct() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = UrlMetric::new(CommonMetricData {
+ name: "url_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["store1".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ dynamic_label: None,
+ });
+
+ let sample_url = "glean://test".to_string();
+ metric.set_sync(&glean, sample_url.clone());
+ assert_eq!(sample_url, metric.get_value(&glean, "store1").unwrap());
+ }
+
+ #[test]
+ fn does_not_record_url_exceeding_maximum_length() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = UrlMetric::new(CommonMetricData {
+ name: "url_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["store1".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ dynamic_label: None,
+ });
+
+ // Whenever the URL is longer than our MAX_URL_LENGTH, we truncate the URL to the
+ // MAX_URL_LENGTH.
+ //
+ // This 8-character string was chosen so we could have an even number that is
+ // a divisor of our MAX_URL_LENGTH.
+ let long_path_base = "abcdefgh";
+
+ // Using 2000 creates a string > 16000 characters, well over MAX_URL_LENGTH.
+ let test_url = format!("glean://{}", long_path_base.repeat(2000));
+ metric.set_sync(&glean, test_url);
+
+ // "glean://" is 8 characters
+ // "abcdefgh" (long_path_base) is 8 characters
+ // `long_path_base` is repeated 1023 times (8184)
+ // 8 + 8184 = 8192 (MAX_URL_LENGTH)
+ let expected = format!("glean://{}", long_path_base.repeat(1023));
+
+ assert_eq!(metric.get_value(&glean, "store1").unwrap(), expected);
+ assert_eq!(
+ 1,
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow)
+ .unwrap()
+ );
+ }
+
+ #[test]
+ fn does_not_record_data_urls() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = UrlMetric::new(CommonMetricData {
+ name: "url_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["store1".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ dynamic_label: None,
+ });
+
+ let test_url = "data:application/json";
+ metric.set_sync(&glean, test_url);
+
+ assert!(metric.get_value(&glean, "store1").is_none());
+
+ assert_eq!(
+ 1,
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).unwrap()
+ );
+ }
+
+ #[test]
+ fn url_validation_works_and_records_errors() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = UrlMetric::new(CommonMetricData {
+ name: "url_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["store1".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ dynamic_label: None,
+ });
+
+ let incorrects = vec![
+ "",
+ // Scheme may only start with upper or lowercase ASCII alpha[^1] character.
+ // [1]: https://infra.spec.whatwg.org/#ascii-alpha
+ "1glean://test",
+ "-glean://test",
+ // Scheme may only have ASCII alphanumeric characters or the `-`, `.`, `+` characters.
+ "шеллы://test",
+ "g!lean://test",
+ "g=lean://test",
+ // Scheme must be followed by `:` character.
+ "glean//test",
+ ];
+
+ let corrects = vec![
+ // The minimum URL
+ "g:",
+ // Empty body is fine
+ "glean://",
+ // "//" is actually not even necessary
+ "glean:",
+ "glean:test",
+ "glean:test.com",
+ // Scheme may only have ASCII alphanumeric characters or the `-`, `.`, `+` characters.
+ "g-lean://test",
+ "g+lean://test",
+ "g.lean://test",
+ // Query parameters are fine
+ "glean://test?hello=world",
+ // Finally, some actual real world URLs
+ "https://infra.spec.whatwg.org/#ascii-alpha",
+ "https://infra.spec.whatwg.org/#ascii-alpha?test=for-glean",
+ ];
+
+ for incorrect in incorrects.clone().into_iter() {
+ metric.set_sync(&glean, incorrect);
+ assert!(metric.get_value(&glean, "store1").is_none());
+ }
+
+ assert_eq!(
+ incorrects.len(),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).unwrap()
+ as usize
+ );
+
+ for correct in corrects.into_iter() {
+ metric.set_sync(&glean, correct);
+ assert_eq!(metric.get_value(&glean, "store1").unwrap(), correct);
+ }
+ }
+}
diff --git a/third_party/rust/glean-core/src/metrics/uuid.rs b/third_party/rust/glean-core/src/metrics/uuid.rs
new file mode 100644
index 0000000000..e78d15ad3b
--- /dev/null
+++ b/third_party/rust/glean-core/src/metrics/uuid.rs
@@ -0,0 +1,159 @@
+// 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::sync::Arc;
+
+use uuid::Uuid;
+
+use crate::common_metric_data::CommonMetricDataInternal;
+use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
+use crate::metrics::Metric;
+use crate::metrics::MetricType;
+use crate::storage::StorageManager;
+use crate::CommonMetricData;
+use crate::Glean;
+
+/// An UUID metric.
+///
+/// Stores UUID v4 (randomly generated) values.
+#[derive(Clone, Debug)]
+pub struct UuidMetric {
+ meta: Arc<CommonMetricDataInternal>,
+}
+
+impl MetricType for UuidMetric {
+ fn meta(&self) -> &CommonMetricDataInternal {
+ &self.meta
+ }
+}
+
+// IMPORTANT:
+//
+// When changing this implementation, make sure all the operations are
+// also declared in the related trait in `../traits/`.
+impl UuidMetric {
+ /// Creates a new UUID metric
+ pub fn new(meta: CommonMetricData) -> Self {
+ Self {
+ meta: Arc::new(meta.into()),
+ }
+ }
+
+ /// Sets to the specified value.
+ ///
+ /// # Arguments
+ ///
+ /// * `value` - The [`Uuid`] to set the metric to.
+ pub fn set(&self, value: String) {
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.set_sync(glean, &value))
+ }
+
+ /// Sets to the specified value synchronously.
+ #[doc(hidden)]
+ pub fn set_sync<S: Into<String>>(&self, glean: &Glean, value: S) {
+ if !self.should_record(glean) {
+ return;
+ }
+
+ let value = value.into();
+
+ if let Ok(uuid) = uuid::Uuid::parse_str(&value) {
+ let value = Metric::Uuid(uuid.as_hyphenated().to_string());
+ glean.storage().record(glean, &self.meta, &value)
+ } else {
+ let msg = format!("Unexpected UUID value '{}'", value);
+ record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
+ }
+ }
+
+ /// Sets to the specified value, from a string.
+ ///
+ /// This should only be used from FFI. When calling directly from Rust, it
+ /// is better to use [`set`](UuidMetric::set).
+ ///
+ /// # Arguments
+ ///
+ /// * `glean` - The Glean instance this metric belongs to.
+ /// * `value` - The [`Uuid`] to set the metric to.
+ #[doc(hidden)]
+ pub fn set_from_uuid_sync(&self, glean: &Glean, value: Uuid) {
+ self.set_sync(glean, value.to_string())
+ }
+
+ /// Generates a new random [`Uuid`'] and sets the metric to it.
+ pub fn generate_and_set(&self) -> String {
+ let uuid = Uuid::new_v4();
+
+ let value = uuid.to_string();
+ let metric = self.clone();
+ crate::launch_with_glean(move |glean| metric.set_sync(glean, value));
+
+ uuid.to_string()
+ }
+
+ /// Generates a new random [`Uuid`'] and sets the metric to it synchronously.
+ #[doc(hidden)]
+ pub fn generate_and_set_sync(&self, storage: &Glean) -> Uuid {
+ let uuid = Uuid::new_v4();
+ self.set_sync(storage, uuid.to_string());
+ uuid
+ }
+
+ /// Gets the current-stored value as a string, or None if there is no value.
+ #[doc(hidden)]
+ pub fn get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ glean: &Glean,
+ ping_name: S,
+ ) -> Option<Uuid> {
+ let queried_ping_name = ping_name
+ .into()
+ .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
+
+ match StorageManager.snapshot_metric_for_test(
+ glean.storage(),
+ queried_ping_name,
+ &self.meta.identifier(glean),
+ self.meta.inner.lifetime,
+ ) {
+ Some(Metric::Uuid(uuid)) => Uuid::parse_str(&uuid).ok(),
+ _ => None,
+ }
+ }
+
+ /// **Test-only API (exported for FFI purposes).**
+ ///
+ /// Gets the currently stored value as a string.
+ ///
+ /// This doesn't clear the stored value.
+ pub fn test_get_value(&self, ping_name: Option<String>) -> Option<String> {
+ crate::block_on_dispatcher();
+ crate::core::with_glean(|glean| {
+ self.get_value(glean, ping_name.as_deref())
+ .map(|uuid| uuid.to_string())
+ })
+ }
+
+ /// **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) -> i32 {
+ crate::block_on_dispatcher();
+
+ crate::core::with_glean(|glean| {
+ test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
+ })
+ }
+}