Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use bounded_vec_deque::BoundedVecDeque;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::time::get_current_time;
|
||||
|
||||
const MAX_BUFFER_SIZE: usize = 1024;
|
||||
|
||||
/// The length of time between events used to determine a 'session' boundary. In other words, if
|
||||
/// new_event occurs >SESSION_CONTINUATION_THRESHOLD_SECONDS after old_event, new_event is
|
||||
/// associated with a new session.
|
||||
pub(super) const SESSION_CONTINUATION_THRESHOLD_SECONDS: u64 = 5 * 60;
|
||||
|
||||
/// A data store for telemetry events. This is a thin wrapper around a [`BoundedVecDeque`] with
|
||||
/// APIs for domain-specific APIs for recording events (appending to the deque).
|
||||
pub(super) struct EventStore {
|
||||
// Bounded for now to save memory.
|
||||
// TODO: write to disk periodically
|
||||
pub(super) events: BoundedVecDeque<Event>,
|
||||
current_session_created_at: DateTime<Utc>,
|
||||
last_event_timestamp_seen: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Event {
|
||||
/// The type of the event and its payload.
|
||||
pub payload: EventPayload,
|
||||
|
||||
// We are using the session creation time as the identifier for the session.
|
||||
// Some metrics platforms (e.g. Amplitude) expect this.
|
||||
pub session_created_at: DateTime<Utc>,
|
||||
|
||||
/// The time at which the event occurred.
|
||||
pub timestamp: DateTime<Utc>,
|
||||
|
||||
/// Whether the event contains user-generated content.
|
||||
pub contains_ugc: bool,
|
||||
}
|
||||
|
||||
/// Represents the type of telemetry event and its contents.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum EventPayload {
|
||||
IdentifyUser {
|
||||
user_id: String,
|
||||
anonymous_id: String,
|
||||
},
|
||||
AppActive {
|
||||
user_id: Option<String>,
|
||||
anonymous_id: String,
|
||||
},
|
||||
NamedEvent {
|
||||
user_id: Option<String>,
|
||||
anonymous_id: String,
|
||||
name: Cow<'static, str>,
|
||||
value: Option<Value>,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventStore {
|
||||
pub(super) fn new() -> Self {
|
||||
let initial_timestamp = get_current_time();
|
||||
Self {
|
||||
events: BoundedVecDeque::new(MAX_BUFFER_SIZE),
|
||||
current_session_created_at: initial_timestamp,
|
||||
last_event_timestamp_seen: initial_timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
// Register a named telemetry event
|
||||
// Create a new session if the session is stale
|
||||
pub(super) fn record_event(
|
||||
&mut self,
|
||||
user_id: Option<String>,
|
||||
anonymous_id: String,
|
||||
name: Cow<'static, str>,
|
||||
payload: Option<Value>,
|
||||
contains_ugc: bool,
|
||||
timestamp: DateTime<Utc>,
|
||||
) {
|
||||
let event = self.create_event(
|
||||
user_id,
|
||||
anonymous_id,
|
||||
name,
|
||||
payload,
|
||||
contains_ugc,
|
||||
timestamp,
|
||||
);
|
||||
|
||||
#[cfg(feature = "log_named_telemetry_events")]
|
||||
log::info!("Recorded telemetry event: {event:#?}");
|
||||
|
||||
self.events.push_back(event);
|
||||
}
|
||||
|
||||
// Register an Identify User telemetry event
|
||||
// Create a new session if the session is stale
|
||||
pub(super) fn record_identify_user_event(
|
||||
&mut self,
|
||||
user_id: String,
|
||||
anonymous_id: String,
|
||||
timestamp: DateTime<Utc>,
|
||||
) {
|
||||
let session_created_at = if self.is_session_stale(timestamp) {
|
||||
self.current_session_created_at = timestamp;
|
||||
timestamp
|
||||
} else {
|
||||
self.current_session_created_at
|
||||
};
|
||||
self.last_event_timestamp_seen = self.last_event_timestamp_seen.max(timestamp);
|
||||
self.events.push_back(Event {
|
||||
session_created_at,
|
||||
payload: EventPayload::IdentifyUser {
|
||||
user_id,
|
||||
anonymous_id,
|
||||
},
|
||||
timestamp,
|
||||
contains_ugc: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Called every time app is active
|
||||
// If session is fresh and the last event on the queue is an App Active event, collapse them
|
||||
// Else, it behaves like `record_event`
|
||||
pub(super) fn record_app_active(
|
||||
&mut self,
|
||||
user_id: Option<String>,
|
||||
anonymous_id: String,
|
||||
timestamp: DateTime<Utc>,
|
||||
) {
|
||||
if !self.is_session_stale(timestamp) {
|
||||
if let Some(last_event) = self.events.back_mut().filter(|event| {
|
||||
event.payload
|
||||
== EventPayload::AppActive {
|
||||
user_id: user_id.clone(),
|
||||
anonymous_id: anonymous_id.clone(),
|
||||
}
|
||||
}) {
|
||||
last_event.timestamp = timestamp;
|
||||
self.last_event_timestamp_seen = self.last_event_timestamp_seen.max(timestamp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let session_created_at = if self.is_session_stale(timestamp) {
|
||||
self.current_session_created_at = timestamp;
|
||||
timestamp
|
||||
} else {
|
||||
self.current_session_created_at
|
||||
};
|
||||
self.last_event_timestamp_seen = self.last_event_timestamp_seen.max(timestamp);
|
||||
self.events.push_back(Event {
|
||||
session_created_at,
|
||||
payload: EventPayload::AppActive {
|
||||
user_id,
|
||||
anonymous_id,
|
||||
},
|
||||
timestamp,
|
||||
contains_ugc: false,
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns a newly [`Event`], while also updating `Self::last_event_timestamp_seen` and
|
||||
/// `Self::current_session_created_at`, if necessary.
|
||||
pub(super) fn create_event(
|
||||
&mut self,
|
||||
user_id: Option<String>,
|
||||
anonymous_id: String,
|
||||
name: Cow<'static, str>,
|
||||
payload: Option<Value>,
|
||||
contains_ugc: bool,
|
||||
timestamp: DateTime<Utc>,
|
||||
) -> Event {
|
||||
let session_created_at = if self.is_session_stale(timestamp) {
|
||||
self.current_session_created_at = timestamp;
|
||||
timestamp
|
||||
} else {
|
||||
self.current_session_created_at
|
||||
};
|
||||
self.last_event_timestamp_seen = self.last_event_timestamp_seen.max(timestamp);
|
||||
Event {
|
||||
session_created_at,
|
||||
payload: EventPayload::NamedEvent {
|
||||
user_id,
|
||||
anonymous_id,
|
||||
name,
|
||||
value: payload,
|
||||
},
|
||||
timestamp,
|
||||
contains_ugc,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_session_stale(&self, now: DateTime<Utc>) -> bool {
|
||||
let session_freshness_threshold = self.last_event_timestamp_seen
|
||||
+ Duration::seconds(SESSION_CONTINUATION_THRESHOLD_SECONDS as i64);
|
||||
now > session_freshness_threshold
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "event_store_test.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,150 @@
|
||||
use crate::telemetry::event_store::{
|
||||
EventPayload, EventStore, SESSION_CONTINUATION_THRESHOLD_SECONDS,
|
||||
};
|
||||
use crate::time::{get_current_time, test_offset_time};
|
||||
use chrono::{Duration, TimeZone, Utc};
|
||||
|
||||
#[test]
|
||||
fn test_initialize_session() {
|
||||
test_offset_time(5);
|
||||
let event_store = EventStore::new();
|
||||
assert_eq!(
|
||||
event_store.current_session_created_at,
|
||||
Utc.timestamp_opt(5, 0).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_event_queue_empty() {
|
||||
let user_id = Some("user123".to_string());
|
||||
let anonymous_id = "anon-user-xyz".to_string();
|
||||
let mut event_store = EventStore::new();
|
||||
event_store.record_app_active(user_id.clone(), anonymous_id.clone(), get_current_time());
|
||||
let session_created_at_0 = event_store.current_session_created_at;
|
||||
|
||||
// Queue is empty and an event comes in while session is fresh
|
||||
event_store.events.clear();
|
||||
test_offset_time(1);
|
||||
event_store.record_app_active(user_id.clone(), anonymous_id.clone(), get_current_time());
|
||||
assert_eq!(
|
||||
event_store.events.back().unwrap().session_created_at,
|
||||
session_created_at_0
|
||||
);
|
||||
|
||||
// Queue is empty and an event comes in while session is stale
|
||||
event_store.events.clear();
|
||||
let inactivity_duration = SESSION_CONTINUATION_THRESHOLD_SECONDS as i64 + 1;
|
||||
test_offset_time(inactivity_duration);
|
||||
let now = get_current_time();
|
||||
event_store.record_app_active(user_id, anonymous_id, get_current_time());
|
||||
assert_eq!(event_store.events.back().unwrap().session_created_at, now);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_app_active_after_inactivity() {
|
||||
let user_id = Some("user123".to_string());
|
||||
let anonymous_id = "anon-user-xyz".to_string();
|
||||
let mut event_store = EventStore::new();
|
||||
let inactivity_duration = SESSION_CONTINUATION_THRESHOLD_SECONDS as i64 + 1;
|
||||
event_store.record_app_active(user_id.clone(), anonymous_id.clone(), get_current_time());
|
||||
let session_created_at_0 = event_store.current_session_created_at;
|
||||
|
||||
test_offset_time(inactivity_duration);
|
||||
|
||||
event_store.record_app_active(user_id.clone(), anonymous_id.clone(), get_current_time());
|
||||
assert_eq!(
|
||||
event_store.events.back().unwrap().payload,
|
||||
EventPayload::AppActive {
|
||||
user_id: user_id.clone(),
|
||||
anonymous_id: anonymous_id.clone()
|
||||
}
|
||||
);
|
||||
let session_created_at_1 = event_store.events.back().unwrap().session_created_at;
|
||||
assert_eq!(
|
||||
session_created_at_1 - session_created_at_0,
|
||||
Duration::seconds(inactivity_duration)
|
||||
);
|
||||
|
||||
test_offset_time(inactivity_duration);
|
||||
|
||||
event_store.record_event(
|
||||
user_id.clone(),
|
||||
anonymous_id.clone(),
|
||||
"Block Creation".into(),
|
||||
None,
|
||||
false, /* contains_ugc */
|
||||
get_current_time(),
|
||||
);
|
||||
assert_eq!(
|
||||
event_store.events.back().unwrap().payload,
|
||||
EventPayload::NamedEvent {
|
||||
user_id,
|
||||
anonymous_id,
|
||||
name: "Block Creation".into(),
|
||||
value: None
|
||||
}
|
||||
);
|
||||
let session_created_at_2 = event_store.events.back().unwrap().session_created_at;
|
||||
assert_eq!(
|
||||
session_created_at_2 - session_created_at_1,
|
||||
Duration::seconds(inactivity_duration)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_app_active_after_activity() {
|
||||
let user_id = Some("user123".to_string());
|
||||
let anonymous_id = "anon-user-xyz".to_string();
|
||||
let mut event_store = EventStore::new();
|
||||
event_store.record_app_active(user_id.clone(), anonymous_id.clone(), get_current_time());
|
||||
assert_eq!(event_store.events.len(), 1);
|
||||
let timestamp_0 = event_store.events.back().unwrap().timestamp;
|
||||
let session_created_at_0 = event_store.events.back().unwrap().session_created_at;
|
||||
|
||||
// Check that the last app active event was updated
|
||||
test_offset_time(5);
|
||||
event_store.record_app_active(user_id.clone(), anonymous_id.clone(), get_current_time());
|
||||
assert_eq!(event_store.events.len(), 1);
|
||||
let timestamp_1 = event_store.events.back().unwrap().timestamp;
|
||||
let session_created_at_1 = event_store.events.back().unwrap().session_created_at;
|
||||
assert_eq!(session_created_at_0, session_created_at_1);
|
||||
assert_eq!(timestamp_1 - timestamp_0, Duration::seconds(5));
|
||||
|
||||
test_offset_time(5);
|
||||
event_store.record_event(
|
||||
user_id.clone(),
|
||||
anonymous_id.clone(),
|
||||
"Block Creation".into(),
|
||||
None,
|
||||
false, /* contains_ugc */
|
||||
get_current_time(),
|
||||
);
|
||||
assert_eq!(event_store.events.len(), 2);
|
||||
let timestamp_2 = event_store.events.back().unwrap().timestamp;
|
||||
let session_created_at_2 = event_store.events.back().unwrap().session_created_at;
|
||||
assert_eq!(session_created_at_1, session_created_at_2);
|
||||
assert_eq!(timestamp_2 - timestamp_1, Duration::seconds(5));
|
||||
|
||||
test_offset_time(5);
|
||||
event_store.record_event(
|
||||
user_id.clone(),
|
||||
anonymous_id.clone(),
|
||||
"Block Creation".into(),
|
||||
None,
|
||||
false, /* contains_ugc */
|
||||
get_current_time(),
|
||||
);
|
||||
assert_eq!(event_store.events.len(), 3);
|
||||
let timestamp_3 = event_store.events.back().unwrap().timestamp;
|
||||
let session_created_at_3 = event_store.events.back().unwrap().session_created_at;
|
||||
assert_eq!(session_created_at_2, session_created_at_3);
|
||||
assert_eq!(timestamp_3 - timestamp_2, Duration::seconds(5));
|
||||
|
||||
test_offset_time(5);
|
||||
event_store.record_app_active(user_id, anonymous_id, get_current_time());
|
||||
assert_eq!(event_store.events.len(), 4);
|
||||
let timestamp_4 = event_store.events.back().unwrap().timestamp;
|
||||
let session_created_at_4 = event_store.events.back().unwrap().session_created_at;
|
||||
assert_eq!(session_created_at_3, session_created_at_4);
|
||||
assert_eq!(timestamp_4 - timestamp_3, Duration::seconds(5));
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
mod event_store;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use serde_json::Value;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use event_store::*;
|
||||
pub use event_store::{Event, EventPayload};
|
||||
|
||||
lazy_static! {
|
||||
static ref TELEMETRY: Mutex<EventStore> = Mutex::new(EventStore::new());
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! record_telemetry_from_ctx {
|
||||
($user_id: expr, $anonymous_id: expr, $name:expr, $payload: expr, $contains_ugc: expr, $ctx: expr) => {{
|
||||
let timestamp = $crate::time::get_current_time();
|
||||
$ctx.background_executor()
|
||||
.spawn(async move {
|
||||
$crate::telemetry::record_event(
|
||||
$user_id,
|
||||
$anonymous_id,
|
||||
$name,
|
||||
$payload,
|
||||
$contains_ugc,
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
.detach();
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! record_telemetry_on_executor {
|
||||
($user_id: expr, $anonymous_id: expr, $name:expr, $payload: expr, $contains_ugc: expr, $executor: expr) => {{
|
||||
let timestamp = $crate::time::get_current_time();
|
||||
let _ = $executor
|
||||
.spawn(async move {
|
||||
$crate::telemetry::record_event(
|
||||
$user_id,
|
||||
$anonymous_id,
|
||||
$name,
|
||||
$payload,
|
||||
$contains_ugc,
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
.detach();
|
||||
}};
|
||||
}
|
||||
|
||||
/// Creates a new `Event`, but does not record it. It is up to the caller to determine when, and
|
||||
/// how, the event should be recorded.
|
||||
pub fn create_event(
|
||||
user_id: Option<String>,
|
||||
anonymous_id: String,
|
||||
name: Cow<'static, str>,
|
||||
payload: Option<Value>,
|
||||
contains_ugc: bool,
|
||||
timestamp: DateTime<Utc>,
|
||||
) -> Event {
|
||||
let mut telemetry = TELEMETRY.lock();
|
||||
telemetry.create_event(
|
||||
user_id,
|
||||
anonymous_id,
|
||||
name,
|
||||
payload,
|
||||
contains_ugc,
|
||||
timestamp,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn record_event(
|
||||
user_id: Option<String>,
|
||||
anonymous_id: String,
|
||||
name: Cow<'static, str>,
|
||||
payload: Option<Value>,
|
||||
contains_ugc: bool,
|
||||
timestamp: DateTime<Utc>,
|
||||
) {
|
||||
let mut telemetry = TELEMETRY.lock();
|
||||
telemetry.record_event(
|
||||
user_id,
|
||||
anonymous_id,
|
||||
name,
|
||||
payload,
|
||||
contains_ugc,
|
||||
timestamp,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn record_identify_user_event(user_id: String, anonymous_id: String, timestamp: DateTime<Utc>) {
|
||||
let mut telemetry = TELEMETRY.lock();
|
||||
telemetry.record_identify_user_event(user_id, anonymous_id, timestamp);
|
||||
}
|
||||
|
||||
/// Adds a 'App Active' event to the global event queue. This should only be called in an async
|
||||
/// context.
|
||||
pub fn record_app_active_event(
|
||||
user_id: Option<String>,
|
||||
anonymous_id: String,
|
||||
timestamp: DateTime<Utc>,
|
||||
) {
|
||||
let mut telemetry = TELEMETRY.lock();
|
||||
telemetry.record_app_active(user_id, anonymous_id, timestamp);
|
||||
}
|
||||
|
||||
pub fn flush_events() -> Vec<Event> {
|
||||
TELEMETRY.lock().events.drain(..).collect()
|
||||
}
|
||||
Reference in New Issue
Block a user