From 488bdf60fbf124a03299b7da06ef1540d4e72017 Mon Sep 17 00:00:00 2001 From: FrameAutomata Date: Wed, 26 Aug 2026 16:19:10 -0500 Subject: [PATCH] fix: stop reporting an AI-trace stats failure as 0ms latency `GetTraceNameStats` on the SQLite telemetry backend swallowed the error from its percentile query with `return stats, nil`. On any failure -- context deadline on a large trace_name, a locked telemetry DB, a scan type error -- the caller got a struct with a real Count, AvgDuration and token totals next to MedianDuration and P95Duration of exactly zero, and nothing was logged or captured. Zero is a plausible latency, so this reads as a fast trace rather than a failed read. The DuckDB and ClickHouse backends compute the percentiles inside the aggregate query and propagate the error, and the SQLite endpoint and task equivalents already `return nil, err`, so this was the one path that fabricated a value. The route treats stats as best-effort -- it nils them and still answers 200 with the trace list -- so propagating the error does not turn this into a 500. It swaps a misleading zero for an absent stats block, and the discard in the controller now reports through CaptureException per the non-stopping-error convention rather than vanishing. No regression test: both queries in this function read the same table, so there is no way to fail only the percentile read from a test without injecting a fake executor, which the repositories do not support. Co-Authored-By: Claude Opus 5 (1M context) --- backend/app/controllers/ai_trace.controller.go | 4 ++++ .../app/repositories/telemetry/sqlite/ai_trace.repository.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/app/controllers/ai_trace.controller.go b/backend/app/controllers/ai_trace.controller.go index 3f44d395..b09c76dd 100644 --- a/backend/app/controllers/ai_trace.controller.go +++ b/backend/app/controllers/ai_trace.controller.go @@ -2,6 +2,7 @@ package controllers import ( "encoding/json" + "fmt" "net/http" "net/url" "sync" @@ -162,8 +163,11 @@ func (a aiTraceController) FindByTraceName(c *gin.Context) { return } + // Stats are best-effort: the trace list still renders without them, so a + // failure here is reported rather than aborting the request. stats, err := telemetry.AiTraceRepository.GetTraceNameStats(c, projectId, traceName, request.FromDate, request.ToDate) if err != nil { + traceway.CaptureException(fmt.Errorf("failed to load ai trace stats (traceName=%s): %w", traceName, err)) stats = nil } diff --git a/backend/app/repositories/telemetry/sqlite/ai_trace.repository.go b/backend/app/repositories/telemetry/sqlite/ai_trace.repository.go index b03d5277..a03f7f88 100644 --- a/backend/app/repositories/telemetry/sqlite/ai_trace.repository.go +++ b/backend/app/repositories/telemetry/sqlite/ai_trace.repository.go @@ -429,7 +429,7 @@ func (r *aiTraceRepository) GetTraceNameStats(ctx context.Context, projectId uui WHERE project_id = :project_id AND trace_name = :trace_name AND recorded_at >= :from AND recorded_at <= :to ORDER BY duration ASC`, params) if err != nil { - return stats, nil + return nil, err } sortedDurations := make([]float64, len(durationRows))