This commit is contained in:
Stefanescu Mihai
2026-03-19 14:53:54 +02:00
parent 4390d43acb
commit d75cabfeec
11 changed files with 717 additions and 76 deletions
+143
View File
@@ -0,0 +1,143 @@
<?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);
}
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace App\Console\Commands;
use App\Models\AnalyticsSession;
use App\Models\Event;
use App\Models\PageView;
use App\Models\Website;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
class PurgeAnalytics extends Command
{
protected $signature = 'analytics:purge
{--website= : Purge only a specific website ID.}
{--force : Skip confirmation prompt.}';
protected $description = 'Delete raw analytics data older than each website\'s retention period';
private const DEFAULT_RETENTION_DAYS = 365;
public function handle(): int
{
$websites = $this->option('website')
? Website::where('id', $this->option('website'))->get()
: Website::all();
if ($websites->isEmpty()) {
$this->warn('No websites found.');
return self::SUCCESS;
}
$summary = $websites->map(fn (Website $w) => [
'id' => $w->id,
'name' => $w->name,
'retention' => $this->getRetentionDays($w) . ' days',
'cutoff' => Carbon::today()->subDays($this->getRetentionDays($w))->toDateString(),
])->toArray();
$this->table(['ID', 'Website', 'Retention', 'Delete before'], $summary);
if (! $this->option('force') && ! $this->confirm('Proceed with purging old raw data?')) {
$this->info('Cancelled.');
return self::SUCCESS;
}
foreach ($websites as $website) {
$this->purgeForWebsite($website);
}
$this->info('Purge complete.');
return self::SUCCESS;
}
private function purgeForWebsite(Website $website): void
{
$retentionDays = $this->getRetentionDays($website);
$cutoff = Carbon::today()->subDays($retentionDays);
$this->info("Purging {$website->name} (data before {$cutoff->toDateString()})...");
$pvDeleted = PageView::where('website_id', $website->id)
->where('viewed_at', '<', $cutoff)
->delete();
$evDeleted = Event::where('website_id', $website->id)
->where('occurred_at', '<', $cutoff)
->delete();
$sessDeleted = AnalyticsSession::where('website_id', $website->id)
->where('started_at', '<', $cutoff)
->delete();
$this->line(" Deleted: {$pvDeleted} page views, {$evDeleted} events, {$sessDeleted} sessions");
}
private function getRetentionDays(Website $website): int
{
return $website->settings['data_retention_days'] ?? self::DEFAULT_RETENTION_DAYS;
}
}