Source code

Revision control

Copy as Markdown

Other Tools

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
mod common;
use crate::common::*;
use std::collections::HashMap;
use std::fs;
use serde_json::json;
use glean_core::metrics::*;
use glean_core::{
get_timestamp_ms, test_get_num_recorded_errors, CommonMetricData, ErrorType, Lifetime,
};
#[test]
fn record_properly_records_without_optional_arguments() {
let store_names = vec!["store1".into(), "store2".into()];
let (glean, _t) = new_glean(None);
let metric = EventMetric::new(
CommonMetricData {
name: "test_event_no_optional".into(),
category: "telemetry".into(),
send_in_pings: store_names.clone(),
disabled: false,
lifetime: Lifetime::Ping,
..Default::default()
},
vec![],
);
metric.record_sync(&glean, 1000, HashMap::new(), 0);
for store_name in store_names {
let events = metric.get_value(&glean, &*store_name).unwrap();
assert_eq!(1, events.len());
assert_eq!("telemetry", events[0].category);
assert_eq!("test_event_no_optional", events[0].name);
assert!(events[0].extra.is_none());
}
}
#[test]
fn record_properly_records_with_optional_arguments() {
let (glean, _t) = new_glean(None);
let store_names = vec!["store1".into(), "store2".into()];
let metric = EventMetric::new(
CommonMetricData {
name: "test_event_no_optional".into(),
category: "telemetry".into(),
send_in_pings: store_names.clone(),
disabled: false,
lifetime: Lifetime::Ping,
..Default::default()
},
vec!["key1".into(), "key2".into()],
);
let extra = [
("key1".into(), "value1".into()),
("key2".into(), "value2".into()),
]
.iter()
.cloned()
.collect();
metric.record_sync(&glean, 1000, extra, 0);
for store_name in store_names {
let events = metric.get_value(&glean, &*store_name).unwrap();
let event = events[0].clone();
assert_eq!(1, events.len());
assert_eq!("telemetry", event.category);
assert_eq!("test_event_no_optional", event.name);
let extra = event.extra.unwrap();
assert_eq!(2, extra.len());
assert_eq!("value1", extra["key1"]);
assert_eq!("value2", extra["key2"]);
}
}
// SKIPPED record() computes the correct time between events
// Timing is now handled in the language-specific part.
#[test]
fn snapshot_returns_none_if_nothing_is_recorded_in_the_store() {
let (glean, _t) = new_glean(None);
assert!(glean
.event_storage()
.snapshot_as_json(&glean, "store1", false)
.is_none())
}
#[test]
fn snapshot_correctly_clears_the_stores() {
let (glean, _t) = new_glean(None);
let store_names = vec!["store1".into(), "store2".into()];
let metric = EventMetric::new(
CommonMetricData {
name: "test_event_clear".into(),
category: "telemetry".into(),
send_in_pings: store_names,
disabled: false,
lifetime: Lifetime::Ping,
..Default::default()
},
vec![],
);
metric.record_sync(&glean, 1000, HashMap::new(), 0);
let snapshot = glean
.event_storage()
.snapshot_as_json(&glean, "store1", true);
assert!(snapshot.is_some());
assert!(glean
.event_storage()
.snapshot_as_json(&glean, "store1", false)
.is_none());
let files: Vec<fs::DirEntry> = fs::read_dir(&glean.event_storage().path)
.unwrap()
.filter_map(|x| x.ok())
.collect();
assert_eq!(1, files.len());
assert_eq!("store2", files[0].file_name());
let snapshot2 = glean
.event_storage()
.snapshot_as_json(&glean, "store2", false);
for s in [snapshot, snapshot2] {
assert!(s.is_some());
let s = s.unwrap();
assert_eq!(1, s.as_array().unwrap().len());
assert_eq!("telemetry", s[0]["category"]);
assert_eq!("test_event_clear", s[0]["name"]);
println!("{:?}", s[0].get("extra"));
assert!(s[0].get("extra").is_none());
}
}
// SKIPPED: Events are serialized in the correct JSON format (no extra)
// SKIPPED: Events are serialized in the correct JSON format (with extra)
// This test won't work as-is since Rust doesn't maintain the insertion order in
// a JSON object, therefore you can't check the JSON output directly against a
// string. This check is redundant with other tests, anyway, and checking against
// the schema is much more useful.
#[test]
fn test_sending_of_event_ping_when_it_fills_up() {
let (mut glean, _t) = new_glean(None);
let store_names: Vec<String> = vec!["events".into()];
for store_name in &store_names {
glean.register_ping_type(&PingType::new(
store_name.clone(),
true,
false,
true,
true,
true,
vec![],
vec!["max_capacity".to_string()],
));
}
let click = EventMetric::new(
CommonMetricData {
name: "click".into(),
category: "ui".into(),
send_in_pings: store_names,
disabled: false,
lifetime: Lifetime::Ping,
..Default::default()
},
vec!["test_event_number".into()],
);
// We send 510 events. We expect to get the first 500 in the ping and 10
// remaining afterward
for i in 0..510 {
let mut extra = HashMap::new();
extra.insert("test_event_number".to_string(), i.to_string());
click.record_sync(&glean, i, extra, 0);
}
assert_eq!(10, click.get_value(&glean, "events").unwrap().len());
let (url, json, _) = &get_queued_pings(glean.get_data_path()).unwrap()[0];
assert!(url.starts_with(format!("/submit/{}/events/", glean.get_application_id()).as_str()));
assert_eq!(500, json["events"].as_array().unwrap().len());
assert_eq!(
"max_capacity",
json["ping_info"].as_object().unwrap()["reason"]
.as_str()
.unwrap()
);
for i in 0..500 {
let event = &json["events"].as_array().unwrap()[i];
assert_eq!(i.to_string(), event["extra"]["test_event_number"]);
}
let snapshot = glean
.event_storage()
.snapshot_as_json(&glean, "events", false)
.unwrap();
assert_eq!(10, snapshot.as_array().unwrap().len());
for i in 0..10 {
let event = &snapshot.as_array().unwrap()[i];
assert_eq!((i + 500).to_string(), event["extra"]["test_event_number"]);
}
}
#[test]
fn test_server_knobs_config_changing_max_events() {
let (mut glean, _t) = new_glean(None);
let store_names: Vec<String> = vec!["events".into()];
for store_name in &store_names {
glean.register_ping_type(&PingType::new(
store_name.clone(),
true,
false,
true,
true,
true,
vec![],
vec!["max_capacity".to_string()],
));
}
// 1. Set up an event to record
let click = EventMetric::new(
CommonMetricData {
name: "click".into(),
category: "ui".into(),
send_in_pings: store_names,
disabled: false,
lifetime: Lifetime::Ping,
..Default::default()
},
vec!["test_event_number".into()],
);
// 2. Set a Server Knobs configuration to disable the metrics
let remote_settings_config = json!(
{
"event_threshold": 50
}
)
.to_string();
glean
.apply_server_knobs_config(RemoteSettingsConfig::try_from(remote_settings_config).unwrap());
// 3. Record 51 events. We expect to get the first 50 in the first ping and 1
// remaining afterward
for i in 0..51 {
let mut extra = HashMap::new();
extra.insert("test_event_number".to_string(), i.to_string());
click.record_sync(&glean, i, extra, 0);
}
assert_eq!(1, click.get_value(&glean, "events").unwrap().len());
let (url, json, _) = &get_queued_pings(glean.get_data_path()).unwrap()[0];
assert!(url.starts_with(format!("/submit/{}/events/", glean.get_application_id()).as_str()));
assert_eq!(50, json["events"].as_array().unwrap().len());
assert_eq!(
"max_capacity",
json["ping_info"].as_object().unwrap()["reason"]
.as_str()
.unwrap()
);
for i in 0..50 {
let event = &json["events"].as_array().unwrap()[i];
assert_eq!(i.to_string(), event["extra"]["test_event_number"]);
}
let snapshot = glean
.event_storage()
.snapshot_as_json(&glean, "events", false)
.unwrap();
assert_eq!(1, snapshot.as_array().unwrap().len());
let event = &snapshot.as_array().unwrap()[0];
assert_eq!(50.to_string(), event["extra"]["test_event_number"]);
}
#[test]
fn extra_keys_must_be_recorded_and_truncated_if_needed() {
let (glean, _t) = new_glean(None);
let store_names: Vec<String> = vec!["store1".into()];
let test_event = EventMetric::new(
CommonMetricData {
name: "testEvent".into(),
category: "ui".into(),
send_in_pings: store_names,
disabled: false,
lifetime: Lifetime::Ping,
..Default::default()
},
vec!["extra1".into(), "truncatedExtra".into()],
);
let test_value = "LeanGleanByFrank";
let test_value_long = test_value.to_string().repeat(32);
// max length for extra values.
let test_value_cap = 500;
assert!(
test_value_long.len() > test_value_cap,
"test value is not long enough"
);
let mut extra = HashMap::new();
extra.insert("extra1".into(), test_value.to_string());
extra.insert("truncatedExtra".into(), test_value_long.clone());
test_event.record_sync(&glean, 0, extra, 0);
let snapshot = glean
.event_storage()
.snapshot_as_json(&glean, "store1", false)
.unwrap();
assert_eq!(1, snapshot.as_array().unwrap().len());
let event = &snapshot.as_array().unwrap()[0];
assert_eq!("ui", event["category"]);
assert_eq!("testEvent", event["name"]);
assert_eq!(2, event["extra"].as_object().unwrap().len());
assert_eq!(test_value, event["extra"]["extra1"]);
assert_eq!(
test_value_long[0..test_value_cap],
event["extra"]["truncatedExtra"]
);
}
#[test]
fn snapshot_sorts_the_timestamps() {
let (glean, _t) = new_glean(None);
let metric = EventMetric::new(
CommonMetricData {
name: "test_event_clear".into(),
category: "telemetry".into(),
send_in_pings: vec!["store1".into()],
disabled: false,
lifetime: Lifetime::Ping,
..Default::default()
},
vec![],
);
metric.record_sync(&glean, 1000, HashMap::new(), 0);
metric.record_sync(&glean, 100, HashMap::new(), 0);
metric.record_sync(&glean, 10000, HashMap::new(), 0);
let snapshot = glean
.event_storage()
.snapshot_as_json(&glean, "store1", true)
.unwrap();
assert_eq!(
0,
snapshot.as_array().unwrap()[0]["timestamp"]
.as_i64()
.unwrap()
);
assert_eq!(
900,
snapshot.as_array().unwrap()[1]["timestamp"]
.as_i64()
.unwrap()
);
assert_eq!(
9900,
snapshot.as_array().unwrap()[2]["timestamp"]
.as_i64()
.unwrap()
);
}
#[test]
fn ensure_custom_ping_events_dont_overflow() {
let (glean, _dir) = new_glean(None);
let store_name = "store-name";
let event_meta = CommonMetricData {
name: "name".into(),
category: "category".into(),
send_in_pings: vec![store_name.into()],
lifetime: Lifetime::Ping,
..Default::default()
};
let event = EventMetric::new(event_meta.clone(), vec![]);
assert!(test_get_num_recorded_errors(
&glean,
&(event_meta.clone()).into(),
ErrorType::InvalidOverflow
)
.is_err());
for _ in 0..500 {
event.record_sync(&glean, 0, HashMap::new(), 0);
}
assert!(test_get_num_recorded_errors(
&glean,
&(event_meta.clone()).into(),
ErrorType::InvalidOverflow
)
.is_err());
// That should top us right up to the limit. Now for going over.
event.record_sync(&glean, 0, HashMap::new(), 0);
assert!(
test_get_num_recorded_errors(&glean, &event_meta.into(), ErrorType::InvalidOverflow)
.is_err()
);
assert_eq!(501, event.get_value(&glean, store_name).unwrap().len());
}
/// Ensure that events from multiple runs serialize properly.
///
/// Records an event once each in two separate sessions,
/// ensuring they both show (and an inserted `glean.restarted` event)
/// in the serialized result.
#[test]
fn ensure_custom_ping_events_from_multiple_runs_work() {
let (mut tempdir, _) = tempdir();
let store_name = "store-name";
let event = EventMetric::new(
CommonMetricData {
name: "name".into(),
category: "category".into(),
send_in_pings: vec![store_name.into()],
lifetime: Lifetime::Ping,
..Default::default()
},
vec![],
);
{
let (glean, dir) = new_glean(Some(tempdir));
// We don't need a full init. Just to deal with on-disk events:
assert!(!glean
.event_storage()
.flush_pending_events_on_startup(&glean, false));
tempdir = dir;
event.record_sync(&glean, 10, HashMap::new(), 0);
}
{
let (glean, _dir) = new_glean(Some(tempdir));
// We don't need a full init. Just to deal with on-disk events:
assert!(!glean
.event_storage()
.flush_pending_events_on_startup(&glean, false));
// Gotta use get_timestamp_ms or this event won't happen "after" the injected
// glean.restarted event from `flush_pending_events_on_startup`.
event.record_sync(&glean, get_timestamp_ms(), HashMap::new(), 0);
let json = glean
.event_storage()
.snapshot_as_json(&glean, store_name, false)
.unwrap();
assert_eq!(json.as_array().unwrap().len(), 3);
assert_eq!(json[0]["category"], "category");
assert_eq!(json[0]["name"], "name");
assert_eq!(json[1]["category"], "glean");
assert_eq!(json[1]["name"], "restarted");
assert_eq!(json[2]["category"], "category");
assert_eq!(json[2]["name"], "name");
}
}
/// Ensure events in an unregistered, non-"events" (ie Custom) ping are trimmed on a subsequent init
/// when we pass `true ` for `trim_data_to_registered_pings` in `on_ready_to_submit_pings`.
#[test]
fn event_storage_trimming() {
let (mut tempdir, _) = tempdir();
let store_name = "store-name";
let store_name_2 = "store-name-2";
let event = EventMetric::new(
CommonMetricData {
name: "name".into(),
category: "category".into(),
send_in_pings: vec![store_name.into(), store_name_2.into()],
lifetime: Lifetime::Ping,
..Default::default()
},
vec![],
);
// First, record the event in the two pings.
// Successfully records just fine because nothing's checking on record that these pings
// exist and are registered.
{
let (glean, dir) = new_glean(Some(tempdir));
tempdir = dir;
event.record_sync(&glean, 10, HashMap::new(), 0);
assert_eq!(1, event.get_value(&glean, store_name).unwrap().len());
assert_eq!(1, event.get_value(&glean, store_name_2).unwrap().len());
}
// Second, construct (but don't init) Glean over again.
// Register exactly one of the two pings.
// Then process the part of init that does the trimming (`on_ready_to_submit_pings`).
// This ought to load the data from the registered ping and trim the data from the unregistered one.
{
let (mut glean, _dir) = new_glean(Some(tempdir));
// In Rust, pings are registered via construction.
// But that's done asynchronously, so we do it synchronously here:
glean.register_ping_type(&PingType::new(
store_name.to_string(),
true,
false,
true,
true,
true,
vec![],
vec![],
));
glean.on_ready_to_submit_pings(true);
assert_eq!(1, event.get_value(&glean, store_name).unwrap().len());
assert!(event.get_value(&glean, store_name_2).is_none());
}
}
#[test]
fn with_event_timestamps() {
use glean_core::{Glean, InternalConfiguration};
let dir = tempfile::tempdir().unwrap();
let cfg = InternalConfiguration {
data_path: dir.path().display().to_string(),
application_id: GLOBAL_APPLICATION_ID.into(),
language_binding_name: "Rust".into(),
upload_enabled: true,
max_events: None,
delay_ping_lifetime_io: false,
app_build: "Unknown".into(),
use_core_mps: false,
trim_data_to_registered_pings: false,
log_level: None,
rate_limit: None,
enable_event_timestamps: true,
experimentation_id: None, // Enabling event timestamps
enable_internal_pings: true,
ping_schedule: Default::default(),
ping_lifetime_threshold: 0,
ping_lifetime_max_time: 0,
};
let glean = Glean::new(cfg).unwrap();
let store_name = "store-name";
let event = EventMetric::new(
CommonMetricData {
name: "name".into(),
category: "category".into(),
send_in_pings: vec![store_name.into()],
lifetime: Lifetime::Ping,
..Default::default()
},
vec![],
);
event.record_sync(&glean, get_timestamp_ms(), HashMap::new(), 12345);
let json = glean
.event_storage()
.snapshot_as_json(&glean, store_name, false)
.unwrap();
assert_eq!(json.as_array().unwrap().len(), 1);
assert_eq!(json[0]["category"], "category");
assert_eq!(json[0]["name"], "name");
let glean_timestamp = json[0]["extra"]["glean_timestamp"]
.as_str()
.expect("glean_timestamp should exist");
let glean_timestamp: i64 = glean_timestamp
.parse()
.expect("glean_timestamp should be an integer");
assert_eq!(12345, glean_timestamp);
}