Skip to content

Commit 52363f9

Browse files
authored
Filter on issues + clear summaries (#2012)
* Add summary to find * Update summaries * Remove dump * Add filter options * Fix phpstan/cs
1 parent 3c47c1b commit 52363f9

3 files changed

Lines changed: 210 additions & 64 deletions

File tree

Lines changed: 31 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,64 @@
11
## Laravel Debugbar
22

3-
Laravel Debugbar integrates PHP Debug Bar with Laravel. It collects data from your application during each request (queries, views, routes, mail, etc.) and stores it for review.
4-
5-
### Artisan Commands
6-
7-
- `debugbar:find` - Search stored debugbar requests with filters. Useful for finding specific requests to inspect.
8-
- `debugbar:get {id}` - View details of a specific debugbar request by its ID.
9-
- `debugbar:queries {id}` - Inspect queries for a specific request, with duplicate detection, EXPLAIN, and result output.
10-
- `debugbar:clear` - Clear all stored debugbar data.
3+
Laravel Debugbar stores data from each request (queries, exceptions, views, routes, mail, etc.) for review via Artisan commands.
114

125
### Finding Requests
136

14-
Use `debugbar:find` to search through stored requests:
15-
167
@verbatim
17-
<code-snippet name="Find recent requests" lang="bash">
8+
<code-snippet name="Find requests" lang="bash">
9+
# List recent requests (shows summary with status, duration, memory, query count)
1810
php artisan debugbar:find
19-
</code-snippet>
20-
@endverbatim
2111

22-
@verbatim
23-
<code-snippet name="Find requests with filters" lang="bash">
24-
# Filter by HTTP method
25-
php artisan debugbar:find --method=POST
12+
# Filter by URI pattern (fnmatch) and/or HTTP method
13+
php artisan debugbar:find --uri="/api/*" --method=POST
2614

27-
# Filter by URI pattern (fnmatch format)
28-
php artisan debugbar:find --uri="/api/*"
15+
# Only show requests with issues (exceptions, slow queries, duplicates, errors)
16+
php artisan debugbar:find --issues --max=50
2917

30-
# Filter by IP address
31-
php artisan debugbar:find --ip=127.0.0.1
18+
# Customize issue thresholds (defaults: --min-queries=50, --min-duration=1000, --min-duplicates=2)
19+
php artisan debugbar:find --issues --min-queries=10 --min-duration=500
3220

33-
# Combine filters with pagination
34-
php artisan debugbar:find --method=GET --uri="/admin/*" --max=50 --offset=0
21+
# Threshold options also work standalone, filtering on just that criteria
22+
php artisan debugbar:find --min-queries=20
3523
</code-snippet>
3624
@endverbatim
3725

38-
### Inspecting a Request
26+
`--issues` flags: exceptions, non-2xx status, high query count, slow queries, duplicate query groups, slow request duration, and failed queries. Issue filtering applies on top of the fetched result set — increase `--max` to scan further back.
3927

40-
After finding a request ID with `debugbar:find`, inspect it with `debugbar:get` to get a summary,
41-
and add --collector=name to get the full details for that collector.
28+
### Inspecting a Request
4229

4330
@verbatim
44-
<code-snippet name="Get request details" lang="bash">
45-
# Show summary of all collectors for the latest request
31+
<code-snippet name="Inspect request" lang="bash">
32+
# Summary of all collectors (available collectors depend on config)
4633
php artisan debugbar:get latest
47-
48-
# Show summary by specific ID
4934
php artisan debugbar:get {id}
5035

51-
# View a specific collector (e.g. queries, views, route, mail)
52-
php artisan debugbar:get latest --collector=queries
53-
54-
# Output raw JSON data
55-
php artisan debugbar:get latest
36+
# Full data for a specific collector
37+
php artisan debugbar:get {id} --collector=exceptions
5638
</code-snippet>
5739
@endverbatim
5840

59-
### Inspecting Queries
41+
Use the collector name from the summary table. Common ones by issue type:
42+
- **Error/500** → `exceptions` · **Slow page** → `queries`, `time` · **Auth** → `auth`, `gate` · **Cache** → `cache`
6043

61-
Use `debugbar:queries` to view queries with duplicate detection, run EXPLAIN, or re-execute a query:
44+
### Analyzing Queries
6245

6346
@verbatim
64-
<code-snippet name="Inspect queries" lang="bash">
65-
# Show all queries for the latest request, with duplicate counts
66-
php artisan debugbar:queries latest
67-
68-
# Show all queries for a specific request, with duplicate counts
47+
<code-snippet name="Query analysis" lang="bash">
48+
# Overview with duplicate detection and slow query flags
6949
php artisan debugbar:queries {id}
7050

71-
# Show details for a specific statement (backtrace, params)
72-
php artisan debugbar:queries {id} --statement=2
73-
74-
# Run EXPLAIN on a specific query
75-
php artisan debugbar:queries {id} --statement=2 --explain
51+
# Backtrace and params for a specific statement
52+
php artisan debugbar:queries {id} --statement=N
7653

77-
# Re-execute a SELECT query and show results
78-
php artisan debugbar:queries {id} --statement=2 --result
54+
# EXPLAIN plan or re-execute a SELECT
55+
php artisan debugbar:queries {id} --statement=N --explain
56+
php artisan debugbar:queries {id} --statement=N --result
7957
</code-snippet>
8058
@endverbatim
8159

82-
### Configuration
60+
Duplicate queries are a strong N+1 signal. Use `--statement=N` to get the backtrace and find the origin.
61+
62+
### Other Commands
8363

84-
- Debugbar is enabled by default when APP_DEBUG=true. It should be disabled in production.
85-
- Collectors can be enabled/disabled individually in `config/debugbar.php` under the `collectors` key.
64+
- `debugbar:clear` — Clear all stored debugbar data.

src/Console/FindCommand.php

Lines changed: 155 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ class FindCommand extends Command
1616
{--uri= : Filter by URI, eg. /admin/*, in fnmatch format}
1717
{--max=20 : Number of results to show}
1818
{--offset=0 : Offset of the results}
19+
{--issues : Only show requests with potential issues (applies defaults for threshold options)}
20+
{--min-queries= : Flag requests with at least this many queries (default: 50 with --issues)}
21+
{--min-duration= : Flag requests slower than this in ms (default: 1000 with --issues)}
22+
{--min-duplicates= : Flag requests with at least this many duplicate query groups (default: 2 with --issues)}
1923
';
2024
protected $description = 'List the Debugbar Storage';
2125

@@ -25,6 +29,7 @@ public function handle(LaravelDebugbar $debugbar): void
2529
$storage = $debugbar->getStorage();
2630
if (!$storage) {
2731
$this->error('No Debugbar Storage found..');
32+
return;
2833
}
2934

3035
$filters = [];
@@ -52,12 +57,157 @@ public function handle(LaravelDebugbar $debugbar): void
5257
return;
5358
}
5459

55-
$result = array_map(function ($row): mixed {
60+
$hasThresholds = $this->option('min-queries') !== null
61+
|| $this->option('min-duration') !== null
62+
|| $this->option('min-duplicates') !== null;
63+
$checkIssues = $this->option('issues') || $hasThresholds;
64+
65+
// Apply defaults when --issues is used, leave null when only specific thresholds are set
66+
$minQueries = $this->option('min-queries') !== null
67+
? (int) $this->option('min-queries')
68+
: ($this->option('issues') ? 50 : null);
69+
$minDuration = $this->option('min-duration') !== null
70+
? (float) $this->option('min-duration')
71+
: ($this->option('issues') ? 1000.0 : null);
72+
$minDuplicates = $this->option('min-duplicates') !== null
73+
? (int) $this->option('min-duplicates')
74+
: ($this->option('issues') ? 2 : null);
75+
76+
$rows = [];
77+
foreach ($result as &$row) {
5678
unset($row['utime']);
57-
return $row;
58-
}, $result);
5979

60-
$latest = $result[0];
61-
$this->table(array_keys($latest), $result);
80+
$data = $storage->get($row['id']);
81+
82+
$summary = [];
83+
if (isset($data['request']['tooltip']['status'])) {
84+
$summary[] = $data['request']['tooltip']['status'];
85+
}
86+
if (isset($data['time']['duration_str'], $data['memory']['peak_usage_str'])) {
87+
$summary[] = $data['time']['duration_str'] . '/' . $data['memory']['peak_usage_str'] . ' request';
88+
} else {
89+
if (isset($data['time']['duration_str'])) {
90+
$summary[] = $data['time']['duration_str'];
91+
}
92+
if (isset($data['memory']['peak_usage_str'])) {
93+
$summary[] = $data['memory']['peak_usage_str'];
94+
}
95+
}
96+
97+
if (isset($data['exceptions']['count']) && $data['exceptions']['count']) {
98+
$summary[] = $data['exceptions']['count'] . ' exception(s)';
99+
}
100+
if (isset($data['queries']['nb_statements'])) {
101+
$summary[] = $data['queries']['nb_statements'] . ' queries in ' . $data['queries']['accumulated_duration_str'];
102+
}
103+
104+
$row['summary'] = implode(', ', $summary);
105+
106+
if ($checkIssues) {
107+
$issues = $this->detectIssues($data, $minQueries, $minDuration, $minDuplicates);
108+
if (count($issues) === 0) {
109+
continue;
110+
}
111+
$row['issues'] = implode(', ', $issues);
112+
}
113+
114+
$rows[] = $row;
115+
}
116+
117+
if (count($rows) === 0) {
118+
$this->info($checkIssues ? 'No issues found in ' . count($result) . ' scanned requests.' : 'No results found');
119+
return;
120+
}
121+
122+
if ($checkIssues) {
123+
$this->warn(count($rows) . ' of ' . count($result) . ' request(s) with potential issues:');
124+
$this->newLine();
125+
}
126+
127+
$this->table(array_keys($rows[0]), $rows);
128+
129+
if ($checkIssues) {
130+
$this->newLine();
131+
$this->line('Run <fg=cyan>php artisan debugbar:get {id}</> to inspect a request.');
132+
$this->line('Run <fg=cyan>php artisan debugbar:queries {id}</> to analyze queries.');
133+
}
134+
}
135+
136+
/**
137+
* @return list<string>
138+
*/
139+
private function detectIssues(array $data, ?int $minQueries, ?float $minDuration, ?int $minDuplicates): array
140+
{
141+
$issues = [];
142+
143+
// Exceptions
144+
$exceptionCount = $data['exceptions']['count'] ?? 0;
145+
if ($exceptionCount > 0) {
146+
$issues[] = "{$exceptionCount} exception(s)";
147+
}
148+
149+
// Non-2xx status
150+
$status = $data['__meta']['status'] ?? $data['request']['tooltip']['status_code'] ?? null;
151+
if ($status !== null && (int) $status >= 400) {
152+
$issues[] = "HTTP {$status}";
153+
}
154+
155+
// High query count
156+
$queryCount = $data['queries']['nb_statements'] ?? 0;
157+
if ($minQueries !== null && $queryCount >= $minQueries) {
158+
$issues[] = "{$queryCount} queries";
159+
}
160+
161+
// Slow queries
162+
$slowCount = 0;
163+
foreach ($data['queries']['statements'] ?? [] as $stmt) {
164+
if ($stmt['slow'] ?? false) {
165+
$slowCount++;
166+
}
167+
}
168+
if ($slowCount > 0) {
169+
$issues[] = "{$slowCount} slow " . ($slowCount === 1 ? 'query' : 'queries');
170+
}
171+
172+
// Duplicate query groups
173+
$dupGroups = $this->countDuplicateGroups($data['queries']['statements'] ?? []);
174+
if ($minDuplicates !== null && $dupGroups >= $minDuplicates) {
175+
$issues[] = "{$dupGroups} duplicate group(s)";
176+
}
177+
178+
// Slow request duration
179+
$duration = $data['time']['duration'] ?? null;
180+
if ($minDuration !== null && $duration !== null && ($duration * 1000) >= $minDuration) {
181+
$durationStr = $data['time']['duration_str'] ?? round($duration * 1000) . 'ms';
182+
$issues[] = "slow ({$durationStr})";
183+
}
184+
185+
// Failed queries
186+
$failedCount = $data['queries']['nb_failed_statements'] ?? 0;
187+
if ($failedCount > 0) {
188+
$issues[] = "{$failedCount} failed " . ($failedCount === 1 ? 'query' : 'queries');
189+
}
190+
191+
return $issues;
192+
}
193+
194+
private function countDuplicateGroups(array $statements): int
195+
{
196+
$seen = [];
197+
foreach ($statements as $stmt) {
198+
if (($stmt['type'] ?? 'query') !== 'query') {
199+
continue;
200+
}
201+
$key = $stmt['sql'] ?? '';
202+
if (isset($stmt['params']) && count($stmt['params']) > 0) {
203+
$key .= json_encode($stmt['params']);
204+
}
205+
if (isset($stmt['connection'])) {
206+
$key .= '@' . $stmt['connection'];
207+
}
208+
$seen[$key] = ($seen[$key] ?? 0) + 1;
209+
}
210+
211+
return count(array_filter($seen, fn(int $count): bool => $count > 1));
62212
}
63213
}

src/Console/GetCommand.php

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Fruitcake\LaravelDebugbar\LaravelDebugbar;
1212
use Illuminate\Console\Command;
1313
use Illuminate\Support\Arr;
14+
use Illuminate\Support\Str;
1415
use Symfony\Component\VarDumper\Cloner\VarCloner;
1516
use Symfony\Component\VarDumper\Dumper\CliDumper;
1617

@@ -69,7 +70,7 @@ private function showSummary(array $result): void
6970
continue;
7071
}
7172

72-
$badge = $data['count'] ?? '';
73+
$badge = $data['count'] ?? null;
7374
if (debugbar()->hasCollector($name)) {
7475
$collector = debugbar()->getCollector($name);
7576
if ($collector instanceof Renderable) {
@@ -80,20 +81,36 @@ private function showSummary(array $result): void
8081
}
8182
}
8283

84+
$plural = match ($name) {
85+
'caches' => 'cache events',
86+
'symfonymailer_mails' => 'mails sent',
87+
'livewire' => 'livewire components',
88+
'http_client' => 'http requests',
89+
'session' => 'session values',
90+
default => Str::plural($name),
91+
};
92+
8393
$summary = match ($name) {
84-
'request' => $data['tooltip'] ?? null,
85-
'queries' => 'Run `php artisan debugbar:queries ' . $result['__meta']['id'] . '` to see the query details',
86-
default => '',
94+
'request' => $data['tooltip'],
95+
'time' => $data['duration_str'] ?? null,
96+
'memory' => $data['peak_usage_str'] ?? null,
97+
'queries' => $data['nb_statements'] . ' queries in ' . $data['accumulated_duration_str'],
98+
'route' => ($data['as'] ?? '') . ' @ ' . ($data['file']['value'] ?? ''),
99+
default => $badge !== null ? $badge . ' ' . $plural : null,
87100
};
88101

89102
if ($summary && !is_string($summary)) {
90-
$summary = $this->dumpResult($summary);
103+
$summary = $this->dumpResult($summary, true);
91104
}
92105

93-
$rows[] = [$name, $badge, $summary];
106+
$rows[] = [$name, $summary];
94107
}
95108

96-
$this->table(['Collector', 'Badge', 'Summary'], $rows);
109+
$this->table(['Collector', 'Summary'], $rows);
110+
111+
if (isset($data['queries'])) {
112+
$this->line('Run `php artisan debugbar:queries ' . $result['__meta']['id'] . '` to see the query details');
113+
}
97114
}
98115

99116
public function dumpResult(array $result, $output = null): ?string

0 commit comments

Comments
 (0)