Skip to content

[bug]: Analytics "Last 7 / 30 days" windows cover one day too many #9830

Description

@CaptainAni187

Is there an existing issue for this?

Yes, I searched — nothing open or closed covers this (date_utils, get_analytics_date_range, last_7_days), and no open PR touches apps/api/plane/utils/date_utils.py.

Current behavior

Every "last N" window in Advance Analytics covers N + 1 days. Last 7 days returns 8 days of data, Last 30 days returns 31, Last 3 months returns 91.

Both helpers in apps/api/plane/utils/date_utils.py build the window as today - timedelta(days=N) through today, and both bounds are inclusive:

  • get_analytics_date_range()L45, L56, L67 — feeds created_at__gte / created_at__lte for the overview and work-item counts
  • get_chart_period_range()L117–L119 — feeds created_at__date__gte / created_at__date__lte for every chart in advance.py and project_analytics.py

The intended length isn't ambiguous, because the same function gets it right twice:

  • the yesterday branch is exactly 1 day
  • the previous comparison windows are exactly 7, 30 and 90 days — previous and current sit in the same dict literal and come out different lengths

And the get_chart_period_range docstring documents "last_3_months": Last 90 days (L102) while the code returns 91.

last_30_days is the default selectedDuration (apps/web/core/store/analytics.store.ts:38), so the default view of Advance Analytics is the one that's off.

Expected behavior

A window labelled Last 7 days covers 7 days. With both bounds inclusive that's today - timedelta(days=N - 1) through today, matching what the previous windows already do.

Steps to reproduce

This is pure date arithmetic, so it reproduces without a database. The script loads the shipped date_utils.py as-is and only stubs plane.db.models.User, which the module imports for a type annotation.

# plane_repro.py  —  python plane_repro.py apps/api/plane/utils/date_utils.py
import importlib.util, sys, types
import django
from django.conf import settings

settings.configure(USE_TZ=True, TIME_ZONE="UTC", INSTALLED_APPS=[])
django.setup()

stub = types.ModuleType("plane.db.models")
stub.User = object
sys.modules["plane"] = types.ModuleType("plane")
sys.modules["plane.db"] = types.ModuleType("plane.db")
sys.modules["plane.db.models"] = stub

spec = importlib.util.spec_from_file_location("date_utils", sys.argv[1])
date_utils = importlib.util.module_from_spec(spec)
spec.loader.exec_module(date_utils)

EXPECTED = {"yesterday": 1, "last_7_days": 7, "last_30_days": 30, "last_3_months": 90}
HEAD = f"{'date_filter':<16}{'window':<10}{'first day':<13}{'last day':<13}{'days':>5}{'expected':>10}"

def row(name, window, lo, hi, want):
    got = (hi - lo).days + 1
    flag = "" if got == want else f"   <-- {got - want:+d}"
    print(f"{name:<16}{window:<10}{str(lo):<13}{str(hi):<13}{got:>5}{want:>10}{flag}")

print("get_analytics_date_range()\n" + HEAD)
for f, want in EXPECTED.items():
    ranges = date_utils.get_analytics_date_range(f)
    for window in ("current", "previous"):
        if window in ranges:
            r = ranges[window]
            row(f, window, r["gte"].date(), r["lte"].date(), want)

print(f"\nget_chart_period_range()\n{HEAD}")
for f, want in EXPECTED.items():
    lo, hi = date_utils.get_chart_period_range(f)
    row(f, "-", lo, hi, want)

Output on 2f895b8 (run 2026-09-14):

get_analytics_date_range()
date_filter     window    first day    last day      days  expected
yesterday       current   2026-09-13   2026-09-13       1         1
last_7_days     current   2026-09-07   2026-09-14       8         7   <-- +1
last_7_days     previous  2026-08-31   2026-09-06       7         7
last_30_days    current   2026-08-15   2026-09-14      31        30   <-- +1
last_30_days    previous  2026-07-16   2026-08-14      30        30
last_3_months   current   2026-06-16   2026-09-14      91        90   <-- +1
last_3_months   previous  2026-03-18   2026-06-15      90        90

get_chart_period_range()
date_filter     window    first day    last day      days  expected
yesterday       -         2026-09-13   2026-09-13       1         1
last_7_days     -         2026-09-07   2026-09-14       8         7   <-- +1
last_30_days    -         2026-08-15   2026-09-14      31        30   <-- +1
last_3_months   -         2026-06-16   2026-09-14      91        90   <-- +1

In the UI: open Analytics → Overview on a workspace with work items created 31 days ago, leave the duration on the default Last 30 days, and those items are counted.

Scope

Only the current windows are user-visible today — advance.py has "filter_count": get_previous_count() commented out, so the period-over-period figure isn't returned yet. Worth noting because it means fixing current to N days also makes the two windows the same length, which is what a comparison needs when that line is uncommented.

Environment

Community, self-hosted from source at 2f895b8 (preview).

Notes

Happy to open a PR with the one-line change in each helper plus unit tests asserting the span of each window, if the 7-means-7 reading is the one you want. The alternative reading — "last 7 days" meaning today plus the 7 before it — is defensible too, but then previous and the last_3_months docstring need to move instead, which is why I'm raising it rather than sending a patch.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions