using Dapper; using Kbx.Shared.Experiments.Generated; using Npgsql; namespace Kbx.Shared.Experiments; public sealed class KbxExperimentOverviewQuery(NpgsqlDataSource dataSource) { public async Task ExecuteAsync(Guid tenantId,CancellationToken ct) { await using var connection=await dataSource.OpenConnectionAsync(ct);var items=new List(); foreach(var def in KbxExperimentCatalog.Experiments.Values) { var runtime=await connection.QuerySingleOrDefaultAsync(new CommandDefinition("select state State,rollout_percent RolloutPercent,kill_switch KillSwitch,updated_at UpdatedAt from kbx.experiment_runtime where tenant_id=@TenantId and experiment_id=@Id",new{TenantId=tenantId,Id=def.Id},cancellationToken:ct)); var since=runtime?.UpdatedAt ?? DateTimeOffset.UtcNow.AddDays(-30); var metrics=(await connection.QueryAsync(new CommandDefinition(""" select experiment_variant Variant, count(*) filter(where event_name='experiment.exposed') Exposures, count(*) filter(where event_name='task.complete') TaskCompletions, count(*) filter(where event_name='interaction.execute') Interactions, count(*) filter(where event_name='command.execute') Commands, count(*) filter(where event_name='validation.failed') ValidationFailures, percentile_cont(0.95) within group(order by duration_ms) filter(where event_name='task.complete' and duration_ms is not null) TaskP95 from kbx.ux_events where tenant_id=@TenantId and experiment_id=@Id and occurred_at>=@Since group by experiment_variant; """,new{TenantId=tenantId,Id=def.Id,Since=since},cancellationToken:ct))).Select(x=>new KbxExperimentVariantMetric(x.Variant,x.Exposures,x.TaskCompletions,x.TaskCompletions==0?null:Math.Round((double)x.Interactions/x.TaskCompletions,2),x.TaskP95,x.Commands==0?null:Math.Round((double)x.ValidationFailures*100d/x.Commands,2))).ToArray(); var state=runtime?.State??def.State;var decision=Decide(def,state,runtime?.KillSwitch??false,metrics); items.Add(new(def.Id,def.ScreenId,state,runtime?.RolloutPercent??def.RolloutPercent,runtime?.KillSwitch??false,decision,metrics,runtime?.UpdatedAt)); } return new(items); } private static string Decide(KbxExperimentDefinition def,string state,bool killed,IReadOnlyList rows) { if(killed||state=="rolled-back")return "rolled-back";var control=rows.FirstOrDefault(x=>x.Variant=="control"),treatment=rows.FirstOrDefault(x=>x.Variant!="control"); if(control is null||treatment is null||control.Exposures0 && treatment.TaskCompletionP95Ms>control.TaskCompletionP95Ms*1.20)return "guardrail-breach"; if(control.ValidationFailureRate is not null&&treatment.ValidationFailureRate>control.ValidationFailureRate+2)return "guardrail-breach"; if(control.InteractionsPerTask is >0&&treatment.InteractionsPerTask<=control.InteractionsPerTask*(1-def.MinimumImprovementPercent/100d))return "candidate"; return "neutral"; } private sealed record RuntimeRow(string State,int RolloutPercent,bool KillSwitch,DateTimeOffset UpdatedAt); private sealed record MetricRow(string Variant,long Exposures,long TaskCompletions,long Interactions,long Commands,long ValidationFailures,double? TaskP95); }