mod common;
use crate::common::*;
use std::collections::HashMap;
use std::thread;
use std::time::Duration;
use glean_core::{
metrics::EventMetric, CommonMetricData, Glean, InternalConfiguration, Lifetime, SessionMode,
};
fn session_cfg(
data_path: &str,
mode: SessionMode,
sample_rate: f64,
timeout_ms: u64,
) -> InternalConfiguration {
InternalConfiguration {
data_path: data_path.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: false,
experimentation_id: None,
enable_internal_pings: true,
ping_schedule: Default::default(),
ping_lifetime_threshold: 0,
ping_lifetime_max_time: 0,
max_pending_pings_count: None,
max_pending_pings_directory_size: None,
session_mode: mode,
session_sample_rate: sample_rate,
session_inactivity_timeout_ms: timeout_ms,
}
}
fn session_start_metric() -> EventMetric {
EventMetric::new(
CommonMetricData {
name: "session_start".into(),
category: "glean".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
..Default::default()
},
vec![],
)
}
fn session_end_metric() -> EventMetric {
EventMetric::new(
CommonMetricData {
name: "session_end".into(),
category: "glean".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
..Default::default()
},
vec![],
)
}
#[test]
fn auto_mode_starts_session_on_first_active() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
assert!(session_start_metric().get_value(&glean, "events").is_none());
glean.handle_client_active();
let events = session_start_metric()
.get_value(&glean, "events")
.expect("expected session_start event");
assert_eq!(1, events.len());
assert_eq!("glean", events[0].category);
assert_eq!("session_start", events[0].name);
let extra = events[0].extra.as_ref().expect("expected extras");
assert!(
extra.contains_key("session_id"),
"session_id missing from extras"
);
assert_eq!("1", extra.get("session_seq").expect("session_seq missing"));
}
#[test]
fn auto_mode_resumes_session_within_timeout() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
glean.handle_client_inactive();
glean.handle_client_active();
assert!(
session_start_metric().get_value(&glean, "events").is_none(),
"expected no new session_start on resume within timeout"
);
assert!(
session_end_metric().get_value(&glean, "events").is_none(),
"expected no session_end on resume within timeout"
);
}
#[test]
fn auto_mode_zero_timeout_means_never_time_out() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 0);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
glean.handle_client_inactive();
thread::sleep(Duration::from_millis(20));
glean.handle_client_active();
assert!(
session_start_metric().get_value(&glean, "events").is_none(),
"timeout_ms=0 must mean 'never time out': no new session_start expected"
);
assert!(
session_end_metric().get_value(&glean, "events").is_none(),
"timeout_ms=0 must mean 'never time out': no session_end expected"
);
}
#[test]
fn auto_mode_starts_new_session_after_timeout() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
glean.handle_client_inactive();
thread::sleep(Duration::from_millis(20));
glean.handle_client_active();
let end_events = session_end_metric()
.get_value(&glean, "events")
.expect("expected session_end event after timeout");
assert_eq!(1, end_events.len());
let end_extra = end_events[0]
.extra
.as_ref()
.expect("expected extras on session_end");
assert_eq!(
"timeout",
end_extra
.get("reason")
.expect("reason missing from session_end")
);
let start_events = session_start_metric()
.get_value(&glean, "events")
.expect("expected session_start after timeout");
assert_eq!(1, start_events.len());
let start_extra = start_events[0]
.extra
.as_ref()
.expect("expected extras on session_start");
assert_eq!(
"2",
start_extra
.get("session_seq")
.expect("session_seq missing from session_start"),
"second session must have seq=2"
);
}
#[test]
fn lifecycle_mode_new_session_per_activation() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Lifecycle, 1.0, 0);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let starts = session_start_metric()
.get_value(&glean, "events")
.expect("expected first session_start");
assert_eq!(1, starts.len());
assert_eq!(
"1",
starts[0]
.extra
.as_ref()
.unwrap()
.get("session_seq")
.unwrap()
);
glean.handle_client_inactive();
glean.handle_client_active();
let starts2 = session_start_metric()
.get_value(&glean, "events")
.expect("expected second session_start");
assert_eq!(1, starts2.len());
assert_eq!(
"2",
starts2[0]
.extra
.as_ref()
.unwrap()
.get("session_seq")
.unwrap(),
"second session must have seq=2"
);
}
#[test]
fn lifecycle_mode_session_end_on_inactive() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Lifecycle, 1.0, 0);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let start_events = session_start_metric()
.get_value(&glean, "events")
.expect("expected session_start");
let started_id = start_events[0]
.extra
.as_ref()
.unwrap()
.get("session_id")
.unwrap()
.clone();
glean.handle_client_inactive();
assert!(
session_end_metric().get_value(&glean, "events").is_none(),
"session_end store must be cleared after events ping submission"
);
assert!(
!glean.session_manager().is_active(),
"session must be inactive after handle_client_inactive in LIFECYCLE mode"
);
assert!(
uuid::Uuid::parse_str(&started_id).is_ok(),
"session_id must be a valid UUID"
);
}
#[test]
fn manual_mode_no_auto_sessions() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Manual, 1.0, 0);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
assert!(
session_start_metric().get_value(&glean, "events").is_none(),
"manual mode: handle_client_active must not start a session"
);
glean.handle_client_inactive();
assert!(
session_end_metric().get_value(&glean, "events").is_none(),
"manual mode: handle_client_inactive must not end a session"
);
}
#[test]
fn session_seq_monotonically_increases_across_sessions() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Lifecycle, 1.0, 0);
let mut glean = Glean::new(cfg).unwrap();
for expected_seq in 1u64..=4 {
glean.handle_client_active();
let starts = session_start_metric()
.get_value(&glean, "events")
.unwrap_or_else(|| panic!("expected session_start at seq={expected_seq}"));
let seq_str = starts
.last()
.unwrap()
.extra
.as_ref()
.unwrap()
.get("session_seq")
.unwrap()
.clone();
assert_eq!(
expected_seq.to_string(),
seq_str,
"session_seq mismatch at iteration {expected_seq}"
);
glean.handle_client_inactive();
}
}
#[test]
fn session_seq_persists_across_restarts() {
let (t, data_path) = tempdir();
{
let cfg = session_cfg(&data_path, SessionMode::Lifecycle, 1.0, 0);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active(); glean.handle_client_inactive();
glean.handle_client_active(); glean.handle_client_inactive();
}
let cfg2 = session_cfg(&data_path, SessionMode::Lifecycle, 1.0, 0);
let mut glean2 = Glean::new(cfg2).unwrap();
glean2.handle_client_active();
let starts = session_start_metric()
.get_value(&glean2, "events")
.expect("expected session_start after restart");
assert_eq!(
"3",
starts[0]
.extra
.as_ref()
.unwrap()
.get("session_seq")
.unwrap(),
"session_seq must continue from last persisted value after restart"
);
drop(t); }
#[test]
fn sampling_rate_zero_blocks_user_events_within_session() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 0.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let user_event = EventMetric::new(
CommonMetricData {
name: "test_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
user_event.record_sync(&glean, 1000, HashMap::new(), 0);
assert!(
user_event.get_value(&glean, "events").is_none(),
"sample_rate=0.0: user event must be suppressed inside a sampled-out session"
);
assert!(
session_start_metric().get_value(&glean, "events").is_some(),
"session_start (in_session=false) must not be suppressed by the sampling gate"
);
}
#[test]
fn sampling_rate_one_passes_all_user_events() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let user_event = EventMetric::new(
CommonMetricData {
name: "test_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
user_event.record_sync(&glean, 1000, HashMap::new(), 0);
assert!(
user_event.get_value(&glean, "events").is_some(),
"sample_rate=1.0: user event must be recorded inside a sampled-in session"
);
}
#[test]
fn events_outside_session_bypass_sampling_gate() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 0.0, 1_800_000);
let glean = Glean::new(cfg).unwrap();
let user_event = EventMetric::new(
CommonMetricData {
name: "test_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
user_event.record_sync(&glean, 1000, HashMap::new(), 0);
assert!(
user_event.get_value(&glean, "events").is_some(),
"events recorded between sessions must not be suppressed"
);
}
#[test]
fn out_of_session_events_bypass_sampling_gate() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 0.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let oos_event = EventMetric::new(
CommonMetricData {
name: "oos_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
..Default::default()
},
vec![],
);
oos_event.record_sync(&glean, 1000, HashMap::new(), 0);
assert!(
oos_event.get_value(&glean, "events").is_some(),
"in_session=false event must bypass the sampling gate"
);
}
#[test]
fn sample_rate_below_zero_clamped_to_zero() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, -0.5, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let user_event = EventMetric::new(
CommonMetricData {
name: "test_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
user_event.record_sync(&glean, 1000, HashMap::new(), 0);
assert!(
user_event.get_value(&glean, "events").is_none(),
"sample_rate=-0.5 must be clamped to 0.0, suppressing user events"
);
}
#[test]
fn sample_rate_above_one_clamped_to_one() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.5, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let user_event = EventMetric::new(
CommonMetricData {
name: "test_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
user_event.record_sync(&glean, 1000, HashMap::new(), 0);
assert!(
user_event.get_value(&glean, "events").is_some(),
"sample_rate=1.5 must be clamped to 1.0, recording user events"
);
}
#[test]
fn session_metadata_attached_to_in_session_events() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let user_event = EventMetric::new(
CommonMetricData {
name: "test_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
user_event.record_sync(&glean, 1000, HashMap::new(), 0);
let events = user_event
.get_value(&glean, "events")
.expect("expected event");
let session = events[0]
.session
.as_ref()
.expect("expected session metadata on in-session event");
assert!(
!session.session_id.is_empty(),
"session_id must not be empty"
);
assert_eq!(
1, session.session_seq,
"session_seq must be 1 for first session"
);
assert_eq!(0, session.event_seq, "event_seq of first event must be 0");
assert!(
(session.session_sample_rate - 1.0).abs() < f64::EPSILON,
"session_sample_rate must match configured rate"
);
}
#[test]
fn out_of_session_events_have_no_session_metadata() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let oos_event = EventMetric::new(
CommonMetricData {
name: "oos_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
..Default::default()
},
vec![],
);
oos_event.record_sync(&glean, 1000, HashMap::new(), 0);
let events = oos_event
.get_value(&glean, "events")
.expect("expected event");
assert!(
events[0].session.is_none(),
"in_session=false event must have no session metadata"
);
}
#[test]
fn event_seq_increments_within_session() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let user_event = EventMetric::new(
CommonMetricData {
name: "test_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
user_event.record_sync(&glean, 1000, HashMap::new(), 0);
user_event.record_sync(&glean, 1001, HashMap::new(), 0);
user_event.record_sync(&glean, 1002, HashMap::new(), 0);
let events = user_event
.get_value(&glean, "events")
.expect("expected events");
assert_eq!(3, events.len());
let seqs: Vec<u64> = events
.iter()
.map(|e| {
e.session
.as_ref()
.expect("session metadata missing")
.event_seq
})
.collect();
assert_eq!(
vec![0, 1, 2],
seqs,
"event_seq must increment with each event"
);
}
#[test]
fn event_seq_resets_on_new_session() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Lifecycle, 1.0, 0);
let mut glean = Glean::new(cfg).unwrap();
let user_event = EventMetric::new(
CommonMetricData {
name: "test_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
glean.handle_client_active();
user_event.record_sync(&glean, 1000, HashMap::new(), 0);
user_event.record_sync(&glean, 1001, HashMap::new(), 0);
glean.handle_client_inactive();
glean.handle_client_active();
user_event.record_sync(&glean, 2000, HashMap::new(), 0);
let events = user_event
.get_value(&glean, "events")
.expect("expected event in second session");
assert_eq!(
1,
events.len(),
"only event from second session should be present"
);
let session = events[0]
.session
.as_ref()
.expect("session metadata missing");
assert_eq!(
0, session.event_seq,
"event_seq must reset to 0 at the start of each new session"
);
assert_eq!(2, session.session_seq, "second session must have seq=2");
}
#[test]
fn manual_mode_explicit_session_start_end() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Manual, 1.0, 0);
let mut glean = Glean::new(cfg).unwrap();
assert!(!glean.session_manager().is_active());
glean.session_start();
assert!(
glean.session_manager().is_active(),
"session must be active after manual session_start"
);
let starts = session_start_metric()
.get_value(&glean, "events")
.expect("expected session_start event after manual start");
assert_eq!(1, starts.len());
let extra = starts[0].extra.as_ref().unwrap();
assert_eq!(
"1",
extra.get("session_seq").unwrap(),
"first manual session must have seq=1"
);
assert!(
uuid::Uuid::parse_str(extra.get("session_id").unwrap()).is_ok(),
"session_id must be a valid UUID"
);
let user_event = EventMetric::new(
CommonMetricData {
name: "test_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
user_event.record_sync(&glean, 1000, HashMap::new(), 0);
let events = user_event
.get_value(&glean, "events")
.expect("expected event");
assert!(
events[0].session.is_some(),
"in-session event must have session metadata in Manual mode"
);
glean.session_end(Some("done"));
assert!(
!glean.session_manager().is_active(),
"session must be inactive after manual session_end"
);
}
#[test]
fn manual_mode_second_session_has_seq_2() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Manual, 1.0, 0);
let mut glean = Glean::new(cfg).unwrap();
glean.session_start();
glean.session_end(None);
glean.session_start();
let starts = session_start_metric()
.get_value(&glean, "events")
.expect("expected session_start for second session");
let seq_str = starts
.last()
.unwrap()
.extra
.as_ref()
.unwrap()
.get("session_seq")
.unwrap()
.clone();
assert_eq!("2", seq_str, "second manual session must have seq=2");
}
#[test]
fn auto_mode_session_resumed_on_restart_before_timeout() {
let (t, data_path) = tempdir();
let original_session_id;
{
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active(); original_session_id = glean.session_manager().session_id().unwrap().to_string();
glean.handle_client_inactive(); }
let cfg2 = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean2 = Glean::new(cfg2).unwrap();
glean2.handle_client_active();
assert!(
session_start_metric()
.get_value(&glean2, "events")
.is_none(),
"no new session_start expected when session is resumed after restart"
);
assert!(
session_end_metric().get_value(&glean2, "events").is_none(),
"no session_end expected when session is resumed after restart"
);
assert_eq!(
original_session_id,
glean2.session_manager().session_id().unwrap().to_string(),
"resumed session must keep the original session_id"
);
drop(t);
}
#[test]
fn auto_mode_event_seq_continuous_across_restart() {
let (t, data_path) = tempdir();
let pre_restart_seq;
{
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let user_event = EventMetric::new(
CommonMetricData {
name: "pre_restart_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
user_event.record_sync(&glean, 100, HashMap::new(), 0);
user_event.record_sync(&glean, 200, HashMap::new(), 0);
user_event.record_sync(&glean, 300, HashMap::new(), 0);
let events = user_event
.get_value(&glean, "events")
.expect("expected pre-restart events");
pre_restart_seq = events
.last()
.unwrap()
.session
.as_ref()
.expect("session metadata missing")
.event_seq;
assert_eq!(
2, pre_restart_seq,
"last pre-restart event must have event_seq=2"
);
glean.handle_client_inactive();
}
let cfg2 = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean2 = Glean::new(cfg2).unwrap();
glean2.handle_client_active();
let post_event = EventMetric::new(
CommonMetricData {
name: "post_restart_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
post_event.record_sync(&glean2, 400, HashMap::new(), 0);
let post_events = post_event
.get_value(&glean2, "events")
.expect("expected post-restart event");
let post_seq = post_events[0]
.session
.as_ref()
.expect("session metadata missing on post-restart event")
.event_seq;
assert_eq!(
pre_restart_seq + 1,
post_seq,
"event_seq must continue from {} after restart, not reset to 0",
pre_restart_seq
);
drop(t);
}
#[test]
fn auto_mode_new_session_on_restart_after_timeout() {
let (t, data_path) = tempdir();
let original_session_id;
{
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
original_session_id = glean.session_manager().session_id().unwrap().to_string();
glean.handle_client_inactive(); }
thread::sleep(Duration::from_millis(20));
let cfg2 = session_cfg(&data_path, SessionMode::Auto, 1.0, 1);
let mut glean2 = Glean::new(cfg2).unwrap();
glean2.handle_client_active();
let end_events = session_end_metric()
.get_value(&glean2, "events")
.expect("expected session_end after timeout on restart");
assert_eq!(1, end_events.len());
assert_eq!(
"timeout",
end_events[0].extra.as_ref().unwrap().get("reason").unwrap()
);
let new_id = glean2.session_manager().session_id().unwrap().to_string();
assert_ne!(
original_session_id, new_id,
"new session must have a fresh session_id"
);
let start_events = session_start_metric()
.get_value(&glean2, "events")
.expect("expected session_start for new session after timeout");
assert_eq!(
"2",
start_events[0]
.extra
.as_ref()
.unwrap()
.get("session_seq")
.unwrap()
);
drop(t);
}
#[test]
fn auto_mode_sampled_out_session_stays_sampled_out_after_restart() {
let (t, data_path) = tempdir();
{
let cfg = session_cfg(&data_path, SessionMode::Auto, 0.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active(); assert!(
!glean.session_manager().sampled_in(),
"session must be sampled-out at rate=0.0"
);
glean.handle_client_inactive(); }
let cfg2 = session_cfg(&data_path, SessionMode::Auto, 0.0, 1_800_000);
let mut glean2 = Glean::new(cfg2).unwrap();
glean2.handle_client_active();
assert!(
!glean2.session_manager().sampled_in(),
"resumed session must remain sampled-out after restart"
);
let user_event = EventMetric::new(
CommonMetricData {
name: "test_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
user_event.record_sync(&glean2, 1000, HashMap::new(), 0);
assert!(
user_event.get_value(&glean2, "events").is_none(),
"user event must remain suppressed in resumed sampled-out session"
);
drop(t);
}
#[test]
fn auto_mode_session_start_time_persists_across_restart() {
let (t, data_path) = tempdir();
let original_start_time;
{
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
original_start_time = glean
.session_manager()
.session_start_time()
.expect("session_start_time must be set");
glean.handle_client_inactive();
}
let cfg2 = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean2 = Glean::new(cfg2).unwrap();
glean2.handle_client_active();
let resumed_start_time = glean2
.session_manager()
.session_start_time()
.expect("session_start_time must be restored after restart");
assert_eq!(
original_start_time, resumed_start_time,
"session_start_time must be the same before and after a clean restart"
);
drop(t);
}
#[test]
fn sessions_seen_increments_regardless_of_sampling() {
use glean_core::metrics::CounterMetric;
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Lifecycle, 0.0, 0);
let mut glean = Glean::new(cfg).unwrap();
let sessions_seen = CounterMetric::new(CommonMetricData {
name: "sessions_seen".into(),
category: "glean".into(),
send_in_pings: vec!["metrics".into()],
lifetime: Lifetime::Ping,
in_session: false,
..Default::default()
});
assert!(
sessions_seen.get_value(&glean, "metrics").is_none(),
"sessions_seen must be 0 before any session starts"
);
for _ in 0..3 {
glean.handle_client_active();
glean.handle_client_inactive();
}
assert_eq!(
3,
sessions_seen.get_value(&glean, "metrics").unwrap_or(0),
"sessions_seen must equal the number of sessions started, even when all are sampled-out"
);
}
#[test]
fn sessions_seen_is_out_of_session() {
use glean_core::metrics::CounterMetric;
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 0.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let sessions_seen = CounterMetric::new(CommonMetricData {
name: "sessions_seen".into(),
category: "glean".into(),
send_in_pings: vec!["metrics".into()],
lifetime: Lifetime::Ping,
in_session: false,
..Default::default()
});
assert_eq!(
1,
sessions_seen.get_value(&glean, "metrics").unwrap_or(0),
"sessions_seen must be recorded even when the session is sampled-out"
);
}
#[test]
fn in_session_events_share_session_id() {
let (_t, data_path) = tempdir();
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
let user_event = EventMetric::new(
CommonMetricData {
name: "test_event".into(),
category: "test".into(),
send_in_pings: vec!["events".into()],
lifetime: Lifetime::Ping,
in_session: true,
..Default::default()
},
vec![],
);
user_event.record_sync(&glean, 1000, HashMap::new(), 0);
user_event.record_sync(&glean, 1001, HashMap::new(), 0);
let events = user_event
.get_value(&glean, "events")
.expect("expected events");
assert_eq!(2, events.len());
let id0 = &events[0].session.as_ref().unwrap().session_id;
let id1 = &events[1].session.as_ref().unwrap().session_id;
assert_eq!(id0, id1, "both events must share the same session_id");
assert!(!id0.is_empty(), "session_id must not be empty");
}
#[test]
fn mode_switch_auto_to_lifecycle_emits_abandoned_session_end() {
let (t, data_path) = tempdir();
let original_session_id;
{
let cfg = session_cfg(&data_path, SessionMode::Auto, 1.0, 1_800_000);
let mut glean = Glean::new(cfg).unwrap();
glean.handle_client_active();
original_session_id = glean.session_manager().session_id().unwrap().to_string();
glean.handle_client_inactive(); }
let cfg2 = session_cfg(&data_path, SessionMode::Lifecycle, 1.0, 0);
let glean2 = Glean::new(cfg2).unwrap();
let end_events = session_end_metric()
.get_value(&glean2, "events")
.expect("expected session_end(\"abandoned\") for orphaned session");
assert_eq!(1, end_events.len());
let extra = end_events[0]
.extra
.as_ref()
.expect("expected extras on session_end");
assert_eq!(
"abandoned",
extra.get("reason").unwrap(),
"orphaned session must produce reason='abandoned'"
);
assert_eq!(
&original_session_id,
extra.get("session_id").unwrap(),
"abandoned session_end must carry the original session_id"
);
assert!(
glean2.session_manager().session_id().is_none(),
"session_id must be cleared after orphaned session cleanup"
);
drop(t);
}