Skip to content

Commit be12556

Browse files
authored
Merge pull request #24 from Naoray/fix/request-file-tracing
wip
2 parents 13b612e + d99cda8 commit be12556

File tree

8 files changed

+329
-54
lines changed

8 files changed

+329
-54
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ This will copy the templates to `resources/views/vendor/github-monolog/` where y
119119
- `comment.md`: Template for comments on existing issues
120120
- `previous_exception.md`: Template for previous exceptions in the chain
121121

122-
> **Important**:
122+
> **Important**:
123123
> - The templates use HTML comments as section markers (e.g. `<!-- stacktrace:start -->` and `<!-- stacktrace:end -->`). These markers are used to intelligently remove empty sections from the rendered output. Please keep these markers intact when customizing the templates.
124124
> - If you've previously published and customized templates, you may need to republish them to get the latest structure with triage headers and collapsible sections. Compare your custom templates with the new defaults and migrate any customizations.
125125

resources/views/issue.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
## Triage Information
2-
31
- **Level:** {level}
42
- **Exception:** {class}
5-
- **Signature:** {signature}
63
- **Timestamp:** {timestamp}
74
- **Environment:** {environment_name}
85
- **Route:** {route_summary}

src/Deduplication/DefaultSignatureGenerator.php

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,7 @@ private function generateFromMessage(LogRecord $record): string
3535
'message' => $this->normalizeMessage($record->message),
3636
'channel' => $record->channel,
3737
'level' => $record->level->name,
38-
// optionally include stable keys if you store them:
39-
'route' => data_get($record->context, 'request.route'),
40-
'job' => data_get($record->context, 'job.class'),
41-
'command' => data_get($record->context, 'command.name'),
38+
'context' => $this->getContextIdentifier($record),
4239
];
4340

4441
return hash('sha256', json_encode($stable, JSON_UNESCAPED_SLASHES));
@@ -56,8 +53,8 @@ private function generateFromException(Throwable $exception, LogRecord $record):
5653
// prefer in-app origin; avoid line numbers
5754
'file' => $frame ? $this->normalizePath($frame['file'] ?? '') : $this->normalizePath($exception->getFile()),
5855
'func' => $frame ? (($frame['class'] ?? '').($frame['type'] ?? '').($frame['function'] ?? '')) : '',
59-
// add route grouping if available (huge for Laravel HTTP)
60-
'route' => data_get($record->context, 'request.route') ?? data_get($record->context, 'route.action'),
56+
'message' => $this->normalizeMessage($exception->getMessage()),
57+
'context' => $this->getContextIdentifier($record),
6158
];
6259

6360
return hash('sha256', json_encode($parts, JSON_UNESCAPED_SLASHES));
@@ -113,21 +110,54 @@ private function normalizePath(string $path): string
113110
return str_replace('\\', '/', $path);
114111
}
115112

113+
/**
114+
* Get context identifier with fallback chain: route → job → command
115+
*
116+
* @return string|null
117+
*/
118+
private function getContextIdentifier(LogRecord $record): ?string
119+
{
120+
// Try route first
121+
$routeData = data_get($record->context, 'request.route') ?? data_get($record->context, 'route');
122+
123+
if (is_array($routeData)) {
124+
return $routeData['name'] ?? $routeData['uri'] ?? null;
125+
}
126+
127+
if (is_string($routeData)) {
128+
return $routeData;
129+
}
130+
131+
// Fall back to job class
132+
$jobClass = data_get($record->context, 'job.class');
133+
if ($jobClass) {
134+
return $jobClass;
135+
}
136+
137+
// Fall back to command name
138+
$commandName = data_get($record->context, 'command.name');
139+
if ($commandName) {
140+
return $commandName;
141+
}
142+
143+
return null;
144+
}
145+
116146
/**
117147
* Normalize a message by replacing unstable values (UUIDs, large numbers)
118148
*/
119149
private function normalizeMessage(string $message): string
120150
{
121151
// Replace UUIDs
122-
$message = preg_replace(
152+
$result = preg_replace(
123153
'/\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/i',
124154
'{uuid}',
125155
$message
126156
);
127157

128158
// Replace long numbers (IDs, timestamps)
129-
$message = preg_replace('/\b\d{6,}\b/', '{num}', $message);
159+
$result = preg_replace('/\b\d{6,}\b/', '{num}', $result ?? $message);
130160

131-
return $message;
161+
return is_string($result) ? $result : $message;
132162
}
133163
}

src/Issues/Formatters/ExceptionFormatter.php

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ public function formatTitle(Throwable $exception, string $level): string
6565

6666
private function formatMessage(string $message): string
6767
{
68+
// Remove exception class prefix if present (e.g., "RuntimeException: ")
69+
if (preg_match('/^[A-Z][a-zA-Z0-9_\\\\]+Exception: (.+)$/s', $message, $matches)) {
70+
$message = trim($matches[1]);
71+
}
72+
6873
if (! str_contains($message, 'Stack trace:')) {
6974
return $message;
7075
}
@@ -93,26 +98,40 @@ private function formatExceptionString(string $exceptionString): array
9398
$stackTrace = '';
9499

95100
// Try to extract the message and stack trace
96-
if (preg_match('/^(.*?)(?:Stack trace:|#\d+ \/)/', $exceptionString, $matches)) {
97-
$message = trim($matches[1]);
101+
if (! preg_match('/^(.*?)(?:Stack trace:|#\d+ \/)/', $exceptionString, $matches)) {
102+
$header = sprintf(
103+
'[%s] Exception: %s at unknown:0',
104+
now()->format('Y-m-d H:i:s'),
105+
$message
106+
);
107+
108+
return [
109+
'message' => $this->formatMessage($message),
110+
'simplified_stack_trace' => $header."\n[stacktrace]\n".$this->stackTraceFormatter->format($stackTrace, true),
111+
'full_stack_trace' => $header."\n[stacktrace]\n".$this->stackTraceFormatter->format($stackTrace, false),
112+
];
113+
}
114+
115+
$message = trim($matches[1]);
116+
117+
// Remove exception class prefix if present (e.g., "RuntimeException: ")
118+
if (preg_match('/^[A-Z][a-zA-Z0-9_\\\\]+Exception: (.+)$/s', $message, $classMatches)) {
119+
$message = trim($classMatches[1]);
120+
}
121+
122+
// Remove file/line info if present
123+
if (preg_match('/^(.*) in \/[^\s]+(?:\.php)? on line \d+$/s', $message, $fileMatches)) {
124+
$message = trim($fileMatches[1]);
125+
}
126+
127+
// Extract stack trace
128+
$traceStart = strpos($exceptionString, 'Stack trace:');
129+
if ($traceStart === false && preg_match('/#\d+ \//', $exceptionString, $traceMatches, PREG_OFFSET_CAPTURE)) {
130+
$traceStart = $traceMatches[0][1];
131+
}
98132

99-
// Remove file/line info if present
100-
if (preg_match('/^(.*) in \/[^\s]+(?:\.php)? on line \d+$/s', $message, $fileMatches)) {
101-
$message = trim($fileMatches[1]);
102-
}
103-
104-
// Extract stack trace
105-
$traceStart = strpos($exceptionString, 'Stack trace:');
106-
if ($traceStart === false) {
107-
// Find the first occurrence of a stack frame pattern
108-
if (preg_match('/#\d+ \//', $exceptionString, $matches, PREG_OFFSET_CAPTURE)) {
109-
$traceStart = $matches[0][1];
110-
}
111-
}
112-
113-
if ($traceStart !== false) {
114-
$stackTrace = substr($exceptionString, $traceStart);
115-
}
133+
if ($traceStart !== false) {
134+
$stackTrace = substr($exceptionString, $traceStart);
116135
}
117136

118137
$header = sprintf(

src/Issues/TemplateRenderer.php

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,14 @@ private function buildReplacements(LogRecord $record, ?string $signature): array
7676
$exceptionDetails = $this->exceptionFormatter->format($record);
7777
$exceptionData = $record->context['exception'] ?? null;
7878

79-
$message = $exceptionDetails['message'] ?? $record->message;
79+
// If exceptionDetails is empty but record message contains stack trace, try to format it
80+
if (empty($exceptionDetails) &&
81+
(str_contains($record->message, 'Stack trace:') || preg_match('/#\d+ \//', $record->message))) {
82+
$tempRecord = $record->with(context: array_merge($record->context, ['exception' => $record->message]));
83+
$exceptionDetails = $this->exceptionFormatter->format($tempRecord);
84+
}
85+
86+
$message = $exceptionDetails['message'] ?? $this->extractMessageFromRecord($record->message);
8087
$class = $this->resolveExceptionClass($exception, $exceptionData);
8188

8289
return [
@@ -130,22 +137,35 @@ private function formatTimestamp(LogRecord $record): string
130137
private function formatRouteSummary(LogRecord $record): string
131138
{
132139
$request = $record->context['request'] ?? null;
140+
$route = $record->context['route'] ?? null;
133141

134-
if (! is_array($request)) {
135-
return '';
136-
}
142+
// Try to get method and path from request context first
143+
if (is_array($request)) {
144+
$method = $request['method'] ?? '';
145+
$url = $request['url'] ?? '';
137146

138-
$method = $request['method'] ?? '';
139-
$url = $request['url'] ?? '';
147+
if ($method !== '' && $url !== '') {
148+
$parsedUrl = parse_url($url);
149+
$path = $parsedUrl['path'] ?? '/';
140150

141-
if ($method === '' || $url === '') {
142-
return '';
151+
return strtoupper($method).' '.$path;
152+
}
143153
}
144154

145-
$parsedUrl = parse_url($url);
146-
$path = $parsedUrl['path'] ?? '/';
155+
// Fallback to route context if request context is missing or incomplete
156+
if (is_array($route)) {
157+
$methods = $route['methods'] ?? [];
158+
$uri = $route['uri'] ?? '';
159+
160+
if (! empty($methods) && $uri !== '') {
161+
$method = is_array($methods) ? ($methods[0] ?? '') : $methods;
162+
if ($method !== '') {
163+
return strtoupper($method).' /'.$uri;
164+
}
165+
}
166+
}
147167

148-
return strtoupper($method).' '.$path;
168+
return '';
149169
}
150170

151171
private function formatUserSummary(LogRecord $record): string
@@ -175,4 +195,32 @@ private function extractEnvironmentName(LogRecord $record): string
175195

176196
return $environment['APP_ENV'] ?? $environment['app_env'] ?? '';
177197
}
198+
199+
/**
200+
* Extract just the message part from a record message that may contain stack trace.
201+
*/
202+
private function extractMessageFromRecord(string $message): string
203+
{
204+
// If message contains stack trace, extract just the message part
205+
if (str_contains($message, 'Stack trace:') || preg_match('/#\d+ \//', $message)) {
206+
// Try to extract the message before the stack trace
207+
if (preg_match('/^(.*?)(?:Stack trace:|#\d+ \/)/', $message, $matches)) {
208+
$extracted = trim($matches[1]);
209+
210+
// Remove exception class prefix (e.g., "RuntimeException: ")
211+
if (preg_match('/^[A-Z][a-zA-Z0-9_\\\\]+Exception: (.+)$/s', $extracted, $classMatches)) {
212+
$extracted = trim($classMatches[1]);
213+
}
214+
215+
// Remove file/line info if present (e.g., "message in /path/to/file.php:123")
216+
if (preg_match('/^(.*) in \/[^\s]+(?:\.php)?(?: on line \d+)?$/s', $extracted, $fileMatches)) {
217+
return trim($fileMatches[1]);
218+
}
219+
220+
return $extracted;
221+
}
222+
}
223+
224+
return $message;
225+
}
178226
}

src/Tracing/RequestDataCollector.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,20 @@ private function formatFiles(array $files): array
5252
return $this->formatFiles($file);
5353
}
5454

55+
/** @var \Illuminate\Http\UploadedFile $file */
56+
$name = $file->getClientOriginalName();
57+
$mimeType = $file->getMimeType();
58+
59+
try {
60+
$size = $file->getSize();
61+
} catch (\Throwable $e) {
62+
$size = null;
63+
}
64+
5565
return [
56-
'name' => $file->getClientOriginalName(),
57-
'size' => $file->getSize(),
58-
'mime_type' => $file->getMimeType(),
66+
'name' => $name,
67+
'size' => $size,
68+
'mime_type' => $mimeType,
5969
];
6070
})
6171
->toArray();

0 commit comments

Comments
 (0)