use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use chrono::{DateTime, FixedOffset, SecondsFormat};
use malloc_size_of_derive::MallocSizeOf;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::metrics::{QuantityMetric, StringMetric};
use crate::storage::INTERNAL_STORAGE;
use crate::{CommonMetricData, Glean, Lifetime};
const SESSION_SEQ_METRIC_NAME: &str = "session#seq";
const SESSION_ID_METRIC_NAME: &str = "session#id";
const SESSION_INACTIVE_SINCE_METRIC_NAME: &str = "session#inactive_since";
const SESSION_START_TIME_METRIC_NAME: &str = "session#start_time";
const SESSION_EVENT_SEQ_METRIC_NAME: &str = "session#event_seq";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default, MallocSizeOf)]
pub enum SessionMode {
#[default]
Auto,
Lifecycle,
Manual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionState {
Inactive,
Active,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, MallocSizeOf)]
pub struct SessionMetadata {
pub session_id: String,
pub session_seq: u64,
pub event_seq: u64,
pub session_sample_rate: f64,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub session_start_time: Option<String>,
}
impl Eq for SessionMetadata {}
#[derive(Debug, Clone, Serialize, Deserialize, MallocSizeOf)]
pub enum EventSessionContext {
OutOfSession,
InSession(SessionMetadata),
}
#[derive(Debug)]
pub struct SessionManager {
pub(crate) mode: SessionMode,
pub(crate) state: SessionState,
pub(crate) session_id: Option<Uuid>,
pub(crate) session_seq: u64,
pub(crate) event_seq: AtomicU64,
pub(crate) configured_sample_rate: f64,
pub(crate) sample_rate: f64,
pub(crate) sampled_in: bool,
pub(crate) session_start_time: Option<DateTime<FixedOffset>>,
pub(crate) inactive_since: Option<DateTime<FixedOffset>>,
pub(crate) inactivity_timeout: Duration,
}
impl SessionManager {
pub fn new(mode: SessionMode, sample_rate: f64, inactivity_timeout: Duration) -> Self {
let clamped = sample_rate.clamp(0.0, 1.0);
Self {
mode,
state: SessionState::Inactive,
session_id: None,
session_seq: 0,
event_seq: AtomicU64::new(0),
configured_sample_rate: clamped,
sample_rate: clamped,
sampled_in: true, session_start_time: None,
inactive_since: None,
inactivity_timeout,
}
}
pub fn is_sampled_in(&self) -> bool {
match self.state {
SessionState::Inactive => true,
SessionState::Active => self.sampled_in,
}
}
pub fn is_active(&self) -> bool {
self.state == SessionState::Active
}
pub fn session_id(&self) -> Option<Uuid> {
self.session_id
}
pub fn sampled_in(&self) -> bool {
self.sampled_in
}
pub fn session_start_time(&self) -> Option<DateTime<FixedOffset>> {
self.session_start_time
}
pub fn current_metadata(&self) -> Option<SessionMetadata> {
if self.state != SessionState::Active {
return None;
}
let id = self.session_id?;
Some(SessionMetadata {
session_id: id.to_string(),
session_seq: self.session_seq,
event_seq: self.event_seq.load(Ordering::Relaxed),
session_sample_rate: self.sample_rate,
session_start_time: self
.session_start_time
.map(|t| t.to_rfc3339_opts(SecondsFormat::Millis, true)),
})
}
pub fn compute_event_context(&self) -> EventSessionContext {
match self.state {
SessionState::Inactive => EventSessionContext::OutOfSession,
SessionState::Active => {
debug_assert!(
self.sampled_in,
"compute_event_context called for unsampled session"
);
match self.current_metadata_with_next_seq() {
Some(meta) => EventSessionContext::InSession(meta),
None => EventSessionContext::OutOfSession,
}
}
}
}
pub fn current_metadata_with_next_seq(&self) -> Option<SessionMetadata> {
if self.state != SessionState::Active {
return None;
}
let id = self.session_id?;
let seq = self.event_seq.fetch_add(1, Ordering::Relaxed);
Some(SessionMetadata {
session_id: id.to_string(),
session_seq: self.session_seq,
event_seq: seq,
session_sample_rate: self.sample_rate,
session_start_time: self
.session_start_time
.map(|t| t.to_rfc3339_opts(SecondsFormat::Millis, true)),
})
}
pub fn reset_state(&mut self) {
self.state = SessionState::Inactive;
self.session_id = None;
self.inactive_since = None;
self.session_start_time = None;
}
}
pub(crate) fn uuid_to_sample_value(uuid: &Uuid) -> f64 {
let bytes = uuid.as_bytes();
let mut arr = [0u8; 8];
arr.copy_from_slice(&bytes[..8]);
let n = u64::from_be_bytes(arr);
(n as f64) / 2.0f64.powi(64)
}
fn make_session_seq_metric() -> QuantityMetric {
QuantityMetric::new(CommonMetricData {
name: SESSION_SEQ_METRIC_NAME.into(),
category: String::new(),
send_in_pings: vec![INTERNAL_STORAGE.into()],
lifetime: Lifetime::User,
..Default::default()
})
}
fn make_session_id_metric() -> StringMetric {
StringMetric::new(CommonMetricData {
name: SESSION_ID_METRIC_NAME.into(),
category: String::new(),
send_in_pings: vec![INTERNAL_STORAGE.into()],
lifetime: Lifetime::User,
..Default::default()
})
}
fn make_inactive_since_metric() -> StringMetric {
StringMetric::new(CommonMetricData {
name: SESSION_INACTIVE_SINCE_METRIC_NAME.into(),
category: String::new(),
send_in_pings: vec![INTERNAL_STORAGE.into()],
lifetime: Lifetime::User,
..Default::default()
})
}
pub(crate) fn read_session_seq(glean: &Glean) -> u64 {
make_session_seq_metric()
.get_value(glean, INTERNAL_STORAGE)
.filter(|&v| v >= 0)
.map(|v| v as u64)
.unwrap_or(0)
}
pub(crate) fn clear(glean: &Glean) {
clear_session_id(glean);
clear_inactive_since(glean);
clear_session_start_time(glean);
clear_session_event_seq(glean);
}
pub(crate) fn store_session_seq(glean: &Glean, seq: u64) {
make_session_seq_metric().set_sync(glean, seq as i64);
}
pub(crate) fn persist_session_id(glean: &Glean, id: &str) {
make_session_id_metric().set_sync(glean, id);
}
pub(crate) fn clear_session_id(glean: &Glean) {
make_session_id_metric().set_sync(glean, "");
}
pub(crate) fn read_session_id(glean: &Glean) -> Option<String> {
let id = make_session_id_metric().get_value(glean, INTERNAL_STORAGE)?;
if id.is_empty() {
None
} else {
Some(id)
}
}
pub(crate) fn persist_inactive_since(glean: &Glean, ts: DateTime<FixedOffset>) {
make_inactive_since_metric().set_sync(
glean,
ts.to_rfc3339_opts(SecondsFormat::Millis, true).as_str(),
);
}
pub(crate) fn read_inactive_since(glean: &Glean) -> Option<DateTime<FixedOffset>> {
let s = make_inactive_since_metric().get_value(glean, INTERNAL_STORAGE)?;
if s.is_empty() {
return None;
}
DateTime::parse_from_rfc3339(&s).ok()
}
pub(crate) fn clear_inactive_since(glean: &Glean) {
make_inactive_since_metric().set_sync(glean, "");
}
fn make_session_start_time_metric() -> StringMetric {
StringMetric::new(CommonMetricData {
name: SESSION_START_TIME_METRIC_NAME.into(),
category: String::new(),
send_in_pings: vec![INTERNAL_STORAGE.into()],
lifetime: Lifetime::User,
..Default::default()
})
}
pub(crate) fn persist_session_start_time(glean: &Glean, ts: DateTime<FixedOffset>) {
make_session_start_time_metric().set_sync(
glean,
ts.to_rfc3339_opts(SecondsFormat::Millis, true).as_str(),
);
}
pub(crate) fn read_session_start_time(glean: &Glean) -> Option<DateTime<FixedOffset>> {
let s = make_session_start_time_metric().get_value(glean, INTERNAL_STORAGE)?;
if s.is_empty() {
return None;
}
DateTime::parse_from_rfc3339(&s).ok()
}
pub(crate) fn clear_session_start_time(glean: &Glean) {
make_session_start_time_metric().set_sync(glean, "");
}
fn make_session_event_seq_metric() -> QuantityMetric {
QuantityMetric::new(CommonMetricData {
name: SESSION_EVENT_SEQ_METRIC_NAME.into(),
category: String::new(),
send_in_pings: vec![INTERNAL_STORAGE.into()],
lifetime: Lifetime::User,
..Default::default()
})
}
pub(crate) fn read_session_event_seq(glean: &Glean) -> u64 {
make_session_event_seq_metric()
.get_value(glean, INTERNAL_STORAGE)
.filter(|&v| v >= 0)
.map(|v| v as u64)
.unwrap_or(0)
}
pub(crate) fn store_session_event_seq(glean: &Glean, seq: u64) {
make_session_event_seq_metric().set_sync(glean, seq as i64);
}
pub(crate) fn clear_session_event_seq(glean: &Glean) {
make_session_event_seq_metric().set_sync(glean, 0);
}