makeplane / makeplane/plane

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

Open Beginner friendly
#9,830 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
59.6k
Forks
5.8k
Avg merge
1d 22h
Merged PRs (30d)
49

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with apps/api/plane/utils/date_utils.py, reading get_analytics_date_range() and get_chart_period_range(), then inspect their callers in advance.py and project_analytics.py. Run the supplied pure date-arithmetic reproduction and add unit coverage for the documented yesterday, last_7_days, last_30_days, and last_3_months spans. Done means both helpers return exactly the labelled number of inclusive days.

Written by the indexing model from the issue text.

Assessment

Tech stack
django, python
Domain
analytics, backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
88/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.