refactor(analytics): remove application traffic overview card

This commit is contained in:
Andras Bacsai
2026-10-05 11:28:03 +02:00
parent 554bd354bf
commit 2aead9c914
5 changed files with 3 additions and 298 deletions
@@ -1,86 +0,0 @@
<?php
namespace App\Livewire\Project\Application;
use App\Models\Application;
use App\Services\SentinelTrafficClient;
use App\Services\TrafficAnalyticsAggregator;
use Livewire\Attributes\Lazy;
use Livewire\Component;
/**
* Compact last-24h traffic KPI card for the application General page. Lazy-loaded so
* the General page isn't blocked by Sentinel's docker-exec round-trip.
*/
#[Lazy]
class TrafficOverview extends Component
{
public Application $application;
public bool $enabled = false;
public bool $eligible = false;
public ?string $serverUuid = null;
public ?array $overview = null;
public function mount(): void
{
// Runs in the deferred lazy-load request, so the Sentinel fetch never blocks
// the initial General-page render.
$server = $this->application->destination?->server;
$this->serverUuid = $server?->uuid;
$this->enabled = (bool) $server?->isTrafficAnalyticsEnabled();
$this->eligible = $server ? (! $server->isSwarm() && ! $server->isBuildServer()) : false;
if ($this->enabled && $server) {
try {
$client = app(SentinelTrafficClient::class, ['server' => $server]);
[$from, $to] = SentinelTrafficClient::rangeWindow('24h');
// Compose services and previews record under their own keys. Sentinel's resource
// scope merges them exactly; an older Sentinel gets every key summed here.
$overview = $client->tryResourceOverview($this->application->uuid, $from, $to);
if ($overview === null) {
$keys = $client->resourceKeys($this->application->uuid);
if (count($keys) > 1) {
$client->prefetchAppOverviews($keys, $from, $to);
}
$overviews = array_map(fn (string $key) => $client->overview($key, $from, $to), $keys);
$overview = TrafficAnalyticsAggregator::sumOverviews($overviews)['overview'];
}
$this->overview = $overview->toArray();
} catch (\Throwable $e) {
$this->overview = null;
}
}
}
public function hasData(): bool
{
return $this->overview !== null && (int) ($this->overview['requests'] ?? 0) > 0;
}
public function errorRate(): float
{
if (! $this->overview || (int) ($this->overview['requests'] ?? 0) === 0) {
return 0.0;
}
$errors = (int) ($this->overview['s4xx'] ?? 0) + (int) ($this->overview['s5xx'] ?? 0);
return round(($errors / $this->overview['requests']) * 100, 2);
}
public function placeholder(): string
{
return <<<'HTML'
<div class="h-24 w-full animate-pulse rounded-xl border border-neutral-200 bg-neutral-50 dark:border-white/[0.08] dark:bg-white/[0.02]"></div>
HTML;
}
public function render()
{
return view('livewire.project.application.traffic-overview');
}
}
@@ -1,66 +0,0 @@
@php
$analyticsRoute = route('project.application.analytics', [
'project_uuid' => $application->environment->project->uuid,
'environment_uuid' => $application->environment->uuid,
'application_uuid' => $application->uuid,
]);
@endphp
<div>
@if (! $enabled)
@if ($eligible && $serverUuid)
<section class="rounded-xl border border-neutral-200 bg-white p-4 shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="min-w-0">
<h3 class="text-[13px] font-semibold text-black dark:text-fg">Traffic analytics</h3>
<p class="mt-0.5 text-[12px] text-neutral-500 dark:text-fg-dim">
Enable Sentinel traffic analytics on this server to see requests, visitors, and
geography for this application. Restarts the proxy + Sentinel.
</p>
</div>
<a class="button shrink-0" href="{{ route('server.analytics', ['server_uuid' => $serverUuid]) }}" {{ wireNavigate() }}>
Server settings
<x-external-link />
</a>
</div>
</section>
@endif
@else
<section class="overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.025]">
<div class="flex items-center justify-between gap-3 border-b border-neutral-200 px-4 py-2.5 dark:border-white/[0.08]">
<div>
<h3 class="text-[12px]! leading-4! font-semibold! text-black dark:text-fg">Traffic (last 24h)</h3>
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">Request activity for this application</p>
</div>
<a class="text-[12px] font-medium text-coollabs hover:underline dark:text-fg" href="{{ $analyticsRoute }}" {{ wireNavigate() }}>
View full analytics &rarr;
</a>
</div>
@if (! $this->hasData())
<p class="px-4 py-4 text-[12px] text-neutral-500 dark:text-fg-dim">
No traffic recorded in the last 24h yet.
</p>
@else
<div class="grid grid-cols-2 gap-px bg-neutral-200 sm:grid-cols-4 dark:bg-white/[0.07]">
<div class="flex flex-col gap-1 bg-white px-4 py-3 dark:bg-base">
<span class="text-[11px] font-medium tracking-wide text-neutral-500 uppercase dark:text-fg-dim">Requests</span>
<span class="text-lg font-semibold text-black dark:text-fg">{{ number_format($overview['requests'] ?? 0) }}</span>
</div>
<div class="flex flex-col gap-1 bg-white px-4 py-3 dark:bg-base">
<span class="text-[11px] font-medium tracking-wide text-neutral-500 uppercase dark:text-fg-dim">Unique visitors</span>
<span class="text-lg font-semibold text-black dark:text-fg">{{ number_format($overview['uniqueVisitors'] ?? 0) }}</span>
</div>
<div class="flex flex-col gap-1 bg-white px-4 py-3 dark:bg-base">
<span class="text-[11px] font-medium tracking-wide text-neutral-500 uppercase dark:text-fg-dim">Error rate</span>
<span class="text-lg font-semibold text-black dark:text-fg">{{ $this->errorRate() }}%</span>
</div>
<div class="flex flex-col gap-1 bg-white px-4 py-3 dark:bg-base">
<span class="text-[11px] font-medium tracking-wide text-neutral-500 uppercase dark:text-fg-dim">p95 latency</span>
<span class="text-lg font-semibold text-black dark:text-fg">{{ number_format($overview['latencyP95'] ?? 0, 1) }} ms</span>
</div>
</div>
@endif
</section>
@endif
</div>
@@ -1,121 +0,0 @@
<?php
use App\Livewire\Project\Application\TrafficOverview;
use App\Models\Application;
use App\Models\Environment;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use App\Services\SentinelTrafficClient;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
uses(RefreshDatabase::class);
class FakeAppOverviewTrafficClient extends SentinelTrafficClient
{
public array $responses = [];
protected function raw(string $url): string
{
foreach ($this->responses as $needle => $response) {
if (str_contains($url, $needle)) {
return $response;
}
}
return '{}';
}
}
beforeEach(function () {
// Servers are memoized via once()/cache; clear both so DB-id reuse across tests doesn't bleed a stale enabled server.
Cache::flush();
Server::flushIdentityMap();
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
$this->actingAs($this->user);
session(['currentTeam' => $this->team]);
$this->privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
});
function makeAppOnServer(bool $analyticsEnabled): Application
{
$server = Server::factory()->create([
'team_id' => test()->team->id,
'private_key_id' => test()->privateKey->id,
]);
$server->settings->is_traffic_analytics_enabled = $analyticsEnabled;
$server->settings->save();
$project = Project::factory()->create(['team_id' => test()->team->id]);
$environment = Environment::factory()->create(['project_id' => $project->id]);
$destination = StandaloneDocker::where('server_id', $server->id)->first()
?? StandaloneDocker::factory()->create(['server_id' => $server->id, 'network' => 'coolify-test']);
return Application::factory()->create([
'name' => 'Widget App',
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => StandaloneDocker::class,
]);
}
it('shows last-24h KPIs and a link to full analytics when enabled with data', function () {
$application = makeAppOnServer(true);
app()->bind(SentinelTrafficClient::class, function ($app, $params) {
$client = new FakeAppOverviewTrafficClient($params['server']);
$client->responses = [
'/traffic/overview' => json_encode([
'requests' => 4200,
'bytes_in' => 5000,
'bytes_out' => 25000,
'status' => ['s2xx' => 4000, 's3xx' => 100, 's4xx' => 80, 's5xx' => 20],
'latency' => ['p50' => 12.5, 'p95' => 45.2, 'p99' => 90.1],
'unique_visitors' => 1234,
]),
];
return $client;
});
$component = loadLazy(Livewire::test(TrafficOverview::class, ['application' => $application]));
$component->assertOk()
->assertSet('enabled', true)
->assertSee('Traffic (last 24h)')
->assertSee('4,200')
->assertSee('1,234')
->assertSee('View full analytics');
});
it('shows the muted no-data note when enabled but no traffic recorded', function () {
$application = makeAppOnServer(true);
app()->bind(SentinelTrafficClient::class, function ($app, $params) {
$client = new FakeAppOverviewTrafficClient($params['server']);
$client->responses = ['/traffic/overview' => json_encode(['requests' => 0])];
return $client;
});
loadLazy(Livewire::test(TrafficOverview::class, ['application' => $application]))
->assertOk()
->assertSee('No traffic recorded in the last 24h yet');
});
it('shows the enable nudge when analytics is disabled on an eligible server', function () {
$application = makeAppOnServer(false);
loadLazy(Livewire::test(TrafficOverview::class, ['application' => $application]))
->assertOk()
->assertSet('enabled', false)
->assertSee('Traffic analytics')
->assertSee('Server settings')
->assertDontSee('Traffic (last 24h)');
});
@@ -2,7 +2,6 @@
use App\Livewire\Analytics as GlobalAnalytics;
use App\Livewire\Project\Application\Analytics as ApplicationAnalytics;
use App\Livewire\Project\Application\TrafficOverview;
use App\Livewire\Project\Service\Analytics as ServiceAnalytics;
use App\Models\Application;
use App\Models\Environment;
@@ -369,31 +368,11 @@ it('falls back to the per-key merge with approximate badges when Sentinel lacks
// The absence is cached: a refresh within the ttl goes straight to the per-key path.
$component->call('loadData')->assertSet('uniquesApproximate', true);
loadLazy(Livewire::test(TrafficOverview::class, ['application' => $this->application]))->assertOk();
loadLazy(Livewire::test(ApplicationAnalytics::class, ['application' => $this->application]))->assertOk();
expect(requestedResourceScopeUrls('/resource/'))->toHaveCount($probes);
});
it('shows the resource overview in the application traffic card', function () {
bindResourceScopeFake(resourceScopeResponses($this->application->uuid, resourceScopeOverview(4200, 33)));
$component = loadLazy(Livewire::test(TrafficOverview::class, ['application' => $this->application]))
->assertOk()
->assertSee('4,200');
expect($component->instance()->overview['uniqueVisitors'])->toBe(33)
->and(requestedResourceScopeUrls('/resource/'))->toHaveCount(1)
->and(requestedResourceScopeUrls('/traffic/dashboard'))->toBeEmpty();
});
it('sums the keys in the application traffic card when Sentinel lacks the resource routes', function () {
bindResourceScopeFake(perKeyFallbackResponses($this->application->uuid, $this->apiKey, $this->webKey));
loadLazy(Livewire::test(TrafficOverview::class, ['application' => $this->application]))
->assertOk()
->assertSee('150');
});
it('uses the resource scope when the global analytics filter selects a resource', function () {
bindResourceScopeFake([
...resourceScopeResponses($this->application->uuid, resourceScopeOverview(150, 12)),
@@ -6,7 +6,6 @@ use App\Data\Traffic\TrafficPathData;
use App\Data\Traffic\TrafficSeriesBucketData;
use App\Livewire\Analytics as GlobalAnalytics;
use App\Livewire\Project\Application\Analytics as ApplicationAnalytics;
use App\Livewire\Project\Application\TrafficOverview;
use App\Models\Application;
use App\Models\Environment;
use App\Models\PrivateKey;
@@ -286,7 +285,7 @@ it('shows a compose application with the data of all its compose service keys',
->and($paths['/checkout']['domain'])->toBe('www.shop.test');
});
it('sums every compose key in the application traffic card', function () {
it('sums every compose key on the application analytics page', function () {
$application = makeTrafficComposeApplication();
$apiKey = $application->uuid.'-'.traefikSafeServiceNameSegment('api');
$webKey = $application->uuid.'-'.traefikSafeServiceNameSegment('web');
@@ -297,7 +296,7 @@ it('sums every compose key in the application traffic card', function () {
"/app/{$webKey}/traffic/overview" => resourceKeysOverview(200),
]);
loadLazy(Livewire::test(TrafficOverview::class, ['application' => $application]))
loadLazy(Livewire::test(ApplicationAnalytics::class, ['application' => $application]))
->assertOk()
->assertSee('4,200');
});