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:
11 files changed
+298
-26
No files matched your search
@@ -48,7 +48,7 @@ it('captures analytics data for web requests', function () {
|
||||
Http::assertSent(function ($request) {
|
||||
$data = $request->data();
|
||||
|
||||
return $request->url() === 'https://analytics.mattitja.cloud/api/send' &&
|
||||
return $request->url() === 'https://analytics.mattitja.se/api/send' &&
|
||||
$data['k'] === 'testsiteky' &&
|
||||
$data['u'] === 'https://example.com/test' &&
|
||||
$data['r'] === 'https://google.com' &&
|
||||
@@ -140,3 +140,50 @@ it('handles empty user agent gracefully', function () {
|
||||
return array_key_exists('ua', $data) && $data['ua'] === '';
|
||||
});
|
||||
});
|
||||
|
||||
it('does not capture analytics without an ingest secret', function () {
|
||||
config(['mattitja-analytics.ingest_secret' => null]);
|
||||
|
||||
handleRequest(createMiddleware(), createTestRequest());
|
||||
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
it('forwards the visitor Accept-Language header', function () {
|
||||
$request = createTestRequest();
|
||||
$request->headers->set('Accept-Language', 'sv-SE,sv;q=0.9');
|
||||
|
||||
handleRequest(createMiddleware(), $request);
|
||||
|
||||
Http::assertSent(fn ($request) => $request->hasHeader('Accept-Language', 'sv-SE,sv;q=0.9'));
|
||||
});
|
||||
|
||||
it('ignores non-GET requests', function () {
|
||||
handleRequest(createMiddleware(), createTestRequest(method: 'POST'));
|
||||
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
it('ignores JSON responses and redirects', function () {
|
||||
$middleware = createMiddleware();
|
||||
$request = createTestRequest();
|
||||
|
||||
foreach ([
|
||||
new \Illuminate\Http\JsonResponse(['ok' => true]),
|
||||
new \Illuminate\Http\RedirectResponse('https://example.com/elsewhere'),
|
||||
] as $response) {
|
||||
$result = $middleware->handle($request, fn () => $response);
|
||||
$middleware->terminate($request, $result);
|
||||
}
|
||||
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
it('ignores requests expecting JSON', function () {
|
||||
$request = createTestRequest();
|
||||
$request->headers->set('Accept', 'application/json');
|
||||
|
||||
handleRequest(createMiddleware(), $request);
|
||||
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
@@ -45,7 +45,8 @@ it('adds env variable if .env exists and key is missing', function () {
|
||||
|
||||
$envContent = File::get($this->envPath);
|
||||
|
||||
expect($envContent)->toContain('MATTITJA_ANALYTICS_SITE_KEY=');
|
||||
expect($envContent)->toContain('MATTITJA_ANALYTICS_SITE_KEY=')
|
||||
->toContain('MATTITJA_ANALYTICS_INGEST_SECRET=');
|
||||
|
||||
File::put($this->envPath, $originalContent);
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ class TestCase extends Orchestra
|
||||
public function getEnvironmentSetUp($app)
|
||||
{
|
||||
config()->set('mattitja-analytics.site_key', 'testsiteky');
|
||||
config()->set('mattitja-analytics.ingest_secret', 'test-secret');
|
||||
config()->set('mattitja-analytics.enabled', true);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
Reference in new issue
Block a user