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::CommonMetricData;
14use crate::Glean;
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_for_test(
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_value(&self, ping_name: Option<String>) -> Option<String> {
144 crate::block_on_dispatcher();
145 crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref()))
146 }
147
148 pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
160 crate::block_on_dispatcher();
161
162 crate::core::with_glean(|glean| {
163 test_get_num_recorded_errors(glean, self.meta(), error).unwrap_or(0)
164 })
165 }
166}
167
168#[cfg(test)]
169mod test {
170 use super::*;
171 use crate::tests::new_glean;
172 use crate::Lifetime;
173
174 #[test]
175 fn payload_is_correct() {
176 let (glean, _t) = new_glean(None);
177
178 let metric = UrlMetric::new(CommonMetricData {
179 name: "url_metric".into(),
180 category: "test".into(),
181 send_in_pings: vec!["store1".into()],
182 lifetime: Lifetime::Application,
183 disabled: false,
184 dynamic_label: None,
185 });
186
187 let sample_url = "glean://test".to_string();
188 metric.set_sync(&glean, sample_url.clone());
189 assert_eq!(sample_url, metric.get_value(&glean, "store1").unwrap());
190 }
191
192 #[test]
193 fn does_not_record_url_exceeding_maximum_length() {
194 let (glean, _t) = new_glean(None);
195
196 let metric = UrlMetric::new(CommonMetricData {
197 name: "url_metric".into(),
198 category: "test".into(),
199 send_in_pings: vec!["store1".into()],
200 lifetime: Lifetime::Application,
201 disabled: false,
202 dynamic_label: None,
203 });
204
205 let long_path_base = "abcdefgh";
211
212 let test_url = format!("glean://{}", long_path_base.repeat(2000));
214 metric.set_sync(&glean, test_url);
215
216 let expected = format!("glean://{}", long_path_base.repeat(1023));
221
222 assert_eq!(metric.get_value(&glean, "store1").unwrap(), expected);
223 assert_eq!(
224 1,
225 test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow)
226 .unwrap()
227 );
228 }
229
230 #[test]
231 fn does_not_record_data_urls() {
232 let (glean, _t) = new_glean(None);
233
234 let metric = UrlMetric::new(CommonMetricData {
235 name: "url_metric".into(),
236 category: "test".into(),
237 send_in_pings: vec!["store1".into()],
238 lifetime: Lifetime::Application,
239 disabled: false,
240 dynamic_label: None,
241 });
242
243 let test_url = "data:application/json";
244 metric.set_sync(&glean, test_url);
245
246 assert!(metric.get_value(&glean, "store1").is_none());
247
248 assert_eq!(
249 1,
250 test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).unwrap()
251 );
252 }
253
254 #[test]
255 fn url_validation_works_and_records_errors() {
256 let (glean, _t) = new_glean(None);
257
258 let metric = UrlMetric::new(CommonMetricData {
259 name: "url_metric".into(),
260 category: "test".into(),
261 send_in_pings: vec!["store1".into()],
262 lifetime: Lifetime::Application,
263 disabled: false,
264 dynamic_label: None,
265 });
266
267 let incorrects = vec![
268 "",
269 "1glean://test",
272 "-glean://test",
273 "шеллы://test",
275 "g!lean://test",
276 "g=lean://test",
277 "glean//test",
279 ];
280
281 let corrects = vec![
282 "g:",
284 "glean://",
286 "glean:",
288 "glean:test",
289 "glean:test.com",
290 "g-lean://test",
292 "g+lean://test",
293 "g.lean://test",
294 "glean://test?hello=world",
296 "https://infra.spec.whatwg.org/#ascii-alpha",
298 "https://infra.spec.whatwg.org/#ascii-alpha?test=for-glean",
299 ];
300
301 for incorrect in incorrects.clone().into_iter() {
302 metric.set_sync(&glean, incorrect);
303 assert!(metric.get_value(&glean, "store1").is_none());
304 }
305
306 assert_eq!(
307 incorrects.len(),
308 test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).unwrap()
309 as usize
310 );
311
312 for correct in corrects.into_iter() {
313 metric.set_sync(&glean, correct);
314 assert_eq!(metric.get_value(&glean, "store1").unwrap(), correct);
315 }
316 }
317}