Files
KArtSell.Aegis/docs/Design/kbx-foundation-v36/backend/Shared/Telemetry/KbxUxMetricsQuery.cs
T

43 lines
3.4 KiB
C#

using Dapper;
using Npgsql;
namespace Kbx.Shared.Telemetry;
public sealed class KbxUxMetricsQuery(NpgsqlDataSource dataSource)
{
public async Task<KbxUxMetricsResponse> ExecuteAsync(Guid tenantId,DateOnly from,DateOnly to,string? screenId,CancellationToken ct)
{
await using var connection=await dataSource.OpenConnectionAsync(ct);
var args=new{TenantId=tenantId,From=from.ToDateTime(TimeOnly.MinValue,DateTimeKind.Utc),To=to.AddDays(1).ToDateTime(TimeOnly.MinValue,DateTimeKind.Utc),ScreenId=screenId};
const string eventsSql="""
select event_name as EventName,count(*) as Samples,
percentile_cont(0.5) within group(order by duration_ms) filter(where duration_ms is not null) as P50,
percentile_cont(0.95) within group(order by duration_ms) filter(where duration_ms is not null) as P95
from kbx.ux_events
where tenant_id=@TenantId and occurred_at>=@From and occurred_at<@To
and (@ScreenId is null or screen_id=@ScreenId)
group by event_name;
""";
var rows=(await connection.QueryAsync<EventAgg>(new CommandDefinition(eventsSql,args,cancellationToken:ct))).ToDictionary(x=>x.EventName,StringComparer.Ordinal);
const string outcomeSql="""
select coalesce(sum(observed_count),0) Observed,coalesce(sum(manual_intervention_count),0) Manual
from kbx.ux_business_outcomes where tenant_id=@TenantId and business_date>=date(@From) and business_date<date(@To) and (@ScreenId is null or screen_id=@ScreenId);
""";
var outcome=await connection.QuerySingleAsync<OutcomeAgg>(new CommandDefinition(outcomeSql,args,cancellationToken:ct));
long Count(string name)=>rows.TryGetValue(name,out var x)?x.Samples:0;
double? Ratio(long n,long d)=>d==0?null:Math.Round((double)n*100d/d,2);
var metrics=new List<KbxUxMetricRow>{
new("task_completion_time_ms","Task Completion Time",rows.GetValueOrDefault("task.complete")?.P50,"ms",Count("task.complete"),rows.GetValueOrDefault("task.complete")?.P50,rows.GetValueOrDefault("task.complete")?.P95),
new("semantic_interactions_per_task","Interactions / Task",Count("task.complete")==0?null:Math.Round((double)Count("interaction.execute")/Count("task.complete"),2),"count",Count("task.complete")),
new("manual_intervention_rate","Manual Intervention Rate",outcome.Observed==0?null:Math.Round((double)outcome.Manual*100d/outcome.Observed,2),"percent",outcome.Observed),
new("validation_failure_rate","Validation Failure Rate",Ratio(Count("validation.failed"),Count("command.execute")),"percent",Count("command.execute")),
new("import_failure_rate","Import Failure Rate",Ratio(Count("excel.import.failed"),Count("excel.import.start")),"percent",Count("excel.import.start")),
new("exception_resolution_time_ms","Exception Resolution Time",rows.GetValueOrDefault("exception.resolved")?.P50,"ms",Count("exception.resolved"),rows.GetValueOrDefault("exception.resolved")?.P50,rows.GetValueOrDefault("exception.resolved")?.P95),
new("ai_proposal_acceptance_rate","AI Proposal Acceptance Rate",Ratio(Count("ai.proposal.accept"),Count("ai.proposal.open")),"percent",Count("ai.proposal.open")),
};
return new(from,to,screenId,metrics);
}
private sealed record EventAgg(string EventName,long Samples,double? P50,double? P95);
private sealed record OutcomeAgg(long Observed,long Manual);
}