From f757f43926b75735abdaa0a822cb83d22aa350c6 Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 16 May 2026 01:03:49 +0200 Subject: [PATCH] feat(analytics): hourly/daily rollups, 90-day raw click prune, schedule --- .../Analytics/Jobs/PruneRawClicksJob.php | 22 +++++++++++ app/Domains/Analytics/Jobs/RollupDailyJob.php | 37 +++++++++++++++++++ .../Analytics/Jobs/RollupHourlyJob.php | 37 +++++++++++++++++++ routes/console.php | 9 +++++ 4 files changed, 105 insertions(+) create mode 100644 app/Domains/Analytics/Jobs/PruneRawClicksJob.php create mode 100644 app/Domains/Analytics/Jobs/RollupDailyJob.php create mode 100644 app/Domains/Analytics/Jobs/RollupHourlyJob.php diff --git a/app/Domains/Analytics/Jobs/PruneRawClicksJob.php b/app/Domains/Analytics/Jobs/PruneRawClicksJob.php new file mode 100644 index 0000000..3bc77dc --- /dev/null +++ b/app/Domains/Analytics/Jobs/PruneRawClicksJob.php @@ -0,0 +1,22 @@ +subDays(90)) + ->chunkById(1000, function ($clicks) { + $clicks->toQuery()->delete(); + }); + } +} diff --git a/app/Domains/Analytics/Jobs/RollupDailyJob.php b/app/Domains/Analytics/Jobs/RollupDailyJob.php new file mode 100644 index 0000000..e086287 --- /dev/null +++ b/app/Domains/Analytics/Jobs/RollupDailyJob.php @@ -0,0 +1,37 @@ +subDay()->startOfDay(); + $nextDay = $targetDay->copy()->addDay(); + + DB::statement(" + INSERT INTO clicks_daily (link_id, day, country, device, count, unique_visitors) + SELECT + link_id, + DATE(clicked_at) AS day, + COALESCE(country, '') AS country, + COALESCE(device, 'unknown') AS device, + COUNT(*) AS count, + COUNT(DISTINCT ip_hash) AS unique_visitors + FROM clicks + WHERE clicked_at >= ? AND clicked_at < ? + GROUP BY link_id, day, country, device + ON DUPLICATE KEY UPDATE + count = count + VALUES(count), + unique_visitors = GREATEST(unique_visitors, VALUES(unique_visitors)) + ", [$targetDay, $nextDay]); + } +} diff --git a/app/Domains/Analytics/Jobs/RollupHourlyJob.php b/app/Domains/Analytics/Jobs/RollupHourlyJob.php new file mode 100644 index 0000000..716b1ca --- /dev/null +++ b/app/Domains/Analytics/Jobs/RollupHourlyJob.php @@ -0,0 +1,37 @@ +startOfHour()->subHour(); + $nextHour = $targetHour->copy()->addHour(); + + DB::statement(" + INSERT INTO clicks_hourly (link_id, hour, country, device, count, unique_visitors) + SELECT + link_id, + DATE_FORMAT(clicked_at, '%Y-%m-%d %H:00:00') AS hour, + COALESCE(country, '') AS country, + COALESCE(device, 'unknown') AS device, + COUNT(*) AS count, + COUNT(DISTINCT ip_hash) AS unique_visitors + FROM clicks + WHERE clicked_at >= ? AND clicked_at < ? + GROUP BY link_id, hour, country, device + ON DUPLICATE KEY UPDATE + count = count + VALUES(count), + unique_visitors = GREATEST(unique_visitors, VALUES(unique_visitors)) + ", [$targetHour, $nextHour]); + } +} diff --git a/routes/console.php b/routes/console.php index 3c9adf1..c32cf4b 100644 --- a/routes/console.php +++ b/routes/console.php @@ -2,7 +2,16 @@ use Illuminate\Foundation\Inspiring; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Schedule; +use App\Domains\Analytics\Jobs\RollupHourlyJob; +use App\Domains\Analytics\Jobs\RollupDailyJob; +use App\Domains\Analytics\Jobs\PruneRawClicksJob; Artisan::command('inspire', function () { $this->comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); + +Schedule::job(new RollupHourlyJob)->everyFifteenMinutes(); +Schedule::job(new RollupDailyJob)->dailyAt('02:00'); +Schedule::job(new PruneRawClicksJob)->dailyAt('03:00'); +Schedule::command('backup:run')->dailyAt('04:00');