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