isBot($request)) { return response()->json(['ok' => false, 'reason' => 'bot'], 200); } $payload = $request->validate([ 'tid' => 'required|string', 'vid' => 'required|string|max:64', 'sid' => 'required|string|max:64', 'type' => 'required|in:pageview,event', 'url' => 'nullable|string|max:2048', 'path' => 'nullable|string|max:2048', 'title' => 'nullable|string|max:512', 'referrer' => 'nullable|string|max:2048', 'event_name' => 'nullable|string|max:255', 'event_category' => 'nullable|string|max:255', 'event_action' => 'nullable|string|max:255', 'event_label' => 'nullable|string|max:255', 'event_value' => 'nullable|numeric', 'event_properties' => 'nullable|array', 'sr' => 'nullable|string|max:20', 'lang' => 'nullable|string|max:10', 'ua' => 'nullable|string|max:512', 'utm_source' => 'nullable|string|max:255', 'utm_medium' => 'nullable|string|max:255', 'utm_campaign' => 'nullable|string|max:255', 'utm_term' => 'nullable|string|max:255', 'utm_content' => 'nullable|string|max:255', 'ref_domain' => 'nullable|string|max:255', 'device' => 'nullable|string|max:20', 'browser' => 'nullable|string|max:50', 'browser_version' => 'nullable|string|max:20', 'os' => 'nullable|string|max:50', 'os_version' => 'nullable|string|max:20', 'time_on_page' => 'nullable|integer|min:0', 'is_new_session' => 'nullable|boolean', ]); if (! $this->isDomainValid($payload)) { return response()->json(['ok' => false, 'reason' => 'domain_mismatch'], 400); } ProcessTrackingPayload::dispatch($payload, $request->ip(), Carbon::now()); return response()->json(['ok' => true]); } /** * 1x1 transparent GIF tracking pixel for no-JS fallback. * Usage: */ public function pixel(Request $request): Response { if ($this->isBot($request)) { return $this->gifResponse(); } $tid = $request->get('tid'); if ($tid) { $payload = [ 'tid' => $tid, 'vid' => $request->get('vid', md5($request->ip() . $request->userAgent())), 'sid' => $request->get('sid', md5($request->ip() . $request->userAgent() . date('Y-m-d-H'))), 'type' => 'pageview', 'url' => $request->get('url', $request->header('Referer')), 'path' => $request->get('path', parse_url($request->header('Referer', '/'), PHP_URL_PATH)), 'referrer' => $request->header('Referer'), ]; ProcessTrackingPayload::dispatch($payload, $request->ip(), Carbon::now()); } return $this->gifResponse(); } private function isBot(Request $request): bool { return (new CrawlerDetect)->isCrawler($request->userAgent()); } private function isDomainValid(array $payload): bool { $url = $payload['url'] ?? null; if (! $url) { return true; } $host = parse_url($url, PHP_URL_HOST); if (! $host) { return true; } $website = Website::where('tracking_id', $payload['tid'])->first(); if (! $website) { return true; // let the job handle the 404 } $domain = $website->domain; // Allow exact match or subdomain match (e.g. www.example.com matches example.com) return $host === $domain || str_ends_with($host, '.' . $domain); } private function gifResponse(): Response { // 1x1 transparent GIF $gif = base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'); return response($gif, 200, [ 'Content-Type' => 'image/gif', 'Content-Length' => strlen($gif), 'Cache-Control' => 'no-store, no-cache, must-revalidate, max-age=0', ]); } }