> $headers * @param array $queryParams */ 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 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) { return 'safari'; } return null; } private function extractHost(?string $url): ?string { if (! $url) { return null; } $parsed = parse_url($url); return $parsed['host'] ?? null; } }