Hi,
I noticed an issue with the internal dashboard traffic statistics in OctoberCMS 4.2.18.
The table dashboard_traffic_stats_pageviews had grown to about 1.7 GB, because traffic recording was not limited to a specific retention period at first. I later configured the dashboard traffic statistics retention to 2 months in the backend.
After that, I ran:
\Dashboard\Models\TrafficStatisticsPageview::purgeOldRecords();
The method is executed, but old records were still present in the table.
After checking the implementation, the method looks like this:
public static function purgeOldRecords()
{
$months = TrafficLogger::instance()->getRetentionMonths();
if (!$months) {
return;
}
$query = (new static)->where('ev_datetime', '<', now()->subMonths($months)->toDateTimeString());
if (Site::hasFeature('dashboard_traffic_statistics')) {
$siteId = Site::getEditSite()?->id;
if ($siteId) {
$query->where('site_id', $siteId);
}
}
$query->delete();
}
The problem seems to be that my records have site_id = NULL.
Example query:
SELECT
site_id,
COUNT(*) AS records,
MIN(ev_datetime) AS oldest,
MAX(ev_datetime) AS newest
FROM dashboard_traffic_stats_pageviews
GROUP BY site_id
ORDER BY site_id;
Result before manual cleanup:
site_id | records | oldest | newest
NULL | 738208 | 2026-01-30 07:55:05 | 2026-04-30 09:57:48
After manually deleting old records with:
DELETE FROM dashboard_traffic_stats_pageviews
WHERE site_id IS NULL
AND ev_datetime < DATE_SUB(NOW(), INTERVAL 2 MONTH);
the result became:
site_id | records | oldest | newest
NULL | 391931 | 2026-02-28 09:59:45 | 2026-04-30 09:59:48
So the retention itself works correctly when applied directly to ev_datetime.
I also checked how site_id is written in modules/dashboard/classes/TrafficLogger.php:
$pageview->site_id = Site::getActiveSite()?->id;
However, even new records still get site_id = NULL.
Example for the last hour:
SELECT
site_id,
COUNT(*) AS records,
MIN(ev_datetime) AS oldest,
MAX(ev_datetime) AS newest
FROM dashboard_traffic_stats_pageviews
WHERE ev_datetime >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY site_id
ORDER BY site_id;
Result:
site_id | records | oldest | newest
NULL | 163 | 2026-04-30 09:05:47 | 2026-04-30 10:05:02
So it looks like Site::getActiveSite()?->id returns null in the traffic logging context, while purgeOldRecords() may use Site::getEditSite()?->id and then only deletes records for the selected edit site.
This means records with site_id = NULL are not removed by purgeOldRecords() when a backend edit site is active.
- Could this be a bug in the dashboard traffic statistics retention logic?
- Should purgeOldRecords() also delete records with site_id IS NULL?
- Should the purge maybe not filter by site_id at all when applying the global retention period?
- Is it expected that TrafficLogger writes site_id = NULL when there is only one active site or no multisite setup?
- Is there an official console command for purging old dashboard traffic statistics, or is TrafficStatisticsPageview::purgeOldRecords() the intended way?