Skip to content

Commit 109a0a4

Browse files
committed
added X-Tracy-Agent header detection for AI agents
1 parent 9ed3586 commit 109a0a4

File tree

13 files changed

+681
-14
lines changed

13 files changed

+681
-14
lines changed

src/Tracy/Bar/Bar.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ public function renderLoader(DeferredContent $defer): void
6565
}
6666

6767

68+
/**
69+
* Renders debug bar as plain text (markdown).
70+
*/
71+
public function renderAsText(): void
72+
{
73+
$time = microtime(true) - Debugger::$time;
74+
$memory = memory_get_peak_usage() / 1_000_000;
75+
$warningsPanel = $this->panels['Tracy:warnings'] ?? null;
76+
require __DIR__ . '/dist/markdown.phtml';
77+
}
78+
79+
6880
/**
6981
* Renders debug bar.
7082
*/
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{*
2+
* Tracy Bar rendered as HTML comment (for AI agents).
3+
*
4+
* @var float $time
5+
* @var float $memory
6+
* @var ?DefaultBarPanel $warningsPanel
7+
*}
8+
9+
<!-- tracy
10+
Tracy Bar | {=number_format($time * 1000, 1)} ms | {=number_format($memory, 2)} MB
11+
{if $warningsPanel instanceof \Tracy\DefaultBarPanel && $warningsPanel->data}
12+
13+
## Warnings
14+
15+
{foreach $warningsPanel->data as $key => $count}
16+
{do [$file, $line, $message] = explode('|', $key, 3)}
17+
- {=$message . ' in ' . $file . ':' . $line . ($count > 1 ? " (×" . $count . ')' : '')}
18+
{/foreach}
19+
{/if}
20+
-->

src/Tracy/Bar/dist/bar-text.phtml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
declare(strict_types=1);
3+
?>
4+
5+
<!-- tracy
6+
Tracy Bar | <?= number_format($time * 1000, 1) ?> ms | <?= number_format($memory, 2) ?> MB
7+
<?php if ($warningsPanel instanceof \Tracy\DefaultBarPanel && $warningsPanel->data): ?>
8+
9+
## Warnings
10+
11+
<?php foreach ($warningsPanel->data as $key => $count): ?>
12+
<?php [$file, $line, $message] = explode('|', $key, 3) ?>
13+
- <?= $message . ' in ' . $file . ':' . $line . ($count > 1 ? "" . $count . ')' : '') ?>
14+
15+
<?php endforeach ?>
16+
<?php endif ?>
17+
-->

src/Tracy/Bar/dist/markdown.phtml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
declare(strict_types=1);
3+
?>
4+
5+
<!-- tracy
6+
Tracy Bar | <?= number_format($time * 1000, 1) ?> ms | <?= number_format($memory, 2) ?> MB
7+
<?php if ($warningsPanel instanceof \Tracy\DefaultBarPanel && $warningsPanel->data): ?>
8+
9+
## Warnings
10+
11+
<?php foreach ($warningsPanel->data as $key => $count): ?>
12+
<?php [$file, $line, $message] = explode('|', $key, 3) ?>
13+
- <?= $message . ' in ' . $file . ':' . $line . ($count > 1 ? "" . $count . ')' : '') ?>
14+
15+
<?php endforeach ?>
16+
<?php endif ?>
17+
-->

src/Tracy/BlueScreen/BlueScreen.php

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,36 @@ public function render(\Throwable $exception): void
124124
}
125125

126126

127+
private static function exceptionTitle(\Throwable $exception): string
128+
{
129+
return $exception instanceof \ErrorException
130+
? Helpers::errorTypeToString($exception->getSeverity())
131+
: get_debug_type($exception);
132+
}
133+
134+
135+
/**
136+
* @param array<int, array<string, mixed>> $trace
137+
* @return array<int, array<string, mixed>>
138+
* @internal
139+
*/
140+
public static function cleanStackTrace(array $trace): array
141+
{
142+
if (in_array($trace[0]['class'] ?? null, [DevelopmentStrategy::class, ProductionStrategy::class], true)) {
143+
array_shift($trace);
144+
}
145+
146+
if (
147+
($trace[0]['class'] ?? null) === Debugger::class
148+
&& in_array($trace[0]['function'], ['shutdownHandler', 'errorHandler'], true)
149+
) {
150+
array_shift($trace);
151+
}
152+
153+
return $trace;
154+
}
155+
156+
127157
/** @internal */
128158
public function renderToAjax(\Throwable $exception, DeferredContent $defer): void
129159
{
@@ -161,9 +191,7 @@ private function renderTemplate(\Throwable $exception, string $template, bool $t
161191
$showEnvironment = $this->showEnvironment && (!str_contains($exception->getMessage(), 'Allowed memory size'));
162192
$info = array_filter($this->info);
163193
$source = Helpers::getSource();
164-
$title = $exception instanceof \ErrorException
165-
? Helpers::errorTypeToString($exception->getSeverity())
166-
: get_debug_type($exception);
194+
$title = self::exceptionTitle($exception);
167195
$lastError = $exception instanceof \ErrorException || $exception instanceof \Error
168196
? null
169197
: error_get_last();
@@ -207,6 +235,33 @@ private function renderTemplate(\Throwable $exception, string $template, bool $t
207235
}
208236

209237

238+
/**
239+
* Renders blue screen as plain text (markdown).
240+
*/
241+
public function renderAsText(\Throwable $exception): void
242+
{
243+
if (!headers_sent()) {
244+
header('Content-Type: text/plain; charset=UTF-8');
245+
}
246+
247+
$showEnvironment = $this->showEnvironment && !str_contains($exception->getMessage(), 'Allowed memory size');
248+
$lastError = $exception instanceof \ErrorException || $exception instanceof \Error
249+
? null
250+
: error_get_last();
251+
$source = Helpers::getSource();
252+
253+
if (function_exists('apache_request_headers')) {
254+
$httpHeaders = apache_request_headers();
255+
} else {
256+
$httpHeaders = array_filter($_SERVER, fn($k) => str_starts_with($k, 'HTTP_'), ARRAY_FILTER_USE_KEY);
257+
$httpHeaders = array_combine(array_map(fn($k) => strtolower(strtr(substr($k, 5), '_', '-')), array_keys($httpHeaders)), $httpHeaders);
258+
}
259+
260+
$blueScreen = $this;
261+
require __DIR__ . '/dist/markdown.phtml';
262+
}
263+
264+
210265
/**
211266
* @return list<\stdClass>
212267
*/
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
{*
2+
* BlueScreen rendered as markdown text (for AI agents and CLI).
3+
*
4+
* @var Throwable $exception
5+
* @var bool $showEnvironment
6+
* @var ?array $lastError
7+
* @var array $httpHeaders
8+
* @var string $source
9+
* @var BlueScreen $blueScreen
10+
*}
11+
{use Tracy\BlueScreen}
12+
{use Tracy\Debugger}
13+
{use Tracy\Dumper}
14+
{use Tracy\Helpers}
15+
This is an error page generated by Tracy (https://tracy.nette.org).
16+
17+
{do $_blocks = []}
18+
{define renderSource $file, $line}
19+
{do $srcLines = @file($file)}
20+
{if $srcLines}
21+
{do $start = max(0, $line - 6)}
22+
{do $end = min(count($srcLines), $line + 4)}
23+
24+
```php
25+
{foreach array_slice($srcLines, $start, $end - $start, true) as $i => $srcLine}
26+
{=sprintf('%s%4d | %s', $i + 1 === $line ? ' > ' : ' ', $i + 1, rtrim($srcLine))}
27+
{/foreach}
28+
```
29+
{do $mapped = Debugger::mapSource($file, $line)}
30+
{if $mapped && @is_file($mapped['file'])}
31+
{do $mLine = $mapped['line']}
32+
33+
Mapped source: {=$mapped['file'] . ($mLine ? ':' . $mLine : '')}
34+
{do $mappedLines = @file($mapped['file'])}
35+
{if $mappedLines && $mLine}
36+
{do $mStart = max(0, $mLine - 6)}
37+
{do $mEnd = min(count($mappedLines), $mLine + 4)}
38+
39+
```
40+
{foreach array_slice($mappedLines, $mStart, $mEnd - $mStart, true) as $i => $srcLine}
41+
{=sprintf('%s%4d | %s', $i + 1 === $mLine ? ' > ' : ' ', $i + 1, rtrim($srcLine))}
42+
{/foreach}
43+
```
44+
{/if}
45+
{/if}
46+
{/if}
47+
{/define}
48+
{foreach Helpers::getExceptionChain($exception) as $i => $ex}
49+
{do $title = $ex instanceof \ErrorException ? Helpers::errorTypeToString($ex->getSeverity()) : get_debug_type($ex)}
50+
{do $code = $ex->getCode() ? ' #' . $ex->getCode() : ''}
51+
{if $i === 0}
52+
# {$title}: {$ex->getMessage()}{$code}
53+
{else}
54+
55+
## Caused by: {$title}: {$ex->getMessage()}{$code}
56+
{/if}
57+
58+
in {$ex->getFile()}:{$ex->getLine()}
59+
{include renderSource $ex->getFile(), $ex->getLine()}
60+
{do $base = new \Exception}
61+
{if count(get_mangled_object_vars($ex)) > count(get_mangled_object_vars($base))}
62+
63+
## Exception Properties
64+
65+
{=Dumper::toText($ex, [Dumper::DEPTH => $blueScreen->maxDepth, Dumper::TRUNCATE => $blueScreen->maxLength, Dumper::ITEMS => $blueScreen->maxItems, Dumper::SCRUBBER => $blueScreen->scrubber, Dumper::KEYS_TO_HIDE => $blueScreen->keysToHide])}
66+
{/if}
67+
{do $stack = BlueScreen::cleanStackTrace($ex->getTrace())}
68+
{if $stack}
69+
70+
## Stack Trace
71+
72+
{foreach $stack as $j => $row}
73+
{do $call = isset($row['class']) ? $row['class'] . ($row['type'] ?? '::') . $row['function'] . '()' : $row['function'] . '()'}
74+
{do $location = isset($row['file']) ? $row['file'] . ':' . ($row['line'] ?? '?') : 'inner-code'}
75+
{=$j + 1}. {$call} {$location}
76+
{if !empty($row['args'])}
77+
{try}
78+
{do $params = isset($row['class']) ? (new \ReflectionMethod($row['class'], $row['function']))->getParameters() : (new \ReflectionFunction($row['function']))->getParameters()}
79+
{rollback}
80+
{do $params = []}
81+
{/try}
82+
{foreach $row['args'] as $k => $v}
83+
{do $name = isset($params[$k]) && !$params[$k]->isVariadic() ? '$' . $params[$k]->getName() : '#' . $k}
84+
{=' ' . $name . ' = ' . Dumper::toText($v, [Dumper::DEPTH => 2, Dumper::TRUNCATE => 100])}
85+
{/foreach}
86+
{/if}
87+
{/foreach}
88+
{do $shown = 0}
89+
{foreach $stack as $row}
90+
{continueIf $shown >= 2}
91+
{if isset($row['file'], $row['line']) && @is_file($row['file'])}
92+
{include renderSource $row['file'], $row['line']}
93+
{do $shown++}
94+
{/if}
95+
{/foreach}
96+
{/if}
97+
{/foreach}
98+
{if $lastError}
99+
100+
## Last Muted Error
101+
102+
{=Helpers::errorTypeToString($lastError['type'])}: {$lastError['message']}
103+
{if isset($lastError['file'])}
104+
in {$lastError['file']}:{=$lastError['line'] ?? '?'}
105+
{if @is_file($lastError['file']) && isset($lastError['line'])}
106+
{include renderSource $lastError['file'], $lastError['line']}
107+
{/if}
108+
{/if}
109+
{/if}
110+
{if !Helpers::isCli() && isset($_SERVER['REQUEST_METHOD'])}
111+
112+
## HTTP Request
113+
114+
{$_SERVER['REQUEST_METHOD']} {$source}
115+
{if $httpHeaders}
116+
117+
### Headers
118+
119+
{foreach $httpHeaders as $k => $v}
120+
- {$k}: {$v}
121+
{/foreach}
122+
{/if}
123+
{if !empty($_GET)}
124+
125+
### $_GET
126+
127+
{=Dumper::toText($_GET, [Dumper::DEPTH => 3, Dumper::TRUNCATE => 200])}
128+
{/if}
129+
{if !empty($_POST)}
130+
131+
### $_POST
132+
133+
{=Dumper::toText($_POST, [Dumper::DEPTH => 3, Dumper::TRUNCATE => 200, Dumper::SCRUBBER => $blueScreen->scrubber, Dumper::KEYS_TO_HIDE => $blueScreen->keysToHide])}
134+
{/if}
135+
{if !empty($_COOKIE)}
136+
137+
### $_COOKIE
138+
139+
{=Dumper::toText($_COOKIE, [Dumper::DEPTH => 3, Dumper::TRUNCATE => 200, Dumper::SCRUBBER => $blueScreen->scrubber, Dumper::KEYS_TO_HIDE => $blueScreen->keysToHide])}
140+
{/if}
141+
{/if}
142+
{if $showEnvironment}
143+
144+
## Environment
145+
146+
| Key | Value |
147+
|-----|-------|
148+
| PHP | {=PHP_VERSION . ' (' . (PHP_ZTS ? 'TS' : 'NTS') . ')'} |
149+
{if isset($_SERVER['REQUEST_METHOD'])}
150+
| Method | {$_SERVER['REQUEST_METHOD']} |
151+
{/if}
152+
| Source | {$source} |
153+
{if isset($_SERVER['SERVER_SOFTWARE'])}
154+
| Server | {$_SERVER['SERVER_SOFTWARE']} |
155+
{/if}
156+
| Tracy | {=Debugger::Version} |
157+
{/if}

src/Tracy/BlueScreen/assets/section-stack-exception.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,7 @@
1212
* @var BlueScreen $blueScreen
1313
*/
1414

15-
$stack = $ex->getTrace();
16-
if (in_array($stack[0]['class'] ?? null, [DevelopmentStrategy::class, ProductionStrategy::class], true)) {
17-
array_shift($stack);
18-
}
19-
if (
20-
($stack[0]['class'] ?? null) === Debugger::class
21-
&& in_array($stack[0]['function'], ['shutdownHandler', 'errorHandler'], true)
22-
) {
23-
array_shift($stack);
24-
}
15+
$stack = BlueScreen::cleanStackTrace($ex->getTrace());
2516

2617
$expanded = null;
2718
if (

0 commit comments

Comments
 (0)