Reporter: foreverUA (forever@poduryan.com)
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:
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 = 9 → February.
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:
- if (type === "lastYear") return differenceInMonths;
+ if (type === "lastYear") return differenceInCalendarMonths;
And add the import:
- 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
Reporter: foreverUA (forever@poduryan.com)
Summary
The
/api/v2/links/{id}/statsendpoint returnslastYear.viewsarray with monthly view counts that are not aligned to calendar months. Visits are bucketed usingdifferenceInMonthsfromdate-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:differenceInMonthsreturns 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 viewWith
differenceInCalendarMonths(April 10, Feb 25)= 2, the visit would correctly land atindex = 11 - 2 = 9→ February.Impact
The magnitude of the error depends on the day of the month when the API is called:
All misplaced visits shift forward by exactly 1 month. This makes
lastYear.viewsunreliable for any calendar-month reporting.Proposed fix
One-line change in
server/utils/utils.js:And add the import:
Note: The same issue likely affects
lastMonth(which usesdifferenceInDays— thoughdifferenceInDaysis less ambiguous since it rounds down full 24h periods, it could still shift daily buckets by 1 depending on timezone/time-of-day).Environment
lastYear.viewsarray against known campaign launch dates across 7 links