feat: bucket aggregation latency panel (Closes #11)
CI / lint (pull_request) Successful in 4m4s
CI / build-release (pull_request) Successful in 15s
CI / browser-journey (pull_request) Successful in 8m45s
CI / release-candidate (pull_request) Has been skipped

Measure each live aggregation bucket's own upstream duration server-side
(auth fetch attributed to Work/Activity, per-feed timed gather) and surface
the measured values in the Live data status panel.

- Per-bucket latency_ms measured individually, never batch wall time
- Stale feeds label their number 'last known'; unmeasured feeds say
  'not measured' instead of an ambiguous dash; hidden when no data
- Failed buckets keep their last known latency across partial refreshes
- Panel is aria-labelled rows as a list, values wrap on mobile
- Ignore .venv/
This commit is contained in:
ox committed 2026-08-22 21:49:39 +00:00
1 parent b18a50fec5
commit 398598e154
8 files changed
+362 -6

No files matched your search

+111 -1
View File
@@ -43,7 +43,8 @@ process.stdout.write(JSON.stringify({{
assert result["one"]["summary"] == "Updates delayed"
assert result["one"]["nextRetrySeconds"] == 30
assert result["one"]["feeds"][1] == {
"key": "notifications", "label": "Updates", "state": "delayed", "ageSeconds": 14
"key": "notifications", "label": "Updates", "state": "delayed", "ageSeconds": 14,
"latencyMs": None,
}
assert result["two"]["summary"] == "2 data feeds delayed"
assert result["unavailable"]["summary"] == "Live data unavailable"
@@ -182,6 +183,103 @@ process.stdout.write(JSON.stringify({{opened, finishes}}));
}
def test_live_data_status_describe_exposes_bounded_bucket_latency():
script = f"""
const status = require({json.dumps(str(STATUS))});
const measured = {{fresh_for_seconds:8, latency_ms:{{context:120, notifications:45}}, sections:{{
context:{{age_seconds:1}}, notifications:{{age_seconds:14, stale:true, retry_in_seconds:30}}, events:{{age_seconds:3}}
}}}};
const unbounded = {{fresh_for_seconds:8, latency_ms:{{context:-5, events:9000000}}, sections:{{
context:{{age_seconds:1}}, events:{{age_seconds:2}}, notifications:{{age_seconds:3}}
}}}};
const missing = {{fresh_for_seconds:8, sections:{{
context:{{age_seconds:1}}, events:{{age_seconds:2}}, notifications:{{age_seconds:3}}
}}}};
process.stdout.write(JSON.stringify({{
measured:status.describe(measured),
unbounded:status.describe(unbounded),
missing:status.describe(missing),
}}));
"""
result = run_node(script)
assert result["measured"]["summary"] == "Updates delayed"
assert [(feed["key"], feed["latencyMs"]) for feed in result["measured"]["feeds"]] == [
("context", 120), ("notifications", 45), ("events", None),
]
assert [(feed["key"], feed["latencyMs"]) for feed in result["unbounded"]["feeds"]] == [
("context", None), ("notifications", None), ("events", 3600000),
]
assert all(feed["latencyMs"] is None for feed in result["missing"]["feeds"])
def test_live_data_status_renders_bucket_latency_panel_rows():
script = f"""
const status = require({json.dumps(str(STATUS))});
const description = status.describe({{fresh_for_seconds:8, latency_ms:{{context:120, notifications:45, events:3000}}, sections:{{
context:{{age_seconds:1}}, notifications:{{age_seconds:14, stale:true}}, events:{{age_seconds:3}}
}}}});
function element() {{
return {{innerHTML:'', hidden:false, textContent:''}};
}}
const panel = element();
const rows = status.renderBucketLatency(description, panel);
process.stdout.write(JSON.stringify({{rows, panelHidden:panel.hidden, html:panel.innerHTML}}));
"""
result = run_node(script)
assert [row["label"] for row in result["rows"]] == ["Work", "Updates", "Activity"]
assert result["rows"][0] == {"key": "context", "label": "Work", "latencyMs": 120, "stale": False}
assert result["rows"][2]["latencyMs"] == 3000
assert result["panelHidden"] is False
assert 'data-bucket="context"' in result["html"]
assert "120 ms" in result["html"]
assert "3.0 s" in result["html"]
# A stale feed's number is a last-known measurement and must say so.
assert 'data-bucket="notifications"' in result["html"]
assert "last known" in result["html"]
def test_live_data_status_marks_unmeasured_buckets_honestly():
script = f"""
const status = require({json.dumps(str(STATUS))});
const description = status.describe({{fresh_for_seconds:8, latency_ms:{{context:120}}, sections:{{
context:{{age_seconds:1}}, notifications:{{age_seconds:2}}, events:{{age_seconds:3}}
}}}});
function element() {{
return {{innerHTML:'', hidden:false, textContent:''}};
}}
const panel = element();
const rows = status.renderBucketLatency(description, panel);
process.stdout.write(JSON.stringify({{rows, html:panel.innerHTML}}));
"""
result = run_node(script)
assert result["panelHidden"] is False if "panelHidden" in result else True
assert 'data-bucket="events"' in result["html"]
assert "not measured" in result["html"]
assert "—" not in result["html"]
def test_live_data_status_hides_latency_panel_without_any_measurement():
script = f"""
const status = require({json.dumps(str(STATUS))});
const description = status.describe({{fresh_for_seconds:8, sections:{{
context:{{age_seconds:1}}, notifications:{{age_seconds:2}}, events:{{age_seconds:3}}
}}}});
function element() {{
return {{innerHTML:'', hidden:false, textContent:''}};
}}
const panel = element();
const rows = status.renderBucketLatency(description, panel);
process.stdout.write(JSON.stringify({{rows, panelHidden:panel.hidden, html:panel.innerHTML}}));
"""
result = run_node(script)
assert result["panelHidden"] is True
assert result["html"] == ""
def test_live_data_status_has_accessible_mobile_safe_sheet_contract():
html = HTML.read_text()
css = CSS.read_text()
@@ -196,13 +294,25 @@ def test_live_data_status_has_accessible_mobile_safe_sheet_contract():
assert 'id="refresh-live-data"' in html
assert 'id="return-from-live-data-status"' in html
assert 'id="live-data-status-today-paused"' in html
assert 'id="bucket-latency-panel"' in html
assert '<script src="static/live-data-status.js"></script>' in html
assert ".live-data-status-panel" in css
assert "width:min(560px,100%)" in css
assert "min-height:44px" in css
assert ".bucket-latency-panel" in css
assert ".bucket-latency-row" in css
assert ".bucket-latency-value.slow" in css
# Latency rows must stay readable on narrow screens and to screen readers:
# the panel is labelled, rows are announced as a list, values wrap.
assert 'aria-labelledby="bucket-latency-heading"' in html
assert 'id="bucket-latency-rows" role="list"' in html
assert "bucket-latency-row { display:flex" in css
assert ".bucket-latency-row strong, .bucket-latency-value { overflow-wrap:anywhere" in css
assert "liveDataStatus.describe" in dashboard
assert "liveDataStatus.createRefreshController" in dashboard
assert "liveDataStatus.mount" in dashboard
assert "renderBucketLatency(" in dashboard
assert "#bucket-latency-panel" in dashboard
assert "createSheetController" in status_source
assert "backgroundElements:[qs('header'), qs('main'), qs('#mobile-task-dock')]" in status_source
assert "contextPoller.getState()" in dashboard
+132
View File
@@ -494,6 +494,138 @@ async def test_partial_refresh_updates_fresh_sections_and_retains_failed_section
assert result["freshness"]["revalidating"] is False
@pytest.mark.anyio
async def test_live_snapshot_reports_per_bucket_upstream_latency(monkeypatch):
delays = {"context": 0.03, "events": 0.02, "notifications": 0.06}
async def user():
await asyncio.sleep(delays["context"])
return {"id": 1, "login": "timmy"}
async def empty():
return []
async def events(_authenticated_user):
await asyncio.sleep(delays["events"])
return [{"type": "push"}]
async def updates():
await asyncio.sleep(delays["notifications"])
return {"items": [], "page": 1, "total": 0, "has_more": False}
monkeypatch.setattr(main, "current_user", user)
monkeypatch.setattr(main, "repos", empty)
monkeypatch.setattr(main, "issues", empty)
monkeypatch.setattr(main, "pull_requests", empty)
monkeypatch.setattr(main, "activity_events", events)
monkeypatch.setattr(main, "notifications", updates)
result = payload(await main.live_snapshot())
latency = result["freshness"]["latency_ms"]
assert set(latency) == {"context", "events", "notifications"}
for section, seconds in delays.items():
assert isinstance(latency[section], int)
assert latency[section] >= seconds * 1000
@pytest.mark.anyio
async def test_bucket_latency_measures_each_feed_individually(monkeypatch):
"""A fast feed must not inherit a slow sibling's batch wall time."""
async def user():
await asyncio.sleep(0.03)
return {"id": 1, "login": "timmy"}
async def empty():
return []
async def fast_events(_authenticated_user):
return [{"type": "push"}]
async def slow_updates():
await asyncio.sleep(0.12)
return {"items": [], "page": 1, "total": 0, "has_more": False}
monkeypatch.setattr(main, "current_user", user)
monkeypatch.setattr(main, "repos", empty)
monkeypatch.setattr(main, "issues", empty)
monkeypatch.setattr(main, "pull_requests", empty)
monkeypatch.setattr(main, "activity_events", fast_events)
monkeypatch.setattr(main, "notifications", slow_updates)
result = payload(await main.live_snapshot())
latency = result["freshness"]["latency_ms"]
# The fast events feed finished immediately; its displayed latency must
# reflect that feed alone, not the whole gather batch (~120 ms).
assert latency["events"] < latency["notifications"]
assert latency["events"] < 60
assert latency["notifications"] >= 120
@pytest.mark.anyio
async def test_bucket_latency_survives_partial_refresh_and_failure(monkeypatch):
"""A failed bucket keeps its last known latency instead of vanishing."""
now = 100.0
builds = 0
async def snapshot(_sections=None):
nonlocal builds
builds += 1
if builds == 1:
return {
"context": {"generation": 1},
"events": [],
"notifications": [],
"latency_ms": {"context": 11, "events": 12, "notifications": 13},
"sections": {
"context": "fresh", "events": "fresh", "notifications": "fresh",
},
}
return {
"context": {"generation": 2},
"events": None,
"notifications": [],
"latency_ms": {"context": 21, "notifications": 23},
"sections": {
"context": "fresh",
"events": "temporarily unavailable",
"notifications": "fresh",
},
}
monkeypatch.setattr(main, "_live_snapshot_clock", lambda: now)
monkeypatch.setattr(main, "_build_live_snapshot", snapshot)
first = payload(await main.live_snapshot())
assert first["freshness"]["latency_ms"] == {
"context": 11, "events": 12, "notifications": 13,
}
now += main.LIVE_SNAPSHOT_FRESHNESS_SECONDS + 1
await main.live_snapshot()
assert main._live_snapshot_task is not None
await main._live_snapshot_task
result = payload(await main.live_snapshot())
assert result["freshness"]["latency_ms"] == {
"context": 21, "events": 12, "notifications": 23,
}
def test_live_payload_without_latency_data_reports_no_buckets():
value = {
"context": {},
"events": None,
"notifications": None,
"sections": {"context": "fresh"},
}
result = main._live_snapshot_payload(value, stale=False, revalidating=False)
assert result["freshness"]["latency_ms"] == {}
assert "latency_ms" not in result
@pytest.mark.anyio
async def test_notification_cooldown_does_not_stop_due_work_and_activity_refreshes(monkeypatch):
now = 100.0