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";
190155import TabContent from " ./TabContent.vue" ;
191156import MailHtmlPreview from " ./MailHtmlPreview.vue" ;
192157import MailTextPreview from " ./MailTextPreview.vue" ;
158+ import LaravelStackTraceDisplay from " ./LaravelStackTraceDisplay.vue" ;
193159import {computed } from " vue" ;
194160
195161const fileStore = useFileStore ();
@@ -218,6 +184,10 @@ const hasContext = (log) => {
218184const 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-
310231const tableColumns = computed (() => {
311232 // the extra two columns are for the expand/collapse and log index columns
312233 return logViewerStore .columns .length + 2 ;
0 commit comments