#1000·kutt

Bug: lastYear.views uses rolling month windows instead of calendar months

Author: foreverUACreated Apr 10, 2026Updated Jun 16, 2026

Reporter: foreverUA ([email protected])

Summary

The /api/v2/links/{id}/stats endpoint returns lastYear.views array with monthly view counts that are not aligned to calendar months. Visits are bucketed using differenceInMonths from date-fns, which creates rolling windows anchored to the current day-of-month, rather than proper calendar month boundaries.

Root cause

In server/utils/utils.js:

javascript
function getDifferenceFunction(type) {
  if (type === "lastYear") return differenceInMonths; // <-- should be differenceInCalendarMonths
}

differenceInMonths returns truncated full months between two dates. This means visits that occurred after the current day-of-month get shifted into the adjacent (more recent) bucket.

Example

If today is April 10, and a visit occurred on February 25:

  • differenceInMonths(April 10, Feb 25) = 1 (only 1 full month has passed: Feb 25 → Mar 25)
  • index = 11 - 1 = 10 → this bucket represents "March" in a calendar view
  • But the visit actually happened in February

With differenceInCalendarMonths(April 10, Feb 25) = 2, the visit would correctly land at index = 11 - 2 = 9February.

Impact

The magnitude of the error depends on the day of the month when the API is called:

API called on % of previous month's visits misplaced
1st ~96%
10th ~64%
15th ~46%
25th ~11%
28th ~0%

All misplaced visits shift forward by exactly 1 month. This makes lastYear.views unreliable for any calendar-month reporting.

Proposed fix

One-line change in server/utils/utils.js:

diff
- if (type === "lastYear") return differenceInMonths;
+ if (type === "lastYear") return differenceInCalendarMonths;

And add the import:

diff
- const { differenceInMonths, ... } = require("date-fns");
+ const { differenceInMonths, differenceInCalendarMonths, ... } = require("date-fns");

Note: The same issue likely affects lastMonth (which uses differenceInDays — though differenceInDays is less ambiguous since it rounds down full 24h periods, it could still shift daily buckets by 1 depending on timezone/time-of-day).

Environment

  • Self-hosted Kutt instance
  • API v2
  • Verified by comparing raw lastYear.views array against known campaign launch dates across 7 links