diff --git a/packages/hone-server/src/Mcp/Support/AggregateWindow.php b/packages/hone-server/src/Mcp/Support/AggregateWindow.php index 0b23edc..1f34a6f 100644 --- a/packages/hone-server/src/Mcp/Support/AggregateWindow.php +++ b/packages/hone-server/src/Mcp/Support/AggregateWindow.php @@ -19,12 +19,16 @@ final class AggregateWindow /** * @return list */ - public function topOffenders(string $recordType, int $days, ?string $app, ?string $deploy, string $sortMetric, int $limit): array + public function topOffenders(string $recordType, int $days, ?string $app, ?string $deploy, string $sortMetric, int $limit, bool $excludeRouteless = false): array { $this->ensureMetric($sortMetric); return $this->combinedQuery($recordType, $days, $app, $deploy) ->addSelect('normalized_key') + // Routeless keys are bare HTTP methods (e.g. "GET") from unmatched requests such as 404 scanner + // traffic. Matched routes always key as "METHOD /path", so requiring a space drops the noise that + // would otherwise dominate the slowest-endpoint ranking with an inflated percentile. + ->when($excludeRouteless, fn (Builder $query) => $query->where('normalized_key', 'like', '% %')) ->groupBy('normalized_key') ->orderByRaw($sortMetric.' DESC NULLS LAST') ->limit($limit) diff --git a/packages/hone-server/src/Mcp/Tools/Concerns/HandlesSlowMetricTool.php b/packages/hone-server/src/Mcp/Tools/Concerns/HandlesSlowMetricTool.php index dff969e..b38db43 100644 --- a/packages/hone-server/src/Mcp/Tools/Concerns/HandlesSlowMetricTool.php +++ b/packages/hone-server/src/Mcp/Tools/Concerns/HandlesSlowMetricTool.php @@ -43,6 +43,7 @@ public function handle(Request $request): Response $validated['deploy'] ?? null, $metric, $limit, + $this->excludesRoutelessKeys(), ), ]); } @@ -62,4 +63,12 @@ public function schema(JsonSchema $schema): array } abstract protected function recordType(): string; + + /** + * Whether to drop routeless keys (bare HTTP methods from unmatched requests) from the ranking. + */ + protected function excludesRoutelessKeys(): bool + { + return false; + } } diff --git a/packages/hone-server/src/Mcp/Tools/SlowRequestsTool.php b/packages/hone-server/src/Mcp/Tools/SlowRequestsTool.php index 2d02edf..78ce58f 100644 --- a/packages/hone-server/src/Mcp/Tools/SlowRequestsTool.php +++ b/packages/hone-server/src/Mcp/Tools/SlowRequestsTool.php @@ -21,4 +21,9 @@ protected function recordType(): string { return 'request'; } + + protected function excludesRoutelessKeys(): bool + { + return true; + } } diff --git a/packages/hone-server/tests/McpMetricToolsTest.php b/packages/hone-server/tests/McpMetricToolsTest.php index 9e10ea6..88eb6e7 100644 --- a/packages/hone-server/tests/McpMetricToolsTest.php +++ b/packages/hone-server/tests/McpMetricToolsTest.php @@ -96,6 +96,38 @@ function seedAggregateBucket( ->assertDontSee('billing-query'); }); +it('excludes routeless request keys from the slow requests ranking', function (): void { + $today = Carbon::now('UTC')->startOfDay(); + + // Unmatched 404 traffic collapses to a bare method key and would otherwise win the p95 ranking. + seedAggregateBucket('checkout', 'request', 'GET', $today, 172, 800, 4000, 2900, 3900); + seedAggregateBucket('checkout', 'request', 'GET /posts/{post}', $today, 72, 300, 700, 560, 690); + + $rows = app(AggregateWindow::class)->topOffenders('request', 7, 'checkout', null, 'p95', 10, true); + + expect($rows)->toHaveCount(1) + ->and($rows[0]['normalized_key'])->toBe('GET /posts/{post}'); + + HoneMcpServer::tool(SlowRequestsTool::class, [ + 'app' => 'checkout', + 'metric' => 'p95', + ]) + ->assertOk() + ->assertSee('GET /posts/{post}') + ->assertDontSee('"GET"'); +}); + +it('still ranks routeless keys for non-request metrics', function (): void { + $today = Carbon::now('UTC')->startOfDay(); + + seedAggregateBucket('checkout', 'query', 'select-users-by-id', $today, 10, 30, 80, 75, 79); + + $rows = app(AggregateWindow::class)->topOffenders('query', 7, 'checkout', null, 'p95', 10); + + expect($rows)->toHaveCount(1) + ->and($rows[0]['normalized_key'])->toBe('select-users-by-id'); +}); + it('returns generic query metrics over the requested window', function (): void { $today = Carbon::now('UTC')->startOfDay();