feat(analytics): add site analytics with grouped navigation sections
- Add Analytics container: models, tasks, jobs, controllers, routes, services - Add analytics config with section groups (structure, institute, content, services) - Add database tables: analytics_sessions, analytics_hits, analytics_daily_stats - Add frontend: Analytics page, section detail, chart, overview, navigation components - Add consent banner and JS tracking service (sendBeacon + Inertia listener) - Add permission sync for view_analytics, view_any_analytic - Fix trailing slash URL matching in GetNavigationSectionsTask - Fix event propagation on expand arrows in navigation sections - Fix RecordVisitJob to not depend on dropped analytics_geo_cache table - Replace plain text views/visitors with badge-style icons - Group navigation sections by category with separate blocks - Add config-driven non-CMS sections (faculties, divisions, DPO, journals) - Remove Events, TV, Sveden from analytics sections
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Analytics\Tasks;
|
||||
|
||||
use App\Containers\Analytics\Models\AnalyticsHit;
|
||||
use App\Containers\Analytics\Models\AnalyticsSession;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GetAnalyticsOverviewTask
|
||||
{
|
||||
public function run(int $days = 30): array
|
||||
{
|
||||
$from = Carbon::now()->subDays($days)->startOfDay();
|
||||
$to = now()->endOfDay();
|
||||
|
||||
if ($days === 0) {
|
||||
$prevFrom = Carbon::yesterday()->startOfDay();
|
||||
$prevTo = Carbon::yesterday()->endOfDay();
|
||||
} else {
|
||||
$prevFrom = Carbon::now()->subDays($days * 2)->startOfDay();
|
||||
$prevTo = Carbon::now()->subDays($days)->startOfDay();
|
||||
}
|
||||
|
||||
$current = $this->getPeriodStats($from, $to);
|
||||
$previous = $this->getPeriodStats($prevFrom, $prevTo);
|
||||
|
||||
return [
|
||||
'unique_visitors' => $this->compare($current['visitors'], $previous['visitors']),
|
||||
'page_views' => $this->compare($current['views'], $previous['views']),
|
||||
'avg_time' => $this->compare($current['avg_time'], $previous['avg_time'], true),
|
||||
'bounce_rate' => $this->compare($current['bounce_rate'], $previous['bounce_rate'], true, true),
|
||||
];
|
||||
}
|
||||
|
||||
private function getPeriodStats(Carbon $from, Carbon $to): array
|
||||
{
|
||||
$hits = AnalyticsHit::whereBetween('created_at', [$from, $to])->count();
|
||||
|
||||
$sessions = AnalyticsSession::whereBetween('started_at', [$from, $to])
|
||||
->get(['id', 'started_at', 'last_activity_at']);
|
||||
|
||||
$uniqueVisitors = AnalyticsSession::whereBetween('started_at', [$from, $to])
|
||||
->distinct('visitor_id')
|
||||
->count('visitor_id');
|
||||
|
||||
$totalSessions = $sessions->count();
|
||||
|
||||
$avgTime = 0;
|
||||
if ($totalSessions > 0) {
|
||||
$totalSeconds = $sessions->sum(function ($s) {
|
||||
return $s->started_at->diffInSeconds($s->last_activity_at);
|
||||
});
|
||||
$avgTime = (int) round($totalSeconds / $totalSessions);
|
||||
}
|
||||
|
||||
$bounceRate = 0;
|
||||
if ($totalSessions > 0) {
|
||||
$sessionIds = $sessions->pluck('id');
|
||||
$singleHitSessions = AnalyticsHit::whereIn('session_id', $sessionIds)
|
||||
->selectRaw('session_id, COUNT(*) as hits')
|
||||
->groupBy('session_id')
|
||||
->havingRaw('COUNT(*) = 1')
|
||||
->count();
|
||||
$bounceRate = (int) round(($singleHitSessions / $totalSessions) * 100);
|
||||
}
|
||||
|
||||
return [
|
||||
'views' => $hits,
|
||||
'visitors' => $uniqueVisitors,
|
||||
'avg_time' => $avgTime,
|
||||
'bounce_rate' => $bounceRate,
|
||||
];
|
||||
}
|
||||
|
||||
private function compare(int $current, int $previous, bool $isRatio = false, bool $invertDirection = false): array
|
||||
{
|
||||
$change = 0;
|
||||
if ($previous > 0) {
|
||||
$change = $isRatio
|
||||
? $current - $previous
|
||||
: (int) round((($current - $previous) / $previous) * 100);
|
||||
} elseif ($current > 0) {
|
||||
$change = $isRatio ? 0 : 100;
|
||||
}
|
||||
|
||||
$direction = $invertDirection
|
||||
? ($change < 0 ? 'up' : ($change > 0 ? 'down' : 'neutral'))
|
||||
: ($change > 0 ? 'up' : ($change < 0 ? 'down' : 'neutral'));
|
||||
|
||||
return [
|
||||
'value' => $current,
|
||||
'change' => $change,
|
||||
'direction' => $direction,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user