feat: unify user analytics and optimize overview aggregation

Merge user accounts and usage reporting into one page with a combined ranking and account table, shared precise time ranges, and simpler range labels.

Parse overview metadata once through a schema-only view migration and disable JIT locally for bucket rebuilds. Preserve automatic backfills.

Add redacted OAuth refresh diagnostics, bucket failure context, and regression coverage. Resolve strict Clippy warnings.
This commit is contained in:
elky
2026-10-05 00:28:31 +08:00
parent 2075cd95de
commit cb7b9c9ecd
27 changed files with 1255 additions and 587 deletions
@@ -1,5 +1,8 @@
use super::super::resolve_usage_user_group_scope;
use super::range::{build_comparison_range, parse_bounded_u32};
use super::range::{
build_comparison_range, parse_bounded_u32, precise_admin_stats_time_range,
resolve_precise_time_bounds,
};
use super::resolve_admin_usage_time_range;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::{
@@ -285,12 +288,36 @@ pub(super) async fn maybe_build_local_admin_stats_analytics_response(
Ok(value) => value,
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
};
let time_range = match resolve_admin_usage_time_range(request_context.query_string()) {
let legacy_time_range = match resolve_admin_usage_time_range(request_context.query_string())
{
Ok(value) => value,
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
};
if let Err(detail) = time_range.validate_for_time_series(granularity) {
return Ok(Some(admin_stats_bad_request_response(detail)));
let precise_bounds = match resolve_precise_time_bounds(request_context.query_string()) {
Ok(value) => value,
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
};
let precise_time_range = match precise_bounds {
Some((from, to)) => {
match precise_admin_stats_time_range(request_context.query_string(), from, to) {
Ok(value) => Some(value),
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
}
}
None => None,
};
let time_range = precise_time_range.as_ref().unwrap_or(&legacy_time_range);
if precise_bounds.is_none() {
if let Err(detail) = time_range.validate_for_time_series(granularity) {
return Ok(Some(admin_stats_bad_request_response(detail)));
}
} else if precise_bounds
.and_then(|(from, to)| to.checked_sub(from))
.is_some_and(|seconds| seconds > 90 * 86_400)
{
return Ok(Some(admin_stats_bad_request_response(
"Query range cannot exceed 90 days".to_string(),
)));
}
if !state.has_usage_data_reader() {
return Ok(Some(admin_stats_time_series_empty_response()));
@@ -314,7 +341,8 @@ pub(super) async fn maybe_build_local_admin_stats_analytics_response(
| AdminStatsGranularity::Week
| AdminStatsGranularity::Month => UsageTimeSeriesGranularity::Day,
};
let Some((created_from_unix_secs, created_until_unix_secs)) = time_range.to_unix_bounds()
let Some((created_from_unix_secs, created_until_unix_secs)) =
precise_bounds.or_else(|| time_range.to_unix_bounds())
else {
return Ok(Some(admin_stats_time_series_empty_response()));
};
@@ -336,7 +364,7 @@ pub(super) async fn maybe_build_local_admin_stats_analytics_response(
})
.await?;
return Ok(Some(build_admin_stats_time_series_response_from_summaries(
&time_range,
time_range,
granularity,
&buckets,
)));
@@ -5,7 +5,7 @@ use super::leaderboard::{
build_user_leaderboard_items_from_summaries, compare_leaderboard_items,
load_user_leaderboard_metadata, AdminStatsLeaderboardItem, AdminStatsLeaderboardNameMode,
};
use super::range::{parse_bounded_u32, parse_nonnegative_usize};
use super::range::{parse_bounded_u32, parse_nonnegative_usize, resolve_precise_time_bounds};
use super::resolve_admin_usage_time_range;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::{query_param_bool, query_param_value};
@@ -228,6 +228,10 @@ pub(super) async fn maybe_build_local_admin_stats_leaderboard_response(
Ok(value) => value,
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
};
let precise_bounds = match resolve_precise_time_bounds(query) {
Ok(value) => value,
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
};
let metric = match AdminStatsLeaderboardMetric::parse(query) {
Ok(value) => value,
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
@@ -272,7 +276,8 @@ pub(super) async fn maybe_build_local_admin_stats_leaderboard_response(
"user_id is not supported for the user group leaderboard".to_string(),
)));
}
let Some((created_from_unix_secs, created_until_unix_secs)) = time_range.to_unix_bounds()
let Some((created_from_unix_secs, created_until_unix_secs)) =
precise_bounds.or_else(|| time_range.to_unix_bounds())
else {
return Ok(Some(build_admin_stats_user_group_leaderboard_response(
metric,
@@ -402,6 +407,10 @@ pub(super) async fn maybe_build_local_admin_stats_leaderboard_response(
Ok(value) => value,
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
};
let precise_bounds = match resolve_precise_time_bounds(query) {
Ok(value) => value,
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
};
let metric = match AdminStatsLeaderboardMetric::parse(query) {
Ok(value) => value,
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
@@ -442,7 +451,8 @@ pub(super) async fn maybe_build_local_admin_stats_leaderboard_response(
Ok(value) => value,
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
};
let Some((created_from_unix_secs, created_until_unix_secs)) = time_range.to_unix_bounds()
let Some((created_from_unix_secs, created_until_unix_secs)) =
precise_bounds.or_else(|| time_range.to_unix_bounds())
else {
return Ok(Some(admin_stats_leaderboard_empty_response(
metric,
@@ -8,7 +8,10 @@ mod leaderboard;
mod leaderboard_routes;
mod provider_quota_routes;
mod range;
pub(crate) use self::range::{parse_bounded_u32, resolve_admin_usage_time_range};
pub(crate) use self::range::{
parse_bounded_u32, precise_admin_stats_time_range, resolve_admin_usage_time_range,
resolve_precise_time_bounds, resolve_usage_time_bounds,
};
pub(crate) use aether_admin::observability::stats::{
admin_stats_bad_request_response, aggregate_usage_stats, round_to, AdminStatsTimeRange,
AdminStatsUsageFilter,
@@ -4,10 +4,14 @@ pub(super) use aether_admin::observability::stats::{
admin_usage_default_days, build_comparison_range, build_time_range_from_days, parse_naive_date,
parse_nonnegative_usize, parse_tz_offset_minutes, resolve_preset_dates, user_today,
};
use chrono::{DateTime, Offset, TimeZone, Utc};
pub(crate) fn resolve_admin_usage_time_range(
query: Option<&str>,
) -> Result<AdminStatsTimeRange, String> {
if let Some((from, to)) = resolve_precise_time_bounds(query)? {
return precise_admin_stats_time_range(query, from, to);
}
match AdminStatsTimeRange::resolve_optional(query)? {
Some(time_range) => Ok(time_range),
None => {
@@ -20,3 +24,153 @@ pub(crate) fn resolve_admin_usage_time_range(
}
}
}
/// Resolve an exact UTC range supplied by the shared admin range picker.
///
/// The older stats handlers use `start_date`/`end_date` and fixed offsets. Keep
/// that parser intact and only opt into this path when both RFC 3339 endpoints
/// are present, so existing callers retain their behavior.
pub(crate) fn resolve_precise_time_bounds(
query: Option<&str>,
) -> Result<Option<(u64, u64)>, String> {
let entries =
url::form_urlencoded::parse(query.unwrap_or_default().as_bytes()).collect::<Vec<_>>();
let from = entries
.iter()
.filter(|(key, _)| key == "from")
.collect::<Vec<_>>();
let to = entries
.iter()
.filter(|(key, _)| key == "to")
.collect::<Vec<_>>();
if from.is_empty() && to.is_empty() {
return Ok(None);
}
if from.len() != 1 || to.len() != 1 {
return Err("from and to must each be provided once".into());
}
if entries
.iter()
.any(|(key, _)| matches!(key.as_ref(), "start_date" | "end_date" | "preset" | "days"))
{
return Err("precise from/to cannot be combined with date presets".into());
}
if let Some(zone) = query_param_value(query, "timezone") {
zone.parse::<chrono_tz::Tz>()
.map_err(|_| "invalid timezone".to_string())?;
}
let parse = |value: &str| -> Result<u64, String> {
let value = DateTime::parse_from_rfc3339(value)
.map_err(|_| "from/to must be RFC 3339 timestamps".to_string())?;
if value.timestamp_subsec_nanos() != 0 {
return Err("request records support second-aligned ranges".into());
}
u64::try_from(value.timestamp()).map_err(|_| "from/to must not precede Unix epoch".into())
};
let bounds = (parse(&from[0].1)?, parse(&to[0].1)?);
if bounds.0 >= bounds.1 || bounds.1 - bounds.0 > 366 * 86_400 {
return Err("from/to must define a nonempty range of at most 366 days".into());
}
Ok(Some(bounds))
}
/// Return the exact range when present, otherwise preserve the legacy stats
/// date/preset behavior.
pub(crate) fn resolve_usage_time_bounds(query: Option<&str>) -> Result<Option<(u64, u64)>, String> {
if let Some(bounds) = resolve_precise_time_bounds(query)? {
return Ok(Some(bounds));
}
Ok(resolve_admin_usage_time_range(query)?.to_unix_bounds())
}
/// Build the date metadata used by the existing stats response builders for an
/// exact range. The data query still uses the exact UTC bounds; this metadata
/// only supplies the local date labels and offset expected by old clients.
pub(crate) fn precise_admin_stats_time_range(
query: Option<&str>,
from: u64,
to: u64,
) -> Result<AdminStatsTimeRange, String> {
let timezone_name = query_param_value(query, "timezone");
let (start_date, end_date, tz_offset_minutes) = if let Some(name) = timezone_name {
let timezone = name
.parse::<chrono_tz::Tz>()
.map_err(|_| "invalid timezone".to_string())?;
let start = Utc
.timestamp_opt(
i64::try_from(from).map_err(|_| "invalid from timestamp")?,
0,
)
.single()
.ok_or_else(|| "invalid from timestamp".to_string())?
.with_timezone(&timezone);
let end = Utc
.timestamp_opt(
i64::try_from(to.saturating_sub(1)).map_err(|_| "invalid to timestamp")?,
0,
)
.single()
.ok_or_else(|| "invalid to timestamp".to_string())?
.with_timezone(&timezone);
(
start.date_naive(),
end.date_naive(),
start.offset().fix().local_minus_utc() / 60,
)
} else {
let offset = parse_tz_offset_minutes(query)?;
let fixed = chrono::FixedOffset::east_opt(offset * 60)
.ok_or_else(|| "invalid timezone offset".to_string())?;
let start = Utc
.timestamp_opt(
i64::try_from(from).map_err(|_| "invalid from timestamp")?,
0,
)
.single()
.ok_or_else(|| "invalid from timestamp".to_string())?
.with_timezone(&fixed);
let end = Utc
.timestamp_opt(
i64::try_from(to.saturating_sub(1)).map_err(|_| "invalid to timestamp")?,
0,
)
.single()
.ok_or_else(|| "invalid to timestamp".to_string())?
.with_timezone(&fixed);
(start.date_naive(), end.date_naive(), offset)
};
Ok(AdminStatsTimeRange {
start_date,
end_date,
tz_offset_minutes,
})
}
fn query_param_value(query: Option<&str>, key: &str) -> Option<String> {
url::form_urlencoded::parse(query.unwrap_or_default().as_bytes())
.find(|(name, _)| name == key)
.map(|(_, value)| value.into_owned())
}
#[cfg(test)]
mod tests {
use super::{precise_admin_stats_time_range, resolve_precise_time_bounds};
#[test]
fn precise_stats_range_preserves_subday_bounds_and_timezone_labels() {
let query = "from=2026-09-01T23:45:00Z&to=2026-09-02T00:15:00Z&timezone=Asia%2FShanghai";
let (from, to) = resolve_precise_time_bounds(Some(query)).unwrap().unwrap();
assert_eq!(to - from, 30 * 60);
let range = precise_admin_stats_time_range(Some(query), from, to).unwrap();
assert_eq!(range.start_date.to_string(), "2026-09-02");
assert_eq!(range.end_date.to_string(), "2026-09-02");
assert_eq!(range.tz_offset_minutes, 480);
}
#[test]
fn precise_stats_range_rejects_mixed_legacy_presets() {
let query = "from=2026-09-01T00:00:00Z&to=2026-09-02T00:00:00Z&preset=today";
assert!(resolve_precise_time_bounds(Some(query)).is_err());
}
}
@@ -1,5 +1,5 @@
use super::super::resolve_usage_user_group_scope;
use super::super::stats::resolve_admin_usage_time_range;
use super::super::stats::resolve_usage_time_bounds;
use super::analytics::admin_usage_api_key_names;
use super::analytics::admin_usage_provider_key_names;
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
@@ -37,45 +37,7 @@ const ADMIN_USAGE_ACTIVE_LIMIT: usize = 50;
pub(super) fn resolve_record_time_bounds(
query: Option<&str>,
) -> Result<Option<(u64, u64)>, String> {
let entries =
url::form_urlencoded::parse(query.unwrap_or_default().as_bytes()).collect::<Vec<_>>();
let from = entries
.iter()
.filter(|(key, _)| key == "from")
.collect::<Vec<_>>();
let to = entries
.iter()
.filter(|(key, _)| key == "to")
.collect::<Vec<_>>();
if from.is_empty() && to.is_empty() {
return resolve_admin_usage_time_range(query).map(|range| range.to_unix_bounds());
}
if from.len() != 1 || to.len() != 1 {
return Err("from and to must each be provided once".into());
}
if entries
.iter()
.any(|(key, _)| matches!(key.as_ref(), "start_date" | "end_date" | "preset" | "days"))
{
return Err("precise from/to cannot be combined with date presets".into());
}
if let Some(zone) = query_param_value(query, "timezone") {
zone.parse::<chrono_tz::Tz>()
.map_err(|_| "invalid timezone".to_string())?;
}
let parse = |value: &str| -> Result<u64, String> {
let value = chrono::DateTime::parse_from_rfc3339(value)
.map_err(|_| "from/to must be RFC 3339 timestamps".to_string())?;
if value.timestamp_subsec_nanos() != 0 {
return Err("request records support second-aligned ranges".into());
}
u64::try_from(value.timestamp()).map_err(|_| "from/to must not precede Unix epoch".into())
};
let bounds = (parse(&from[0].1)?, parse(&to[0].1)?);
if bounds.0 >= bounds.1 || bounds.1 - bounds.0 > 366 * 86_400 {
return Err("from/to must define a nonempty range of at most 366 days".into());
}
Ok(Some(bounds))
resolve_usage_time_bounds(query)
}
async fn load_admin_usage_by_ids(