nimuli/app/Domains/Analytics/Jobs/RecordClickJob.php

110 lines
3.8 KiB
PHP

<?php
namespace App\Domains\Analytics\Jobs;
use App\Domains\Analytics\Models\Click;
use App\Domains\Link\Models\Link;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
class RecordClickJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public function __construct(
public readonly int $linkId,
public readonly string $ip,
public readonly ?string $userAgent,
public readonly array $headers,
public readonly array $queryParams = [],
) {}
public function handle(): void
{
$link = Link::find($this->linkId);
if (! $link) {
return;
}
$ipHash = $this->hashIp($this->ip);
$device = $this->detectDevice($this->userAgent ?? '');
$click = Click::create([
'link_id' => $this->linkId,
'workspace_id' => $link->workspace_id,
'clicked_at' => now(),
'ip_hash' => $ipHash,
'device' => $device,
'os' => $this->detectOs($this->userAgent ?? ''),
'browser' => $this->detectBrowser($this->userAgent ?? ''),
'referrer_host' => $this->extractHost($this->headers['referer'][0] ?? null),
'utm_source' => $this->queryParams['utm_source'] ?? null,
'utm_medium' => $this->queryParams['utm_medium'] ?? null,
'utm_campaign' => $this->queryParams['utm_campaign'] ?? null,
'language' => substr($this->headers['accept-language'][0] ?? 'de', 0, 10),
]);
// Increment Redis counters
$date = now()->format('Y-m-d');
Redis::incr("clicks:{$this->linkId}:{$date}");
Redis::incr("clicks:{$link->workspace_id}:monthly:" . now()->format('Ym'));
// Broadcast event for live dashboard
event(new \App\Domains\Analytics\Events\ClickRecorded($click));
}
private function hashIp(string $ip): string
{
$salt = Cache::remember('ip_salt:' . today()->format('Y-m-d'), 86400, fn () => bin2hex(random_bytes(16)));
return hash('sha256', $ip . $salt);
}
private function detectDevice(string $ua): string
{
$ua = strtolower($ua);
if (str_contains($ua, 'bot') || str_contains($ua, 'crawler') || str_contains($ua, 'spider')) {
return 'bot';
}
if (preg_match('/mobile|iphone|android.*mobile|windows phone/i', $ua)) {
return 'mobile';
}
if (preg_match('/ipad|android(?!.*mobile)|tablet/i', $ua)) {
return 'tablet';
}
if (strlen($ua) === 0) {
return 'unknown';
}
return 'desktop';
}
private function detectOs(string $ua): ?string
{
if (stripos($ua, 'iPhone') !== false || stripos($ua, 'iPad') !== false) return 'ios';
if (stripos($ua, 'Android') !== false) return 'android';
if (stripos($ua, 'Windows') !== false) return 'windows';
if (stripos($ua, 'Mac OS') !== false) return 'macos';
if (stripos($ua, 'Linux') !== false) return 'linux';
return null;
}
private function detectBrowser(string $ua): ?string
{
if (stripos($ua, 'Edge') !== false || stripos($ua, 'Edg/') !== false) return 'edge';
if (stripos($ua, 'Chrome') !== false) return 'chrome';
if (stripos($ua, 'Firefox') !== false) return 'firefox';
if (stripos($ua, 'Safari') !== false && stripos($ua, 'Chrome') === false) return 'safari';
return null;
}
private function extractHost(?string $url): ?string
{
if (! $url) return null;
$parsed = parse_url($url);
return $parsed['host'] ?? null;
}
}