ip(); $ua = $req->userAgent() ?? ''; $isBot = (bool) $req->attributes->get('is_bot', false); $geo = cache()->remember("geo_{$ip}", 3600, fn() => self::geoIp($ip)); UserActivityLog::create([ 'user_id' => $userId ?? auth()->id(), 'session_id' => session()->getId(), 'action' => $action, 'subject_type' => $subjectType, 'subject_id' => $subjectId, 'ip' => $ip, 'country' => $geo['country'] ?? null, 'city' => $geo['city'] ?? null, 'device' => self::device($ua), 'browser' => self::browser($ua), 'user_agent' => mb_substr($ua, 0, 500), 'is_bot' => $isBot, 'meta' => $meta, 'created_at' => now(), ]); } catch (\Throwable) { // never break the app } } private static function geoIp(string $ip): array { try { if (in_array($ip, ['127.0.0.1', '::1']) || str_starts_with($ip, '192.168.') || str_starts_with($ip, '10.')) { return ['country' => 'Local', 'city' => null]; } $r = \Illuminate\Support\Facades\Http::timeout(2)->get("http://ip-api.com/json/{$ip}?fields=country,city,status"); $d = $r->json(); return ($d['status'] ?? '') === 'success' ? ['country' => $d['country'], 'city' => $d['city']] : []; } catch (\Throwable) { return []; } } private static function device(string $ua): string { $ua = strtolower($ua); if (str_contains($ua, 'mobile') || str_contains($ua, 'android') || str_contains($ua, 'iphone')) return 'mobile'; if (str_contains($ua, 'tablet') || str_contains($ua, 'ipad')) return 'tablet'; return 'desktop'; } private static function browser(string $ua): string { if (str_contains($ua, 'Chrome')) return 'Chrome'; if (str_contains($ua, 'Firefox')) return 'Firefox'; if (str_contains($ua, 'Safari')) return 'Safari'; if (str_contains($ua, 'Edge')) return 'Edge'; if (str_contains($ua, 'Opera')) return 'Opera'; return 'Other'; } }