1use std::sync::Arc;
6
7use crate::common_metric_data::CommonMetricDataInternal;
8use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
9use crate::metrics::Metric;
10use crate::metrics::MetricType;
11use crate::util::truncate_string_at_boundary_with_error;
12use crate::Glean;
13use crate::{CommonMetricData, TestGetValue};
14
15const MAX_URL_LENGTH: usize = 8192;
17
18#[derive(Clone, Debug)]
23pub struct UrlMetric {
24 meta: Arc<CommonMetricDataInternal>,
25}
26
27impl MetricType for UrlMetric {
28 fn meta(&self) -> &CommonMetricDataInternal {
29 &self.meta
30 }
31}
32
33impl UrlMetric {
38 pub fn new(meta: CommonMetricData) -> Self {
40 Self {
41 meta: Arc::new(meta.into()),
42 }
43 }
44
45 fn is_valid_url_scheme(&self, value: String) -> bool {
46 let mut splits = value.split(':');
47 if let Some(scheme) = splits.next() {
48 if scheme.is_empty() {
49 return false;
50 }
51 let mut chars = scheme.chars();
52 return chars.next().unwrap().is_ascii_alphabetic()
55 && chars.all(|c| c.is_ascii_alphanumeric() || ['+', '-', '.'].contains(&c));
56 }
57
58 false
60 }
61
62 pub fn set<S: Into<String>>(&self, value: S) {
72 let value = value.into();
73 let metric = self.clone();
74 crate::launch_with_glean(move |glean| metric.set_sync(glean, value))
75 }
76
77 #[doc(hidden)]
79 pub fn set_sync<S: Into<String>>(&self, glean: &Glean, value: S) {
80 if !self.should_record(glean) {
81 return;
82 }
83
84 let s = truncate_string_at_boundary_with_error(glean, &self.meta, value, MAX_URL_LENGTH);
85
86 if s.starts_with("data:") {
87 record_error(
88 glean,
89 &self.meta,
90 ErrorType::InvalidValue,
91 "URL metric does not support data URLs.",
92 None,
93 );
94 return;
95 }
96
97 if !self.is_valid_url_scheme(s.clone()) {
98 let msg = format!("\"{}\" does not start with a valid URL scheme.", s);
99 record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None);
100 return;
101 }
102
103 let value = Metric::Url(s);
104 glean.storage().record(glean, &self.meta, &value)
105 }
106
107 #[doc(hidden)]
108 pub(crate) fn get_value<'a, S: Into<Option<&'a str>>>(
109 &self,
110 glean: &Glean,
111 ping_name: S,
112 ) -> Option<String> {
113 let queried_ping_name = ping_name
114 .into()
115 .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]);
116
117 match glean.storage().get_metric(
118 #[cfg(not(feature = "sqlite"))]
119 glean,
120 self.meta(),
121 queried_ping_name,
122 ) {
123 Some(Metric::Url(s)) => Some(s),
124 _ => None,
125 }
126 }
127
128 pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
140 crate::block_on_dispatcher();
141
142 crate::core::with_glean(|glean| {
143 test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
144 })
145 }
146}
147
148impl TestGetValue for UrlMetric {
149 type Output = String;
150
151 fn test_get_value(&self, ping_name: Option<String>) -> Option<String> {
166 crate::block_on_dispatcher();
167 crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
168 }
169}
170
171#[cfg(test)]
172mod test {
173 use super::*;
174 use crate::tests::new_glean;
175 use crate::Lifetime;
176
177 #[test]
178 fn payload_is_correct() {
179 let (glean, _t) = new_glean(None);
180
181 let metric = UrlMetric::new(CommonMetricData {
182 name: "url_metric".into(),
183 category: "test".into(),
184 send_in_pings: vec!["store1".into()],
185 lifetime: Lifetime::Application,
186 ..Default::default()
187 });
188
189 let sample_url = "glean://test".to_string();
190 metric.set_sync(&glean, sample_url.clone());
191 assert_eq!(sample_url, metric.get_value(&glean, "store1").unwrap());
192 }
193
194 #[test]
195 fn does_not_record_url_exceeding_maximum_length() {
196 let (glean, _t) = new_glean(None);
197
198 let metric = UrlMetric::new(CommonMetricData {
199 name: "url_metric".into(),
200 category: "test".into(),
201 send_in_pings: vec!["store1".into()],
202 lifetime: Lifetime::Application,
203 ..Default::default()
204 });
205
206 let long_path_base = "abcdefgh";
212
213 let test_url = format!("glean://{}", long_path_base.repeat(2000));
215 metric.set_sync(&glean, test_url);
216
217 let expected = format!("glean://{}", long_path_base.repeat(1023));
222
223 assert_eq!(metric.get_value(&glean, "store1").unwrap(), expected);
224 assert_eq!(
225 1,
226 test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow)
227 .unwrap()
228 );
229 }
230
231 #[test]
232 fn does_not_record_data_urls() {
233 let (glean, _t) = new_glean(None);
234
235 let metric = UrlMetric::new(CommonMetricData {
236 name: "url_metric".into(),
237 category: "test".into(),
238 send_in_pings: vec!["store1".into()],
239 lifetime: Lifetime::Application,
240 disabled: false,
241 label: None,
242 in_session: false,
243 });
244
245 let test_url = "data:application/json";
246 metric.set_sync(&glean, test_url);
247
248 assert!(metric.get_value(&glean, "store1").is_none());
249
250 assert_eq!(
251 1,
252 test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).unwrap()
253 );
254 }
255
256 #[test]
257 fn url_validation_works_and_records_errors() {
258 let (glean, _t) = new_glean(None);
259
260 let metric = UrlMetric::new(CommonMetricData {
261 name: "url_metric".into(),
262 category: "test".into(),
263 send_in_pings: vec!["store1".into()],
264 lifetime: Lifetime::Application,
265 ..Default::default()
266 });
267
268 let incorrects = vec![
269 "",
270 "1glean://test",
273 "-glean://test",
274 "шеллы://test",
276 "g!lean://test",
277 "g=lean://test",
278 "glean//test",
280 ];
281
282 let corrects = vec![
283 "g:",
285 "glean://",
287 "glean:",
289 "glean:test",
290 "glean:test.com",
291 "g-lean://test",
293 "g+lean://test",
294 "g.lean://test",
295 "glean://test?hello=world",
297 "https://infra.spec.whatwg.org/#ascii-alpha",
299 "https://infra.spec.whatwg.org/#ascii-alpha?test=for-glean",
300 ];
301
302 for incorrect in incorrects.clone().into_iter() {
303 metric.set_sync(&glean, incorrect);
304 assert!(metric.get_value(&glean, "store1").is_none());
305 }
306
307 assert_eq!(
308 incorrects.len(),
309 test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).unwrap()
310 as usize
311 );
312
313 for correct in corrects.into_iter() {
314 metric.set_sync(&glean, correct);
315 assert_eq!(metric.get_value(&glean, "store1").unwrap(), correct);
316 }
317 }
318}