includes/dashboard/counters.php
<?php
declare(strict_types=1);
/*
|--------------------------------------------------------------------------
| Dashboard counters
|--------------------------------------------------------------------------
*/
$today = one($db, "
SELECT
COALESCE(SUM(unique_visitors), 0) AS unique_visitors,
COALESCE(SUM(visits), 0) AS visits,
COALESCE(SUM(pageviews), 0) AS pageviews,
COALESCE(SUM(bots), 0) AS bots
FROM hits_daily
WHERE $rangeSql
", $rangeParams);
if ($includeArchives) {
$archivedToday = archiveHitsTotals(
$rangeStart,
$rangeEnd,
$currentSite
);
foreach ($archivedToday as $key => $value) {
$today[$key] = (int)($today[$key] ?? 0) + $value;
}
}
/*
|--------------------------------------------------------------------------
| Trends
|--------------------------------------------------------------------------
|
| Always compares "the same amount of elapsed time" on both sides, so a
| still-running period is never measured against a fully completed one:
| - today: uses hits_hourly, only fully completed hours count on both days
| - week/month/year: the previous period is capped to the same number of
| elapsed days as the current (possibly still running) one
|
| Also scopes the comparison to the currently selected site, like $today
| above already does (previously this always compared all sites combined,
| even when a single site was selected in the dashboard).
|
*/
$siteFilterSql = $currentSite !== '' ? ' AND site = ?' : '';
$siteFilterParams = $currentSite !== '' ? [$currentSite] : [];
$visitsChangeLabel = '';
$visitorsChange = null;
$visitsChange = null;
$pageviewsChange = null;
$previous = null;
$currentForTrend = $today;
// Minimum fully-completed hours before "today" is compared against
// yesterday at all — below that (00:00-02:59), a single hit can swing the
// percentage wildly, so the placeholder stays up a bit longer.
$minHoursForTrend = 3;
// Minimum sample size (on the smaller side of the comparison) before a
// trend percentage is shown. A small site can still cross the 3-hour mark
// with just 1 or 2 visits, which still isn't enough for a meaningful %.
// Configurable via Settings > Trends > sensitivity (settings.php).
$minSampleForTrend = brivaciaTrendMinSample();
$trendPlaceholderText = t(brivaciaTrendPlaceholderKey());
if ($view === 'today') {
$todayDate = date('Y-m-d');
$hourNow = (int)date('G');
$visitsChangeLabel = t('metric.vs.yesterday');
if ($day === $todayDate && $hourNow >= $minHoursForTrend) {
// "Today" is still running: only compare fully completed hours,
// on both days, so 11:30 today is never measured against a full
// 24h yesterday.
$yesterday = date('Y-m-d', strtotime($day . ' -1 day'));
$currentForTrend = one($db, "
SELECT
COALESCE(SUM(unique_visitors), 0) AS unique_visitors,
COALESCE(SUM(visits), 0) AS visits,
COALESCE(SUM(pageviews), 0) AS pageviews
FROM hits_hourly
WHERE day = ? AND hour < ?$siteFilterSql
", array_merge([$day, $hourNow], $siteFilterParams));
$previous = one($db, "
SELECT
COALESCE(SUM(unique_visitors), 0) AS unique_visitors,
COALESCE(SUM(visits), 0) AS visits,
COALESCE(SUM(pageviews), 0) AS pageviews
FROM hits_hourly
WHERE day = ? AND hour < ?$siteFilterSql
", array_merge([$yesterday, $hourNow], $siteFilterParams));
} elseif ($day !== $todayDate) {
// A specific past day was picked (day picker): it's fully
// completed, so a plain whole-day comparison is already fair.
$yesterday = date('Y-m-d', strtotime($day . ' -1 day'));
$previous = one($db, "
SELECT
COALESCE(SUM(unique_visitors), 0) AS unique_visitors,
COALESCE(SUM(visits), 0) AS visits,
COALESCE(SUM(pageviews), 0) AS pageviews
FROM hits_daily
WHERE day = ?$siteFilterSql
", array_merge([$yesterday], $siteFilterParams));
if (dashboardRangeNeedsArchives($yesterday, $yesterday)) {
$archivedPrevious = archiveHitsTotals(
$yesterday,
$yesterday,
$currentSite
);
foreach ($archivedPrevious as $key => $value) {
$previous[$key] = (int)($previous[$key] ?? 0) + $value;
}
}
}
// else: before the 3rd fully-completed hour, there isn't enough
// elapsed time yet to make a fair comparison, so the trend stays
// hidden (placeholder).
} elseif (in_array($view, ['week', 'month', 'year'], true)) {
$todayDate = date('Y-m-d');
$elapsedEnd = $todayDate < $rangeEnd ? $todayDate : $rangeEnd;
$elapsedDays = (int)((strtotime($elapsedEnd) - strtotime($rangeStart)) / 86400);
switch ($view) {
case 'week':
$previousStart = date('Y-m-d', strtotime($rangeStart . ' -7 days'));
$visitsChangeLabel = t('metric.vs.previous.week');
break;
case 'month':
$previousStart = date('Y-m', strtotime($rangeStart . ' -1 month')) . '-01';
$visitsChangeLabel = t('metric.vs.previous.month');
break;
case 'year':
$previousStart = date('Y-m-d', strtotime($rangeStart . ' -1 year'));
$visitsChangeLabel = t('metric.vs.previous.year');
break;
}
$previousEnd = date('Y-m-d', strtotime($previousStart . " +$elapsedDays days"));
$previous = one($db, "
SELECT
COALESCE(SUM(unique_visitors), 0) AS unique_visitors,
COALESCE(SUM(visits), 0) AS visits,
COALESCE(SUM(pageviews), 0) AS pageviews
FROM hits_daily
WHERE day BETWEEN ? AND ?$siteFilterSql
", array_merge([$previousStart, $previousEnd], $siteFilterParams));
if (dashboardRangeNeedsArchives($previousStart, $previousEnd)) {
$archivedPrevious = archiveHitsTotals(
$previousStart,
$previousEnd,
$currentSite
);
foreach ($archivedPrevious as $key => $value) {
$previous[$key] = (int)($previous[$key] ?? 0) + $value;
}
}
}
if ($previous !== null) {
$previousVisitorsCount = (int)($previous['unique_visitors'] ?? 0);
$currentVisitorsCount = (int)($currentForTrend['unique_visitors'] ?? 0);
if (min($previousVisitorsCount, $currentVisitorsCount) >= $minSampleForTrend) {
$visitorsChange = round((($currentVisitorsCount - $previousVisitorsCount) / $previousVisitorsCount) * 100, 1);
}
$previousVisitsCount = (int)($previous['visits'] ?? 0);
$currentVisitsCount = (int)($currentForTrend['visits'] ?? 0);
if (min($previousVisitsCount, $currentVisitsCount) >= $minSampleForTrend) {
$visitsChange = round((($currentVisitsCount - $previousVisitsCount) / $previousVisitsCount) * 100, 1);
}
$previousPageviewsCount = (int)($previous['pageviews'] ?? 0);
$currentPageviewsCount = (int)($currentForTrend['pageviews'] ?? 0);
if (min($previousPageviewsCount, $currentPageviewsCount) >= $minSampleForTrend) {
$pageviewsChange = round((($currentPageviewsCount - $previousPageviewsCount) / $previousPageviewsCount) * 100, 1);
}
}
/*
|--------------------------------------------------------------------------
| Averages
|--------------------------------------------------------------------------
*/
$avgSiteWhere = $currentSite !== ''
? 'WHERE site = ?'
: '';
$avgSiteParams = $currentSite !== ''
? [$currentSite]
: [];
$avgDay = one($db, '
SELECT
ROUND(AVG(unique_visitors), 1) AS avg_unique,
ROUND(AVG(visits), 1) AS avg_visits,
ROUND(AVG(pageviews), 1) AS avg_views
FROM (
SELECT
day,
SUM(unique_visitors) AS unique_visitors,
SUM(visits) AS visits,
SUM(pageviews) AS pageviews
FROM hits_daily
' . $avgSiteWhere . '
GROUP BY day
HAVING unique_visitors > 0 OR visits > 0 OR pageviews > 0
)
', $avgSiteParams);
/*
|--------------------------------------------------------------------------
| Countries
|--------------------------------------------------------------------------
*/
$countries = [];
if ($showCountries) {
$countriesRaw = fetchAll($db, "
SELECT country, SUM(views) AS views
FROM countries_daily
WHERE $rangeSql
GROUP BY country
ORDER BY views DESC
", $rangeParams);
foreach ($countriesRaw as $row) {
$country = (string)($row['country'] ?? '');
if ($country === '') {
continue;
}
$countries[$country] = [
'country' => $country,
'views' => (int)($row['views'] ?? 0),
];
}
if ($includeArchives) {
foreach (
archiveGroupedViews(
'countries',
'country',
$rangeStart,
$rangeEnd,
$currentSite
) as $row
) {
$country = (string)($row['country'] ?? '');
if ($country === '') {
continue;
}
$countries[$country] ??= [
'country' => $country,
'views' => 0,
];
$countries[$country]['views'] += (int)($row['views'] ?? 0);
}
}
$countries = array_values($countries);
usort($countries, fn($a, $b) => $b['views'] <=> $a['views']);
}
/*
|--------------------------------------------------------------------------
| Archived totals
|--------------------------------------------------------------------------
*/
$archivesTotal = archiveHitsTotals();