Skip to content

Commit 78256e2

Browse files
authored
feat: add copyable error reports to detailed exception pages (#10220)
1 parent e7393ea commit 78256e2

7 files changed

Lines changed: 398 additions & 3 deletions

File tree

app/Views/errors/html/debug.css

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,16 @@ p.lead {
4646
.header .container {
4747
padding: 1rem;
4848
}
49+
.header-title {
50+
align-items: flex-start;
51+
display: flex;
52+
gap: 1rem;
53+
justify-content: space-between;
54+
}
4955
.header h1 {
5056
font-size: 2.5rem;
5157
font-weight: 500;
58+
min-width: 0;
5259
}
5360
.header p {
5461
font-size: 1.2rem;
@@ -65,8 +72,58 @@ p.lead {
6572
display: inline;
6673
}
6774

75+
.error-report {
76+
flex: 0 0 auto;
77+
margin-top: 0.35rem;
78+
}
79+
80+
.error-report-button {
81+
align-items: center;
82+
background: rgba(255,255,255,0.35);
83+
border: 1px solid rgba(0,0,0,0.14);
84+
border-radius: 5px;
85+
box-sizing: border-box;
86+
color: var(--main-text-color);
87+
cursor: pointer;
88+
display: inline-flex;
89+
font-size: 0.82rem;
90+
font-weight: 500;
91+
gap: 0.35rem;
92+
height: 1.875rem;
93+
justify-content: center;
94+
line-height: 1;
95+
padding: 0 0.65rem;
96+
transition: background-color 160ms ease-in-out, border-color 160ms ease-in-out, color 160ms ease-in-out, box-shadow 160ms ease-in-out;
97+
white-space: nowrap;
98+
width: 7.15rem;
99+
}
100+
101+
.error-report-button:hover {
102+
background: rgba(255,255,255,0.6);
103+
border-color: rgba(0,0,0,0.22);
104+
color: var(--dark-text-color);
105+
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
106+
}
107+
108+
.error-report-button:focus-visible {
109+
border-color: rgba(0,0,0,0.35);
110+
outline: 0;
111+
box-shadow: 0 0 0 2px rgba(220,72,20,0.16);
112+
}
113+
114+
.error-report-button:active {
115+
background: rgba(255,255,255,0.75);
116+
}
117+
118+
.error-report-icon {
119+
flex: 0 0 auto;
120+
height: 0.72rem;
121+
width: 0.72rem;
122+
}
123+
68124
.environment {
69125
background: var(--brand-primary-color);
126+
box-sizing: border-box;
70127
color: var(--main-bg-color);
71128
text-align: center;
72129
padding: calc(4px + 0.2083vw);
@@ -75,6 +132,26 @@ p.lead {
75132
position: fixed;
76133
}
77134

135+
@media (max-width: 40rem) {
136+
.header {
137+
margin-top: 0;
138+
}
139+
140+
.header p {
141+
font-size: 1.1rem;
142+
line-height: 1.45;
143+
margin-top: 0.35rem;
144+
overflow-wrap: anywhere;
145+
}
146+
147+
.environment {
148+
font-size: 0.9rem;
149+
line-height: 1.25;
150+
padding: 0.45rem 0.75rem;
151+
position: static;
152+
}
153+
}
154+
78155
.source {
79156
background: #343434;
80157
color: var(--light-text-color);

app/Views/errors/html/debug.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,29 @@ function toggle(elem)
114114

115115
return false;
116116
}
117+
118+
function copyErrorReport(reportId, button)
119+
{
120+
if (! navigator.clipboard || ! window.isSecureContext)
121+
{
122+
return false;
123+
}
124+
125+
var report = document.getElementById(reportId);
126+
navigator.clipboard.writeText(report.value).then(function () {
127+
showCopiedButton(button);
128+
});
129+
130+
return false;
131+
}
132+
133+
function showCopiedButton(button)
134+
{
135+
button.defaultHtml = button.defaultHtml || button.innerHTML;
136+
button.innerHTML = 'Copied!';
137+
138+
window.clearTimeout(button.copyResetTimer);
139+
button.copyResetTimer = window.setTimeout(function () {
140+
button.innerHTML = button.defaultHtml;
141+
}, 1500);
142+
}

app/Views/errors/html/error_exception.php

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use CodeIgniter\CodeIgniter;
44

55
$errorId = uniqid('error', true);
6+
$copyableErrorReportId = $errorId . 'copyableErrorReport';
67
?>
78
<!doctype html>
89
<html>
@@ -30,7 +31,38 @@
3031
Environment: <?= ENVIRONMENT ?>
3132
</div>
3233
<div class="container">
33-
<h1><?= esc($title), esc($exception->getCode() ? ' #' . $exception->getCode() : '') ?></h1>
34+
<div class="header-title">
35+
<h1><?= esc($title), esc($exception->getCode() ? ' #' . $exception->getCode() : '') ?></h1>
36+
<div class="error-report">
37+
<button
38+
class="error-report-button"
39+
type="button"
40+
aria-live="polite"
41+
onclick="return copyErrorReport('<?= esc($copyableErrorReportId, 'attr') ?>', this);"
42+
>
43+
<svg
44+
aria-hidden="true"
45+
class="error-report-icon"
46+
xmlns="http://www.w3.org/2000/svg"
47+
viewBox="0 0 24 24"
48+
fill="none"
49+
stroke="currentColor"
50+
stroke-width="2"
51+
stroke-linecap="round"
52+
stroke-linejoin="round"
53+
>
54+
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
55+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
56+
</svg>
57+
<span class="error-report-label">Copy Details</span>
58+
</button>
59+
<textarea
60+
id="<?= esc($copyableErrorReportId, 'attr') ?>"
61+
readonly
62+
hidden
63+
><?php include __DIR__ . '/error_report.php'; ?></textarea>
64+
</div>
65+
</div>
3466
<p>
3567
<?= nl2br(esc($exception->getMessage())) ?>
3668
<a href="https://www.duckduckgo.com/?q=<?= urlencode($title . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $exception->getMessage())) ?>"
@@ -342,8 +374,9 @@
342374

343375
<!-- Response -->
344376
<?php
345-
$response = service('response');
346-
$response->setStatusCode(http_response_code());
377+
$response = service('response');
378+
$responseStatusCode = http_response_code();
379+
$response->setStatusCode($responseStatusCode === false || $responseStatusCode === 0 ? $code : $responseStatusCode);
347380
?>
348381
<div class="content" id="response">
349382
<table>
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
use CodeIgniter\CodeIgniter;
4+
use CodeIgniter\HTTP\IncomingRequest;
5+
use CodeIgniter\HTTP\URI;
6+
7+
$reportMessage = str_replace(["\r\n", "\r"], "\n", $message);
8+
$reportTitle = trim($reportMessage);
9+
$reportTitle = $reportTitle === '' ? $title : explode("\n", $reportTitle, 2)[0];
10+
$messageLines = str_contains($reportMessage, "\n");
11+
12+
$reportResponse = service('response', null, false);
13+
$reportResponse->setStatusCode($code);
14+
15+
$report = [
16+
'# ' . $reportTitle,
17+
'',
18+
'## Exception',
19+
'',
20+
'- Type: ' . $type,
21+
'- Status Code: ' . $code,
22+
'- Status: ' . $reportResponse->getReasonPhrase(),
23+
$messageLines ? '- Message:' : '- Message: ' . $reportMessage,
24+
];
25+
26+
if ($messageLines) {
27+
$report[] = '';
28+
$report[] = '```text';
29+
$report[] = $reportMessage;
30+
$report[] = '```';
31+
}
32+
33+
$report[] = '';
34+
$report[] = '## Environment';
35+
$report[] = '';
36+
$report[] = '- PHP: ' . PHP_VERSION;
37+
$report[] = '- CodeIgniter: ' . CodeIgniter::CI_VERSION;
38+
$report[] = '- Environment: ' . ENVIRONMENT;
39+
$report[] = '- SAPI: ' . PHP_SAPI;
40+
$report[] = '- Time: ' . date('Y-m-d H:i:s e');
41+
$report[] = '- Memory Usage: ' . number_format(memory_get_usage(true) / 1024 / 1024, 2) . ' MB';
42+
43+
$reportRequest = service('request');
44+
45+
if ($reportRequest instanceof IncomingRequest) {
46+
$reportPath = '/' . ltrim($reportRequest->getPath(), '/');
47+
$reportUri = $reportRequest->getUri();
48+
$reportUrl = $reportPath;
49+
50+
if ($reportUri->getHost() !== '') {
51+
$reportUrl = URI::createURIString(
52+
$reportUri->getScheme(),
53+
$reportUri->getHost() . ($reportUri->getPort() === null ? '' : ':' . $reportUri->getPort()),
54+
$reportPath,
55+
);
56+
}
57+
58+
$report[] = '';
59+
$report[] = '## Request';
60+
$report[] = '';
61+
$report[] = '- Method: ' . $reportRequest->getMethod();
62+
$report[] = '- Path: ' . $reportPath;
63+
$report[] = '- URL: ' . $reportUrl;
64+
$report[] = '- User Agent: ' . $reportRequest->getUserAgent()->getAgentString();
65+
}
66+
67+
$report[] = '';
68+
$report[] = '## Source';
69+
$report[] = '';
70+
$report[] = '`' . clean_path($file) . ':' . $line . '`';
71+
72+
if (is_file($file) && is_readable($file)) {
73+
$sourceLines = file($file, FILE_IGNORE_NEW_LINES);
74+
75+
if ($sourceLines !== false) {
76+
$startLine = max($line - 5, 1);
77+
$endLine = min($line + 5, count($sourceLines));
78+
79+
$report[] = '';
80+
$report[] = '```php';
81+
82+
for ($sourceLine = $startLine; $sourceLine <= $endLine; $sourceLine++) {
83+
$report[] = sprintf(
84+
'%s%4d %s',
85+
$sourceLine === $line ? '>' : ' ',
86+
$sourceLine,
87+
$sourceLines[$sourceLine - 1],
88+
);
89+
}
90+
91+
$report[] = '```';
92+
}
93+
}
94+
95+
$previousException = $exception->getPrevious();
96+
97+
if ($previousException instanceof Throwable) {
98+
$report[] = '';
99+
$report[] = '## Previous Exceptions';
100+
101+
while ($previousException instanceof Throwable) {
102+
$report[] = '* ' . $previousException::class . ' - ' . $previousException->getMessage();
103+
$report[] = ' ' . clean_path($previousException->getFile()) . ':' . $previousException->getLine();
104+
105+
$previousException = $previousException->getPrevious();
106+
}
107+
}
108+
109+
if ($trace !== []) {
110+
$report[] = '';
111+
$report[] = '## Stack Trace';
112+
$report[] = '';
113+
$report[] = '```text';
114+
115+
foreach (array_slice($trace, 0, 50) as $reportIndex => $reportRow) {
116+
$reportLocation = isset($reportRow['file'], $reportRow['line'])
117+
? clean_path($reportRow['file']) . ':' . $reportRow['line']
118+
: '{PHP internal code}';
119+
$reportCall = ($reportRow['class'] ?? '') . ($reportRow['type'] ?? '') . ($reportRow['function'] ?? '');
120+
121+
$report[] = $reportIndex . ' ' . $reportLocation . ($reportCall === '' ? '' : ' ' . $reportCall . '()');
122+
}
123+
124+
$report[] = '```';
125+
}
126+
127+
echo esc(implode("\n", $report)) . "\n";

0 commit comments

Comments
 (0)