mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 02:24:11 -05:00
fix(traffic-analytics): clear stale chart and cache dashboard 404s
- Dispatch the chart refresh event even when loadData() finds no overviews, so a wire:ignore'd chart flips to its no-data state instead of keeping stale data. - Cache dashboard-route absence for 60s in SentinelTrafficClient so older Sentinel installs that 404 the dashboard endpoint aren't re-probed over SSH on every refresh within the same window. - Make Pest's loadLazy() throw when no __lazyLoad trigger is found instead of silently testing the un-mounted placeholder.
This commit is contained in:
@@ -361,6 +361,10 @@ class Analytics extends Component
|
||||
|
||||
if (empty($overviews)) {
|
||||
$this->resetData();
|
||||
// The chart lives under wire:ignore, so it only updates via this event — dispatch
|
||||
// even when cleared so a previously-populated chart flips to its no-data state
|
||||
// instead of keeping stale data.
|
||||
$this->dispatch("refreshChartData-{$this->chartId}-status", $this->chartPayload());
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -416,15 +420,16 @@ class Analytics extends Component
|
||||
protected function chartPayload(): array
|
||||
{
|
||||
$device = $this->deviceChartData();
|
||||
$overview = $this->overview ?? [];
|
||||
|
||||
return [
|
||||
'hasSeries' => $this->hasSeries,
|
||||
'range' => $this->range,
|
||||
'seriesData' => [
|
||||
$this->overview['s2xx'] ?? 0,
|
||||
$this->overview['s3xx'] ?? 0,
|
||||
$this->overview['s4xx'] ?? 0,
|
||||
$this->overview['s5xx'] ?? 0,
|
||||
$overview['s2xx'] ?? 0,
|
||||
$overview['s3xx'] ?? 0,
|
||||
$overview['s4xx'] ?? 0,
|
||||
$overview['s5xx'] ?? 0,
|
||||
],
|
||||
'timeSeries' => [
|
||||
'categories' => array_column($this->series, 'bucket'),
|
||||
|
||||
@@ -174,13 +174,26 @@ class SentinelTrafficClient
|
||||
*/
|
||||
private function fetchDashboard(?string $appKey, string $from, string $to, string $range, int $pathLimit, int $breakdownLimit, int $appsLimit): ?array
|
||||
{
|
||||
// Older Sentinel 404s this route. raw() throws on that (and doesn't cache the failure),
|
||||
// so without a marker every refresh would re-probe over SSH before falling back to the
|
||||
// batch. Remember the absence for the same 60s window as the data cache: at most one
|
||||
// wasted probe per minute, and a Sentinel upgrade is picked up on the next window.
|
||||
$absenceKey = 'traffic:dashboard-absent:'.$this->server->uuid;
|
||||
if (Cache::get($absenceKey) === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$decoded = json_decode($this->raw($this->dashboardUrl($appKey, $from, $to, $range, $pathLimit, $breakdownLimit, $appsLimit)), true);
|
||||
} catch (\Throwable) {
|
||||
Cache::put($absenceKey, true, 60);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! is_array($decoded) || ! array_key_exists('overview', $decoded)) {
|
||||
Cache::put($absenceKey, true, 60);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -375,6 +375,32 @@ it('scopes the view to a single application and hides the leaderboard when filte
|
||||
->assertDontSee('Top applications');
|
||||
});
|
||||
|
||||
it('still dispatches the chart refresh when every server fails so a stale chart clears', function () {
|
||||
$server = bootEnabledGlobalServer();
|
||||
|
||||
// Server unreachable: the overview fetch throws, so no overviews are collected and
|
||||
// loadData short-circuits via resetData(). The chart lives under wire:ignore, so it
|
||||
// must still receive a refresh event to flip to its no-data state instead of keeping
|
||||
// whatever it last plotted.
|
||||
$fake = new class($server) extends SentinelTrafficClient
|
||||
{
|
||||
protected function raw(string $url): string
|
||||
{
|
||||
if (str_contains($url, '/traffic/overview')) {
|
||||
throw new RuntimeException('server unreachable');
|
||||
}
|
||||
|
||||
return '{}';
|
||||
}
|
||||
};
|
||||
app()->bind(SentinelTrafficClient::class, fn () => $fake);
|
||||
|
||||
loadLazy(Livewire::test(Analytics::class))
|
||||
->assertOk()
|
||||
->assertSet('overview', null)
|
||||
->assertDispatched('refreshChartData-global-analytics-status');
|
||||
});
|
||||
|
||||
it('shows the not-enabled empty state when no server has traffic analytics on', function () {
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
|
||||
@@ -241,6 +241,49 @@ it('warms every server-wide endpoint in a single batched exec and per-call metho
|
||||
$client->attribution();
|
||||
});
|
||||
|
||||
it('probes the absent dashboard route only once per cache window, then reuses the batch fallback', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
|
||||
// Older Sentinel: the dashboard route 404s (unparseable body), so raw() throws and the
|
||||
// client falls back to the batch. The absence must be remembered so a second prefetch in
|
||||
// the same window doesn't re-probe the dashboard over SSH.
|
||||
$client = new class($server) extends SentinelTrafficClient
|
||||
{
|
||||
public int $dashboardProbes = 0;
|
||||
|
||||
public int $batchCalls = 0;
|
||||
|
||||
protected function remoteFetch(string $url): string
|
||||
{
|
||||
if (str_contains($url, '/traffic/dashboard')) {
|
||||
$this->dashboardProbes++;
|
||||
|
||||
return 'Not Found';
|
||||
}
|
||||
throw new RuntimeException("unexpected individual fetch: {$url}");
|
||||
}
|
||||
|
||||
protected function batchRemoteFetch(array $urls): string
|
||||
{
|
||||
$this->batchCalls++;
|
||||
$bodies = array_map(fn ($url) => match (true) {
|
||||
str_contains($url, '/traffic/apps') => json_encode(['app-a']),
|
||||
str_contains($url, '/attribution') => '{"attribution":"demo"}',
|
||||
str_contains($url, '/overview') => '{"requests":1}',
|
||||
default => '[]',
|
||||
}, $urls);
|
||||
|
||||
return implode("\x1e", $bodies)."\x1e";
|
||||
}
|
||||
};
|
||||
|
||||
$client->prefetchServerWide(null, 'F', 'T', ['country'], '24h');
|
||||
$client->prefetchServerWide(null, 'F', 'T', ['country'], '24h');
|
||||
|
||||
expect($client->dashboardProbes)->toBe(1)
|
||||
->and($client->batchCalls)->toBe(1);
|
||||
});
|
||||
|
||||
it('double-quotes the url in the remote curl command so & is not a shell background operator', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$client = new class($server) extends SentinelTrafficClient
|
||||
|
||||
+4
-1
@@ -55,7 +55,10 @@ function loadLazy(Testable $component): Testable
|
||||
preg_match('/__lazyLoad\('([^&]+)'\)/', $component->html(), $matches);
|
||||
|
||||
if (empty($matches)) {
|
||||
return $component;
|
||||
// No trigger means the component isn't lazy (or the placeholder markup changed).
|
||||
// Fail loudly rather than silently asserting against the un-mounted placeholder,
|
||||
// which would turn a lazy-load regression into a false-positive pass.
|
||||
throw new RuntimeException('loadLazy: no __lazyLoad trigger found — component is not #[Lazy] or its placeholder markup changed.');
|
||||
}
|
||||
|
||||
return $component->call('__lazyLoad', $matches[1]);
|
||||
|
||||
Reference in New Issue
Block a user