fix: maintenance repaint no longer zeroes no_data uptime; strengthen redaction test

This commit is contained in:
2026-08-24 14:12:38 +00:00
parent 21c2bb2646
commit d192589790
2 changed files with 79 additions and 10 deletions
+29 -7
View File
@@ -132,11 +132,15 @@ func assembleSnapshot(in snapshotInput) StatusSnapshot {
name := publicName(entry, mon.MonitorID)
names[entry.MonitorID] = name
// Uptime is computed from the days as reported by rollups, before
// any maintenance repaint — a no_data day must never be counted as
// zero uptime just because it is later redrawn as "maintenance".
days := buildDays(in.Rollups[entry.MonitorID], in.Now)
comp := PublicComponent{
Name: name,
Days: buildDays(in.Rollups[entry.MonitorID], underMaintenance[entry.MonitorID], in.Now),
}
comp.Uptime90d = uptimeFromDays(comp.Days)
comp.Uptime90d = uptimeFromDays(days)
comp.Days = applyMaintenanceRepaint(days, underMaintenance[entry.MonitorID])
comp.Status = componentStatus(mon, known, underMaintenance[entry.MonitorID], in.Now)
out.Components = append(out.Components, comp)
}
@@ -272,7 +276,14 @@ func publicFromAuthored(inc models.StatusIncident, names map[string]string) Publ
}
// buildDays produces exactly HistoryDays cells, oldest first, ending today.
func buildDays(rollups []models.Rollup, inMaintenance bool, now time.Time) []PublicDay {
// It carries no maintenance state: a maintenance repaint is a display concern
// applied afterwards by applyMaintenanceRepaint, once uptimeFromDays has
// already read the true no_data/up/down state of each day. Folding the
// repaint in here would let a today cell with no rollups yet flip from
// no_data to maintenance before its uptime contribution was decided, and
// uptimeFromDays skips no_data days by their State — so that day would stop
// being skipped and start counting as a zero.
func buildDays(rollups []models.Rollup, now time.Time) []PublicDay {
type bucket struct{ checks, up int }
byDay := map[string]*bucket{}
for _, r := range rollups {
@@ -300,15 +311,26 @@ func buildDays(rollups []models.Rollup, inMaintenance bool, now time.Time) []Pub
day.State = PublicDown
}
}
// Maintenance repaints today's cell only, and never touches Uptime.
if inMaintenance && i == 0 {
day.State = PublicMaintenance
}
days = append(days, day)
}
return days
}
// applyMaintenanceRepaint redraws today's cell as "maintenance" for display,
// after uptimeFromDays has already computed the component's Uptime90d from
// the unpainted days. It never touches Uptime, and it must run after that
// computation, not before: repainting first would turn a today cell with no
// rollups yet from no_data (skipped) into maintenance (a 0% day counted in
// the average), and repainting a day that DOES have rollups must still leave
// that day's real up/down contribution in the average — maintenance changes
// how a day is drawn, never what the numbers say.
func applyMaintenanceRepaint(days []PublicDay, inMaintenance bool) []PublicDay {
if inMaintenance && len(days) > 0 {
days[len(days)-1].State = PublicMaintenance
}
return days
}
// uptimeFromDays ignores no_data days rather than counting them as zero. A
// component created last week is not 92% available.
func uptimeFromDays(days []PublicDay) float64 {
@@ -77,6 +77,7 @@ func TestAssembleSnapshotRedactsMonitorInternals(t *testing.T) {
"chan-123",
"SECRETKEYWORD",
"prod-api-internal",
"db-primary",
"5432",
}
for _, leak := range leaks {
@@ -88,9 +89,10 @@ func TestAssembleSnapshotRedactsMonitorInternals(t *testing.T) {
func TestAssembleSnapshotUsesDisplayNameThenMonitorName(t *testing.T) {
in := testInput(time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC))
// mon-2 has no override, and its monitor name is in the leak list above,
// so an un-overridden entry must fall back to something safe. It falls
// back to the monitor id, never the internal name.
// mon-2 has no DisplayName override, and its monitor name ("db-primary")
// is in TestAssembleSnapshotRedactsMonitorInternals's leak list, so an
// un-overridden entry must fall back to something safe. It falls back to
// the monitor id, never the internal name.
snap := assembleSnapshot(in)
comps := snap.Sections[0].Components
if comps[0].Name != "Public API" {
@@ -161,6 +163,51 @@ func TestAssembleSnapshotMaintenanceDoesNotChangeUptime(t *testing.T) {
}
}
// TestAssembleSnapshotMaintenanceRepaintDoesNotCountNoDataAsZero guards
// against the maintenance repaint corrupting Uptime90d for a component whose
// today rollup has not landed yet — an in-progress maintenance window on a
// young component, or one that simply started before today's hourly rollup
// was written. Repainting today's no_data cell to "maintenance" must never
// make uptimeFromDays stop skipping it: doing so would turn a component with
// one good day of history from 100% into 50%.
func TestAssembleSnapshotMaintenanceRepaintDoesNotCountNoDataAsZero(t *testing.T) {
now := time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC)
in := testInput(now)
in.Rollups = map[string][]models.Rollup{
// Only yesterday has a rollup, fully up. Today has none.
"mon-2": {{MonitorID: "mon-2", PeriodStart: now.Add(-24 * time.Hour), Checks: 60, UpCount: 60}},
}
start := now.Add(-time.Hour)
end := now.Add(time.Hour)
in.Authored = []models.StatusIncident{{
IncidentID: "mnt-2",
PageIDs: []string{"api"},
Kind: models.StatusKindMaintenance,
Title: "Database upgrade",
Status: models.MaintenanceInProgress,
AffectedMonitors: []string{"mon-2"},
ScheduledStart: &start,
ScheduledEnd: &end,
StartedAt: start,
}}
snap := assembleSnapshot(in)
comp := snap.Sections[0].Components[1]
if comp.Status != "maintenance" {
t.Errorf("status = %q, want maintenance", comp.Status)
}
// Today's cell is redrawn as maintenance for display...
if comp.Days[89].State != "maintenance" {
t.Errorf("today state = %q, want maintenance", comp.Days[89].State)
}
// ...but it carries no rollup, so it must not have been averaged in as a
// zero. The only day with data was 100% up, so the 90-day figure is 100,
// not (100+0)/2 = 50.
if comp.Uptime90d != 100 {
t.Errorf("uptime = %v, want 100 (today's no_data must not count as zero)", comp.Uptime90d)
}
}
func TestAssembleSnapshotOnlyIncludesAuthoredIncidentsForThisPage(t *testing.T) {
now := time.Date(2026, 8, 24, 12, 0, 0, 0, time.UTC)
in := testInput(now)