Add charts and bounce rate to admin Analytics, filter self-referrers
- Bar chart: visitors per day - Line chart: pageviews per day (with fill) - Bounce rate KPI with color coding (green/yellow/red) - Filter out self-referrers (ticket.e-cosplay.fr, esyweb.local) - Uses Chart.js via cdn.jsdelivr.net (already in CSP) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -724,6 +724,10 @@ class AdminController extends AbstractController
|
|||||||
->where('e.createdAt >= :since')
|
->where('e.createdAt >= :since')
|
||||||
->andWhere('e.referrer IS NOT NULL')
|
->andWhere('e.referrer IS NOT NULL')
|
||||||
->andWhere("e.referrer != ''")
|
->andWhere("e.referrer != ''")
|
||||||
|
->andWhere("e.referrer NOT LIKE :self1")
|
||||||
|
->andWhere("e.referrer NOT LIKE :self2")
|
||||||
|
->setParameter('self1', '%ticket.e-cosplay.fr%')
|
||||||
|
->setParameter('self2', '%esyweb.local%')
|
||||||
->setParameter('since', $since)
|
->setParameter('since', $since)
|
||||||
->groupBy('e.referrer')
|
->groupBy('e.referrer')
|
||||||
->orderBy('hits', 'DESC')
|
->orderBy('hits', 'DESC')
|
||||||
@@ -763,6 +767,40 @@ class AdminController extends AbstractController
|
|||||||
->getQuery()
|
->getQuery()
|
||||||
->getArrayResult();
|
->getArrayResult();
|
||||||
|
|
||||||
|
// Daily chart data
|
||||||
|
$visitorsPerDay = $em->createQueryBuilder()
|
||||||
|
->select("SUBSTRING(v.createdAt, 1, 10) AS day, COUNT(v.id) AS cnt")
|
||||||
|
->from(AnalyticsUniqId::class, 'v')
|
||||||
|
->where('v.createdAt >= :since')
|
||||||
|
->setParameter('since', $since)
|
||||||
|
->groupBy('day')
|
||||||
|
->orderBy('day', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getArrayResult();
|
||||||
|
|
||||||
|
$pageviewsPerDay = $em->createQueryBuilder()
|
||||||
|
->select("SUBSTRING(e.createdAt, 1, 10) AS day, COUNT(e.id) AS cnt")
|
||||||
|
->from(AnalyticsEvent::class, 'e')
|
||||||
|
->where('e.createdAt >= :since')
|
||||||
|
->setParameter('since', $since)
|
||||||
|
->groupBy('day')
|
||||||
|
->orderBy('day', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getArrayResult();
|
||||||
|
|
||||||
|
// Merge into aligned arrays
|
||||||
|
$allDays = [];
|
||||||
|
foreach ($visitorsPerDay as $r) { $allDays[$r['day']] = true; }
|
||||||
|
foreach ($pageviewsPerDay as $r) { $allDays[$r['day']] = true; }
|
||||||
|
ksort($allDays);
|
||||||
|
|
||||||
|
$visitorsMap = array_column($visitorsPerDay, 'cnt', 'day');
|
||||||
|
$pageviewsMap = array_column($pageviewsPerDay, 'cnt', 'day');
|
||||||
|
|
||||||
|
$chartLabels = array_keys($allDays);
|
||||||
|
$chartVisitors = array_map(fn ($d) => (int) ($visitorsMap[$d] ?? 0), $chartLabels);
|
||||||
|
$chartPageviews = array_map(fn ($d) => (int) ($pageviewsMap[$d] ?? 0), $chartLabels);
|
||||||
|
|
||||||
return $this->render('admin/analytics.html.twig', [
|
return $this->render('admin/analytics.html.twig', [
|
||||||
'period' => $period,
|
'period' => $period,
|
||||||
'visitors' => $visitors,
|
'visitors' => $visitors,
|
||||||
@@ -773,6 +811,9 @@ class AdminController extends AbstractController
|
|||||||
'devices' => $devices,
|
'devices' => $devices,
|
||||||
'browsers' => $browsers,
|
'browsers' => $browsers,
|
||||||
'os_list' => $osList,
|
'os_list' => $osList,
|
||||||
|
'chart_labels' => $chartLabels,
|
||||||
|
'chart_visitors' => $chartVisitors,
|
||||||
|
'chart_pageviews' => $chartPageviews,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Charts #}
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||||
|
<div class="admin-card">
|
||||||
|
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Visiteurs par jour</h2>
|
||||||
|
<canvas id="chart-visitors" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="admin-card">
|
||||||
|
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Pages vues par jour</h2>
|
||||||
|
<canvas id="chart-pageviews" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||||
{# Top pages #}
|
{# Top pages #}
|
||||||
<div class="admin-card">
|
<div class="admin-card">
|
||||||
@@ -121,4 +133,30 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const labels = {{ chart_labels|json_encode|raw }};
|
||||||
|
const opts = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
x: { grid: { display: false }, ticks: { font: { weight: 'bold', size: 10 } } },
|
||||||
|
y: { beginAtZero: true, ticks: { font: { weight: 'bold', size: 10 } } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
new Chart(document.getElementById('chart-visitors'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: { labels, datasets: [{ data: {{ chart_visitors|json_encode|raw }}, backgroundColor: '#fabf04', borderColor: '#111827', borderWidth: 2 }] },
|
||||||
|
options: opts
|
||||||
|
});
|
||||||
|
new Chart(document.getElementById('chart-pageviews'), {
|
||||||
|
type: 'line',
|
||||||
|
data: { labels, datasets: [{ data: {{ chart_pageviews|json_encode|raw }}, borderColor: '#111827', backgroundColor: 'rgba(250,191,4,0.2)', borderWidth: 3, fill: true, tension: 0.3, pointBackgroundColor: '#fabf04', pointBorderColor: '#111827', pointBorderWidth: 2 }] },
|
||||||
|
options: opts
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user