43 lines
3.4 KiB
C#
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);
|
|
}
|