mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-11 03:39:49 +08:00
fix: align dashboard charts with customer billing and history coverage
This commit is contained in:
@@ -205,7 +205,7 @@ CREATE TRIGGER overview_usage_delete_attribution BEFORE DELETE ON public.usage
|
||||
-- procurement cost remains in actual_total_cost_usd for legacy reporting.
|
||||
CREATE OR REPLACE FUNCTION public.usage_customer_billable_amount(
|
||||
metadata jsonb, base_cost numeric, legacy_cost numeric
|
||||
) RETURNS numeric LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE AS $$
|
||||
) RETURNS numeric LANGUAGE plpgsql IMMUTABLE PARALLEL UNSAFE AS $$
|
||||
DECLARE factor jsonb; multiplier numeric; amount numeric;
|
||||
factor_name text; factor_value jsonb; factor_number double precision;
|
||||
expected_multiplier double precision := 1.0; factor_count integer := 0;
|
||||
|
||||
@@ -28,6 +28,8 @@ use crate::lifecycle::bootstrap::postgres::{
|
||||
};
|
||||
|
||||
mod customer_billing_upgrade;
|
||||
mod dashboard_chart_billing;
|
||||
mod dashboard_chart_history;
|
||||
mod dashboard_user_anonymization;
|
||||
mod legacy_overview_upgrade;
|
||||
mod migration_deadlines;
|
||||
@@ -1600,6 +1602,7 @@ fn pending_migrations_from_applied_skips_versions_already_applied() {
|
||||
20261001000000,
|
||||
20261004000000,
|
||||
20261007000000,
|
||||
20261008000000,
|
||||
20261009000000,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
use super::*;
|
||||
use aether_data_contracts::repository::usage::{
|
||||
UsageAnalyticsQuery, UsageAnalyticsView, UsageDashboardAnalyticsQuery,
|
||||
};
|
||||
use chrono::{Duration, Utc};
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn dashboard_charts_match_summary_customer_charges_and_request_scope() {
|
||||
let Some(server) = ManagedPostgresServer::try_start().await.unwrap() else {
|
||||
return;
|
||||
};
|
||||
let pool = PgPool::connect(server.database_url()).await.unwrap();
|
||||
prepare_and_apply_clean_postgres_database(&pool).await;
|
||||
let repo = aether_data_postgres::SqlxUsageReadRepository::new(pool.clone());
|
||||
let summary_query = UsageDashboardAnalyticsQuery {
|
||||
timezone: "Asia/Shanghai".into(),
|
||||
};
|
||||
let start = summary_query.today_start(Utc::now()).unwrap();
|
||||
query("UPDATE dashboard_stats_state SET stats_since=$1")
|
||||
.bind(start)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let composite = json!({"billing_multiplier_snapshot":{"version":1,"factors":{"routing_group":2,"user_group":0.25},"multiplier":0.5}});
|
||||
for (id, provider, status, billing, base, actual, metadata) in [
|
||||
(
|
||||
"composite",
|
||||
"alpha",
|
||||
"completed",
|
||||
"settled",
|
||||
10.0,
|
||||
Some(3.0),
|
||||
composite,
|
||||
),
|
||||
(
|
||||
"legacy",
|
||||
"alpha",
|
||||
"completed",
|
||||
"settled",
|
||||
2.0,
|
||||
Some(1.25),
|
||||
json!({}),
|
||||
),
|
||||
(
|
||||
"free",
|
||||
"alpha",
|
||||
"completed",
|
||||
"settled",
|
||||
50.0,
|
||||
Some(40.0),
|
||||
json!({"routing_group_billing_multiplier":0}),
|
||||
),
|
||||
(
|
||||
"invalid",
|
||||
"alpha",
|
||||
"completed",
|
||||
"settled",
|
||||
99.0,
|
||||
Some(90.0),
|
||||
json!({"billing_multiplier_snapshot":null}),
|
||||
),
|
||||
(
|
||||
"unpriced",
|
||||
"beta",
|
||||
"completed",
|
||||
"settled",
|
||||
80.0,
|
||||
Some(70.0),
|
||||
json!({"usage_pricing_available":false}),
|
||||
),
|
||||
(
|
||||
"failed",
|
||||
"unknown",
|
||||
"failed",
|
||||
"settled",
|
||||
0.0,
|
||||
Some(0.0),
|
||||
json!({}),
|
||||
),
|
||||
(
|
||||
"pending",
|
||||
"pending",
|
||||
"pending",
|
||||
"pending",
|
||||
0.0,
|
||||
None,
|
||||
json!({}),
|
||||
),
|
||||
(
|
||||
"streaming",
|
||||
"beta",
|
||||
"streaming",
|
||||
"pending",
|
||||
0.0,
|
||||
None,
|
||||
json!({}),
|
||||
),
|
||||
(
|
||||
"session",
|
||||
"alpha",
|
||||
"completed",
|
||||
"settled",
|
||||
500.0,
|
||||
Some(500.0),
|
||||
json!({}),
|
||||
),
|
||||
] {
|
||||
query("INSERT INTO usage(id,request_id,model,provider_name,status,billing_status,total_cost_usd,actual_total_cost_usd,total_tokens,created_at,request_metadata,response_time_ms) VALUES($1,$1,'model',$2,$3,$4,$5,$6,10,$7,$8,1000)")
|
||||
.bind(id).bind(provider).bind(status).bind(billing).bind(base).bind(actual)
|
||||
.bind(start + Duration::seconds(1)).bind(metadata).execute(&pool).await.unwrap();
|
||||
}
|
||||
// The captured settlement cost, rather than the mutable audit float, is rated.
|
||||
query("INSERT INTO usage_settlement_snapshots(request_id,billing_status,billing_total_cost_usd,billing_actual_total_cost_usd) VALUES('composite','settled',12,4)")
|
||||
.execute(&pool).await.unwrap();
|
||||
query(
|
||||
"UPDATE usage_attribution_snapshots SET record_kind='session' WHERE request_id='session'",
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
// Exclude both the previous local day and the next day's boundary.
|
||||
for (id, at) in [
|
||||
("before", start - Duration::seconds(1)),
|
||||
("after", start + Duration::days(1)),
|
||||
] {
|
||||
query("INSERT INTO usage(id,request_id,model,provider_name,status,billing_status,total_cost_usd,actual_total_cost_usd,created_at) VALUES($1,$1,'outside','outside','completed','settled',999,999,$2)")
|
||||
.bind(id).bind(at).execute(&pool).await.unwrap();
|
||||
}
|
||||
let summary = repo.query_dashboard_summary(&summary_query).await.unwrap();
|
||||
let charts = repo
|
||||
.query_usage_analytics(&UsageAnalyticsQuery {
|
||||
from_unix_ms: start.timestamp_millis() as u64,
|
||||
to_unix_ms: (start + Duration::days(1)).timestamp_millis() as u64,
|
||||
timezone: summary_query.timezone.clone(),
|
||||
view: UsageAnalyticsView::DashboardCharts,
|
||||
limit: 10_000,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(summary.today.request_count, 8);
|
||||
assert_eq!(summary.today.billable_amount.as_deref(), Some("7.25000000"));
|
||||
assert_eq!(charts.summary.request_count, summary.today.request_count);
|
||||
assert_eq!(
|
||||
charts.summary.billable_amount,
|
||||
summary.today.billable_amount
|
||||
);
|
||||
assert_eq!(
|
||||
charts.summary.pricing_available_count,
|
||||
summary.today.pricing_available_count
|
||||
);
|
||||
assert_eq!(charts.summary.in_flight_request_count, 2);
|
||||
assert_eq!(charts.rows.len(), 1);
|
||||
assert_eq!(charts.rows[0].metrics.request_count, 8);
|
||||
assert_eq!(
|
||||
charts.rows[0].metrics.billable_amount,
|
||||
summary.today.billable_amount
|
||||
);
|
||||
assert_eq!(charts.rows[0].metrics.unique_providers, Some(2));
|
||||
assert_eq!(
|
||||
charts.rows[0].bucket_start.as_deref(),
|
||||
Some(
|
||||
start
|
||||
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
|
||||
.as_str()
|
||||
)
|
||||
);
|
||||
for rows in [&charts.model_rows, &charts.provider_rows] {
|
||||
assert_eq!(rows.iter().map(|r| r.metrics.request_count).sum::<u64>(), 8);
|
||||
let charges: f64 = rows
|
||||
.iter()
|
||||
.filter_map(|r| r.metrics.billable_amount.as_ref())
|
||||
.map(|v| v.parse::<f64>().unwrap())
|
||||
.sum();
|
||||
assert_eq!(charges, 7.25);
|
||||
}
|
||||
assert_eq!(
|
||||
charts.provider_rows.len(),
|
||||
4,
|
||||
"legacy provider names must not collapse into one null-ID group"
|
||||
);
|
||||
// Retained older requests can have no contribution ledger entry. Mixing
|
||||
// fallback rows with current cached rows must keep unknown pricing distinct
|
||||
// from free usage and preserve the immutable composite settlement amount.
|
||||
query("DELETE FROM dashboard_request_contributions WHERE request_id IN ('composite','free','invalid')")
|
||||
.execute(&pool).await.unwrap();
|
||||
let mixed = repo
|
||||
.query_usage_analytics(&UsageAnalyticsQuery {
|
||||
from_unix_ms: start.timestamp_millis() as u64,
|
||||
to_unix_ms: (start + Duration::days(1)).timestamp_millis() as u64,
|
||||
timezone: summary_query.timezone.clone(),
|
||||
view: UsageAnalyticsView::DashboardCharts,
|
||||
limit: 10_000,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
mixed.summary.billable_amount,
|
||||
charts.summary.billable_amount
|
||||
);
|
||||
assert_eq!(mixed.summary.total_tokens, charts.summary.total_tokens);
|
||||
assert_eq!(mixed.summary.request_count, charts.summary.request_count);
|
||||
assert_eq!(
|
||||
mixed.summary.pricing_available_count,
|
||||
charts.summary.pricing_available_count
|
||||
);
|
||||
assert_eq!(mixed.rows.len(), charts.rows.len());
|
||||
for (mixed, cached) in mixed.rows.iter().zip(&charts.rows) {
|
||||
assert_eq!(mixed.bucket_start, cached.bucket_start);
|
||||
assert_eq!(
|
||||
mixed.metrics.billable_amount,
|
||||
cached.metrics.billable_amount
|
||||
);
|
||||
assert_eq!(mixed.metrics.total_tokens, cached.metrics.total_tokens);
|
||||
assert_eq!(
|
||||
mixed.metrics.pricing_available_count,
|
||||
cached.metrics.pricing_available_count
|
||||
);
|
||||
}
|
||||
let empty = repo
|
||||
.query_usage_analytics(&UsageAnalyticsQuery {
|
||||
from_unix_ms: (start - Duration::days(2)).timestamp_millis() as u64,
|
||||
to_unix_ms: (start - Duration::days(1)).timestamp_millis() as u64,
|
||||
timezone: summary_query.timezone,
|
||||
view: UsageAnalyticsView::DashboardCharts,
|
||||
limit: 10_000,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(empty.summary.request_count, 0);
|
||||
assert_eq!(empty.summary.billable_amount.as_deref(), Some("0.00000000"));
|
||||
assert_eq!(empty.rows[0].metrics.unique_providers, Some(0));
|
||||
pool.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn customer_billing_parallel_fix_preserves_history_without_backfill() {
|
||||
let Some(server) = ManagedPostgresServer::try_start().await.unwrap() else {
|
||||
return;
|
||||
};
|
||||
let mut connection = PgConnection::connect(server.database_url()).await.unwrap();
|
||||
connection.ensure_migrations_table().await.unwrap();
|
||||
for migration in POSTGRES_MIGRATOR
|
||||
.iter()
|
||||
.filter(|m| m.version < 20261008000000)
|
||||
{
|
||||
connection.apply(migration).await.unwrap();
|
||||
}
|
||||
let pool = PgPool::connect(server.database_url()).await.unwrap();
|
||||
query("INSERT INTO stats_daily(id,date,total_requests,total_cost,actual_total_cost,is_complete) VALUES('untouched','2020-01-01',1,10,3,true)")
|
||||
.execute(&pool).await.unwrap();
|
||||
query("INSERT INTO usage(id,request_id,model,provider_name,status,billing_status,total_cost_usd,actual_total_cost_usd,created_at,request_metadata) VALUES('untouched','untouched','m','p','completed','settled',10,3,'2020-01-01','{\"routing_group_billing_multiplier\":2}')")
|
||||
.execute(&pool).await.unwrap();
|
||||
let before: (serde_json::Value,serde_json::Value) = sqlx::query_as("SELECT (SELECT to_jsonb(d) FROM stats_daily d WHERE id='untouched'), (SELECT to_jsonb(u) FROM usage u WHERE id='untouched')").fetch_one(&pool).await.unwrap();
|
||||
connection
|
||||
.apply(
|
||||
POSTGRES_MIGRATOR
|
||||
.iter()
|
||||
.find(|m| m.version == 20261008000000)
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let after = sqlx::query_as("SELECT (SELECT to_jsonb(d) FROM stats_daily d WHERE id='untouched'), (SELECT to_jsonb(u) FROM usage u WHERE id='untouched')").fetch_one(&pool).await.unwrap();
|
||||
assert_eq!(before, after);
|
||||
let parallel: String=query_scalar("SELECT proparallel::text FROM pg_proc WHERE oid='public.usage_customer_billable_amount(jsonb,numeric,numeric)'::regprocedure").fetch_one(&pool).await.unwrap();
|
||||
assert_eq!(parallel, "u");
|
||||
// Encourage a parallel scan; the exception-handling function must keep it serial.
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
sqlx::raw_sql("CREATE TABLE billing_parallel_probe AS SELECT i::numeric AS cost FROM generate_series(1,10000) i; ALTER TABLE billing_parallel_probe SET (parallel_workers=2); ANALYZE billing_parallel_probe; SET LOCAL min_parallel_table_scan_size=0; SET LOCAL parallel_setup_cost=0; SET LOCAL parallel_tuple_cost=0;").execute(&mut *tx).await.unwrap();
|
||||
let charge:String=query_scalar("SELECT sum(public.usage_customer_billable_amount('{\"routing_group_billing_multiplier\":2}'::jsonb,cost,1))::text FROM billing_parallel_probe").fetch_one(&mut *tx).await.unwrap();
|
||||
assert_eq!(charge, "100010000.00000000");
|
||||
tx.rollback().await.unwrap();
|
||||
pool.close().await;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
use super::*;
|
||||
use aether_data_contracts::repository::usage::{UsageAnalyticsQuery, UsageAnalyticsView};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test]
|
||||
async fn dashboard_chart_history_coverage_matches_legacy_request_scope_without_raw_rescans() {
|
||||
let Some(server) = ManagedPostgresServer::try_start().await.unwrap() else {
|
||||
return;
|
||||
};
|
||||
let pool = PgPool::connect(server.database_url()).await.unwrap();
|
||||
prepare_and_apply_clean_postgres_database(&pool).await;
|
||||
let repo = aether_data_postgres::SqlxUsageReadRepository::new(pool.clone());
|
||||
let start = "2020-01-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
|
||||
query("INSERT INTO stats_summary(id,cutoff_date) VALUES('history',$1)")
|
||||
.bind(start + Duration::days(4))
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
for (day, count, complete) in [
|
||||
(0, 2, true),
|
||||
(1, 2, true),
|
||||
(2, 1, true),
|
||||
(3, 1, false),
|
||||
(4, 1, true),
|
||||
] {
|
||||
query("INSERT INTO stats_daily(id,date,total_requests,total_cost,actual_total_cost,is_complete) VALUES($1,$2,$3,9999,7777,$4)")
|
||||
.bind(format!("day-{day}"))
|
||||
.bind(start + Duration::days(day))
|
||||
.bind(count)
|
||||
.bind(complete)
|
||||
.execute(&pool).await.unwrap();
|
||||
}
|
||||
for (id, day, status, provider, session) in [
|
||||
("retained-request", 0, "completed", "provider", false),
|
||||
("retained-session", 0, "completed", "provider", true),
|
||||
("partial-request", 1, "completed", "provider", false),
|
||||
("partial-pending", 1, "pending", "provider", false),
|
||||
("partial-unknown", 1, "failed", "unknown", false),
|
||||
] {
|
||||
query("INSERT INTO usage(id,request_id,model,provider_name,status,billing_status,total_cost_usd,actual_total_cost_usd,created_at,request_metadata) VALUES($1,$1,'model',$2,$3,'settled',2,1,$4,$5)")
|
||||
.bind(id).bind(provider).bind(status)
|
||||
.bind(start + Duration::days(day) + Duration::seconds(1))
|
||||
.bind(json!({"routing_group_billing_multiplier":2,"analytics_attribution":{"record_kind":if session { "session" } else { "request" }}}))
|
||||
.execute(&pool).await.unwrap();
|
||||
}
|
||||
query("UPDATE usage SET total_tokens=999,input_tokens=90,output_tokens=20,cache_creation_input_tokens=11,cache_read_input_tokens=13 WHERE id='retained-request'")
|
||||
.execute(&pool).await.unwrap();
|
||||
query("INSERT INTO usage_settlement_snapshots(request_id,billing_status,billing_effective_input_tokens,billing_output_tokens,billing_cache_creation_tokens,billing_cache_read_tokens) VALUES('retained-request','settled',7,3,5,2)")
|
||||
.execute(&pool).await.unwrap();
|
||||
let day_query = |day| UsageAnalyticsQuery {
|
||||
from_unix_ms: (start + Duration::days(day)).timestamp_millis() as u64,
|
||||
to_unix_ms: (start + Duration::days(day + 1)).timestamp_millis() as u64,
|
||||
timezone: "UTC".into(),
|
||||
view: UsageAnalyticsView::DashboardCharts,
|
||||
limit: 10_000,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// A retained session belongs to the old rollup, but never to chart totals.
|
||||
let complete = repo.query_usage_analytics(&day_query(0)).await.unwrap();
|
||||
assert_eq!(complete.unrecoverable_bucket_count, 0);
|
||||
assert_eq!(complete.summary.request_count, 1);
|
||||
assert_eq!(complete.summary.total_tokens, 17);
|
||||
assert_eq!(
|
||||
complete.summary.billable_amount.as_deref(),
|
||||
Some("4.00000000")
|
||||
);
|
||||
let mut canonical_query = day_query(0);
|
||||
canonical_query.model = Some("model".into());
|
||||
let canonical = repo.query_usage_analytics(&canonical_query).await.unwrap();
|
||||
assert_eq!(
|
||||
complete.summary.total_tokens,
|
||||
canonical.summary.total_tokens
|
||||
);
|
||||
assert_eq!(
|
||||
complete.summary.billable_amount,
|
||||
canonical.summary.billable_amount
|
||||
);
|
||||
|
||||
// Pending and unknown-provider rows cannot disguise a missing legacy request.
|
||||
let partial = repo.query_usage_analytics(&day_query(1)).await.unwrap();
|
||||
assert_eq!(partial.unrecoverable_bucket_count, 24);
|
||||
assert_eq!(partial.summary.request_count, 3);
|
||||
assert_eq!(
|
||||
partial.summary.billable_amount.as_deref(),
|
||||
Some("12.00000000")
|
||||
);
|
||||
|
||||
let missing = repo.query_usage_analytics(&day_query(2)).await.unwrap();
|
||||
assert_eq!(missing.unrecoverable_bucket_count, 24);
|
||||
assert_eq!(missing.summary.request_count, 0);
|
||||
assert_eq!(missing.summary.billable_amount, None);
|
||||
assert_eq!(missing.rows[0].metrics.billable_amount, None);
|
||||
|
||||
// Incomplete rollups, unpublished days and filtered views cannot establish
|
||||
// that request details are missing from global legacy daily totals.
|
||||
for day in [3, 4] {
|
||||
let result = repo.query_usage_analytics(&day_query(day)).await.unwrap();
|
||||
assert_eq!(result.unrecoverable_bucket_count, 0);
|
||||
assert_eq!(
|
||||
result.summary.billable_amount.as_deref(),
|
||||
Some("0.00000000")
|
||||
);
|
||||
}
|
||||
let mut filtered_query = day_query(2);
|
||||
filtered_query.model = Some("model".into());
|
||||
let filtered = repo.query_usage_analytics(&filtered_query).await.unwrap();
|
||||
assert_eq!(filtered.unrecoverable_bucket_count, 0);
|
||||
let mut partial_day_query = day_query(2);
|
||||
partial_day_query.from_unix_ms += 60 * 60 * 1000;
|
||||
let partial_day = repo
|
||||
.query_usage_analytics(&partial_day_query)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(partial_day.unrecoverable_bucket_count, 23);
|
||||
assert_eq!(partial_day.summary.billable_amount, None);
|
||||
// A local day crosses two UTC archive days. Read both complete UTC days for
|
||||
// coverage, while chart totals remain bounded to the original local range.
|
||||
let local_day_query = UsageAnalyticsQuery {
|
||||
from_unix_ms: (start + Duration::days(1) + Duration::hours(16)).timestamp_millis() as u64,
|
||||
to_unix_ms: (start + Duration::days(2) + Duration::hours(16)).timestamp_millis() as u64,
|
||||
timezone: "Asia/Shanghai".into(),
|
||||
..day_query(2)
|
||||
};
|
||||
let local_day = repo.query_usage_analytics(&local_day_query).await.unwrap();
|
||||
assert_eq!(local_day.unrecoverable_bucket_count, 24);
|
||||
assert_eq!(local_day.summary.request_count, 0);
|
||||
assert_eq!(local_day.rows.len(), 1);
|
||||
assert_eq!(local_day.rows[0].metrics.billable_amount, None);
|
||||
let mut retained_partial = day_query(0);
|
||||
retained_partial.from_unix_ms += 60 * 60 * 1000;
|
||||
let retained_partial = repo.query_usage_analytics(&retained_partial).await.unwrap();
|
||||
assert_eq!(retained_partial.unrecoverable_bucket_count, 0);
|
||||
assert_eq!(retained_partial.summary.request_count, 0);
|
||||
|
||||
// Existing lost-hour evidence and the inferred day's coverage are one set.
|
||||
query("INSERT INTO stats_overview_dirty_events(transaction_id,projection_version,granularity,bucket_start,unrecoverable) VALUES(txid_current(),'overview-v2','hour',$1,true)")
|
||||
.bind(start + Duration::days(2) + Duration::hours(3))
|
||||
.execute(&pool).await.unwrap();
|
||||
let deduplicated = repo.query_usage_analytics(&day_query(2)).await.unwrap();
|
||||
assert_eq!(deduplicated.unrecoverable_bucket_count, 24);
|
||||
pool.close().await;
|
||||
}
|
||||
@@ -116,6 +116,7 @@ WHERE version=20260919000000;
|
||||
20261001000000,
|
||||
20261004000000,
|
||||
20261007000000,
|
||||
20261008000000,
|
||||
20261009000000,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -435,6 +435,20 @@ impl InMemoryUsageReadRepository {
|
||||
let metrics = |rows: &[&StoredRequestUsageAudit], slow| {
|
||||
let mut result = metrics(rows, slow, &keys);
|
||||
apply_allocations(&mut result, rows, &allocations);
|
||||
if query.view == UsageAnalyticsView::DashboardCharts {
|
||||
result.pricing_available_count = rows
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.billing_status == "settled"
|
||||
&& available(row, USAGE_PRICING_AVAILABLE_METADATA_KEY)
|
||||
&& row.billing_cost().is_some()
|
||||
})
|
||||
.count() as u64;
|
||||
if rows.is_empty() {
|
||||
result.billable_amount = Some("0.00000000".into());
|
||||
result.rated_amount = Some("0.00000000".into());
|
||||
}
|
||||
}
|
||||
result
|
||||
};
|
||||
let mut summary = metrics(&filtered, query.slow_threshold_ms.unwrap_or(5000));
|
||||
@@ -682,17 +696,39 @@ impl InMemoryUsageReadRepository {
|
||||
&& query.group_by == UsageAnalyticsGroupBy::Provider;
|
||||
let mut grouped = groups
|
||||
.into_iter()
|
||||
.map(|(id, rows)| UsageAnalyticsRow {
|
||||
label: if provider_breakdown {
|
||||
provider_display_label(&rows, id.as_deref())
|
||||
} else {
|
||||
id.clone()
|
||||
},
|
||||
bucket_start: (query.view != UsageAnalyticsView::Breakdown)
|
||||
.then(|| id.clone())
|
||||
.flatten(),
|
||||
id,
|
||||
metrics: metrics(&rows, query.slow_threshold_ms.unwrap_or(5000)),
|
||||
.map(|(id, rows)| {
|
||||
let mut metrics = metrics(&rows, query.slow_threshold_ms.unwrap_or(5000));
|
||||
if query.view == UsageAnalyticsView::DashboardCharts {
|
||||
metrics.unique_providers = Some(
|
||||
rows.iter()
|
||||
.filter_map(|row| {
|
||||
row.provider_id
|
||||
.as_deref()
|
||||
.filter(|id| !id.is_empty())
|
||||
.or_else(|| {
|
||||
(!matches!(
|
||||
row.provider_name.as_str(),
|
||||
"" | "unknown" | "pending"
|
||||
))
|
||||
.then_some(row.provider_name.as_str())
|
||||
})
|
||||
})
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len() as u64,
|
||||
);
|
||||
}
|
||||
UsageAnalyticsRow {
|
||||
label: if provider_breakdown {
|
||||
provider_display_label(&rows, id.as_deref())
|
||||
} else {
|
||||
id.clone()
|
||||
},
|
||||
bucket_start: (query.view != UsageAnalyticsView::Breakdown)
|
||||
.then(|| id.clone())
|
||||
.flatten(),
|
||||
id,
|
||||
metrics,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if query.view == UsageAnalyticsView::Breakdown {
|
||||
@@ -754,7 +790,14 @@ impl InMemoryUsageReadRepository {
|
||||
.today_start(at)?
|
||||
};
|
||||
providers
|
||||
.entry(row.provider_id.clone())
|
||||
.entry(
|
||||
row.provider_id
|
||||
.clone()
|
||||
.filter(|id| !id.is_empty())
|
||||
.or_else(|| {
|
||||
(!row.provider_name.is_empty()).then(|| row.provider_name.clone())
|
||||
}),
|
||||
)
|
||||
.or_default()
|
||||
.push(row);
|
||||
models
|
||||
|
||||
Reference in New Issue
Block a user