fix: send pageviews to the analytics API correctly

- Use the correct endpoint (analytics.mattitja.se instead of the
  non-existent .cloud domain)
- Authenticate with the site ingest secret as a Bearer token so the API
  trusts the forwarded visitor IP and user agent
- Forward the visitor Accept-Language header (required by bot detection)
- Only track successful GET requests rendering HTML
- Short timeouts and a file-based circuit breaker that pauses sending
  after connection failures or 5xx responses, so no queue, worker or
  cache is needed and a down API never slows the host site
- Log rejected requests instead of failing silently
This commit is contained in:
mathiaspalmqvist committed 2026-11-12 09:08:27 +01:00
1 parent e3911ca12c
commit 19f632d652
11 files changed
+298 -26

No files matched your search

+34 -3
View File
@@ -36,7 +36,7 @@ class AnalyticsMiddleware
{
$response = $next($request);
if ($this->shouldCaptureAnalytics($request)) {
if ($this->shouldCaptureAnalytics($request, $response)) {
$this->pendingData = $this->buildAnalyticsData($request);
}
@@ -56,10 +56,12 @@ class AnalyticsMiddleware
/**
* Determine if analytics should be captured for this request.
*/
protected function shouldCaptureAnalytics(Request $request): bool
protected function shouldCaptureAnalytics(Request $request, Response $response): bool
{
return $this->isAnalyticsEnabled()
&& $this->hasSiteKey()
&& $this->hasIngestSecret()
&& $this->isPageview($request, $response)
&& ! $this->isIgnoredPath($request)
&& ! $this->isIgnoredIp($request)
&& ! $this->shouldIgnoreBot($request)
@@ -82,6 +84,34 @@ class AnalyticsMiddleware
return ! empty(config('mattitja-analytics.site_key'));
}
/**
* Check if the ingest secret is configured. Without it the API ignores the
* forwarded visitor IP/UA and rejects the server-to-server request.
*/
private function hasIngestSecret(): bool
{
return ! empty(config('mattitja-analytics.ingest_secret'));
}
/**
* Only successful GET requests rendering HTML count as pageviews
* (skips form posts, redirects, JSON/Livewire calls and assets).
*/
protected function isPageview(Request $request, Response $response): bool
{
if (! $request->isMethod('GET') || $request->expectsJson()) {
return false;
}
if (! $response->isSuccessful()) {
return false;
}
$contentType = (string) $response->headers->get('Content-Type', 'text/html');
return str_contains($contentType, 'text/html');
}
/**
* Check if bot traffic should be ignored.
*/
@@ -156,6 +186,7 @@ class AnalyticsMiddleware
ip: $request->ip(),
userAgent: $request->userAgent() ?? '',
referrer: $request->header('referer'),
acceptLanguage: $request->header('Accept-Language'),
);
}
@@ -166,7 +197,7 @@ class AnalyticsMiddleware
{
try {
app(AnalyticsClient::class)->send($data);
} catch (\Exception $e) {
} catch (\Throwable $e) {
Log::error('Failed to send analytics data', [
'error' => $e->getMessage(),
]);