Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/libs/shared/src/day.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::types::utils::CalendarDate;
use time::{Duration, OffsetDateTime};
use time::{Duration, OffsetDateTime, Time};

/// Converts a Unix timestamp (in nanoseconds) to an `OffsetDateTime`.
///
Expand Down Expand Up @@ -43,3 +43,28 @@ pub fn day_of_the_year(timestamp: &u64) -> usize {
pub fn calendar_date(timestamp: &u64) -> CalendarDate {
CalendarDate::from(&to_date(timestamp).to_calendar_date())
}

/// Converts a Unix timestamp (in nanoseconds) to the start of its day (00:00:00 UTC).
///
/// # Arguments
/// - `timestamp`: A reference to a `u64` Unix timestamp in nanoseconds.
///
/// # Returns
/// - `Ok(u64)` containing the nanosecond timestamp at the start of the day.
/// - `Err(String)` if the conversion to midnight fails.
///
/// # Errors
/// This function returns an error if constructing the midnight time fails,
/// or if the resulting timestamp cannot be safely converted to `u64`.
pub fn start_of_day(timestamp: &u64) -> Result<u64, String> {
let datetime = to_date(timestamp);
let date_only = datetime.date();

let midnight = Time::from_hms(0, 0, 0)
.map_err(|e| format!("Failed to create midnight time: {e}"))?;

let start_of_day = date_only.with_time(midnight);
let nanos = start_of_day.assume_utc().unix_timestamp_nanos();

nanos.try_into().map_err(|_| "Timestamp too large for u64".to_string())
}
1 change: 1 addition & 0 deletions src/orbiter/src/analytics/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod raw;
File renamed without changes.
108 changes: 108 additions & 0 deletions src/orbiter/src/events/analytics/filters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use crate::state::types::state::{AnalyticKey, AnalyticSatelliteKey};
use crate::types::interface::GetAnalytics;
use junobuild_shared::types::state::SatelliteId;
use std::ops::RangeBounds;
use junobuild_shared::day::start_of_day;
use crate::state::types::state::analytics::{DailyAnalyticKey, DailyAnalyticSatelliteKey, DailySessionsAnalyticKey, DailySessionsAnalyticSatelliteKey};

pub fn filter_daily_analytics(
GetAnalytics {
from,
to,
satellite_id: _,
method: _
}: &GetAnalytics,
) -> impl RangeBounds<DailyAnalyticKey> {
let start_key = DailyAnalyticKey {
start_of_day: from
.map(|from| start_of_day(&from).unwrap_or(from)) // We fallback for simplicity reasons
.unwrap_or(u64::MIN),
};

let end_key = DailyAnalyticKey {
start_of_day: to
.map(|to| start_of_day(&to).unwrap_or(to))
.unwrap_or(u64::MAX),
};

start_key..end_key
}

pub fn filter_daily_satellites_analytics(
GetAnalytics {
from,
to,
satellite_id: _,
method: _
}: &GetAnalytics,
satellite_id: SatelliteId,
) -> impl RangeBounds<DailyAnalyticSatelliteKey> {
let start_key = DailyAnalyticSatelliteKey {
satellite_id,
start_of_day: from
.map(|from| start_of_day(&from).unwrap_or(from)) // We fallback for simplicity reasons
.unwrap_or(u64::MIN),
};

let end_key = DailyAnalyticSatelliteKey {
satellite_id,
start_of_day: to
.map(|to| start_of_day(&to).unwrap_or(to))
.unwrap_or(u64::MAX),
};

start_key..end_key
}

pub fn filter_daily_sessions_analytics(
GetAnalytics {
from,
to,
satellite_id: _,
method: _
}: &GetAnalytics,
) -> impl RangeBounds<DailySessionsAnalyticKey> {
let start_key = DailySessionsAnalyticKey {
start_of_day: from
.map(|from| start_of_day(&from).unwrap_or(from)) // We fallback for simplicity reasons
.unwrap_or(u64::MIN),
session_id: "".to_string()
};

let end_key = DailySessionsAnalyticKey {
start_of_day: to
.map(|to| start_of_day(&to).unwrap_or(to))
.unwrap_or(u64::MAX),
session_id: "".to_string()
};

start_key..end_key
}

pub fn filter_daily_sessions_satellites_analytics(
GetAnalytics {
from,
to,
satellite_id: _,
method: _
}: &GetAnalytics,
satellite_id: SatelliteId,
) -> impl RangeBounds<DailySessionsAnalyticSatelliteKey> {
let start_key = DailySessionsAnalyticSatelliteKey {
satellite_id,
start_of_day: from
.map(|from| start_of_day(&from).unwrap_or(from)) // We fallback for simplicity reasons
.unwrap_or(u64::MIN),
session_id: "".to_string()
};

let end_key = DailySessionsAnalyticSatelliteKey {
satellite_id,
start_of_day: to
.map(|to| start_of_day(&to).unwrap_or(to))
.unwrap_or(u64::MAX),
session_id: "".to_string()
};

start_key..end_key
}
3 changes: 3 additions & 0 deletions src/orbiter/src/events/analytics/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod precompute;
mod store;
mod filters;
14 changes: 14 additions & 0 deletions src/orbiter/src/events/analytics/precompute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use crate::events::analytics::store::{increment_daily_page_views, increment_daily_session_views};
use crate::state::types::state::{AnalyticKey, PageView};
use crate::state::types::state::analytics::{DailyAnalyticKey, DailySessionsAnalyticKey};

pub fn update_page_view_analytics(key: &AnalyticKey, page_view: &PageView) -> Result<(), String> {
let daily_key = DailyAnalyticKey::from_analytic_key(key)?;
increment_daily_page_views(&daily_key, page_view)?;

let daily_session_key = DailySessionsAnalyticKey::from_analytic_key_and_page_view(key, page_view)?;
increment_daily_session_views(&daily_session_key, page_view)?;

Ok(())
}

139 changes: 139 additions & 0 deletions src/orbiter/src/events/analytics/store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use std::collections::HashSet;
use junobuild_shared::version::next_version;
use crate::events::analytics::filters::{filter_daily_analytics, filter_daily_satellites_analytics, filter_daily_sessions_analytics, filter_daily_sessions_satellites_analytics};
use crate::events::filters::{filter_analytics, filter_satellites_analytics};
use crate::state::memory::STATE;
use crate::state::types::state::{AnalyticKey, AnalyticSatelliteKey, PageView, StableState};
use crate::state::types::state::analytics::{DailyPageViews, DailyAnalyticKey, DailyAnalyticSatelliteKey, DailySessionsAnalyticKey, DailySessionViews, DailySessionsAnalyticSatelliteKey};
use crate::types::interface::GetAnalytics;
// ---------------------------------------------------------
// Read
// ---------------------------------------------------------

pub fn get_daily_page_views(filter: &GetAnalytics) -> Vec<(DailyAnalyticKey, DailyPageViews)> {
STATE.with(|state| get_daily_page_views_impl(filter, &state.borrow_mut().stable))
}

fn get_daily_page_views_impl(filter: &GetAnalytics, state: &StableState) -> Vec<(DailyAnalyticKey, DailyPageViews)> {
match filter.satellite_id {
None => state
.daily_page_views
.range(filter_daily_analytics(filter))
.map(|(key, daily_page_view)| (key, daily_page_view))
.collect(),
Some(satellite_id) => {
let satellites_keys: Vec<(DailyAnalyticSatelliteKey, DailyAnalyticKey)> = state
.satellites_daily_page_views
.range(filter_daily_satellites_analytics(filter, satellite_id))
.collect();
satellites_keys
.iter()
.filter_map(|(_, key)| {
let daily_page_view = state.daily_page_views.get(key);
daily_page_view.map(|daily_page_view| (key.clone(), daily_page_view))
})
.collect()
}
}
}

pub fn get_daily_sessions(filter: &GetAnalytics) -> Vec<(DailySessionsAnalyticKey, DailySessionViews)> {
STATE.with(|state| get_daily_sessions_impl(filter, &state.borrow_mut().stable))
}

fn get_daily_sessions_impl(filter: &GetAnalytics, state: &StableState) -> Vec<(DailySessionsAnalyticKey, DailySessionViews)> {
match filter.satellite_id {
None => state
.daily_session_views
.range(filter_daily_sessions_analytics(filter))
.map(|(key, daily_session)| (key, daily_session))
.collect(),
Some(satellite_id) => {
let satellites_keys: Vec<(DailySessionsAnalyticSatelliteKey, DailySessionsAnalyticKey)> = state
.satellites_daily_session_views
.range(filter_daily_sessions_satellites_analytics(filter, satellite_id))
.collect();
satellites_keys
.iter()
.filter_map(|(_, key)| {
let daily_session_views = state.daily_session_views.get(key);
daily_session_views.map(|daily_session_view| (key.clone(), daily_session_view))
})
.collect()
}
}
}

// ---------------------------------------------------------
// Save
// ---------------------------------------------------------

pub fn increment_daily_page_views(daily_key: &DailyAnalyticKey, page_view: &PageView) -> Result<(), String> {
STATE.with(|state| increment_daily_page_view_analytics_impl(daily_key, page_view, &mut state.borrow_mut().stable))
}

pub fn increment_daily_session_views(daily_session_key: &DailySessionsAnalyticKey, page_view: &PageView) -> Result<(), String> {
STATE.with(|state| increment_daily_session_view_analytics_impl(daily_session_key, page_view, &mut state.borrow_mut().stable))
}

fn increment_daily_page_view_analytics_impl(
daily_key: &DailyAnalyticKey, page_view: &PageView,
state: &mut StableState,
) -> Result<(), String> {
let current_daily_total_page_views = state.daily_page_views.get(&daily_key);

let version = next_version(&current_daily_total_page_views);

let count = current_daily_total_page_views
.map(|v| v.count.saturating_add(1))
.unwrap_or(1);

let new_daily_page_views: DailyPageViews = DailyPageViews {
count,
version: Some(version),
};

state.daily_page_views.insert(daily_key.clone(), new_daily_page_views);

state.satellites_daily_page_views.insert(
DailyAnalyticSatelliteKey::from_key(&daily_key, &page_view.satellite_id),
daily_key.clone(),
);

Ok(())
}

fn increment_daily_session_view_analytics_impl(
daily_session_key: &DailySessionsAnalyticKey, page_view: &PageView,
state: &mut StableState,
) -> Result<(), String> {
let mut current_daily_session_views = state.daily_session_views.get(&daily_session_key);

let version = next_version(&current_daily_session_views);

let count = current_daily_session_views
.as_ref()
.map(|v| v.count.saturating_add(1))
.unwrap_or(1);

let mut hrefs = current_daily_session_views
.map(|v| v.hrefs)
.unwrap_or_else(HashSet::new);

hrefs.insert(page_view.href.clone());

let new_daily_session_views: DailySessionViews = DailySessionViews {
count,
version: Some(version),
hrefs,
};

state.daily_session_views.insert(daily_session_key.clone(), new_daily_session_views);

state.satellites_daily_session_views.insert(
DailySessionsAnalyticSatelliteKey::from_key(&daily_session_key, &page_view.satellite_id, &page_view.session_id),
daily_session_key.clone(),
);

Ok(())
}
2 changes: 2 additions & 0 deletions src/orbiter/src/events/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub fn filter_analytics(
from,
to,
satellite_id: _,
method: _
}: &GetAnalytics,
) -> impl RangeBounds<AnalyticKey> {
let start_key = AnalyticKey {
Expand All @@ -28,6 +29,7 @@ pub fn filter_satellites_analytics(
from,
to,
satellite_id: _,
method: _
}: &GetAnalytics,
satellite_id: SatelliteId,
) -> impl RangeBounds<AnalyticSatelliteKey> {
Expand Down
7 changes: 6 additions & 1 deletion src/orbiter/src/events/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::assert::config::{
assert_page_views_enabled, assert_performance_metrics_enabled, assert_track_events_enabled,
};
use crate::config::store::get_satellite_config;
use crate::events::analytics::precompute::update_page_view_analytics;
use crate::events::store::{insert_page_view, insert_performance_metric, insert_track_event};
use crate::state::types::state::{AnalyticKey, PageView, PerformanceMetric, TrackEvent};
use crate::types::interface::{SetPageView, SetPerformanceMetric, SetTrackEvent};
Expand All @@ -12,7 +13,11 @@ pub fn assert_and_insert_page_view(
) -> Result<PageView, String> {
assert_page_views_enabled(&get_satellite_config(&page_view.satellite_id))?;

insert_page_view(key, page_view)
let saved_page_view = insert_page_view(key.clone(), page_view)?;

update_page_view_analytics(&key, &saved_page_view)?;

Ok(saved_page_view)
}

pub fn assert_and_insert_track_event(
Expand Down
1 change: 1 addition & 0 deletions src/orbiter/src/events/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod filters;
pub mod helpers;
pub mod store;
mod analytics;
4 changes: 2 additions & 2 deletions src/orbiter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
mod analytics;
mod assert;
mod config;
mod constants;
Expand All @@ -12,8 +11,9 @@ mod msg;
mod serializers;
mod state;
mod types;
mod analytics;

use crate::analytics::{
use analytics::raw::{
analytics_page_views_clients, analytics_page_views_metrics, analytics_page_views_top_10,
analytics_performance_metrics_web_vitals, analytics_track_events,
};
Expand Down
Loading