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

+99 -2
View File
@@ -6,8 +6,23 @@ use Mattitja\Analytics\Services\AnalyticsClient;
beforeEach(function () {
Http::fake();
@unlink(storage_path('framework/mattitja-analytics.paused'));
});
afterEach(function () {
@unlink(storage_path('framework/mattitja-analytics.paused'));
});
function testPageview(): PageviewData
{
return new PageviewData(
siteKey: 'testsiteky',
url: 'https://example.com/test',
ip: '192.168.1.1',
userAgent: 'Mozilla/5.0 Chrome',
);
}
it('sends analytics data to the correct endpoint', function () {
$client = new AnalyticsClient;
$data = new PageviewData(
@@ -21,7 +36,7 @@ it('sends analytics data to the correct endpoint', function () {
$client->send($data);
Http::assertSent(function ($request) {
return $request->url() === 'https://analytics.mattitja.cloud/api/send' &&
return $request->url() === 'https://analytics.mattitja.se/api/send' &&
$request->method() === 'POST' &&
$request->data() === [
'k' => 'testsiteky',
@@ -35,7 +50,7 @@ it('sends analytics data to the correct endpoint', function () {
it('handles API failures gracefully', function () {
Http::fake([
'analytics.mattitja.cloud/*' => Http::response('Server Error', 500),
'analytics.mattitja.se/*' => Http::response('Server Error', 500),
]);
$client = new AnalyticsClient;
@@ -140,3 +155,85 @@ it('excludes null referrer from payload', function () {
return ! isset($request->data()['r']);
});
});
it('sends the ingest secret as bearer token and forwards Accept-Language', function () {
(new AnalyticsClient)->send(new PageviewData(
siteKey: 'testsiteky',
url: 'https://example.com/test',
ip: '192.168.1.1',
userAgent: 'Mozilla/5.0 Chrome',
acceptLanguage: 'sv-SE,sv;q=0.9',
));
Http::assertSent(function ($request) {
return $request->hasHeader('Authorization', 'Bearer test-secret')
&& $request->hasHeader('Accept-Language', 'sv-SE,sv;q=0.9')
&& ! array_key_exists('acceptLanguage', $request->data());
});
});
it('logs a warning when the API rejects the pageview', function () {
Http::swap(new \Illuminate\Http\Client\Factory);
Http::fake(['*' => Http::response('', 401)]);
\Illuminate\Support\Facades\Log::spy();
(new AnalyticsClient)->send(new PageviewData(
siteKey: 'testsiteky',
url: 'https://example.com/test',
ip: '192.168.1.1',
userAgent: 'Mozilla/5.0 Chrome',
));
\Illuminate\Support\Facades\Log::shouldHaveReceived('warning')
->withArgs(fn ($message, $context) => str_contains($message, 'rejected') && $context['status'] === 401)
->once();
});
it('pauses sending after a connection failure', function () {
Http::swap(new \Illuminate\Http\Client\Factory);
$attempts = 0;
Http::fake(function () use (&$attempts) {
$attempts++;
throw new \Illuminate\Http\Client\ConnectionException('timeout');
});
$client = new AnalyticsClient;
$client->send(testPageview());
$client->send(testPageview());
expect($client->isPaused())->toBeTrue()
->and($attempts)->toBe(1);
});
it('pauses sending after a server error', function () {
Http::swap(new \Illuminate\Http\Client\Factory);
Http::fake(['*' => Http::response('', 503)]);
$client = new AnalyticsClient;
$client->send(testPageview());
$client->send(testPageview());
expect($client->isPaused())->toBeTrue();
Http::assertSentCount(1);
});
it('does not pause on client errors', function () {
Http::swap(new \Illuminate\Http\Client\Factory);
Http::fake(['*' => Http::response('', 401)]);
$client = new AnalyticsClient;
$client->send(testPageview());
expect($client->isPaused())->toBeFalse();
});
it('resumes sending after the pause expires', function () {
touch(storage_path('framework/mattitja-analytics.paused'), time() - 120);
$client = new AnalyticsClient;
$client->send(testPageview());
Http::assertSentCount(1);
expect(file_exists(storage_path('framework/mattitja-analytics.paused')))->toBeFalse();
});