relay_event_normalization/eap/
trace_metric.rs

1//! Normalizations specific to trace metrics.
2
3use std::{borrow::Cow, sync::LazyLock};
4
5use regex::Regex;
6use relay_base_schema::metrics::MetricUnit;
7use relay_event_schema::protocol::TraceMetric;
8use relay_protocol::Annotated;
9
10/// Returned by [`normalize_metric_name`].
11#[derive(Debug, thiserror::Error, PartialEq, Eq)]
12#[error("Metric has an invalid name")]
13pub struct InvalidMetricName;
14
15/// Normalizes a trace metric name.
16///
17/// Metric names cannot be empty, must only consist of ASCII alphanumerics, underscores, dashes, and periods.
18/// The implementation will replace dashes with underscores.
19///
20/// Empty metric names are rejected with [`InvalidMetricName`].
21pub fn normalize_metric_name(metric: &mut TraceMetric) -> Result<(), InvalidMetricName> {
22    static NORMALIZE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new("[^a-zA-Z0-9_.]+").unwrap());
23
24    let name = metric.name.value_mut().as_mut();
25    let Some(name) = name.filter(|s| !s.trim().is_empty()) else {
26        return Err(InvalidMetricName);
27    };
28
29    if let Cow::Owned(new_name) = NORMALIZE_RE.replace_all(name, "_") {
30        *name = new_name;
31    }
32
33    Ok(())
34}
35
36/// Strips custom metric units, replacing them with [`MetricUnit::None`].
37///
38/// Only known unit families (duration, information, fraction) are allowed.
39pub fn normalize_metric_unit(metric: &mut TraceMetric) {
40    if let Some(MetricUnit::Custom(_)) = metric.unit.value() {
41        metric.unit = Annotated::new(MetricUnit::None);
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use relay_protocol::Annotated;
48
49    use super::*;
50
51    fn metric(name: impl Into<String>) -> TraceMetric {
52        TraceMetric {
53            name: Annotated::new(name.into()),
54            ..Default::default()
55        }
56    }
57
58    macro_rules! assert_metric_name {
59        ($name:expr, err) => {{
60            assert_eq!(
61                normalize_metric_name(&mut metric($name)),
62                Err(InvalidMetricName)
63            )
64        }};
65        ($name:expr, $expected:expr) => {{
66            let mut metric = metric($name);
67            assert_eq!(normalize_metric_name(&mut metric), Ok(()));
68            let name = metric.name.value_mut().as_mut().unwrap();
69            assert_eq!(name, $expected);
70        }};
71    }
72
73    #[test]
74    fn test_normalize_name_invalid() {
75        assert_metric_name!("", err);
76        assert_metric_name!("     ", err);
77    }
78
79    #[test]
80    fn test_normalize_metric_name() {
81        assert_metric_name!("foo.bar123", "foo.bar123");
82        assert_metric_name!("foo bar", "foo_bar");
83        assert_metric_name!("foo!@#bar", "foo_bar");
84        assert_metric_name!("   foo.bar    ", "_foo.bar_");
85        assert_metric_name!("unicøøde", "unic_de");
86    }
87
88    #[test]
89    fn test_normalize_metric_unit_strips_custom() {
90        use relay_base_schema::metrics::DurationUnit;
91
92        let mut m = metric("test");
93        m.unit = Annotated::new(MetricUnit::Custom("customunit".parse().unwrap()));
94        normalize_metric_unit(&mut m);
95        assert_eq!(m.unit.value(), Some(&MetricUnit::None));
96
97        let mut m = metric("test");
98        m.unit = Annotated::new(MetricUnit::Duration(DurationUnit::Second));
99        normalize_metric_unit(&mut m);
100        assert_eq!(
101            m.unit.value(),
102            Some(&MetricUnit::Duration(DurationUnit::Second))
103        );
104
105        let mut m = metric("test");
106        m.unit = Annotated::new(MetricUnit::None);
107        normalize_metric_unit(&mut m);
108        assert_eq!(m.unit.value(), Some(&MetricUnit::None));
109    }
110}