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 >
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' , value: ' raw' },
201- ].filter (Boolean );
237+ const tabs = [... getExtraTabsForLog (log)];
238+
239+ tabs .push ({ name: ' Raw' , value: ' raw' });
240+
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+
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