Browse Source

Analytics

imwald
Nuša Pukšič 3 months ago
parent
commit
1fa2201ed3
  1. 15
      src/Controller/Administration/VisitorAnalyticsController.php
  2. 106
      src/Repository/VisitRepository.php
  3. 95
      templates/admin/analytics.html.twig

15
src/Controller/Administration/VisitorAnalyticsController.php

@ -33,6 +33,14 @@ class VisitorAnalyticsController extends AbstractController
// Session-based visit breakdown // Session-based visit breakdown
$sessionStats = $visitRepository->getVisitsBySession($last7d); $sessionStats = $visitRepository->getVisitsBySession($last7d);
// New metrics for improved dashboard
$totalVisits = $visitRepository->getTotalVisits();
$avgVisitsPerSession = $visitRepository->getAverageVisitsPerSession();
$bounceRate = $visitRepository->getBounceRate();
$visitsPerDay = $visitRepository->getVisitsPerDay(30);
$mostPopularRoutes = $visitRepository->getMostPopularRoutes(5);
$recentVisits = $visitRepository->getRecentVisits(10);
return $this->render('admin/analytics.html.twig', [ return $this->render('admin/analytics.html.twig', [
'visitStats' => $visitStats, 'visitStats' => $visitStats,
'last24hCount' => $last24hCount, 'last24hCount' => $last24hCount,
@ -41,6 +49,13 @@ class VisitorAnalyticsController extends AbstractController
'uniqueVisitors7d' => $uniqueVisitors7d, 'uniqueVisitors7d' => $uniqueVisitors7d,
'totalUniqueVisitors' => $totalUniqueVisitors, 'totalUniqueVisitors' => $totalUniqueVisitors,
'sessionStats' => $sessionStats, 'sessionStats' => $sessionStats,
// New variables for dashboard
'totalVisits' => $totalVisits,
'avgVisitsPerSession' => $avgVisitsPerSession,
'bounceRate' => $bounceRate,
'visitsPerDay' => $visitsPerDay,
'mostPopularRoutes' => $mostPopularRoutes,
'recentVisits' => $recentVisits,
]); ]);
} }
} }

106
src/Repository/VisitRepository.php

@ -98,4 +98,110 @@ class VisitRepository extends ServiceEntityRepository
return (int) $qb->getQuery()->getSingleScalarResult(); return (int) $qb->getQuery()->getSingleScalarResult();
} }
/**
* Returns total number of visits.
*/
public function getTotalVisits(): int
{
return (int) $this->createQueryBuilder('v')
->select('COUNT(v.id)')
->getQuery()
->getSingleScalarResult();
}
/**
* Returns number of unique visitors (distinct sessionId).
*/
public function getUniqueVisitors(): int
{
return (int) $this->createQueryBuilder('v')
->select('COUNT(DISTINCT v.sessionId)')
->where('v.sessionId IS NOT NULL')
->getQuery()
->getSingleScalarResult();
}
/**
* Returns visits grouped by day (YYYY-MM-DD => count) using native SQL for PostgreSQL compatibility.
*/
public function getVisitsPerDay(int $days = 30): array
{
$from = (new \DateTimeImmutable())->modify("-{$days} days");
$conn = $this->getEntityManager()->getConnection();
$sql = 'SELECT DATE(visited_at) as day, COUNT(id) as count
FROM visit
WHERE visited_at >= :from
GROUP BY day
ORDER BY day ASC';
$result = $conn->executeQuery(
$sql,
['from' => $from->format('Y-m-d H:i:s')]
);
return $result->fetchAllAssociative();
}
/**
* Returns the most popular routes (top N).
*/
public function getMostPopularRoutes(int $limit = 5): array
{
return $this->createQueryBuilder('v')
->select('v.route, COUNT(v.id) as count')
->groupBy('v.route')
->orderBy('count', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
/**
* Returns the most recent visits (with route, sessionId, visitedAt).
*/
public function getRecentVisits(int $limit = 10): array
{
return $this->createQueryBuilder('v')
->select('v.route, v.sessionId, v.visitedAt')
->orderBy('v.visitedAt', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
/**
* Returns the average number of visits per session.
*/
public function getAverageVisitsPerSession(): float
{
$totalVisits = $this->getTotalVisits();
$uniqueSessions = $this->getUniqueVisitors();
if ($uniqueSessions === 0) {
return 0.0;
}
return round($totalVisits / $uniqueSessions, 2);
}
/**
* Returns the bounce rate (percentage of sessions with only one visit).
*/
public function getBounceRate(): float
{
$qb = $this->createQueryBuilder('v')
->select('v.sessionId, COUNT(v.id) as visitCount')
->where('v.sessionId IS NOT NULL')
->groupBy('v.sessionId');
$sessions = $qb->getQuery()->getResult();
$singleVisitSessions = 0;
$totalSessions = 0;
foreach ($sessions as $session) {
$totalSessions++;
if ($session['visitCount'] == 1) {
$singleVisitSessions++;
}
}
if ($totalSessions === 0) {
return 0.0;
}
return round(($singleVisitSessions / $totalSessions) * 100, 2);
}
} }

95
templates/admin/analytics.html.twig

@ -6,11 +6,13 @@
<div class="analytics-container"> <div class="analytics-container">
<h1>Page Visit Analytics</h1> <h1>Page Visit Analytics</h1>
<div class="analytics-summary-row">
<div class="analytics-card"> <div class="analytics-card">
<h2>Total Visits</h2> <h2>Total Visits</h2>
<ul class="analytics-stats"> <ul class="analytics-stats">
<li><strong>Last 24 hours:</strong> {{ last24hCount }}</li> <li><strong>Last 24 hours:</strong> {{ last24hCount }}</li>
<li><strong>Last 7 days:</strong> {{ last7dCount }}</li> <li><strong>Last 7 days:</strong> {{ last7dCount }}</li>
<li><strong>All time:</strong> {{ totalVisits }}</li>
</ul> </ul>
</div> </div>
@ -25,9 +27,92 @@
</div> </div>
<div class="analytics-card"> <div class="analytics-card">
<h2>Visit Count by Route (Last 7 Days)</h2> <h2>Engagement</h2>
<ul class="analytics-stats">
<li><strong>Avg. Visits/Session:</strong> {{ avgVisitsPerSession }}</li>
<li><strong>Bounce Rate:</strong> {{ bounceRate }}%</li>
</ul>
</div>
</div>
<div class="analytics-card">
<h2>Visits Per Day (Last 30 Days)</h2>
{% if visitsPerDay|length > 0 %}
<table class="analytics-table">
<thead>
<tr>
<th>Date</th>
<th class="text-right">Visits</th>
</tr>
</thead>
<tbody>
{% for stat in visitsPerDay %}
<tr>
<td>{{ stat.day|date('Y-m-d') }}</td>
<td class="text-right">{{ stat.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No visit data for the last 30 days.</p>
{% endif %}
</div>
<div class="analytics-card">
<h2>Most Popular Routes (All Time)</h2>
{% if mostPopularRoutes|length > 0 %}
<table class="analytics-table">
<thead>
<tr>
<th>Route</th>
<th class="text-right">Visits</th>
</tr>
</thead>
<tbody>
{% for stat in mostPopularRoutes %}
<tr>
<td>{{ stat.route }}</td>
<td class="text-right">{{ stat.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No route data available.</p>
{% endif %}
</div>
<div class="analytics-card">
<h2>Recent Visits</h2>
{% if recentVisits|length > 0 %}
<table class="analytics-table">
<thead>
<tr>
<th>Route</th>
<th>Session ID</th>
<th>Visited At</th>
</tr>
</thead>
<tbody>
{% for visit in recentVisits %}
<tr>
<td>{{ visit.route }}</td>
<td><code>{{ visit.sessionId|slice(0, 12) }}...</code></td>
<td>{{ visit.visitedAt|date('Y-m-d H:i') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No recent visits recorded.</p>
{% endif %}
</div>
{% if visitStats|length > 0 %} <div class="analytics-card">
<h2>Visit Count by Route (Last 7 Days)</h2>
{% set filteredVisitStats = visitStats|filter(stat => stat.count >= 5) %}
{% if filteredVisitStats|length > 0 %}
<table class="analytics-table"> <table class="analytics-table">
<thead> <thead>
<tr> <tr>
@ -36,7 +121,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for stat in visitStats %} {% for stat in filteredVisitStats %}
<tr> <tr>
<td>{{ stat.route }}</td> <td>{{ stat.route }}</td>
<td class="text-right">{{ stat.count }}</td> <td class="text-right">{{ stat.count }}</td>
@ -45,7 +130,7 @@
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
<p>No visit data recorded in the last 7 days.</p> <p>No routes with 5 or more visits recorded in the last 7 days.</p>
{% endif %} {% endif %}
</div> </div>
@ -63,12 +148,14 @@
</thead> </thead>
<tbody> <tbody>
{% for stat in sessionStats %} {% for stat in sessionStats %}
{% if stat.visitCount > 1 %}
<tr> <tr>
<td><code>{{ stat.sessionId|slice(0, 12) }}...</code></td> <td><code>{{ stat.sessionId|slice(0, 12) }}...</code></td>
<td class="text-right">{{ stat.visitCount }}</td> <td class="text-right">{{ stat.visitCount }}</td>
<td class="text-right">{{ stat.firstVisit|date('M d, H:i') }}</td> <td class="text-right">{{ stat.firstVisit|date('M d, H:i') }}</td>
<td class="text-right">{{ stat.lastVisit|date('M d, H:i') }}</td> <td class="text-right">{{ stat.lastVisit|date('M d, H:i') }}</td>
</tr> </tr>
{% endif %}
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

Loading…
Cancel
Save