Skip to content

Commit afe4fb6

Browse files
committed
fix(stats): include today in visitors chart on stats page
The rightmost bar on the visitors chart was always 0 because Plausible's `"28d"` date_range preset maps internally to `[today-28, today-1]` and deliberately excludes today (see plausible/analytics PR #5282, where `last = today - 1 day`). Our zero-filled `[today-27, today]` window then never found a matching row for today. Switch to an explicit custom date range `[today-27, today]` so today's partial-day visitors appear in the rightmost bar, matching what the Plausible dashboard's "Last 28 days" view shows.
1 parent 6adea80 commit afe4fb6

2 files changed

Lines changed: 67 additions & 5 deletions

File tree

api/routers/insights.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -633,9 +633,15 @@ async def _fetch() -> RelatedSpecsResponse:
633633
async def _fetch_plausible_visitors() -> VisitorsResponse:
634634
"""Query the Plausible Stats API v2 for unique visitors per day (last 28d).
635635
636-
The 28-day window matches Plausible's own default "Last 28 days" report so
637-
the totals here align with what the dashboard at plausible.io/anyplot.ai
638-
shows by default.
636+
The 28-day window ends on today (inclusive) so the rightmost bar reflects
637+
current-day traffic as a partial day, matching how Plausible's dashboard
638+
"Last 28 days" view renders.
639+
640+
Note: we deliberately do NOT use Plausible's `"28d"` preset because it
641+
maps to `[today-28, today-1]` (the source uses `last = today - 1 day`,
642+
see plausible/analytics PR #5282), which excludes today and left the
643+
rightmost chart bar permanently at 0. An explicit `[today-27, today]`
644+
custom range pulls today's partial row in.
639645
640646
Returns an empty `points` list when the API key is not configured or the
641647
upstream call fails — the stats page treats `points: []` as "no data" and
@@ -647,10 +653,12 @@ async def _fetch_plausible_visitors() -> VisitorsResponse:
647653
if not settings.plausible_api_key:
648654
return VisitorsResponse(points=[])
649655

656+
today = datetime.now(timezone.utc).date()
657+
start = today - timedelta(days=27)
650658
payload = {
651659
"site_id": settings.plausible_site_id,
652660
"metrics": ["visitors"],
653-
"date_range": "28d",
661+
"date_range": [start.isoformat(), today.isoformat()],
654662
"dimensions": ["time:day"],
655663
}
656664
try:
@@ -682,7 +690,6 @@ async def _fetch_plausible_visitors() -> VisitorsResponse:
682690
count = 0
683691
by_date[day_str] = count
684692

685-
today = datetime.now(timezone.utc).date()
686693
points = [
687694
VisitorPoint(
688695
date=(today - timedelta(days=offset)).isoformat(),

tests/unit/api/test_routers.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1953,6 +1953,61 @@ def now(cls, tz=None): # type: ignore[override]
19531953
today_point = next(p for p in points if p["date"] == today_iso)
19541954
assert today_point["visitors"] == 42
19551955

1956+
def test_visitors_requests_explicit_date_range_including_today(self, client: TestClient) -> None:
1957+
"""Plausible's `"28d"` preset excludes today (it maps to [today-28, today-1]),
1958+
so we must send an explicit `[today-27, today]` custom date range to
1959+
ensure today's partial-day visitors appear in the rightmost chart bar.
1960+
Locking this in via the outgoing payload so the bug can't silently
1961+
regress to the "always 0 today" behavior.
1962+
"""
1963+
from datetime import datetime as _dt
1964+
from datetime import timedelta
1965+
from datetime import timezone as _tz
1966+
1967+
frozen_now = _dt(2026, 5, 13, 12, 0, 0, tzinfo=_tz.utc)
1968+
today_iso = frozen_now.date().isoformat()
1969+
expected_start_iso = (frozen_now.date() - timedelta(days=27)).isoformat()
1970+
1971+
captured: dict = {}
1972+
1973+
class _MockResp:
1974+
status_code = 200
1975+
1976+
def raise_for_status(self) -> None:
1977+
return None
1978+
1979+
def json(self) -> dict:
1980+
return {"results": []}
1981+
1982+
class _MockClient:
1983+
async def __aenter__(self):
1984+
return self
1985+
1986+
async def __aexit__(self, *_args):
1987+
return False
1988+
1989+
async def post(self, *_args, **kwargs):
1990+
captured["payload"] = kwargs.get("json")
1991+
return _MockResp()
1992+
1993+
class _FrozenDatetime(_dt):
1994+
@classmethod
1995+
def now(cls, tz=None): # type: ignore[override]
1996+
return frozen_now if tz is _tz.utc else frozen_now.replace(tzinfo=None)
1997+
1998+
with (
1999+
patch("api.routers.insights.get_or_set_cache", side_effect=_passthrough_cache),
2000+
patch("api.routers.insights.settings") as mock_settings,
2001+
patch("api.routers.insights.httpx.AsyncClient", return_value=_MockClient()),
2002+
patch("api.routers.insights.datetime", _FrozenDatetime),
2003+
):
2004+
mock_settings.plausible_api_key = "test-key"
2005+
mock_settings.plausible_site_id = "anyplot.ai"
2006+
mock_settings.plausible_api_url = "https://plausible.io/api/v2/query"
2007+
response = client.get("/insights/visitors")
2008+
assert response.status_code == 200
2009+
assert captured["payload"]["date_range"] == [expected_start_iso, today_iso]
2010+
19562011

19572012
class TestSpecCodeEndpoint:
19582013
"""Tests for the /specs/{spec_id}/{library}/code endpoint."""

0 commit comments

Comments
 (0)