Skip to content

Commit 192aeb5

Browse files
committed
small improvements to the Laravel stack trace display and performance
1 parent f3da6c1 commit 192aeb5

File tree

5 files changed

+124
-90
lines changed

5 files changed

+124
-90
lines changed

public/app.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/app.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/mix-manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"/app.js": "/app.js?id=64391c136df5323279f6bc14315c3654",
3-
"/app.css": "/app.css?id=5593a0331dd40729ff41e32a6035d872",
2+
"/app.js": "/app.js?id=b5eb6497b80ecd00237a857b35fcc1d6",
3+
"/app.css": "/app.css?id=bf9e77abce3da8caacd004d57e4e8429",
44
"/img/log-viewer-128.png": "/img/log-viewer-128.png?id=d576c6d2e16074d3f064e60fe4f35166",
55
"/img/log-viewer-32.png": "/img/log-viewer-32.png?id=f8ec67d10f996aa8baf00df3b61eea6d",
66
"/img/log-viewer-64.png": "/img/log-viewer-64.png?id=8902d596fc883ca9eb8105bb683568c6"

resources/js/components/BaseLogTable.vue

Lines changed: 7 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -87,43 +87,8 @@
8787
<mail-text-preview :mail="log.extra.mail_preview" />
8888
</tab-content>
8989

90-
<tab-content v-if="hasLaravelStackTrace(log)" tab-value="stack_trace">
91-
<div class="p-4 lg:p-8">
92-
<!-- Exception Header -->
93-
<div v-if="getStackTraceData(log).header" class="mb-6 pb-4 border-b border-gray-200 dark:border-gray-600">
94-
<div class="text-red-600 dark:text-red-400 font-semibold text-lg mb-2">
95-
{{ getStackTraceData(log).header.type }}
96-
</div>
97-
<div class="text-gray-800 dark:text-gray-200 text-base mb-2">
98-
{{ getStackTraceData(log).header.message }}
99-
</div>
100-
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
101-
in {{ getStackTraceData(log).header.file }}:{{ getStackTraceData(log).header.line }}
102-
</div>
103-
</div>
104-
105-
<!-- Stack Trace Frames -->
106-
<div class="space-y-2">
107-
<div v-for="(frame, frameIndex) in getStackTraceData(log).frames" :key="frameIndex"
108-
class="mb-2 border-b border-gray-100 dark:border-gray-700 pb-2 last:border-b-0">
109-
<div class="flex items-start gap-3">
110-
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono w-8 flex-shrink-0 pt-1">
111-
#{{ frame.number }}
112-
</div>
113-
<div class="flex-1 min-w-0">
114-
<div v-if="frame.file" class="text-sm mb-1">
115-
<span class="font-mono text-blue-600 dark:text-blue-400 break-all">{{ frame.file }}</span>
116-
<span class="text-gray-500 dark:text-gray-400">:</span>
117-
<span class="font-mono text-orange-600 dark:text-orange-400">{{ frame.line }}</span>
118-
</div>
119-
<div class="text-sm text-gray-800 dark:text-gray-200 font-mono break-all">
120-
{{ frame.call }}
121-
</div>
122-
</div>
123-
</div>
124-
</div>
125-
</div>
126-
</div>
90+
<tab-content v-if="hasLaravelStackTrace(log)" tab-value="laravel_stack_trace">
91+
<LaravelStackTraceDisplay :log="log" />
12792
</tab-content>
12893

12994
<tab-content tab-value="raw">
@@ -190,6 +155,7 @@ import TabContainer from "./TabContainer.vue";
190155
import TabContent from "./TabContent.vue";
191156
import MailHtmlPreview from "./MailHtmlPreview.vue";
192157
import MailTextPreview from "./MailTextPreview.vue";
158+
import LaravelStackTraceDisplay from "./LaravelStackTraceDisplay.vue";
193159
import {computed} from "vue";
194160
195161
const fileStore = useFileStore();
@@ -218,6 +184,10 @@ const hasContext = (log) => {
218184
const getExtraTabsForLog = (log) => {
219185
let tabs = [];
220186
187+
if (hasLaravelStackTrace(log)) {
188+
tabs.push({ name: 'Stack Trace', value: 'laravel_stack_trace' });
189+
}
190+
221191
if (! log.extra || ! log.extra.mail_preview) {
222192
return tabs;
223193
}
@@ -238,11 +208,6 @@ const getTabsForLog = (log) => {
238208
239209
tabs.push({ name: 'Raw', value: 'raw' });
240210
241-
// Add Stack Trace tab for Laravel logs with stack traces
242-
if (hasLaravelStackTrace(log)) {
243-
tabs.push({ name: 'Stack Trace', value: 'stack_trace' });
244-
}
245-
246211
return tabs.filter(Boolean);
247212
}
248213
@@ -263,50 +228,6 @@ const hasLaravelStackTrace = (log) => {
263228
return exception && typeof exception === 'string' && exception.includes('[stacktrace]');
264229
}
265230
266-
const getStackTraceData = (log) => {
267-
const exception = Array.isArray(log.context)
268-
? log.context.find(item => item.exception)?.exception
269-
: log.context.exception;
270-
271-
if (!exception || typeof exception !== 'string') {
272-
return { header: null, frames: [] };
273-
}
274-
275-
// Parse exception header
276-
const headerMatch = exception.match(/^\[object\]\s*\(([^(]+)\(code:\s*\d+\):\s*(.+?)\s+at\s+(.+?):(\d+)\)/);
277-
const header = headerMatch ? {
278-
type: headerMatch[1].trim(),
279-
message: headerMatch[2].trim(),
280-
file: headerMatch[3].trim(),
281-
line: parseInt(headerMatch[4])
282-
} : null;
283-
284-
// Parse stack trace frames
285-
const stacktraceMatch = exception.match(/\[stacktrace\]([\s\S]*?)(?:\n\n|\n$|$)/);
286-
const frames = [];
287-
if (stacktraceMatch) {
288-
const frameRegex = /#(\d+)\s+(.+?)(?:\n|$)/g;
289-
let match;
290-
while ((match = frameRegex.exec(stacktraceMatch[1])) !== null) {
291-
const frameLine = match[2].trim();
292-
const fileMatch = frameLine.match(/^(.+?)\((\d+)\):\s*(.+)$/);
293-
frames.push(fileMatch ? {
294-
number: parseInt(match[1]),
295-
file: fileMatch[1],
296-
line: parseInt(fileMatch[2]),
297-
call: fileMatch[3]
298-
} : {
299-
number: parseInt(match[1]),
300-
file: '',
301-
line: 0,
302-
call: frameLine
303-
});
304-
}
305-
}
306-
307-
return { header, frames };
308-
}
309-
310231
const tableColumns = computed(() => {
311232
// the extra two columns are for the expand/collapse and log index columns
312233
return logViewerStore.columns.length + 2;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<template>
2+
<div class="p-4 lg:p-8">
3+
<!-- Exception Header -->
4+
<div v-if="stackTrace.header" class="mb-6 pb-4 border-b border-gray-200 dark:border-gray-600">
5+
<div class="text-red-600 dark:text-red-400 font-semibold text-lg mb-2">
6+
{{ stackTrace.header.type }}
7+
</div>
8+
<div class="text-gray-800 dark:text-gray-200 text-base mb-2">
9+
{{ stackTrace.header.message }}
10+
</div>
11+
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
12+
in {{ stackTrace.header.file }}:{{ stackTrace.header.line }}
13+
</div>
14+
</div>
15+
16+
<!-- Stack Trace Frames -->
17+
<div class="space-y-2">
18+
<div v-for="(frame, frameIndex) in stackTrace.frames" :key="frameIndex"
19+
class="mb-2 border-b border-gray-100 dark:border-gray-700 pb-2 last:border-b-0">
20+
<div class="flex items-start gap-2">
21+
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono w-8 flex-shrink-0 pt-1">
22+
#{{ frame.number }}
23+
</div>
24+
<div class="flex-1 min-w-0">
25+
<div v-if="frame.file" class="text-xs mb-1">
26+
<span class="font-mono text-blue-600 dark:text-blue-400 break-all">{{ frame.file }}</span>
27+
<span class="text-gray-500 dark:text-gray-400 mx-0.5">:</span>
28+
<span class="font-mono text-orange-600 dark:text-orange-400">{{ frame.line }}</span>
29+
</div>
30+
<div class="text-xs text-gray-800 dark:text-gray-200 font-mono break-all">
31+
{{ frame.call }}
32+
</div>
33+
</div>
34+
</div>
35+
</div>
36+
</div>
37+
38+
<!-- Error Fallback -->
39+
<div v-if="!stackTrace.header && stackTrace.frames.length === 0" class="text-gray-500 dark:text-gray-400 text-sm italic">
40+
Unable to parse stack trace. View the Raw tab for full details.
41+
</div>
42+
</div>
43+
</template>
44+
45+
<script setup>
46+
import { computed } from 'vue';
47+
48+
const props = defineProps({
49+
log: {
50+
type: Object,
51+
required: true
52+
}
53+
});
54+
55+
/**
56+
* Parses the Laravel exception stack trace from the log context.
57+
* This computed property ensures the expensive parsing operation only happens once per log.
58+
*/
59+
const stackTrace = computed(() => {
60+
try {
61+
const exception = Array.isArray(props.log.context)
62+
? props.log.context.find(item => item.exception)?.exception
63+
: props.log.context?.exception;
64+
65+
if (!exception || typeof exception !== 'string') {
66+
return { header: null, frames: [] };
67+
}
68+
69+
// Parse exception header
70+
// Format: [object] (ExceptionType(code: 0): Message at /path/file.php:123)
71+
const headerMatch = exception.match(/^\[object\]\s*\(([^(]+)\(code:\s*\d+\):\s*(.+?)\s+at\s+(.+?):(\d+)\)/);
72+
const header = headerMatch ? {
73+
type: headerMatch[1].trim(),
74+
message: headerMatch[2].trim(),
75+
file: headerMatch[3].trim(),
76+
line: parseInt(headerMatch[4])
77+
} : null;
78+
79+
// Parse stack trace frames
80+
// Format: #0 /path/file.php(123): Class::method()
81+
const stacktraceMatch = exception.match(/\[stacktrace\]([\s\S]*?)(?:\n\n|\n$|$)/);
82+
const frames = [];
83+
84+
if (stacktraceMatch) {
85+
const frameRegex = /#(\d+)\s+(.+?)(?:\n|$)/g;
86+
let match;
87+
88+
while ((match = frameRegex.exec(stacktraceMatch[1])) !== null) {
89+
const frameLine = match[2].trim();
90+
const fileMatch = frameLine.match(/^(.+?)\((\d+)\):\s*(.+)$/);
91+
92+
frames.push(fileMatch ? {
93+
number: parseInt(match[1]),
94+
file: fileMatch[1],
95+
line: parseInt(fileMatch[2]),
96+
call: fileMatch[3]
97+
} : {
98+
number: parseInt(match[1]),
99+
file: '',
100+
line: 0,
101+
call: frameLine
102+
});
103+
}
104+
}
105+
106+
return { header, frames };
107+
} catch (error) {
108+
// Gracefully handle parsing errors
109+
console.error('Error parsing stack trace:', error);
110+
return { header: null, frames: [] };
111+
}
112+
});
113+
</script>

0 commit comments

Comments
 (0)