1 // This Source Code Form is subject to the terms of the Mozilla Public
2 // License, v. 2.0. If a copy of the MPL was not distributed with this
3 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4 
5 mod common;
6 use crate::common::*;
7 
8 use serde_json::json;
9 
10 use glean_core::metrics::*;
11 use glean_core::storage::StorageManager;
12 use glean_core::{test_get_num_recorded_errors, ErrorType};
13 use glean_core::{CommonMetricData, Lifetime};
14 
15 #[test]
can_create_labeled_counter_metric()16 fn can_create_labeled_counter_metric() {
17     let (glean, _t) = new_glean(None);
18     let labeled = LabeledMetric::new(
19         CounterMetric::new(CommonMetricData {
20             name: "labeled_metric".into(),
21             category: "telemetry".into(),
22             send_in_pings: vec!["store1".into()],
23             disabled: false,
24             lifetime: Lifetime::Ping,
25             ..Default::default()
26         }),
27         Some(vec!["label1".into()]),
28     );
29 
30     let metric = labeled.get("label1");
31     metric.add(&glean, 1);
32 
33     let snapshot = StorageManager
34         .snapshot_as_json(glean.storage(), "store1", true)
35         .unwrap();
36 
37     assert_eq!(
38         json!({
39             "labeled_counter": {
40                 "telemetry.labeled_metric": { "label1": 1 }
41             }
42         }),
43         snapshot
44     );
45 }
46 
47 #[test]
can_create_labeled_string_metric()48 fn can_create_labeled_string_metric() {
49     let (glean, _t) = new_glean(None);
50     let labeled = LabeledMetric::new(
51         StringMetric::new(CommonMetricData {
52             name: "labeled_metric".into(),
53             category: "telemetry".into(),
54             send_in_pings: vec!["store1".into()],
55             disabled: false,
56             lifetime: Lifetime::Ping,
57             ..Default::default()
58         }),
59         Some(vec!["label1".into()]),
60     );
61 
62     let metric = labeled.get("label1");
63     metric.set(&glean, "text");
64 
65     let snapshot = StorageManager
66         .snapshot_as_json(glean.storage(), "store1", true)
67         .unwrap();
68 
69     assert_eq!(
70         json!({
71             "labeled_string": {
72                 "telemetry.labeled_metric": { "label1": "text" }
73             }
74         }),
75         snapshot
76     );
77 }
78 
79 #[test]
can_create_labeled_bool_metric()80 fn can_create_labeled_bool_metric() {
81     let (glean, _t) = new_glean(None);
82     let labeled = LabeledMetric::new(
83         BooleanMetric::new(CommonMetricData {
84             name: "labeled_metric".into(),
85             category: "telemetry".into(),
86             send_in_pings: vec!["store1".into()],
87             disabled: false,
88             lifetime: Lifetime::Ping,
89             ..Default::default()
90         }),
91         Some(vec!["label1".into()]),
92     );
93 
94     let metric = labeled.get("label1");
95     metric.set(&glean, true);
96 
97     let snapshot = StorageManager
98         .snapshot_as_json(glean.storage(), "store1", true)
99         .unwrap();
100 
101     assert_eq!(
102         json!({
103             "labeled_boolean": {
104                 "telemetry.labeled_metric": { "label1": true }
105             }
106         }),
107         snapshot
108     );
109 }
110 
111 #[test]
can_use_multiple_labels()112 fn can_use_multiple_labels() {
113     let (glean, _t) = new_glean(None);
114     let labeled = LabeledMetric::new(
115         CounterMetric::new(CommonMetricData {
116             name: "labeled_metric".into(),
117             category: "telemetry".into(),
118             send_in_pings: vec!["store1".into()],
119             disabled: false,
120             lifetime: Lifetime::Ping,
121             ..Default::default()
122         }),
123         None,
124     );
125 
126     let metric = labeled.get("label1");
127     metric.add(&glean, 1);
128 
129     let metric = labeled.get("label2");
130     metric.add(&glean, 2);
131 
132     let snapshot = StorageManager
133         .snapshot_as_json(glean.storage(), "store1", true)
134         .unwrap();
135 
136     assert_eq!(
137         json!({
138             "labeled_counter": {
139                 "telemetry.labeled_metric": {
140                     "label1": 1,
141                     "label2": 2,
142                 }
143             }
144         }),
145         snapshot
146     );
147 }
148 
149 #[test]
can_record_error_for_submetric()150 fn can_record_error_for_submetric() {
151     let (glean, _t) = new_glean(None);
152     let labeled = LabeledMetric::new(
153         StringMetric::new(CommonMetricData {
154             name: "labeled_metric".into(),
155             category: "telemetry".into(),
156             send_in_pings: vec!["store1".into()],
157             disabled: false,
158             lifetime: Lifetime::Ping,
159             ..Default::default()
160         }),
161         Some(vec!["label1".into()]),
162     );
163 
164     let metric = labeled.get("label1");
165     metric.set(&glean, "01234567890".repeat(20));
166 
167     // Make sure that the errors have been recorded
168     assert_eq!(
169         Ok(1),
170         test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow, None)
171     );
172 }
173 
174 #[test]
labels_are_checked_against_static_list()175 fn labels_are_checked_against_static_list() {
176     let (glean, _t) = new_glean(None);
177     let labeled = LabeledMetric::new(
178         CounterMetric::new(CommonMetricData {
179             name: "labeled_metric".into(),
180             category: "telemetry".into(),
181             send_in_pings: vec!["store1".into()],
182             disabled: false,
183             lifetime: Lifetime::Ping,
184             ..Default::default()
185         }),
186         Some(vec!["label1".into(), "label2".into()]),
187     );
188 
189     let metric = labeled.get("label1");
190     metric.add(&glean, 1);
191 
192     let metric = labeled.get("label2");
193     metric.add(&glean, 2);
194 
195     // All non-registed labels get mapped to the `other` label
196     let metric = labeled.get("label3");
197     metric.add(&glean, 3);
198     let metric = labeled.get("label4");
199     metric.add(&glean, 4);
200 
201     let snapshot = StorageManager
202         .snapshot_as_json(glean.storage(), "store1", true)
203         .unwrap();
204 
205     assert_eq!(
206         json!({
207             "labeled_counter": {
208                 "telemetry.labeled_metric": {
209                     "label1": 1,
210                     "label2": 2,
211                     "__other__": 7,
212                 }
213             }
214         }),
215         snapshot
216     );
217 }
218 
219 #[test]
dynamic_labels_too_long()220 fn dynamic_labels_too_long() {
221     let (glean, _t) = new_glean(None);
222     let labeled = LabeledMetric::new(
223         CounterMetric::new(CommonMetricData {
224             name: "labeled_metric".into(),
225             category: "telemetry".into(),
226             send_in_pings: vec!["store1".into()],
227             disabled: false,
228             lifetime: Lifetime::Ping,
229             ..Default::default()
230         }),
231         None,
232     );
233 
234     let metric = labeled.get("this_string_has_more_than_thirty_characters");
235     metric.add(&glean, 1);
236 
237     let snapshot = StorageManager
238         .snapshot_as_json(glean.storage(), "store1", true)
239         .unwrap();
240 
241     assert_eq!(
242         json!({
243             "labeled_counter": {
244                 "glean.error.invalid_label": { "telemetry.labeled_metric": 1 },
245                 "telemetry.labeled_metric": {
246                     "__other__": 1,
247                 }
248             }
249         }),
250         snapshot
251     );
252 }
253 
254 #[test]
dynamic_labels_regex_mismatch()255 fn dynamic_labels_regex_mismatch() {
256     let (glean, _t) = new_glean(None);
257     let labeled = LabeledMetric::new(
258         CounterMetric::new(CommonMetricData {
259             name: "labeled_metric".into(),
260             category: "telemetry".into(),
261             send_in_pings: vec!["store1".into()],
262             disabled: false,
263             lifetime: Lifetime::Ping,
264             ..Default::default()
265         }),
266         None,
267     );
268 
269     let labels_not_validating = vec![
270         "notSnakeCase",
271         "",
272         "with/slash",
273         "1.not_fine",
274         "this.$isnotfine",
275         "-.not_fine",
276         "this.is_not_fine.2",
277     ];
278     let num_non_validating = labels_not_validating.len();
279 
280     for label in &labels_not_validating {
281         labeled.get(label).add(&glean, 1);
282     }
283 
284     let snapshot = StorageManager
285         .snapshot_as_json(glean.storage(), "store1", true)
286         .unwrap();
287 
288     assert_eq!(
289         json!({
290             "labeled_counter": {
291                 "glean.error.invalid_label": { "telemetry.labeled_metric": num_non_validating },
292                 "telemetry.labeled_metric": {
293                     "__other__": num_non_validating,
294                 }
295             }
296         }),
297         snapshot
298     );
299 }
300 
301 #[test]
dynamic_labels_regex_allowed()302 fn dynamic_labels_regex_allowed() {
303     let (glean, _t) = new_glean(None);
304     let labeled = LabeledMetric::new(
305         CounterMetric::new(CommonMetricData {
306             name: "labeled_metric".into(),
307             category: "telemetry".into(),
308             send_in_pings: vec!["store1".into()],
309             disabled: false,
310             lifetime: Lifetime::Ping,
311             ..Default::default()
312         }),
313         None,
314     );
315 
316     let labels_validating = vec![
317         "this.is.fine",
318         "this_is_fine_too",
319         "this.is_still_fine",
320         "thisisfine",
321         "_.is_fine",
322         "this.is-fine",
323         "this-is-fine",
324     ];
325 
326     for label in &labels_validating {
327         labeled.get(label).add(&glean, 1);
328     }
329 
330     let snapshot = StorageManager
331         .snapshot_as_json(glean.storage(), "store1", true)
332         .unwrap();
333 
334     assert_eq!(
335         json!({
336             "labeled_counter": {
337                 "telemetry.labeled_metric": {
338                     "this.is.fine": 1,
339                     "this_is_fine_too": 1,
340                     "this.is_still_fine": 1,
341                     "thisisfine": 1,
342                     "_.is_fine": 1,
343                     "this.is-fine": 1,
344                     "this-is-fine": 1
345                 }
346             }
347         }),
348         snapshot
349     );
350 }
351 
352 #[test]
seen_labels_get_reloaded_from_disk()353 fn seen_labels_get_reloaded_from_disk() {
354     let (mut tempdir, _) = tempdir();
355 
356     let (glean, dir) = new_glean(Some(tempdir));
357     tempdir = dir;
358 
359     let labeled = LabeledMetric::new(
360         CounterMetric::new(CommonMetricData {
361             name: "labeled_metric".into(),
362             category: "telemetry".into(),
363             send_in_pings: vec!["store1".into()],
364             disabled: false,
365             lifetime: Lifetime::Ping,
366             ..Default::default()
367         }),
368         None,
369     );
370 
371     // Store some data into labeled metrics
372     {
373         // Set the maximum number of labels
374         for i in 1..=16 {
375             let label = format!("label{}", i);
376             labeled.get(&label).add(&glean, i);
377         }
378 
379         let snapshot = StorageManager
380             .snapshot_as_json(glean.storage(), "store1", false)
381             .unwrap();
382 
383         // Check that the data is there
384         for i in 1..=16 {
385             let label = format!("label{}", i);
386             assert_eq!(
387                 i,
388                 snapshot["labeled_counter"]["telemetry.labeled_metric"][&label]
389             );
390         }
391 
392         drop(glean);
393     }
394 
395     // Force a reload
396     {
397         let (glean, _) = new_glean(Some(tempdir));
398 
399         // Try to store another label
400         labeled.get("new_label").add(&glean, 40);
401 
402         let snapshot = StorageManager
403             .snapshot_as_json(glean.storage(), "store1", false)
404             .unwrap();
405 
406         // Check that the old data is still there
407         for i in 1..=16 {
408             let label = format!("label{}", i);
409             assert_eq!(
410                 i,
411                 snapshot["labeled_counter"]["telemetry.labeled_metric"][&label]
412             );
413         }
414 
415         // The new label lands in the __other__ bucket, due to too many labels
416         assert_eq!(
417             40,
418             snapshot["labeled_counter"]["telemetry.labeled_metric"]["__other__"]
419         );
420     }
421 }
422 
423 #[test]
caching_metrics_with_dynamic_labels()424 fn caching_metrics_with_dynamic_labels() {
425     let (glean, _t) = new_glean(None);
426     let labeled = LabeledMetric::new(
427         CounterMetric::new(CommonMetricData {
428             name: "cached_labels".into(),
429             category: "telemetry".into(),
430             send_in_pings: vec!["store1".into()],
431             disabled: false,
432             lifetime: Lifetime::Ping,
433             ..Default::default()
434         }),
435         None,
436     );
437 
438     // Create multiple metric instances and cache them for later use.
439     let metrics = (1..=20)
440         .map(|i| {
441             let label = format!("label{}", i);
442             labeled.get(&label)
443         })
444         .collect::<Vec<_>>();
445 
446     // Only now use them.
447     for metric in metrics {
448         metric.add(&glean, 1);
449     }
450 
451     // The maximum number of labels we store is 16.
452     // So we should have put 4 metrics in the __other__ bucket.
453     let other = labeled.get("__other__");
454     assert_eq!(Some(4), other.test_get_value(&glean, "store1"));
455 }
456 
457 #[test]
caching_metrics_with_dynamic_labels_across_pings()458 fn caching_metrics_with_dynamic_labels_across_pings() {
459     let (glean, _t) = new_glean(None);
460     let labeled = LabeledMetric::new(
461         CounterMetric::new(CommonMetricData {
462             name: "cached_labels2".into(),
463             category: "telemetry".into(),
464             send_in_pings: vec!["store1".into()],
465             disabled: false,
466             lifetime: Lifetime::Ping,
467             ..Default::default()
468         }),
469         None,
470     );
471 
472     // Create multiple metric instances and cache them for later use.
473     let metrics = (1..=20)
474         .map(|i| {
475             let label = format!("label{}", i);
476             labeled.get(&label)
477         })
478         .collect::<Vec<_>>();
479 
480     // Only now use them.
481     for metric in &metrics {
482         metric.add(&glean, 1);
483     }
484 
485     // The maximum number of labels we store is 16.
486     // So we should have put 4 metrics in the __other__ bucket.
487     let other = labeled.get("__other__");
488     assert_eq!(Some(4), other.test_get_value(&glean, "store1"));
489 
490     // Snapshot (so we can inspect the JSON)
491     // and clear out storage (the same way submitting a ping would)
492     let snapshot = StorageManager
493         .snapshot_as_json(glean.storage(), "store1", true)
494         .unwrap();
495 
496     // We didn't send the 20th label
497     assert_eq!(
498         json!(null),
499         snapshot["labeled_counter"]["telemetry.cached_labels2"]["label20"]
500     );
501 
502     // We now set the ones that ended up in `__other__` before.
503     // Note: indexing is zero-based,
504     // but we later check the names, so let's offset it by 1.
505     metrics[16].add(&glean, 17);
506     metrics[17].add(&glean, 18);
507     metrics[18].add(&glean, 19);
508     metrics[19].add(&glean, 20);
509 
510     assert_eq!(Some(17), metrics[16].test_get_value(&glean, "store1"));
511     assert_eq!(Some(18), metrics[17].test_get_value(&glean, "store1"));
512     assert_eq!(Some(19), metrics[18].test_get_value(&glean, "store1"));
513     assert_eq!(Some(20), metrics[19].test_get_value(&glean, "store1"));
514     assert_eq!(None, other.test_get_value(&glean, "store1"));
515 
516     let snapshot = StorageManager
517         .snapshot_as_json(glean.storage(), "store1", true)
518         .unwrap();
519 
520     let cached_labels = &snapshot["labeled_counter"]["telemetry.cached_labels2"];
521     assert_eq!(json!(17), cached_labels["label17"]);
522     assert_eq!(json!(18), cached_labels["label18"]);
523     assert_eq!(json!(19), cached_labels["label19"]);
524     assert_eq!(json!(20), cached_labels["label20"]);
525     assert_eq!(json!(null), cached_labels["__other__"]);
526 }
527