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" span >
116+                           <span  class =" text-gray-500 dark:text-gray-400" span >
117+                           <span  class =" font-mono text-orange-600 dark:text-orange-400" 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 >
127+             </tab-content >
128+ 
90129            <tab-content  tab-value =" raw" 
91130              <pre  class =" log-stack" v-html =" highlightSearchResult(log.full_text, searchStore.query)" pre >
92131              <template  v-if =" hasContext (log )" >
@@ -195,10 +234,16 @@ const getExtraTabsForLog = (log) => {
195234} 
196235
197236const  getTabsForLog  =  (log ) =>  {
198-   return  [ 
199-     ... getExtraTabsForLog (log), 
200-     { name:  ' Raw' :  ' raw'  
201-   ].filter (Boolean ); 
237+   const  tabs  =  [... getExtraTabsForLog (log)]; 
238+ 
239+   tabs .push ({ name:  ' Raw' :  ' raw'  
240+ 
241+   //  Add Stack Trace tab for Laravel logs with stack traces 
242+   if  (hasLaravelStackTrace (log)) { 
243+     tabs .push ({ name:  ' Stack Trace' :  ' stack_trace'  
244+   } 
245+ 
246+   return  tabs .filter (Boolean ); 
202247} 
203248
204249const  prepareContextForOutput  =  (context ) =>  {
@@ -211,6 +256,57 @@ const prepareContextForOutput = (context) => {
211256  }, 2 ); 
212257} 
213258
259+ const  hasLaravelStackTrace  =  (log ) =>  {
260+   const  exception  =  Array .isArray (log .context ) 
261+     ?  log .context .find (item  =>  item .exception )? .exception  
262+     :  log .context .exception ; 
263+   return  exception &&  typeof  exception ===  ' string' &&  exception .includes (' [stacktrace]'  
264+ } 
265+ 
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+ 
214310const  tableColumns  =  computed (() =>  {
215311  //  the extra two columns are for the expand/collapse and log index columns 
216312  return  logViewerStore .columns .length  +  2 ; 
0 commit comments