fix(analytics): hide traffic UI when disabled and skip default-on (#11700)

This commit is contained in:
Andras Bacsai
2026-09-09 14:28:15 +02:00
committed by GitHub
parent f4bec96bd6
commit 936a2566c6
25 changed files with 309 additions and 87 deletions
+28 -2
View File
@@ -11,6 +11,7 @@ use Illuminate\Contracts\View\View;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Livewire\Attributes\Lazy;
use Livewire\Attributes\On;
use Livewire\Attributes\Url;
use Livewire\Component;
@@ -178,6 +179,23 @@ class Analytics extends Component
$this->live = ! $this->live;
}
#[On('trafficAnalyticsStateChanged')]
public function refreshTrafficAnalyticsState(): void
{
if ($this->scopedServerUuid === null) {
return;
}
$server = Server::ownedByCurrentTeam()->whereUuid($this->scopedServerUuid)->firstOrFail();
$this->overview = null;
$this->servers = $server->isTrafficAnalyticsEnabled() ? collect([$server]) : collect();
if ($this->servers->isNotEmpty()) {
$this->refreshAppOptions();
$this->loadData();
}
}
public function isLivePollable(): bool
{
return $this->live && $this->range === '24h';
@@ -564,10 +582,18 @@ class Analytics extends Component
return [$from->toIso8601ZuluString(), $to->toIso8601ZuluString()];
}
public function placeholder(): View
public function placeholder(array $params = []): View
{
$scopedServerUuid = $params['scopedServerUuid'] ?? null;
$hideSkeleton = false;
if (is_string($scopedServerUuid)) {
$server = Server::ownedByCurrentTeamCached()->firstWhere('uuid', $scopedServerUuid);
$hideSkeleton = $server !== null && ! $server->isTrafficAnalyticsEnabled();
}
// Rendered instantly; the Sentinel round-trips run in the deferred lazy-load request.
return view('livewire.analytics-placeholder');
return view('livewire.analytics-placeholder', compact('hideSkeleton'));
}
public function render()
+10 -1
View File
@@ -221,8 +221,17 @@ class Analytics extends Component
return [$from->toIso8601ZuluString(), $to->toIso8601ZuluString()];
}
public function placeholder(): View
public function placeholder(array $params = []): View
{
$application = $params['application'] ?? null;
if ($application instanceof Application && ! $application->destination?->server?->isTrafficAnalyticsEnabled()) {
$this->application = $application;
$this->enabled = false;
return view('livewire.project.application.analytics');
}
// Rendered instantly; the Sentinel round-trip runs in the deferred lazy-load request.
return view('livewire.project.application.analytics-placeholder');
}
-1
View File
@@ -56,7 +56,6 @@ class Proxy extends Component
$this->redirectEnabled = data_get($this->server, 'proxy.redirect_enabled', true);
$this->redirectUrl = data_get($this->server, 'proxy.redirect_url');
$this->syncData(false);
$this->loadProxyConfiguration();
$this->clearAppliedTraefikBranchWarning();
}
@@ -3,6 +3,7 @@
namespace App\Livewire\Server;
use App\Actions\Server\ConfigureTrafficAnalytics;
use App\Livewire\Analytics;
use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\View\View;
@@ -83,6 +84,7 @@ class TrafficAnalyticsSettings extends Component
ConfigureTrafficAnalytics::run($this->server, $enable);
$this->server->refresh();
$this->isTrafficAnalyticsEnabled = $this->server->isTrafficAnalyticsEnabled();
$this->dispatch('trafficAnalyticsStateChanged')->to(Analytics::class);
$this->dispatch('success', $enable
? 'Traffic analytics enabled. Restarting proxy and Sentinel.'
: 'Traffic analytics disabled. Restarting proxy and Sentinel.');
-9
View File
@@ -170,15 +170,6 @@ class ServerSetting extends Model
{
static::creating(function ($setting) {
try {
// Enable traffic analytics by default for eligible servers, unless the
// caller explicitly set a value. Swarm and build servers are ineligible,
// mirroring the toggle guard so the flag never contradicts capability.
// Runs before sentinel generation, which may throw and be swallowed below.
if (! $setting->isDirty('is_traffic_analytics_enabled')) {
$isSwarm = $setting->is_swarm_manager || $setting->is_swarm_worker;
$isBuild = (bool) $setting->is_build_server;
$setting->is_traffic_analytics_enabled = ! $isSwarm && ! $isBuild;
}
if (str($setting->sentinel_token)->isEmpty()) {
$setting->generateSentinelToken(save: false, ignoreEvent: true);
}
@@ -1,4 +1,5 @@
<div class="flex w-full min-w-0 flex-col gap-6">
@if (! ($hideSkeleton ?? false))
{{-- Header (real chrome; only the data below is a skeleton) --}}
<div class="flex flex-col gap-4">
@if (empty($scopedServerUuid ?? null))
@@ -54,4 +55,5 @@
<x-application.settings-section id="analytics-country-section" title="Countries" flush>
<x-skeleton class="h-64 w-full" />
</x-application.settings-section>
@endif
</div>
+6 -6
View File
@@ -123,17 +123,17 @@ $appListboxOptions = array_merge(
@endif
@if ($servers->isEmpty())
<x-empty size="sm" title="Traffic analytics is not enabled"
description="{{ $scopedServerUuid === null ? 'Enable traffic analytics on a server to see request analytics here.' : 'Enable traffic analytics in the settings below to begin collecting requests for this server.' }}"
icon-name="analytics">
@if ($scopedServerUuid === null)
@if ($scopedServerUuid === null)
<x-empty size="sm" title="Traffic analytics is not enabled"
description="Enable traffic analytics on a server to see request analytics here."
icon-name="analytics">
<x-slot:contents>
<a class="button" href="{{ route('server.index') }}" {{ wireNavigate() }}>
View servers
</a>
</x-slot:contents>
@endif
</x-empty>
</x-empty>
@endif
@elseif (! $overview)
<x-empty size="sm" title="No analytics data yet"
description="We could not load traffic analytics for the selected filters and range. Try a different range or check back shortly."
+4 -1
View File
@@ -11,12 +11,15 @@
$dashboardItemLimit = 8;
$dashboardProjects = $projects->sortBy('name', SORT_NATURAL)->take($dashboardItemLimit);
$dashboardServers = $servers->sortBy('name', SORT_NATURAL)->take($dashboardItemLimit);
$hasTrafficAnalytics = $servers->contains(fn ($server) => $server->isTrafficAnalyticsEnabled());
@endphp
<div class="flex min-w-0 flex-col gap-8">
<livewire:dashboard.active-deployments />
<livewire:dashboard.traffic-analytics />
@if ($hasTrafficAnalytics)
<livewire:dashboard.traffic-analytics />
@endif
<section class="mb-0! min-w-0">
<x-section-heading title="Projects" subtitle="Your deployment workspaces"
@@ -1,19 +1 @@
<section class="mb-0! min-w-0">
{{-- Real section header; only the KPI grid below is a skeleton. --}}
<div class="mb-3 flex items-end justify-between gap-4">
<div>
<h2 class="text-[14px]! leading-5! font-semibold! text-black dark:text-fg">
Traffic analytics
</h2>
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">
Team-wide request volume across servers with traffic analytics enabled
</p>
</div>
<div class="flex items-center gap-2">
<x-skeleton class="h-8 w-40 rounded-lg" />
<x-skeleton class="h-4 w-24 rounded-full" />
</div>
</div>
<x-skeleton.tiles :count="4" grid="grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" rounded="rounded-xl" />
</section>
<div class="contents"></div>
@@ -7,6 +7,8 @@ $approxBadge = fn (string $tooltip) => '<span title="'.e($tooltip).'" class="ml-
$spark = 'refreshChartData-'.$chartId.'-status';
?>
<div class="contents">
@if ($servers->isNotEmpty() && $overview)
<section class="mb-0! min-w-0">
<div class="mb-3 flex items-end justify-between gap-4">
<div>
@@ -42,29 +44,14 @@ $spark = 'refreshChartData-'.$chartId.'-status';
</div>
@endif
<a href="{{ route('analytics') }}" {{ wireNavigate() }}
class="inline-flex shrink-0 items-center gap-1 text-[12px] font-medium text-neutral-500 transition-colors hover:text-black dark:text-fg-dim dark:hover:text-fg">
class="group inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md border border-neutral-200 bg-white px-2.5 text-[12px] font-medium text-neutral-600 transition-[color,background-color,transform] duration-100 ease-out hover:bg-neutral-100 hover:text-black active:scale-[0.97] dark:border-white/[0.08] dark:bg-white/[0.06] dark:text-fg-dim dark:hover:bg-white/[0.1] dark:hover:text-fg">
Open analytics
<x-reicon name="arrow-right" class="size-3" />
<x-reicon name="arrow-right" class="size-3 opacity-70 transition-transform duration-150 ease-out group-hover:translate-x-0.5" />
</a>
</div>
</div>
@if ($servers->isEmpty())
<x-empty size="sm" title="Traffic analytics is not enabled"
description="Enable Sentinel traffic analytics on a server to see a team-wide summary here."
icon-name="network">
<x-slot:contents>
<a class="button" href="{{ route('server.index') }}" {{ wireNavigate() }}>
View servers
</a>
</x-slot:contents>
</x-empty>
@elseif (! $overview)
<x-empty size="sm" title="No analytics data yet"
description="We could not load traffic analytics for the selected range. Try a different range or check back shortly."
icon-name="network" />
@else
{{-- Sparkline KPI cards. Each links through to the full analytics page. --}}
{{-- Sparkline KPI cards. Each links through to the full analytics page. --}}
<div class="grid grid-cols-1 gap-px overflow-hidden rounded-xl border border-neutral-200 bg-neutral-200 sm:grid-cols-2 lg:grid-cols-4 dark:border-white/[0.08] dark:bg-white/[0.07]">
<a href="{{ route('analytics') }}" {{ wireNavigate() }}
class="group flex flex-col bg-white px-4 py-3 transition-colors hover:bg-neutral-50 dark:bg-[color-mix(in_srgb,var(--color-app)_95%,white)] dark:hover:bg-[color-mix(in_srgb,var(--color-app)_93%,white)]">
@@ -128,5 +115,6 @@ $spark = 'refreshChartData-'.$chartId.'-status';
</div>
</a>
</div>
@endif
</section>
@endif
</div>
@@ -18,7 +18,7 @@ $analyticsServerUuid = $application->destination?->server?->uuid;
<x-slot:actions>
<a class="button" href="{{ route('server.analytics', ['server_uuid' => $analyticsServerUuid]) }}"
{{ wireNavigate() }}>
Server settings
Server analytics
<x-external-link />
</a>
</x-slot:actions>
@@ -46,7 +46,8 @@
@elseif ($currentRoute === 'project.application.metrics')
<livewire:project.shared.metrics :resource="$application" />
@elseif ($currentRoute === 'project.application.analytics')
<livewire:project.application.analytics :application="$application" />
<livewire:project.application.analytics :application="$application"
:lazy="$application->destination?->server?->isTrafficAnalyticsEnabled()" />
@elseif ($currentRoute === 'project.application.tags')
<livewire:project.shared.tags :resource="$application" />
@elseif ($currentRoute === 'project.application.danger')
@@ -25,9 +25,6 @@
</x-application.settings-section>
<livewire:project.application.traffic-overview :application="$application"
:key="'application-traffic-overview-'.$application->id" />
<x-application.settings-section id="access-section" title="Access" helper="Manage how this application is reached publicly and from the Docker network.">
<section id="public-access-section" @class([
'border-b border-neutral-200 pb-5 dark:border-white/[0.07]' => $buildPack !== 'dockercompose',
@@ -24,9 +24,9 @@
<x-external-link />
</a>
</x-slot:actions>
<x-callout type="info" title="Metrics are not enabled">
Enable Sentinel and metrics for this server before collecting application usage data.
</x-callout>
<x-empty size="sm" title="Metrics are not enabled"
description="Enable Sentinel and metrics for this server before collecting application usage data."
icon-name="dashboard" />
</x-application.settings-section>
@elseif (!str($resource->status)->contains('running'))
<x-application.settings-section id="metrics-overview-section" title="Metrics"
@@ -15,7 +15,8 @@
:key="'server-traffic-analytics-settings-'.$server->uuid" />
@endcan
<livewire:analytics :scoped-server-uuid="$server->uuid" :key="'server-analytics-'.$server->uuid" />
<livewire:analytics :scoped-server-uuid="$server->uuid" :key="'server-analytics-'.$server->uuid"
:lazy="$server->isTrafficAnalyticsEnabled()" />
</div>
</div>
</div>
@@ -90,6 +90,7 @@
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value || $server->proxyType() === 'CADDY')
<x-application.settings-section id="server-proxy-file-section" :title="$proxyTitle"
x-init="$wire.loadProxyConfiguration()"
helper="Edit the generated proxy compose configuration used on this server.">
<x-slot:actions>
@can('update', $server)
@@ -128,6 +129,11 @@
@endif
@endif
<div wire:loading.flex wire:target="loadProxyConfiguration"
class="min-h-32 items-center justify-center">
<x-loading text="Loading proxy configuration…" />
</div>
@if ($proxySettings)
<div class="relative mt-4" wire:loading.class="pointer-events-none opacity-50"
wire:target="submit,resetProxyConfiguration" aria-live="polite">
@@ -8,10 +8,22 @@
<x-application.settings-section id="server-traffic-analytics-settings-section" title="Traffic analytics"
helper="Control proxy traffic collection, retention, and visitor geolocation for this server.">
<x-slot:actions>
<x-forms.button canGate="update" :canResource="$server" wire:click="toggleTrafficAnalytics"
wire:confirm="{{ $isTrafficAnalyticsEnabled ? 'Disable' : 'Enable' }} traffic analytics? The proxy and Sentinel will restart, causing a brief connectivity blip for all applications on this server.">
{{ $isTrafficAnalyticsEnabled ? 'Disable' : 'Enable' }} traffic analytics
</x-forms.button>
@if ($isTrafficAnalyticsEnabled)
<div class="flex items-center gap-3">
<x-loading wire:loading.flex wire:target="toggleTrafficAnalytics"
text="Restarting Sentinel and proxy..." compact />
<x-modal-confirmation title="Disable traffic analytics?"
buttonTitle="Disable traffic analytics" submitAction="toggleTrafficAnalytics"
:actions="[
'Disabling traffic analytics will restart Sentinel and the proxy. Your applications will experience a brief interruption.',
]"
warningMessage="Application traffic may be interrupted while Sentinel and the proxy restart."
:confirmWithText="false" :confirmWithPassword="false" :ignoreWire="false"
step2ButtonText="Disable traffic analytics"
:disabled="! auth()->user()->can('update', $server)"
:authDisabled="! auth()->user()->can('update', $server)" />
</div>
@endif
</x-slot:actions>
@if ($isTrafficAnalyticsEnabled)
@@ -45,7 +57,24 @@
@else
<x-empty size="sm" title="Traffic analytics is disabled"
description="Enable traffic analytics to collect proxy access logs and geolocate visitor traffic."
icon-name="dashboard" />
icon-name="dashboard">
<x-slot:contents>
<div class="flex items-center gap-3">
<x-loading wire:loading.flex wire:target="toggleTrafficAnalytics"
text="Restarting Sentinel and proxy..." compact />
<x-modal-confirmation title="Enable traffic analytics?"
buttonTitle="Enable traffic analytics" submitAction="toggleTrafficAnalytics"
:actions="[
'Enabling traffic analytics will restart Sentinel and the proxy. Your applications will experience a brief interruption.',
]"
warningMessage="Application traffic may be interrupted while Sentinel and the proxy restart."
:confirmWithText="false" :confirmWithPassword="false" :ignoreWire="false"
step2ButtonText="Enable traffic analytics" isHighlightedButton
:disabled="! auth()->user()->can('update', $server)"
:authDisabled="! auth()->user()->can('update', $server)" />
</div>
</x-slot:contents>
</x-empty>
@endif
</x-application.settings-section>
</form>
@@ -21,6 +21,12 @@ test('compose file loading waits for the user to confirm the file location', fun
->not->toContain('x-init="$wire.dispatch(\'loadCompose\', true)"');
});
test('traffic analytics is only shown on the dedicated analytics page', function () {
$view = file_get_contents(resource_path('views/livewire/project/application/general.blade.php'));
expect($view)->not->toContain('<livewire:project.application.traffic-overview');
});
test('docker compose heading separates its title and action', function () {
$view = file_get_contents(resource_path('views/livewire/project/application/general.blade.php'));
@@ -0,0 +1,11 @@
<?php
test('disabled application metrics use the standard empty state', function () {
$view = file_get_contents(resource_path('views/livewire/project/shared/metrics.blade.php'));
expect($view)
->toContain('<x-empty size="sm" title="Metrics are not enabled"')
->toContain('description="Enable Sentinel and metrics for this server before collecting application usage data."')
->toContain('icon-name="dashboard"')
->not->toContain('<x-callout type="info" title="Metrics are not enabled">');
});
@@ -10,3 +10,21 @@ it('disables proxy configuration controls and covers the editor while saving', f
->toContain('Updating proxy configuration')
->toContain('aria-live="polite"');
});
it('loads only the compose file from the frontend and shows the shared loading indicator', function () {
$page = file_get_contents(resource_path('views/livewire/server/proxy/show.blade.php'));
$proxy = file_get_contents(resource_path('views/livewire/server/proxy.blade.php'));
$component = file_get_contents(app_path('Livewire/Server/Proxy.php'));
expect($page)
->toContain('<livewire:server.proxy :server="$server" />')
->not->toContain('<livewire:server.proxy :server="$server" lazy />');
expect($proxy)
->toContain('x-init="$wire.loadProxyConfiguration()"')
->toContain('wire:loading.flex wire:target="loadProxyConfiguration"')
->toContain('<x-loading text="Loading proxy configuration…" />');
expect($component)
->not->toContain('$this->loadProxyConfiguration();');
});
@@ -3,6 +3,7 @@
use App\Livewire\Project\Application\Analytics;
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
@@ -82,6 +83,7 @@ beforeEach(function () {
// Server (from a prior enabled test) can leak into a later test and be treated as
// analytics-enabled, mounting the component against an unreachable server.
Server::flushIdentityMap();
InstanceSettings::forceCreate(['id' => 0]);
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
@@ -114,6 +116,35 @@ function makeAnalyticsApplication(Team $team, PrivateKey $privateKey, Environmen
]);
}
it('only lazy loads application analytics when traffic analytics is enabled', function () {
$configuration = file_get_contents(resource_path('views/livewire/project/application/configuration.blade.php'));
expect($configuration)
->toContain(':lazy="$application->destination?->server?->isTrafficAnalyticsEnabled()"');
});
it('renders the disabled state in the initial application analytics page response', function () {
$application = makeAnalyticsApplication($this->team, $this->privateKey, $this->environment, false);
$this->get(route('project.application.analytics', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'application_uuid' => $application->uuid,
]))
->assertOk()
->assertSee('Traffic analytics is not enabled')
->assertDontSee('__lazyLoad', escape: false)
->assertDontSee('analytics-range-section');
});
it('guards the lazy placeholder when traffic analytics is disabled', function () {
$application = makeAnalyticsApplication($this->team, $this->privateKey, $this->environment, false);
Livewire::test(Analytics::class, ['application' => $application, 'lazy' => true])
->assertSee('Traffic analytics is not enabled')
->assertDontSee('analytics-range-section');
});
it('renders KPIs from a mocked traffic client when analytics is enabled', function () {
$application = makeAnalyticsApplication($this->team, $this->privateKey, $this->environment, true);
@@ -189,9 +220,13 @@ it('falls back to the donut for the per-app chart when the series endpoint is ab
it('shows an empty state when traffic analytics is disabled for the server', function () {
$application = makeAnalyticsApplication($this->team, $this->privateKey, $this->environment, false);
loadLazy(Livewire::test(Analytics::class, ['application' => $application]))
Livewire::test(Analytics::class, ['application' => $application, 'lazy' => false])
->assertOk()
->assertSee('Analytics')
->assertSee('Traffic analytics is not enabled')
->assertSee('Server analytics')
->assertSeeHtml(route('server.analytics', ['server_uuid' => $application->destination->server->uuid]))
->assertDontSee('__lazyLoad', escape: false)
->assertDontSee('Unique visitors');
});
@@ -204,8 +239,10 @@ it('renders the disabled empty-state without crashing when the application has n
expect($application->destination)->toBeNull();
loadLazy(Livewire::test(Analytics::class, ['application' => $application]))
Livewire::test(Analytics::class, ['application' => $application, 'lazy' => false])
->assertOk()
->assertSee('Analytics')
->assertDontSee('Server settings');
->assertSee('Traffic analytics is not enabled')
->assertDontSee('__lazyLoad', escape: false)
->assertDontSee('Server analytics');
});
@@ -1,5 +1,6 @@
<?php
use App\Livewire\Dashboard;
use App\Livewire\Dashboard\TrafficAnalytics;
use App\Models\Application;
use App\Models\Environment;
@@ -67,6 +68,19 @@ beforeEach(function () {
$this->privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
});
it('hides traffic analytics from the dashboard when no server has it enabled', function () {
$server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->privateKey->id,
]);
$server->settings->is_traffic_analytics_enabled = false;
$server->settings->save();
Livewire::test(Dashboard::class)
->assertOk()
->assertDontSee('Traffic analytics');
});
it('renders the team traffic summary aggregated across servers with an approximate badge', function () {
$serverOne = Server::factory()->create([
'team_id' => $this->team->id,
@@ -150,6 +164,14 @@ it('shows loading states while the dashboard range refreshes', function () {
->toContain('aria-label="Loading analytics"');
});
it('styles the open analytics link as a dashboard action button', function () {
$view = file_get_contents(resource_path('views/livewire/dashboard/traffic-analytics.blade.php'));
expect($view)
->toContain('class="group inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md border border-neutral-200 bg-white px-2.5')
->toContain('group-hover:translate-x-0.5');
});
it('uses the dashboard surface treatment for the analytics KPI group', function () {
$view = file_get_contents(resource_path('views/livewire/dashboard/traffic-analytics.blade.php'));
@@ -162,7 +184,7 @@ it('uses the dashboard surface treatment for the analytics KPI group', function
->not->toContain('dark:bg-base dark:hover:bg-white/[0.03]');
});
it('shows a failure empty-state instead of an all-zero KPI panel when every server fetch fails', function () {
it('hides dashboard analytics when every server fetch fails', function () {
$serverOne = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->privateKey->id,
@@ -183,12 +205,14 @@ it('shows a failure empty-state instead of an all-zero KPI panel when every serv
loadLazy(Livewire::test(TrafficAnalytics::class))
->assertOk()
->assertSee('No analytics data yet')
->assertSeeHtml('class="contents"')
->assertDontSee('Traffic analytics')
->assertDontSee('No analytics data yet')
->assertDontSee('Unique visitors')
->assertDontSee('Error rate');
});
it('shows an empty state when no server in the team has traffic analytics enabled', function () {
it('renders nothing when no server in the team has traffic analytics enabled', function () {
$server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->privateKey->id,
@@ -200,5 +224,13 @@ it('shows an empty state when no server in the team has traffic analytics enable
loadLazy(Livewire::test(TrafficAnalytics::class))
->assertOk()
->assertDontSee('Unique visitors')
->assertSee('not enabled');
->assertDontSee('Traffic analytics');
});
it('uses an empty lazy placeholder so analytics only appears after data loads', function () {
$view = file_get_contents(resource_path('views/livewire/dashboard/traffic-analytics-placeholder.blade.php'));
expect($view)
->toContain('class="contents"')
->not->toContain('Traffic analytics');
});
@@ -3,6 +3,7 @@
use App\Livewire\Analytics;
use App\Livewire\Server\Analytics\Show;
use App\Livewire\Server\TrafficAnalyticsSettings;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
@@ -13,6 +14,7 @@ use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::forceCreate(['id' => 0]);
$this->user = User::factory()->create();
$this->team = $this->user->teams()->first();
$this->actingAs($this->user);
@@ -60,6 +62,20 @@ it('moves traffic analytics configuration out of sentinel and onto analytics', f
->not->toContain('id="trafficTopn"');
});
it('matches the server metrics empty state when traffic analytics is disabled', function () {
$view = file_get_contents(resource_path('views/livewire/server/traffic-analytics-settings.blade.php'));
$disabledState = str($view)
->after('@else')
->before('@endif')
->toString();
expect($disabledState)
->toContain('title="Traffic analytics is disabled"')
->toContain('<x-slot:contents>')
->toContain('isHighlightedButton')
->toContain('buttonTitle="Enable traffic analytics"');
});
it('renders traffic analytics settings above the server analytics dashboard', function () {
$view = file_get_contents(resource_path('views/livewire/server/analytics/show.blade.php'));
@@ -67,6 +83,13 @@ it('renders traffic analytics settings above the server analytics dashboard', fu
->toBeLessThan(strpos($view, '<livewire:analytics'));
});
it('only lazy loads the scoped dashboard when traffic analytics is enabled', function () {
$view = file_get_contents(resource_path('views/livewire/server/analytics/show.blade.php'));
expect($view)
->toContain(':lazy="$server->isTrafficAnalyticsEnabled()"');
});
it('matches other server pages without a visible page title', function () {
$view = file_get_contents(resource_path('views/livewire/server/analytics/show.blade.php'));
@@ -88,6 +111,41 @@ it('scopes the server analytics page to its route server', function () {
->assertDontSee($otherServer->name);
});
it('does not duplicate the disabled state on a scoped server analytics dashboard', function () {
$server = Server::factory()->create(['team_id' => $this->team->id]);
$server->settings->is_traffic_analytics_enabled = false;
$server->settings->save();
Livewire::test(Analytics::class, ['scopedServerUuid' => $server->uuid])
->assertDontSee('Traffic analytics is not enabled');
});
it('removes stale analytics content when traffic analytics is disabled', function () {
$server = Server::factory()->create(['team_id' => $this->team->id]);
$server->settings->is_traffic_analytics_enabled = true;
$server->settings->save();
$component = Livewire::test(Analytics::class, ['scopedServerUuid' => $server->uuid]);
$server->settings->is_traffic_analytics_enabled = false;
$server->settings->save();
$component
->dispatch('trafficAnalyticsStateChanged')
->assertSet('servers', fn ($servers) => $servers->isEmpty())
->assertDontSee('No analytics data yet');
});
it('does not render a skeleton placeholder for a disabled scoped server', function () {
$server = Server::factory()->create(['team_id' => $this->team->id]);
$server->settings->is_traffic_analytics_enabled = false;
$server->settings->save();
Livewire::test(Analytics::class, ['scopedServerUuid' => $server->uuid, 'lazy' => true])
->assertDontSee('analytics-overview-section')
->assertDontSee('analytics-requests-section');
});
it('saves traffic analytics settings from the server analytics page', function () {
Queue::fake();
@@ -14,14 +14,14 @@ beforeEach(function () {
$this->team = $user->teams()->first();
});
it('defaults traffic analytics to enabled for a normal server and exposes a server helper', function () {
it('defaults traffic analytics to disabled for a normal server and exposes a server helper', function () {
$server = Server::factory()->create(['team_id' => $this->team->id]);
expect($server->settings->is_traffic_analytics_enabled)->toBeTrue();
expect($server->isTrafficAnalyticsEnabled())->toBeTrue();
expect($server->settings->is_traffic_analytics_enabled)->toBeFalse();
expect($server->isTrafficAnalyticsEnabled())->toBeFalse();
$server->settings->is_traffic_analytics_enabled = false;
$server->settings->is_traffic_analytics_enabled = true;
$server->settings->save();
expect($server->fresh()->isTrafficAnalyticsEnabled())->toBeFalse();
expect($server->fresh()->isTrafficAnalyticsEnabled())->toBeTrue();
});
it('defaults traffic analytics to disabled for a swarm server', function () {
@@ -44,14 +44,14 @@ it('defaults traffic analytics to disabled for a build server', function () {
expect($setting->is_traffic_analytics_enabled)->toBeFalse();
});
it('respects an explicit traffic analytics value on creation', function () {
it('respects an explicit enabled traffic analytics value on creation', function () {
$server = Server::factory()->create(['team_id' => $this->team->id]);
$setting = ServerSetting::create([
'server_id' => $server->id,
'is_traffic_analytics_enabled' => false,
'is_traffic_analytics_enabled' => true,
]);
expect($setting->is_traffic_analytics_enabled)->toBeFalse();
expect($setting->is_traffic_analytics_enabled)->toBeTrue();
});
it('defaults traffic collection and geoip settings to sentinel values', function () {
@@ -1,7 +1,9 @@
<?php
use App\Actions\Server\ConfigureTrafficAnalytics;
use App\Livewire\Analytics;
use App\Livewire\Server\TrafficAnalyticsSettings;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -11,6 +13,7 @@ use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::forceCreate(['id' => 0]);
$this->user = User::factory()->create();
$this->team = $this->user->teams()->first();
$this->actingAs($this->user);
@@ -32,11 +35,32 @@ it('toggles traffic analytics via the sentinel settings component', function ()
Livewire::test(TrafficAnalyticsSettings::class, ['server' => $server])
->call('toggleTrafficAnalytics')
->assertDispatchedTo(Analytics::class, 'trafficAnalyticsStateChanged')
->assertHasNoErrors();
expect($server->fresh()->isTrafficAnalyticsEnabled())->toBeTrue();
});
it('warns about the application interruption before enabling traffic analytics', function () {
$server = Server::factory()->create(['team_id' => $this->team->id]);
$server->settings->is_traffic_analytics_enabled = false;
$server->settings->save();
Livewire::test(TrafficAnalyticsSettings::class, ['server' => $server])
->assertSee('Enable traffic analytics?')
->assertDontSeeHtml('wire:confirm')
->assertSeeHtml('wire:loading.flex')
->assertSeeHtml('wire:target="toggleTrafficAnalytics"')
->assertSee('Restarting Sentinel and proxy...')
->assertSee('Enabling traffic analytics will restart Sentinel and the proxy. Your applications will experience a brief interruption.');
});
it('allows the analytics toggle modal to update after the state changes', function () {
$view = file_get_contents(resource_path('views/livewire/server/traffic-analytics-settings.blade.php'));
expect($view)->toContain(':ignoreWire="false"');
});
it('does not enable traffic analytics on a swarm server', function () {
ConfigureTrafficAnalytics::partialMock()->shouldReceive('handle')->never();