Artifact 269b3ddef493c85659f591b42b51d0c6afc9f0e4445c20d0de72bfe652f08e9b:
- File
internal/obsduck/trend_test.go
— part of check-in
[a0fa952495]
at
2026-08-26 11:24:13
on branch landing-merge
— ops: read the history back — a route across days
The store recorded renders and nothing read them. The panels are one process's memory, so the page could say how fast a route is now and never whether it is worse than it was — which is the question after a deploy.
The drilldown gains a per-day table from the store: p50, p95, max. Nearest-rank percentiles, matching how the snapshot computes them, so two numbers on one page both called p95 agree.
Empty for no traffic and empty for history-off are different answers and the page gives the right one. (user: dmestas, size: 3416) [annotate] [blame] [check-ins using] [more...]
//go:build !js && !wasip1 package obsduck import ( "testing" "time" "craftdesign.group/quarry/internal/obs" ) // The live panels are one process's memory, so they cannot answer the // question an operator has after a deploy: is this getting worse. The // store held the rows and nothing read them back. func TestTrendSeparatesTheDays(t *testing.T) { s := openSink(t) now := time.Now() for i := 0; i < 20; i++ { s.Render(obs.Render{At: now.Add(-48 * time.Hour), Path: "GET /{$}", Elapsed: time.Duration(10+i) * time.Millisecond}) s.Render(obs.Render{At: now, Path: "GET /{$}", Elapsed: time.Duration(100+i*10) * time.Millisecond}) } pts, err := s.Trend("GET /{$}", 7) if err != nil { t.Fatal(err) } if len(pts) != 2 { t.Fatalf("got %d days, want 2: %+v", len(pts), pts) } if pts[0].Day >= pts[1].Day { t.Error("days are not oldest first") } if pts[0].N != 20 || pts[1].N != 20 { t.Errorf("counts %d/%d, want 20 each", pts[0].N, pts[1].N) } // The whole point: the regression is visible. if pts[1].P95Ms <= pts[0].P95Ms { t.Errorf("p95 went %d -> %d; a route that got 10x slower reads as flat", pts[0].P95Ms, pts[1].P95Ms) } if pts[0].P50Ms > pts[0].P95Ms || pts[0].P95Ms > pts[0].MaxMs { t.Errorf("percentiles out of order: %+v", pts[0]) } } // Trend must answer for one route, not for the forge's whole traffic // mixed together — the drilldown exists to isolate a route. func TestTrendIsScopedToItsRoute(t *testing.T) { s := openSink(t) now := time.Now() s.Render(obs.Render{At: now, Path: "GET /{$}", Elapsed: 500 * time.Millisecond}) s.Render(obs.Render{At: now, Path: "GET /r/{repo}", Elapsed: 5 * time.Millisecond}) pts, err := s.Trend("GET /{$}", 7) if err != nil { t.Fatal(err) } if len(pts) != 1 || pts[0].N != 1 || pts[0].MaxMs != 500 { t.Fatalf("route trend picked up the other route: %+v", pts) } // ...and the whole forge when nothing is named. if all, err := s.Trend("", 7); err != nil || len(all) != 1 || all[0].N != 2 { t.Fatalf("fleet-wide trend %+v err=%v, want one day of 2", all, err) } } // The window is a window: rows outside it are not the answer to "is // this getting worse lately". func TestTrendHonoursItsWindow(t *testing.T) { s := openSink(t) now := time.Now() s.Render(obs.Render{At: now.Add(-30 * 24 * time.Hour), Path: "GET /{$}", Elapsed: time.Second}) s.Render(obs.Render{At: now, Path: "GET /{$}", Elapsed: 5 * time.Millisecond}) pts, err := s.Trend("GET /{$}", 7) if err != nil { t.Fatal(err) } if len(pts) != 1 { t.Fatalf("a 7-day window returned %d days: %+v", len(pts), pts) } } // A recorder whose sink cannot answer must return nothing rather than // inventing a trend from the ring, which covers minutes. func TestRecorderWithoutADurableSinkHasNoTrend(t *testing.T) { r := obs.NewRecorder(8, nil) pts, err := r.Trend("GET /{$}", 7) if err != nil || pts != nil { t.Fatalf("history-off recorder returned %+v err=%v", pts, err) } } // ...and one that can, does — the seam the ops op reaches through. func TestRecorderReachesTheDurableTrend(t *testing.T) { s := openSink(t) r := obs.NewRecorder(8, nil) r.SetSink(s) s.Render(obs.Render{At: time.Now(), Path: "GET /{$}", Elapsed: 42 * time.Millisecond}) pts, err := r.Trend("GET /{$}", 7) if err != nil { t.Fatal(err) } if len(pts) != 1 || pts[0].MaxMs != 42 { t.Fatalf("recorder did not reach the store: %+v", pts) } }