mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
refactor(analytics): remove application traffic overview card
This commit is contained in:
@@ -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 →
|
||||
</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');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user