144 lines
4.9 KiB
PHP
144 lines
4.9 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace App\Console\Commands;
|
||
|
|
|
||
|
|
use App\Models\AnalyticsSession;
|
||
|
|
use App\Models\DailyAggregate;
|
||
|
|
use App\Models\PageDailyAggregate;
|
||
|
|
use App\Models\PageView;
|
||
|
|
use App\Models\Website;
|
||
|
|
use Illuminate\Console\Command;
|
||
|
|
use Illuminate\Support\Carbon;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
class AggregateAnalytics extends Command
|
||
|
|
{
|
||
|
|
protected $signature = 'analytics:aggregate
|
||
|
|
{--date= : The date to aggregate (YYYY-MM-DD). Defaults to yesterday.}
|
||
|
|
{--website= : Aggregate only a specific website ID.}';
|
||
|
|
|
||
|
|
protected $description = 'Roll up raw analytics data into daily aggregate tables';
|
||
|
|
|
||
|
|
public function handle(): int
|
||
|
|
{
|
||
|
|
$date = $this->option('date')
|
||
|
|
? Carbon::parse($this->option('date'))
|
||
|
|
: Carbon::yesterday();
|
||
|
|
|
||
|
|
if ($date->isToday()) {
|
||
|
|
$this->error('Cannot aggregate today\'s data — it is still being collected.');
|
||
|
|
|
||
|
|
return self::FAILURE;
|
||
|
|
}
|
||
|
|
|
||
|
|
$websites = $this->option('website')
|
||
|
|
? Website::where('id', $this->option('website'))->get()
|
||
|
|
: Website::all();
|
||
|
|
|
||
|
|
if ($websites->isEmpty()) {
|
||
|
|
$this->warn('No websites found.');
|
||
|
|
|
||
|
|
return self::SUCCESS;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->info("Aggregating data for {$date->toDateString()} across {$websites->count()} website(s)...");
|
||
|
|
|
||
|
|
$bar = $this->output->createProgressBar($websites->count());
|
||
|
|
$bar->start();
|
||
|
|
|
||
|
|
foreach ($websites as $website) {
|
||
|
|
$this->aggregateForWebsite($website, $date);
|
||
|
|
$bar->advance();
|
||
|
|
}
|
||
|
|
|
||
|
|
$bar->finish();
|
||
|
|
$this->newLine();
|
||
|
|
$this->info('Aggregation complete.');
|
||
|
|
|
||
|
|
return self::SUCCESS;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function aggregateForWebsite(Website $website, Carbon $date): void
|
||
|
|
{
|
||
|
|
$dayStart = $date->copy()->startOfDay();
|
||
|
|
$dayEnd = $date->copy()->endOfDay();
|
||
|
|
|
||
|
|
DB::transaction(function () use ($website, $date, $dayStart, $dayEnd) {
|
||
|
|
$this->aggregateDaily($website, $date, $dayStart, $dayEnd);
|
||
|
|
$this->aggregatePageDaily($website, $date, $dayStart, $dayEnd);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private function aggregateDaily(Website $website, Carbon $date, Carbon $dayStart, Carbon $dayEnd): void
|
||
|
|
{
|
||
|
|
$visitors = PageView::where('website_id', $website->id)
|
||
|
|
->whereBetween('viewed_at', [$dayStart, $dayEnd])
|
||
|
|
->distinct('visitor_id')
|
||
|
|
->count('visitor_id');
|
||
|
|
|
||
|
|
$sessions = AnalyticsSession::where('website_id', $website->id)
|
||
|
|
->whereBetween('started_at', [$dayStart, $dayEnd])
|
||
|
|
->count();
|
||
|
|
|
||
|
|
$pageViews = PageView::where('website_id', $website->id)
|
||
|
|
->whereBetween('viewed_at', [$dayStart, $dayEnd])
|
||
|
|
->count();
|
||
|
|
|
||
|
|
$bounceCount = AnalyticsSession::where('website_id', $website->id)
|
||
|
|
->whereBetween('started_at', [$dayStart, $dayEnd])
|
||
|
|
->where('is_bounce', true)
|
||
|
|
->count();
|
||
|
|
|
||
|
|
$totalDuration = (int) AnalyticsSession::where('website_id', $website->id)
|
||
|
|
->whereBetween('started_at', [$dayStart, $dayEnd])
|
||
|
|
->sum('duration');
|
||
|
|
|
||
|
|
DailyAggregate::updateOrCreate(
|
||
|
|
['website_id' => $website->id, 'date' => $date->toDateString()],
|
||
|
|
compact('visitors', 'sessions', 'page_views', 'bounce_count', 'total_duration'),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function aggregatePageDaily(Website $website, Carbon $date, Carbon $dayStart, Carbon $dayEnd): void
|
||
|
|
{
|
||
|
|
$pages = PageView::where('website_id', $website->id)
|
||
|
|
->whereBetween('viewed_at', [$dayStart, $dayEnd])
|
||
|
|
->select(
|
||
|
|
'path',
|
||
|
|
DB::raw('COUNT(*) as views'),
|
||
|
|
DB::raw('COUNT(DISTINCT visitor_id) as unique_views'),
|
||
|
|
DB::raw('COALESCE(SUM(time_on_page), 0) as total_time_on_page'),
|
||
|
|
)
|
||
|
|
->groupBy('path')
|
||
|
|
->get();
|
||
|
|
|
||
|
|
$bouncePaths = AnalyticsSession::where('website_id', $website->id)
|
||
|
|
->whereBetween('started_at', [$dayStart, $dayEnd])
|
||
|
|
->where('is_bounce', true)
|
||
|
|
->whereNotNull('entry_page')
|
||
|
|
->select('entry_page as path', DB::raw('COUNT(*) as bounces'))
|
||
|
|
->groupBy('entry_page')
|
||
|
|
->pluck('bounces', 'path');
|
||
|
|
|
||
|
|
PageDailyAggregate::where('website_id', $website->id)
|
||
|
|
->where('date', $date->toDateString())
|
||
|
|
->delete();
|
||
|
|
|
||
|
|
$rows = $pages->map(fn ($page) => [
|
||
|
|
'website_id' => $website->id,
|
||
|
|
'date' => $date->toDateString(),
|
||
|
|
'path' => $page->path,
|
||
|
|
'views' => $page->views,
|
||
|
|
'unique_views' => $page->unique_views,
|
||
|
|
'total_time_on_page' => $page->total_time_on_page,
|
||
|
|
'bounces' => $bouncePaths->get($page->path, 0),
|
||
|
|
'created_at' => now(),
|
||
|
|
'updated_at' => now(),
|
||
|
|
])->toArray();
|
||
|
|
|
||
|
|
foreach (array_chunk($rows, 500) as $chunk) {
|
||
|
|
PageDailyAggregate::insert($chunk);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|