Files
VeillaxandClaude Sonnet 5.5 b4fd31a182
Release / release (push) Successful in 3m49s
CI / test (push) Successful in 4m45s
Add website analytics
A Tianji-style Websites section: a small tracking script, a public collection endpoint, and a
page per website with visitors, page views, visits, bounce rate, a chart and top lists for pages,
referrers, browsers, systems, devices, countries, languages and events. No cookies and no stored
IP address or user agent. SANDBAR_TRUST_PROXY makes the client address come from the proxy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 04:22:55 -06:00

89 lines
2.6 KiB
Go

package sandbar
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
)
// Every check lands in a per-day count that outlives the raw checks, and the status page
// gets one [checks, failed] pair per day, oldest first and ending today.
func TestUptimeBars(t *testing.T) {
s, clk, _ := newTestServer(t)
m := &Monitor{ID: "m", Name: "Site", Kind: "http", Target: "http://x", Enabled: true, Public: true, Retries: 1}
s.st.put("monitor", m.ID, m)
// Two days ago: 4 checks, 1 failed. Yesterday: nothing. Today: 2 checks, both fine.
clk.Advance(-48 * time.Hour)
for i := 0; i < 4; i++ {
var err error
if i == 3 {
err = errors.New("timeout")
}
s.recordResult(m, s.monitorState(m.ID), clk.Now(), 10*time.Millisecond, err)
}
clk.Advance(48 * time.Hour)
s.recordResult(m, s.monitorState(m.ID), clk.Now(), 10*time.Millisecond, nil)
s.recordResult(m, s.monitorState(m.ID), clk.Now(), 10*time.Millisecond, nil)
days := s.dailyUptime(m.ID, 5, clk.Now())
want := [][2]int{{0, 0}, {0, 0}, {4, 1}, {0, 0}, {2, 0}}
for i := range want {
if days[i] != want[i] {
t.Fatalf("days = %v, want %v", days, want)
}
}
// The rollup survives pruning the raw checks.
s.st.prune(clk.Now().AddDate(0, 0, 60), 30, 1, 365)
if got := s.dailyUptime(m.ID, 5, clk.Now()); got[2] != [2]int{4, 1} {
t.Fatalf("rollup lost after pruning: %v", got)
}
ts := httptest.NewServer(s.Handler())
defer ts.Close()
resp, err := http.Get(ts.URL + "/status.json")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
var out struct {
Today string
Monitors []struct {
Uptime float64
Days [][2]int
}
}
json.NewDecoder(resp.Body).Decode(&out)
if out.Today != "2026-01-05" || len(out.Monitors) != 1 || len(out.Monitors[0].Days) != uptimeDays {
t.Fatalf("status.json: %+v", out)
}
m0 := out.Monitors[0]
if last := m0.Days[uptimeDays-1]; last != [2]int{2, 0} {
t.Fatalf("today's bar: %v", last)
}
if want := 100 * 5.0 / 6.0; m0.Uptime < want-0.01 || m0.Uptime > want+0.01 {
t.Fatalf("uptime %.2f, want %.2f", m0.Uptime, want)
}
}
// A database from before the daily table gets it built from the checks it has.
func TestDailyBackfill(t *testing.T) {
s, clk, _ := newTestServer(t)
now := clk.Now().Unix()
s.st.db.Exec(`INSERT INTO checks(monitor_id,time,ok,ms,detail) VALUES('m',?,1,5,''),('m',?,0,5,'x')`, now, now+1)
s.st.db.Exec(`DELETE FROM daily`)
var have int
s.st.db.QueryRow(`SELECT count(*) FROM daily`).Scan(&have)
if have != 0 {
t.Fatal("setup")
}
s.st.backfillDaily()
if got := s.dailyUptime("m", 1, clk.Now()); got[0] != [2]int{2, 1} {
t.Fatalf("backfill: %v", got)
}
}