Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion public/app.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/app.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions public/mix-manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"/app.js": "/app.js?id=64391c136df5323279f6bc14315c3654",
"/app.css": "/app.css?id=5593a0331dd40729ff41e32a6035d872",
"/app.js": "/app.js?id=b5eb6497b80ecd00237a857b35fcc1d6",
"/app.css": "/app.css?id=bf9e77abce3da8caacd004d57e4e8429",
"/img/log-viewer-128.png": "/img/log-viewer-128.png?id=d576c6d2e16074d3f064e60fe4f35166",
"/img/log-viewer-32.png": "/img/log-viewer-32.png?id=f8ec67d10f996aa8baf00df3b61eea6d",
"/img/log-viewer-64.png": "/img/log-viewer-64.png?id=8902d596fc883ca9eb8105bb683568c6"
Expand Down
93 changes: 7 additions & 86 deletions resources/js/components/BaseLogTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,43 +87,8 @@
<mail-text-preview :mail="log.extra.mail_preview" />
</tab-content>

<tab-content v-if="hasLaravelStackTrace(log)" tab-value="stack_trace">
<div class="p-4 lg:p-8">
<!-- Exception Header -->
<div v-if="getStackTraceData(log).header" class="mb-6 pb-4 border-b border-gray-200 dark:border-gray-600">
<div class="text-red-600 dark:text-red-400 font-semibold text-lg mb-2">
{{ getStackTraceData(log).header.type }}
</div>
<div class="text-gray-800 dark:text-gray-200 text-base mb-2">
{{ getStackTraceData(log).header.message }}
</div>
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
in {{ getStackTraceData(log).header.file }}:{{ getStackTraceData(log).header.line }}
</div>
</div>

<!-- Stack Trace Frames -->
<div class="space-y-2">
<div v-for="(frame, frameIndex) in getStackTraceData(log).frames" :key="frameIndex"
class="mb-2 border-b border-gray-100 dark:border-gray-700 pb-2 last:border-b-0">
<div class="flex items-start gap-3">
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono w-8 flex-shrink-0 pt-1">
#{{ frame.number }}
</div>
<div class="flex-1 min-w-0">
<div v-if="frame.file" class="text-sm mb-1">
<span class="font-mono text-blue-600 dark:text-blue-400 break-all">{{ frame.file }}</span>
<span class="text-gray-500 dark:text-gray-400">:</span>
<span class="font-mono text-orange-600 dark:text-orange-400">{{ frame.line }}</span>
</div>
<div class="text-sm text-gray-800 dark:text-gray-200 font-mono break-all">
{{ frame.call }}
</div>
</div>
</div>
</div>
</div>
</div>
<tab-content v-if="hasLaravelStackTrace(log)" tab-value="laravel_stack_trace">
<LaravelStackTraceDisplay :log="log" />
</tab-content>

<tab-content tab-value="raw">
Expand Down Expand Up @@ -190,6 +155,7 @@ import TabContainer from "./TabContainer.vue";
import TabContent from "./TabContent.vue";
import MailHtmlPreview from "./MailHtmlPreview.vue";
import MailTextPreview from "./MailTextPreview.vue";
import LaravelStackTraceDisplay from "./LaravelStackTraceDisplay.vue";
import {computed} from "vue";

const fileStore = useFileStore();
Expand Down Expand Up @@ -218,6 +184,10 @@ const hasContext = (log) => {
const getExtraTabsForLog = (log) => {
let tabs = [];

if (hasLaravelStackTrace(log)) {
tabs.push({ name: 'Stack Trace', value: 'laravel_stack_trace' });
}

if (! log.extra || ! log.extra.mail_preview) {
return tabs;
}
Expand All @@ -238,11 +208,6 @@ const getTabsForLog = (log) => {

tabs.push({ name: 'Raw', value: 'raw' });

// Add Stack Trace tab for Laravel logs with stack traces
if (hasLaravelStackTrace(log)) {
tabs.push({ name: 'Stack Trace', value: 'stack_trace' });
}

return tabs.filter(Boolean);
}

Expand All @@ -263,50 +228,6 @@ const hasLaravelStackTrace = (log) => {
return exception && typeof exception === 'string' && exception.includes('[stacktrace]');
}

const getStackTraceData = (log) => {
const exception = Array.isArray(log.context)
? log.context.find(item => item.exception)?.exception
: log.context.exception;

if (!exception || typeof exception !== 'string') {
return { header: null, frames: [] };
}

// Parse exception header
const headerMatch = exception.match(/^\[object\]\s*\(([^(]+)\(code:\s*\d+\):\s*(.+?)\s+at\s+(.+?):(\d+)\)/);
const header = headerMatch ? {
type: headerMatch[1].trim(),
message: headerMatch[2].trim(),
file: headerMatch[3].trim(),
line: parseInt(headerMatch[4])
} : null;

// Parse stack trace frames
const stacktraceMatch = exception.match(/\[stacktrace\]([\s\S]*?)(?:\n\n|\n$|$)/);
const frames = [];
if (stacktraceMatch) {
const frameRegex = /#(\d+)\s+(.+?)(?:\n|$)/g;
let match;
while ((match = frameRegex.exec(stacktraceMatch[1])) !== null) {
const frameLine = match[2].trim();
const fileMatch = frameLine.match(/^(.+?)\((\d+)\):\s*(.+)$/);
frames.push(fileMatch ? {
number: parseInt(match[1]),
file: fileMatch[1],
line: parseInt(fileMatch[2]),
call: fileMatch[3]
} : {
number: parseInt(match[1]),
file: '',
line: 0,
call: frameLine
});
}
}

return { header, frames };
}

const tableColumns = computed(() => {
// the extra two columns are for the expand/collapse and log index columns
return logViewerStore.columns.length + 2;
Expand Down
113 changes: 113 additions & 0 deletions resources/js/components/LaravelStackTraceDisplay.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<template>
<div class="p-4 lg:p-8">
<!-- Exception Header -->
<div v-if="stackTrace.header" class="mb-6 pb-4 border-b border-gray-200 dark:border-gray-600">
<div class="text-red-600 dark:text-red-400 font-semibold text-lg mb-2">
{{ stackTrace.header.type }}
</div>
<div class="text-gray-800 dark:text-gray-200 text-base mb-2">
{{ stackTrace.header.message }}
</div>
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
in {{ stackTrace.header.file }}:{{ stackTrace.header.line }}
</div>
</div>

<!-- Stack Trace Frames -->
<div class="space-y-2">
<div v-for="(frame, frameIndex) in stackTrace.frames" :key="frameIndex"
class="mb-2 border-b border-gray-100 dark:border-gray-700 pb-2 last:border-b-0">
<div class="flex items-start gap-2">
<div class="text-xs text-gray-500 dark:text-gray-400 font-mono w-8 flex-shrink-0 pt-1">
#{{ frame.number }}
</div>
<div class="flex-1 min-w-0">
<div v-if="frame.file" class="text-xs mb-1">
<span class="font-mono text-blue-600 dark:text-blue-400 break-all">{{ frame.file }}</span>
<span class="text-gray-500 dark:text-gray-400 mx-0.5">:</span>
<span class="font-mono text-orange-600 dark:text-orange-400">{{ frame.line }}</span>
</div>
<div class="text-xs text-gray-800 dark:text-gray-200 font-mono break-all">
{{ frame.call }}
</div>
</div>
</div>
</div>
</div>

<!-- Error Fallback -->
<div v-if="!stackTrace.header && stackTrace.frames.length === 0" class="text-gray-500 dark:text-gray-400 text-sm italic">
Unable to parse stack trace. View the Raw tab for full details.
</div>
</div>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
log: {
type: Object,
required: true
}
});

/**
* Parses the Laravel exception stack trace from the log context.
* This computed property ensures the expensive parsing operation only happens once per log.
*/
const stackTrace = computed(() => {
try {
const exception = Array.isArray(props.log.context)
? props.log.context.find(item => item.exception)?.exception
: props.log.context?.exception;

if (!exception || typeof exception !== 'string') {
return { header: null, frames: [] };
}

// Parse exception header
// Format: [object] (ExceptionType(code: 0): Message at /path/file.php:123)
const headerMatch = exception.match(/^\[object\]\s*\(([^(]+)\(code:\s*\d+\):\s*(.+?)\s+at\s+(.+?):(\d+)\)/);
const header = headerMatch ? {
type: headerMatch[1].trim(),
message: headerMatch[2].trim(),
file: headerMatch[3].trim(),
line: parseInt(headerMatch[4])
} : null;

// Parse stack trace frames
// Format: #0 /path/file.php(123): Class::method()
const stacktraceMatch = exception.match(/\[stacktrace\]([\s\S]*?)(?:\n\n|\n$|$)/);
const frames = [];

if (stacktraceMatch) {
const frameRegex = /#(\d+)\s+(.+?)(?:\n|$)/g;
let match;

while ((match = frameRegex.exec(stacktraceMatch[1])) !== null) {
const frameLine = match[2].trim();
const fileMatch = frameLine.match(/^(.+?)\((\d+)\):\s*(.+)$/);

frames.push(fileMatch ? {
number: parseInt(match[1]),
file: fileMatch[1],
line: parseInt(fileMatch[2]),
call: fileMatch[3]
} : {
number: parseInt(match[1]),
file: '',
line: 0,
call: frameLine
});
}
}

return { header, frames };
} catch (error) {
// Gracefully handle parsing errors
console.error('Error parsing stack trace:', error);
return { header: null, frames: [] };
}
});
</script>
Loading