mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 17:37:47 +08:00
fix: restore dashboard history and complete activity calendar
Combine retained daily aggregates with recent usage without replaying historical records or changing the activation boundary. Complete activation-day metrics and expose the UTC calendar used by legacy history. Render all 365 calendar dates, size heatmap cells responsively, and soften cell corners. Add regression coverage for history cutoffs, retention, sparse dates, and timezone boundaries.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
//! Compatibility reads for installations whose retained daily totals predate
|
||||
//! the incremental dashboard. Never replay or mutate the activation boundary.
|
||||
use crate::error::SqlxResultExt;
|
||||
use aether_data_contracts::{repository::usage::*, DataLayerError};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_json::Value;
|
||||
use sqlx::{Postgres, Row, Transaction};
|
||||
|
||||
pub(super) struct DashboardHistory {
|
||||
pub since: DateTime<Utc>,
|
||||
pub metrics: Value,
|
||||
pub days: Vec<DashboardActivityDay>,
|
||||
}
|
||||
|
||||
pub(super) async fn read_dashboard_history(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
activation: DateTime<Utc>,
|
||||
now: DateTime<Utc>,
|
||||
) -> Result<Option<DashboardHistory>, DataLayerError> {
|
||||
// Both timestamp and integer dates exist in supported imported schemas.
|
||||
let state = sqlx::query(
|
||||
r#"
|
||||
WITH summary AS (
|
||||
SELECT CASE WHEN pg_typeof(cutoff_date) IN ('bigint'::regtype,'integer'::regtype)
|
||||
THEN to_timestamp(cutoff_date::text::double precision)
|
||||
ELSE cutoff_date::text::timestamptz END AS cutoff
|
||||
FROM stats_summary ORDER BY updated_at DESC, created_at DESC LIMIT 1
|
||||
), daily AS (
|
||||
SELECT CASE WHEN pg_typeof(date) IN ('bigint'::regtype,'integer'::regtype)
|
||||
THEN to_timestamp(date::text::double precision)
|
||||
ELSE date::text::timestamptz END AS day
|
||||
FROM stats_daily WHERE total_requests>0
|
||||
) SELECT cutoff, (SELECT min(day) FROM daily WHERE day<LEAST(cutoff,$1)) AS since
|
||||
FROM summary WHERE cutoff<=$2"#,
|
||||
)
|
||||
.bind(activation)
|
||||
.bind(now)
|
||||
.fetch_optional(&mut **tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let Some(state) = state else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(since) = state
|
||||
.try_get::<Option<DateTime<Utc>>, _>("since")
|
||||
.map_postgres_err()?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let cutoff: DateTime<Utc> = state.try_get("cutoff").map_postgres_err()?;
|
||||
// Canonical fact views expand several joins. JIT compilation can cost more
|
||||
// than the small recent range itself on upgraded installations.
|
||||
sqlx::query("SET LOCAL jit=off")
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
// The published summary cutoff is advanced atomically with daily rollups.
|
||||
// Read that same snapshot and use disjoint [history, cutoff), [cutoff, now)
|
||||
// ranges. This keeps working after raw history is pruned or the cutoff moves.
|
||||
let row = sqlx::query(include_str!("queries/dashboard_history.sql"))
|
||||
.bind(cutoff)
|
||||
.bind(now)
|
||||
.fetch_one(&mut **tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let days: Value = row.try_get("days").map_postgres_err()?;
|
||||
Ok(Some(DashboardHistory {
|
||||
since,
|
||||
metrics: row.try_get("metrics").map_postgres_err()?,
|
||||
days: serde_json::from_value(days)
|
||||
.map_err(|error| DataLayerError::UnexpectedValue(error.to_string()))?,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn complete_activation_day(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
today: &DashboardSummaryMetrics,
|
||||
today_start: DateTime<Utc>,
|
||||
activation: DateTime<Utc>,
|
||||
now: DateTime<Utc>,
|
||||
) -> Result<Value, DataLayerError> {
|
||||
// Match dashboard_request_fact exactly for the missing part of today.
|
||||
// The actor UNION deduplicates users present on both sides of activation.
|
||||
let row = sqlx::query(include_str!("queries/dashboard_activation_day.sql"))
|
||||
.bind(today_start)
|
||||
.bind(activation)
|
||||
.bind(now)
|
||||
.bind(
|
||||
serde_json::to_value(today)
|
||||
.map_err(|error| DataLayerError::UnexpectedValue(error.to_string()))?,
|
||||
)
|
||||
.fetch_one(&mut **tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
row.try_get("metrics").map_postgres_err()
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
use super::SqlxUsageReadRepository;
|
||||
use aether_data_contracts::repository::usage::*;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use futures_util::FutureExt;
|
||||
use std::{collections::BTreeMap, panic::AssertUnwindSafe};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires local AETHER_TEST_DATABASE_URL with temporary database creation"]
|
||||
async fn live_dashboard_restores_legacy_history_without_replaying_or_double_counting() {
|
||||
let options = std::env::var("AETHER_TEST_DATABASE_URL")
|
||||
.unwrap()
|
||||
.parse::<sqlx::postgres::PgConnectOptions>()
|
||||
.unwrap();
|
||||
let admin = sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect_with(options.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
let database = format!("dashboard_history_{}", uuid::Uuid::new_v4().simple());
|
||||
sqlx::query(&format!("CREATE DATABASE {database}"))
|
||||
.execute(&admin)
|
||||
.await
|
||||
.unwrap();
|
||||
let pool = sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(4)
|
||||
.connect_with(options.database(&database))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let outcome = AssertUnwindSafe(async {
|
||||
crate::POSTGRES_MIGRATOR.run(&pool).await.unwrap();
|
||||
let repo = SqlxUsageReadRepository::new(pool.clone());
|
||||
let query = UsageDashboardAnalyticsQuery {
|
||||
timezone: "Asia/Kathmandu".into(),
|
||||
};
|
||||
let now: DateTime<Utc> = sqlx::query_scalar("SELECT CURRENT_TIMESTAMP")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let utc_today = now.date_naive().and_hms_opt(0, 0, 0).unwrap().and_utc();
|
||||
let local_today = query.today_start(now).unwrap();
|
||||
let activation = local_today
|
||||
+ Duration::microseconds((now - local_today).num_microseconds().unwrap() / 2);
|
||||
let before_activation = local_today
|
||||
+ Duration::microseconds((activation - local_today).num_microseconds().unwrap() / 2);
|
||||
let after_activation = activation
|
||||
+ Duration::microseconds((now - activation).num_microseconds().unwrap() / 2);
|
||||
let oldest = utc_today - Duration::days(400);
|
||||
let recent_history = utc_today - Duration::days(3);
|
||||
let cutoff = utc_today - Duration::days(2);
|
||||
sqlx::query("UPDATE dashboard_stats_state SET stats_since=$1")
|
||||
.bind(activation)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query("INSERT INTO users(id,username,email_verified,is_deleted) VALUES ('shared','shared',false,false),('before-only','before-only',false,false),('deleted','deleted',false,true)")
|
||||
.execute(&pool).await.unwrap();
|
||||
sqlx::query("INSERT INTO stats_summary(id,cutoff_date) VALUES('legacy',$1)")
|
||||
.bind(cutoff).execute(&pool).await.unwrap();
|
||||
|
||||
// The oldest day's request rows have already expired. Its exact NUMERIC
|
||||
// cost and lifetime activity must survive outside the visible heatmap.
|
||||
for (id, date, requests, input, output, creation, read, cost) in [
|
||||
("expired", oldest, 10, 1000_i64, 200_i64, 30_i64, 40_i64, "123456789.12345678"),
|
||||
("recent", recent_history, 3, 300, 60, 9, 12, "0.30000003"),
|
||||
("at-cutoff", cutoff, 1, 100, 20, 3, 4, "0.10000001"),
|
||||
] {
|
||||
sqlx::query("INSERT INTO stats_daily(id,date,total_requests,input_tokens,output_tokens,cache_creation_tokens,cache_read_tokens,actual_total_cost) VALUES($1,$2,$3,$4,$5,$6,$7,$8::text::numeric)")
|
||||
.bind(id).bind(date).bind(requests).bind(input).bind(output)
|
||||
.bind(creation).bind(read).bind(cost).execute(&pool).await.unwrap();
|
||||
}
|
||||
// Retained detail below the cutoff must not be counted a second time;
|
||||
// a request exactly at the cutoff must still be included once.
|
||||
sqlx::query("INSERT INTO usage(id,request_id,model,provider_name,status,billing_status,input_tokens,output_tokens,total_tokens,cache_creation_input_tokens,cache_read_input_tokens,actual_total_cost_usd,created_at) VALUES('overlap','overlap','m','p','completed','settled',999,999,1998,0,0,999,$1),('boundary','boundary','m','p','completed','settled',100,20,127,3,4,0.10000001,$2)")
|
||||
.bind(recent_history + Duration::hours(12)).bind(cutoff)
|
||||
.execute(&pool).await.unwrap();
|
||||
sqlx::query("INSERT INTO usage(id,request_id,model,provider_name,status,billing_status,input_tokens,output_tokens,total_tokens,actual_total_cost_usd,created_at) VALUES('pending','pending','m','p','pending','pending',999,999,1998,999,$1),('unknown','unknown','m','unknown','completed','settled',999,999,1998,999,$1)")
|
||||
.bind(cutoff + Duration::hours(12)).execute(&pool).await.unwrap();
|
||||
for (id, user, at) in [
|
||||
("before-shared", "shared", before_activation),
|
||||
("before-unique", "before-only", before_activation),
|
||||
("before-deleted", "deleted", before_activation),
|
||||
("after-shared", "shared", after_activation),
|
||||
] {
|
||||
sqlx::query("INSERT INTO usage(id,request_id,user_id,model,provider_name,status,billing_status,input_tokens,output_tokens,total_tokens,actual_total_cost_usd,total_cost_usd,created_at,first_byte_time_ms,response_time_ms,upstream_is_stream) VALUES($1,$1,$2,'m','p','completed','settled',100,20,120,0.25,0.25,$3,100,800,true)")
|
||||
.bind(id).bind(user).bind(at).execute(&pool).await.unwrap();
|
||||
}
|
||||
// Today's missing prefix must use final settlement facts, just like
|
||||
// the already-active projection, instead of stale capture counters.
|
||||
sqlx::query("INSERT INTO usage_settlement_snapshots(request_id,billing_status,billing_input_tokens,billing_effective_input_tokens,billing_output_tokens,billing_cache_read_tokens,billing_cache_creation_tokens,billing_total_input_context,billing_total_cost_usd,billing_actual_total_cost_usd) VALUES('before-shared','settled',100,100,30,40,10,150,0.12345678,0.12345678)")
|
||||
.execute(&pool).await.unwrap();
|
||||
|
||||
let restored = repo.query_dashboard_summary(&query).await.unwrap();
|
||||
assert_eq!(restored.total.request_count, 18);
|
||||
assert_eq!(restored.total.total_tokens, 2318);
|
||||
assert_eq!(restored.total.billable_amount.as_deref(), Some("123456790.39691360"));
|
||||
assert_eq!(restored.stats_since, oldest.to_rfc3339());
|
||||
assert_eq!(restored.timezone, query.timezone);
|
||||
assert_eq!(restored.activity_timezone, "UTC");
|
||||
assert_eq!(restored.today_from, local_today.to_rfc3339());
|
||||
assert_eq!(restored.today.request_count, 4);
|
||||
assert_eq!(restored.today.total_tokens, 540);
|
||||
assert_eq!(restored.today.billable_amount.as_deref(), Some("0.87345678"));
|
||||
assert_eq!(restored.today.active_users, 2, "users on both sides of activation count once; deleted users do not count");
|
||||
assert_eq!(restored.today.first_byte_sample_count, 4);
|
||||
assert_eq!(restored.today.response_sample_count, 4);
|
||||
assert_eq!(restored.today.response_sum_ms, 3200.0);
|
||||
assert_eq!(restored.today.stream_requests, 4);
|
||||
|
||||
let mut expected_days = BTreeMap::from([
|
||||
(recent_history.date_naive().to_string(), 3_u64),
|
||||
(cutoff.date_naive().to_string(), 1),
|
||||
]);
|
||||
*expected_days.entry(before_activation.date_naive().to_string()).or_default() += 3;
|
||||
*expected_days.entry(after_activation.date_naive().to_string()).or_default() += 1;
|
||||
assert_eq!(restored.active_days, expected_days.len() as u64 + 1);
|
||||
assert_eq!(restored.activity_days.iter().map(|day| (day.date.clone(), day.requests)).collect::<BTreeMap<_, _>>(), expected_days);
|
||||
assert_eq!(sqlx::query_scalar::<_, DateTime<Utc>>("SELECT stats_since FROM dashboard_stats_state WHERE singleton")
|
||||
.fetch_one(&pool).await.unwrap(), activation, "reading history must not change the projection activation boundary");
|
||||
assert_eq!(sqlx::query_scalar::<_, i64>("SELECT count(*) FROM dashboard_request_contributions")
|
||||
.fetch_one(&pool).await.unwrap(), 1, "old requests are read, not replayed into projection ledgers");
|
||||
|
||||
// The daily aggregator advances its exclusive boundary over the same
|
||||
// request. Existing daily rows above the old cutoff were not duplicates.
|
||||
sqlx::query("UPDATE stats_summary SET cutoff_date=$1 WHERE id='legacy'")
|
||||
.bind(cutoff + Duration::days(1)).execute(&pool).await.unwrap();
|
||||
sqlx::query("DELETE FROM usage WHERE request_id='boundary'")
|
||||
.execute(&pool).await.unwrap();
|
||||
let advanced = repo.query_dashboard_summary(&query).await.unwrap();
|
||||
assert_eq!(advanced.total.request_count, restored.total.request_count);
|
||||
assert_eq!(advanced.total.total_tokens, restored.total.total_tokens);
|
||||
assert_eq!(advanced.total.billable_amount, restored.total.billable_amount);
|
||||
assert_eq!(advanced.today, restored.today);
|
||||
assert_eq!(advanced.activity_days, restored.activity_days);
|
||||
assert_eq!(advanced.active_days, restored.active_days);
|
||||
|
||||
// A summary cutoff without legacy daily history must leave the normal
|
||||
// future-only projection and its requested calendar unchanged.
|
||||
sqlx::query("DELETE FROM stats_daily").execute(&pool).await.unwrap();
|
||||
let future_only = repo.query_dashboard_summary(&query).await.unwrap();
|
||||
assert_eq!(future_only.stats_since, activation.to_rfc3339());
|
||||
assert_eq!(future_only.activity_timezone, query.timezone);
|
||||
assert_eq!(future_only.total.request_count, 1);
|
||||
assert_eq!(future_only.today.request_count, 1);
|
||||
assert_eq!(future_only.today_from, activation.to_rfc3339());
|
||||
})
|
||||
.catch_unwind()
|
||||
.await;
|
||||
|
||||
pool.close().await;
|
||||
sqlx::query(&format!("DROP DATABASE {database} WITH (FORCE)"))
|
||||
.execute(&admin)
|
||||
.await
|
||||
.unwrap();
|
||||
admin.close().await;
|
||||
if let Err(error) = outcome {
|
||||
std::panic::resume_unwind(error);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::dashboard_history::{complete_activation_day, read_dashboard_history};
|
||||
use super::SqlxUsageReadRepository;
|
||||
use crate::error::SqlxResultExt;
|
||||
use aether_data_contracts::{repository::usage::*, DataLayerError};
|
||||
@@ -46,8 +47,14 @@ impl SqlxUsageReadRepository {
|
||||
let since: DateTime<Utc> = state.try_get("stats_since").map_postgres_err()?;
|
||||
let now: DateTime<Utc> = state.try_get("now").map_postgres_err()?;
|
||||
let today_start = query.today_start(now)?;
|
||||
let today_from = today_start.max(since);
|
||||
let minute_from = today_from
|
||||
let history = read_dashboard_history(&mut tx, since, now).await?;
|
||||
let today_from = if history.is_some() {
|
||||
today_start
|
||||
} else {
|
||||
today_start.max(since)
|
||||
};
|
||||
let minute_from = today_start
|
||||
.max(since)
|
||||
.with_second(0)
|
||||
.and_then(|v| v.with_nanosecond(0))
|
||||
.expect("valid minute");
|
||||
@@ -78,10 +85,21 @@ impl SqlxUsageReadRepository {
|
||||
}
|
||||
today.active_users = sqlx::query_scalar::<_,i64>("SELECT count(DISTINCT a.actor_user_id) FROM dashboard_actor_minute a JOIN users u ON u.id=a.actor_user_id AND NOT u.is_deleted WHERE a.bucket_start >= $1 AND a.bucket_start <= $2 AND a.request_count > 0")
|
||||
.bind(minute_from).bind(now).fetch_one(&mut *tx).await.map_postgres_err()? as u64;
|
||||
if history.is_some() && since > today_start {
|
||||
today = decode_metrics(
|
||||
complete_activation_day(&mut tx, &today, today_start, since, now).await?,
|
||||
)?;
|
||||
}
|
||||
// Most UTC hours belong to one local day. Only hours straddling a local
|
||||
// midnight (e.g. Kathmandu) need their minute counts read. Lifetime
|
||||
// activity therefore scans narrow hourly counters, not metric JSON.
|
||||
let days = sqlx::query(r#"WITH hours AS MATERIALIZED (
|
||||
let activity_timezone = if history.is_some() {
|
||||
"UTC"
|
||||
} else {
|
||||
query.timezone.as_str()
|
||||
};
|
||||
let mut days = if history.is_none() {
|
||||
sqlx::query(r#"WITH hours AS MATERIALIZED (
|
||||
SELECT bucket_start,shard,request_count,
|
||||
(bucket_start AT TIME ZONE $1)::date AS day,
|
||||
((bucket_start+INTERVAL '1 hour'-INTERVAL '1 microsecond') AT TIME ZONE $1)::date AS last_day
|
||||
@@ -105,11 +123,23 @@ impl SqlxUsageReadRepository {
|
||||
) m ON TRUE WHERE h.day<>h.last_day
|
||||
) SELECT day AS date, sum(requests)::bigint AS requests FROM daily
|
||||
GROUP BY day HAVING sum(requests)>0 ORDER BY day"#)
|
||||
.bind(&query.timezone).bind(now).fetch_all(&mut *tx).await.map_postgres_err()?;
|
||||
.bind(activity_timezone).bind(now).fetch_all(&mut *tx).await.map_postgres_err()?
|
||||
.into_iter().map(|row| Ok(DashboardActivityDay {
|
||||
date: row.try_get::<NaiveDate, _>("date").map_postgres_err()?.to_string(),
|
||||
requests: row.try_get::<i64, _>("requests").map_postgres_err()?.max(0) as u64,
|
||||
})).collect::<Result<Vec<_>, DataLayerError>>()?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let mut display_since = since;
|
||||
if let Some(history) = history {
|
||||
display_since = history.since;
|
||||
total = decode_metrics(history.metrics)?;
|
||||
days = history.days;
|
||||
}
|
||||
let local_today = now
|
||||
.with_timezone(
|
||||
&query
|
||||
.timezone
|
||||
&activity_timezone
|
||||
.parse::<chrono_tz::Tz>()
|
||||
.map_err(|_| DataLayerError::InvalidInput("invalid timezone".into()))?,
|
||||
)
|
||||
@@ -118,7 +148,10 @@ impl SqlxUsageReadRepository {
|
||||
let active_days = days.len() as u64;
|
||||
let dates: Vec<NaiveDate> = days
|
||||
.iter()
|
||||
.map(|row| row.try_get("date").map_postgres_err())
|
||||
.map(|row| {
|
||||
NaiveDate::parse_from_str(&row.date, "%Y-%m-%d")
|
||||
.map_err(|error| DataLayerError::UnexpectedValue(error.to_string()))
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
let consecutive_active_days =
|
||||
dashboard_consecutive_active_days(dates.iter().copied(), local_today);
|
||||
@@ -127,16 +160,17 @@ impl SqlxUsageReadRepository {
|
||||
if date >= first_heatmap_day {
|
||||
activity_days.push(DashboardActivityDay {
|
||||
date: date.to_string(),
|
||||
requests: row.try_get::<i64, _>("requests").map_postgres_err()?.max(0) as u64,
|
||||
requests: row.requests,
|
||||
});
|
||||
}
|
||||
}
|
||||
let events = sqlx::query("SELECT COALESCE(sum(created_count),0)::bigint AS created, COALESCE(sum(deleted_count),0)::bigint AS deleted FROM dashboard_user_events_minute WHERE bucket_start >= $1 AND bucket_start <= $2")
|
||||
.bind(minute_from).bind(now).fetch_one(&mut *tx).await.map_postgres_err()?;
|
||||
let summary = StoredDashboardSummary {
|
||||
stats_since: since.to_rfc3339(),
|
||||
stats_since: display_since.to_rfc3339(),
|
||||
generated_at: now.to_rfc3339(),
|
||||
timezone: query.timezone.clone(),
|
||||
activity_timezone: activity_timezone.to_string(),
|
||||
today_from: today_from.to_rfc3339(),
|
||||
window_seconds: (now - today_from).num_milliseconds().max(0) as f64 / 1000.0,
|
||||
today,
|
||||
|
||||
@@ -63,6 +63,9 @@ mod analytics_tests;
|
||||
mod attribution;
|
||||
pub mod cleanup;
|
||||
mod dashboard;
|
||||
mod dashboard_history;
|
||||
#[cfg(test)]
|
||||
mod dashboard_history_tests;
|
||||
mod dashboard_retention;
|
||||
mod dashboard_summary;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
WITH missing AS MATERIALIZED (
|
||||
SELECT f.actor_user_id, f.input_tokens, f.output_tokens, f.total_tokens,
|
||||
f.usage_available, f.billable_amount, f.cache_read_input_tokens,
|
||||
f.cache_creation_input_tokens, f.first_byte_time_ms, f.response_time_ms,
|
||||
f.upstream_is_stream, b.total_input_context
|
||||
FROM usage_analytics_facts_v1 f
|
||||
JOIN usage_billing_facts b ON b.request_id=f.request_id
|
||||
-- Bound both view inputs so the planner cannot scan historical billing rows.
|
||||
AND b.created_at >= $1 AND b.created_at < $2
|
||||
WHERE f.created_at >= $1 AND f.created_at < $2 AND f.record_kind <> 'session'
|
||||
), metrics AS (
|
||||
SELECT jsonb_build_object(
|
||||
'request_count', count(*),
|
||||
'input_tokens', COALESCE(sum(input_tokens),0),
|
||||
'output_tokens', COALESCE(sum(output_tokens),0),
|
||||
'total_tokens', COALESCE(sum(total_tokens),0),
|
||||
'usage_available_count', count(*) FILTER (WHERE usage_available),
|
||||
'pricing_available_count', count(billable_amount),
|
||||
'billable_amount', COALESCE(sum(billable_amount),0),
|
||||
'cache_read_tokens', COALESCE(sum(cache_read_input_tokens),0),
|
||||
'cache_creation_tokens', COALESCE(sum(cache_creation_input_tokens),0),
|
||||
'cache_input_tokens', COALESCE(sum(total_input_context) FILTER (WHERE usage_available),0),
|
||||
'first_byte_sum_ms', COALESCE(sum(first_byte_time_ms),0),
|
||||
'first_byte_sample_count', count(first_byte_time_ms),
|
||||
'response_sum_ms', COALESCE(sum(response_time_ms),0),
|
||||
'response_sample_count', count(response_time_ms),
|
||||
'stream_requests', count(*) FILTER (WHERE COALESCE(upstream_is_stream,false)),
|
||||
'standard_requests', count(*) FILTER (WHERE NOT COALESCE(upstream_is_stream,false))
|
||||
) AS value FROM missing
|
||||
), actors AS (
|
||||
SELECT actor_user_id FROM missing WHERE actor_user_id IS NOT NULL
|
||||
UNION
|
||||
SELECT actor_user_id FROM dashboard_actor_minute
|
||||
WHERE bucket_start >= date_trunc('minute',$2::timestamptz)
|
||||
AND bucket_start <= $3 AND request_count>0
|
||||
), combined AS (
|
||||
SELECT dashboard_metrics_add($4::jsonb, value) AS value FROM metrics
|
||||
)
|
||||
SELECT value || jsonb_build_object(
|
||||
'billable_amount', round((value->>'billable_amount')::numeric,8)::text,
|
||||
'active_users', (SELECT count(*) FROM actors a JOIN users u
|
||||
ON u.id=a.actor_user_id AND NOT u.is_deleted)
|
||||
) AS metrics FROM combined
|
||||
@@ -0,0 +1,51 @@
|
||||
WITH daily AS (
|
||||
SELECT CASE WHEN pg_typeof(date) IN ('bigint'::regtype,'integer'::regtype)
|
||||
THEN to_timestamp(date::text::double precision)
|
||||
ELSE date::text::timestamptz END AS day,
|
||||
total_requests AS request_count, input_tokens, output_tokens,
|
||||
CASE WHEN effective_input_tokens=0 AND total_input_context=0 AND input_tokens>0
|
||||
THEN input_tokens ELSE effective_input_tokens END
|
||||
+ output_tokens + cache_creation_tokens + cache_read_tokens AS total_tokens,
|
||||
cache_creation_tokens, cache_read_tokens,
|
||||
CASE WHEN total_input_context=0 THEN
|
||||
CASE WHEN effective_input_tokens=0 AND input_tokens>0 THEN input_tokens
|
||||
ELSE effective_input_tokens END + cache_creation_tokens + cache_read_tokens
|
||||
ELSE total_input_context END AS cache_input_tokens,
|
||||
actual_total_cost::numeric AS billable_amount
|
||||
FROM stats_daily
|
||||
), facts AS MATERIALIZED (
|
||||
SELECT (day AT TIME ZONE 'UTC')::date AS day, request_count,
|
||||
input_tokens, output_tokens, total_tokens, cache_creation_tokens,
|
||||
cache_read_tokens, cache_input_tokens, billable_amount
|
||||
FROM daily WHERE day<$1
|
||||
UNION ALL
|
||||
SELECT (b.created_at AT TIME ZONE 'UTC')::date, 1::bigint,
|
||||
b.input_tokens, b.output_tokens, b.total_tokens, b.cache_creation_input_tokens,
|
||||
b.cache_read_input_tokens, b.total_input_context,
|
||||
COALESCE(s.billing_actual_total_cost_usd::numeric,u.actual_total_cost_usd::numeric)
|
||||
FROM usage_billing_facts b
|
||||
JOIN usage u USING (request_id)
|
||||
LEFT JOIN usage_settlement_snapshots s USING (request_id)
|
||||
WHERE b.created_at >= $1 AND b.created_at < $2
|
||||
AND u.created_at >= $1 AND u.created_at < $2
|
||||
AND b.status NOT IN ('pending','streaming')
|
||||
AND b.provider_name NOT IN ('unknown','pending')
|
||||
), activity AS (
|
||||
SELECT day, sum(request_count)::bigint AS requests FROM facts
|
||||
GROUP BY day HAVING sum(request_count)>0
|
||||
)
|
||||
SELECT jsonb_build_object(
|
||||
'request_count', COALESCE(sum(request_count),0)::bigint,
|
||||
'input_tokens', COALESCE(sum(input_tokens),0)::bigint,
|
||||
'output_tokens', COALESCE(sum(output_tokens),0)::bigint,
|
||||
'total_tokens', COALESCE(sum(total_tokens),0)::bigint,
|
||||
'cache_creation_tokens', COALESCE(sum(cache_creation_tokens),0)::bigint,
|
||||
'cache_read_tokens', COALESCE(sum(cache_read_tokens),0)::bigint,
|
||||
'cache_input_tokens', COALESCE(sum(cache_input_tokens),0)::bigint,
|
||||
'usage_available_count', COALESCE(sum(request_count),0)::bigint,
|
||||
'pricing_available_count', COALESCE(sum(request_count) FILTER (WHERE billable_amount IS NOT NULL),0)::bigint,
|
||||
'billable_amount', round(COALESCE(sum(billable_amount),0),8)::text
|
||||
) AS metrics,
|
||||
(SELECT COALESCE(jsonb_agg(jsonb_build_object('date',day::text,'requests',requests)
|
||||
ORDER BY day),'[]'::jsonb) FROM activity) AS days
|
||||
FROM facts
|
||||
Reference in New Issue
Block a user