Merge remote-tracking branch 'origin/main' into next

This commit is contained in:
github-actions[bot]
2026-10-07 11:48:40 +00:00
3 changed files with 35 additions and 9 deletions
+3 -2
View File
@@ -122,7 +122,8 @@ class TrafficAnalyticsAggregator
/**
* Merge path rows keyed by (app, path), so the same path under two apps stays two
* rows, each with the domain of the row's Sentinel `app` key.
* rows, each with the domain of the row's Sentinel `app` key. Sentinel's `__other__`
* overflow rows (the long tail past its top-N cap) fold into one row without a domain.
*
* @param iterable<int, mixed> $paths
* @param callable(string): ?string $domainForKey
@@ -132,7 +133,7 @@ class TrafficAnalyticsAggregator
foreach ($paths as $path) {
$data = $path->toArray();
$pathStr = (string) ($data['path'] ?? '');
$appId = (string) ($data['app'] ?? '');
$appId = $pathStr === '__other__' ? '' : (string) ($data['app'] ?? '');
$key = $appId."\n".$pathStr;
$this->paths[$key] ??= [
@@ -2,7 +2,9 @@
Paginated "top paths" list. Each row shows the request path, a proportional
request-volume bar, and right-aligned compact metrics (requests / bytes / p95).
The path links to its live URL when the owning domain is known (new tab,
rel="noopener noreferrer nofollow"). Expects `$paths` in scope; optional
rel="noopener noreferrer nofollow"); the domain shows next to the path so the
same path of two apps stays apart. Sentinel's `__other__` overflow row reads
as "Other paths". Expects `$paths` in scope; optional
`$keyPrefix` to namespace wire:keys.
@param iterable $paths path rows: ['path', 'domain'?, 'requests', 'bytesOut', 'p95']
@@ -32,21 +34,32 @@
@php
$domain = $path['domain'] ?? null;
$pathStr = (string) ($path['path'] ?? '');
$href = $domain ? 'https://'.$domain.$pathStr : null;
$isOther = $pathStr === '__other__';
$href = $domain && ! $isOther ? 'https://'.$domain.$pathStr : null;
$requests = (int) ($path['requests'] ?? 0);
$s4xx = (int) ($path['s4xx'] ?? 0);
$s5xx = (int) ($path['s5xx'] ?? 0);
$errorRate = $requests > 0 ? round((($s4xx + $s5xx) / $requests) * 100, 1) : 0;
$width = min(100, round(($requests / $maxRequests) * 100, 1));
@endphp
<div wire:key="{{ $keyPrefix }}-{{ md5($pathStr) }}"
<div wire:key="{{ $keyPrefix }}-{{ md5($domain."\n".$pathStr) }}"
x-show="{{ $loop->index }} >= page * per && {{ $loop->index }} < (page + 1) * per"
class="flex min-h-11 items-center gap-3 border-b border-neutral-200 px-4 py-2 last:border-b-0 dark:border-white/[0.07]">
@if ($href)
<a href="{{ $href }}" target="_blank" rel="noopener noreferrer nofollow"
class="min-w-0 flex-1 truncate font-mono text-[12px] text-black hover:underline dark:text-fg">{{ $pathStr }}</a>
@if ($isOther)
<span class="min-w-0 flex-1 truncate text-[12px] text-neutral-500 dark:text-fg-dim"
title="All paths outside the top paths that Sentinel tracks">Other paths</span>
@else
<span class="min-w-0 flex-1 truncate font-mono text-[12px] text-black dark:text-fg">{{ $pathStr }}</span>
<span class="flex min-w-0 flex-1 items-baseline gap-1.5">
@if ($href)
<a href="{{ $href }}" target="_blank" rel="noopener noreferrer nofollow"
class="truncate font-mono text-[12px] text-black hover:underline dark:text-fg">{{ $pathStr }}</a>
@else
<span class="truncate font-mono text-[12px] text-black dark:text-fg">{{ $pathStr }}</span>
@endif
@if ($domain)
<span class="max-w-48 shrink-0 truncate text-[11px] text-neutral-400 dark:text-fg-faint" title="{{ $domain }}">{{ $domain }}</span>
@endif
</span>
@endif
<div class="hidden h-1 w-16 shrink-0 overflow-hidden rounded-full bg-neutral-100 sm:block dark:bg-white/[0.06]">
<div class="h-full rounded-full bg-[var(--chart-status-3xx)]" style="width: {{ $width }}%;"></div>
@@ -237,6 +237,18 @@ it('merges overviews, paths, breakdowns, and series of several keys', function (
->and($aggregator->series()[0]['s2xx'])->toBe(5);
});
it('folds the overflow path rows of all keys into one unlinked row', function () {
$aggregator = new TrafficAnalyticsAggregator([]);
$domains = ['k-api' => 'api.test', 'k-web' => 'web.test'];
$domainForKey = fn (string $key) => $domains[$key] ?? null;
$aggregator->addPaths(collect([TrafficPathData::fromSentinel(['path' => '__other__', 'app' => 'k-api', 'requests' => 7, 's4xx' => 1, 'p95' => 3])]), $domainForKey);
$aggregator->addPaths(collect([TrafficPathData::fromSentinel(['path' => '__other__', 'app' => 'k-web', 'requests' => 5, 's4xx' => 2, 'p95' => 8])]), $domainForKey);
expect($aggregator->topPaths())->toHaveCount(1)
->and($aggregator->topPaths()[0])->toMatchArray(['path' => '__other__', 'domain' => null, 'requests' => 12, 's4xx' => 3, 'p95' => 8.0]);
});
it('shows a compose application with the data of all its compose service keys', function () {
$application = makeTrafficComposeApplication();
$apiKey = $application->uuid.'-'.traefikSafeServiceNameSegment('api');