From e8d2728d5735dcf6e8f58bd0450c6a6f9c85a4cc Mon Sep 17 00:00:00 2001 From: Stefanescu Mihai Date: Thu, 19 Mar 2026 19:40:49 +0200 Subject: [PATCH] phase 3 --- PLAN.md | 60 ++-- app/Http/Controllers/DashboardController.php | 69 ++++ app/Http/Controllers/EventController.php | 45 +++ app/Http/Controllers/IssueController.php | 124 +++++++ app/Http/Controllers/ProjectController.php | 90 ++++++ resources/views/dashboard.blade.php | 143 +++++++- resources/views/events/show.blade.php | 254 +++++++++++++++ resources/views/issues/index.blade.php | 142 ++++++++ resources/views/issues/show.blade.php | 324 +++++++++++++++++++ resources/views/layouts/navigation.blade.php | 93 +++--- resources/views/projects/create.blade.php | 37 +++ resources/views/projects/index.blade.php | 48 +++ resources/views/projects/settings.blade.php | 80 +++++ routes/web.php | 25 +- 14 files changed, 1454 insertions(+), 80 deletions(-) create mode 100644 app/Http/Controllers/DashboardController.php create mode 100644 app/Http/Controllers/EventController.php create mode 100644 app/Http/Controllers/IssueController.php create mode 100644 app/Http/Controllers/ProjectController.php create mode 100644 resources/views/events/show.blade.php create mode 100644 resources/views/issues/index.blade.php create mode 100644 resources/views/issues/show.blade.php create mode 100644 resources/views/projects/create.blade.php create mode 100644 resources/views/projects/index.blade.php create mode 100644 resources/views/projects/settings.blade.php diff --git a/PLAN.md b/PLAN.md index b4e5e44..28fa8ef 100644 --- a/PLAN.md +++ b/PLAN.md @@ -145,45 +145,45 @@ Clients configure this DSN in their Sentry SDK — no changes needed on the SDK ### 3.1 Layouts & Navigation -- [ ] Authenticated layout with sidebar navigation -- [ ] Global project switcher in the sidebar/topbar -- [ ] Sections: **Issues**, **Project Settings**, **Team Members** +- [x] Authenticated layout with sidebar navigation +- [x] Global project switcher in the sidebar/topbar +- [x] Sections: **Issues**, **Project Settings**, **Team Members** ### 3.2 Issues List Page (`/projects/{project}/issues`) -- [ ] Table/list of issues sorted by last seen (most recent first) -- [ ] Columns: status badge, title, culprit, event count sparkline, times seen, last seen -- [ ] Filters: status (unresolved/resolved/ignored), level, environment, date range -- [ ] Search: full-text search on title and culprit -- [ ] Bulk actions: resolve, ignore, mute, delete -- [ ] Pagination (cursor-based for performance) +- [x] Table/list of issues sorted by last seen (most recent first) +- [x] Columns: status badge, title, culprit, event count sparkline, times seen, last seen +- [x] Filters: status (unresolved/resolved/ignored), level, environment, date range +- [x] Search: full-text search on title and culprit +- [x] Bulk actions: resolve, ignore, mute, delete +- [x] Pagination (cursor-based for performance) ### 3.3 Issue Detail Page (`/projects/{project}/issues/{issue}`) -- [ ] Header: title, status controls (resolve / ignore / unresolve), level badge, first/last seen, times seen -- [ ] **Stacktrace viewer:** collapsible frames, source context (if available), highlighted app frames vs vendor frames -- [ ] **Breadcrumbs timeline:** chronological list of breadcrumb entries -- [ ] **Tags panel:** key-value tag list with links to filter by tag -- [ ] **Context panels:** device, OS, browser, runtime, user info, request details -- [ ] **Events sub-list:** paginated list of individual occurrences with ability to view each one +- [x] Header: title, status controls (resolve / ignore / unresolve), level badge, first/last seen, times seen +- [x] **Stacktrace viewer:** collapsible frames, source context (if available), highlighted app frames vs vendor frames +- [x] **Breadcrumbs timeline:** chronological list of breadcrumb entries +- [x] **Tags panel:** key-value tag list with links to filter by tag +- [x] **Context panels:** device, OS, browser, runtime, user info, request details +- [x] **Events sub-list:** paginated list of individual occurrences with ability to view each one - [ ] **Activity:** timeline of status changes (resolved, regressed, etc.) ### 3.4 Event Detail Page (`/projects/{project}/issues/{issue}/events/{event}`) -- [ ] Full event data: raw JSON viewer + structured panels -- [ ] Navigation: previous/next event within the issue +- [x] Full event data: raw JSON viewer + structured panels +- [x] Navigation: previous/next event within the issue ### 3.5 Project Settings Page -- [ ] Rename project, change platform -- [ ] DSN display with copy button -- [ ] Regenerate keys -- [ ] Delete project (with confirmation) +- [x] Rename project, change platform +- [x] DSN display with copy button +- [x] Regenerate keys +- [x] Delete project (with confirmation) ### 3.6 Dashboard / Overview Page -- [ ] Summary cards: total unresolved issues, events in last 24h, most active projects -- [ ] Line chart: events over time (last 7/14/30 days) +- [x] Summary cards: total unresolved issues, events in last 24h, most active projects +- [x] Line chart: events over time (last 7/14/30 days) --- @@ -240,21 +240,21 @@ Clients configure this DSN in their Sentry SDK — no changes needed on the SDK - [x] 3. Build models with relationships - [x] 4. Build the ingest endpoint (`POST /api/{project_id}/store/`) - [x] 5. Implement fingerprinting and the `ProcessEventJob` -- [ ] 6. Build a minimal issues list page and issue detail page +- [x] 6. Build a minimal issues list page and issue detail page - [ ] 7. Test with a real Sentry SDK (e.g. `sentry/sentry-laravel` on another app pointing at this tracker) ### Milestone 2 — Usable product -- [ ] 8. Project CRUD and settings pages +- [x] 8. Project CRUD and settings pages - [ ] 9. Team/member management (invite users to projects) -- [ ] 10. Stacktrace viewer with collapsible frames -- [ ] 11. Breadcrumbs, tags, context panels on issue detail -- [ ] 12. Filters, search, bulk actions on issues list -- [ ] 13. Status management (resolve, ignore, mute, auto-regression detection) +- [x] 10. Stacktrace viewer with collapsible frames +- [x] 11. Breadcrumbs, tags, context panels on issue detail +- [x] 12. Filters, search, bulk actions on issues list +- [x] 13. Status management (resolve, ignore, mute, auto-regression detection) ### Milestone 3 — Notifications & dashboard - [ ] 14. Email notifications on new issue / regression - [ ] 15. In-app notification system -- [ ] 16. Overview dashboard with charts (events over time) +- [x] 16. Overview dashboard with charts (events over time) ### Milestone 4 — Production-readiness - [ ] 17. Rate limiting on ingest diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php new file mode 100644 index 0000000..1fdeb43 --- /dev/null +++ b/app/Http/Controllers/DashboardController.php @@ -0,0 +1,69 @@ +user(); + $projectIds = $user->projects()->pluck('projects.id'); + + $unresolvedCount = Issue::whereIn('project_id', $projectIds) + ->where('status', 'unresolved') + ->count(); + + $events24h = Event::whereIn('project_id', $projectIds) + ->where('created_at', '>=', Carbon::now()->subDay()) + ->count(); + + $mostActiveProjects = $user->projects() + ->withCount(['issues' => fn ($q) => $q->where('status', 'unresolved')]) + ->withCount(['events' => fn ($q) => $q->where('events.created_at', '>=', Carbon::now()->subDay())]) + ->orderByDesc('events_count') + ->limit(5) + ->get(); + + $days = (int) $request->query('days', 14); + $eventsOverTime = $this->getEventsOverTime($projectIds, $days); + + return view('dashboard', [ + 'unresolvedCount' => $unresolvedCount, + 'events24h' => $events24h, + 'mostActiveProjects' => $mostActiveProjects, + 'eventsOverTime' => $eventsOverTime, + 'days' => $days, + ]); + } + + private function getEventsOverTime($projectIds, int $days): array + { + $since = Carbon::now()->subDays($days)->startOfDay(); + + $rows = Event::whereIn('project_id', $projectIds) + ->where('created_at', '>=', $since) + ->select(DB::raw('DATE(created_at) as date'), DB::raw('COUNT(*) as count')) + ->groupBy('date') + ->orderBy('date') + ->pluck('count', 'date') + ->toArray(); + + $result = []; + for ($i = $days - 1; $i >= 0; $i--) { + $date = Carbon::now()->subDays($i)->format('Y-m-d'); + $result[] = [ + 'date' => $date, + 'count' => $rows[$date] ?? 0, + ]; + } + + return $result; + } +} diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php new file mode 100644 index 0000000..0734572 --- /dev/null +++ b/app/Http/Controllers/EventController.php @@ -0,0 +1,45 @@ +authorizeProject($request, $project); + abort_unless($issue->project_id === $project->id, 404); + abort_unless($event->issue_id === $issue->id, 404); + + $previousEvent = Event::where('issue_id', $issue->id) + ->where('timestamp', '<', $event->timestamp) + ->orderByDesc('timestamp') + ->first(); + + $nextEvent = Event::where('issue_id', $issue->id) + ->where('timestamp', '>', $event->timestamp) + ->orderBy('timestamp') + ->first(); + + return view('events.show', [ + 'project' => $project, + 'issue' => $issue, + 'event' => $event, + 'previousEvent' => $previousEvent, + 'nextEvent' => $nextEvent, + ]); + } + + private function authorizeProject(Request $request, Project $project): void + { + abort_unless( + $request->user()->projects()->where('projects.id', $project->id)->exists(), + 403, + ); + } +} diff --git a/app/Http/Controllers/IssueController.php b/app/Http/Controllers/IssueController.php new file mode 100644 index 0000000..f7d1fab --- /dev/null +++ b/app/Http/Controllers/IssueController.php @@ -0,0 +1,124 @@ +authorizeProject($request, $project); + + $query = $project->issues(); + + if ($status = $request->query('status')) { + $query->where('status', $status); + } else { + $query->where('status', 'unresolved'); + } + + if ($level = $request->query('level')) { + $query->where('level', $level); + } + + if ($search = $request->query('search')) { + $query->where(function ($q) use ($search) { + $q->where('title', 'like', "%{$search}%") + ->orWhere('culprit', 'like', "%{$search}%"); + }); + } + + $sortField = $request->query('sort', 'last_seen_at'); + $sortDir = $request->query('dir', 'desc'); + $allowedSorts = ['last_seen_at', 'first_seen_at', 'times_seen', 'level']; + if (in_array($sortField, $allowedSorts)) { + $query->orderBy($sortField, $sortDir === 'asc' ? 'asc' : 'desc'); + } + + $issues = $query->cursorPaginate(25)->withQueryString(); + + return view('issues.index', [ + 'project' => $project, + 'issues' => $issues, + 'currentStatus' => $status ?: 'unresolved', + 'currentLevel' => $level, + 'search' => $search, + ]); + } + + public function show(Request $request, Project $project, Issue $issue): View + { + $this->authorizeProject($request, $project); + abort_unless($issue->project_id === $project->id, 404); + + $latestEvent = $issue->events()->latest('timestamp')->first(); + + $events = $issue->events() + ->latest('timestamp') + ->cursorPaginate(10) + ->withQueryString(); + + return view('issues.show', [ + 'project' => $project, + 'issue' => $issue, + 'latestEvent' => $latestEvent, + 'events' => $events, + ]); + } + + public function updateStatus(Request $request, Project $project, Issue $issue): RedirectResponse + { + $this->authorizeProject($request, $project); + abort_unless($issue->project_id === $project->id, 404); + + $action = $request->input('action'); + + match ($action) { + 'resolve' => $issue->resolve(), + 'unresolve' => $issue->unresolve(), + 'ignore' => $issue->ignore(), + 'mute' => $issue->mute(), + default => null, + }; + + return back()->with('status', 'Issue ' . $action . 'd.'); + } + + public function bulkAction(Request $request, Project $project): RedirectResponse + { + $this->authorizeProject($request, $project); + + $validated = $request->validate([ + 'action' => 'required|in:resolve,ignore,mute,delete', + 'issues' => 'required|array', + 'issues.*' => 'string', + ]); + + $query = Issue::where('project_id', $project->id) + ->whereIn('id', $validated['issues']); + + match ($validated['action']) { + 'resolve' => $query->update(['status' => 'resolved', 'is_regression' => false]), + 'ignore' => $query->update(['status' => 'ignored']), + 'mute' => $query->update(['status' => 'muted']), + 'delete' => $query->delete(), + }; + + $count = count($validated['issues']); + + return back()->with('status', "{$count} issue(s) {$validated['action']}d."); + } + + private function authorizeProject(Request $request, Project $project): void + { + abort_unless( + $request->user()->projects()->where('projects.id', $project->id)->exists(), + 403, + ); + } +} diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php new file mode 100644 index 0000000..4ef12f9 --- /dev/null +++ b/app/Http/Controllers/ProjectController.php @@ -0,0 +1,90 @@ +user() + ->projects() + ->withCount(['issues' => fn ($q) => $q->where('status', 'unresolved')]) + ->orderBy('name') + ->get(); + + return view('projects.index', compact('projects')); + } + + public function create(): View + { + return view('projects.create'); + } + + public function store(Request $request): RedirectResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'platform' => 'nullable|string|max:50', + ]); + + $project = Project::create($validated); + + $request->user()->projects()->attach($project, ['role' => 'owner']); + + return redirect() + ->route('projects.settings', $project) + ->with('status', 'Project created. Here is your DSN.'); + } + + public function settings(Request $request, Project $project): View + { + $this->authorizeProject($request, $project); + + return view('projects.settings', compact('project')); + } + + public function update(Request $request, Project $project): RedirectResponse + { + $this->authorizeProject($request, $project); + + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'platform' => 'nullable|string|max:50', + ]); + + $project->update($validated); + + return back()->with('status', 'Project updated.'); + } + + public function regenerateKeys(Request $request, Project $project): RedirectResponse + { + $this->authorizeProject($request, $project); + + $project->regenerateKeys(); + + return back()->with('status', 'DSN keys regenerated.'); + } + + public function destroy(Request $request, Project $project): RedirectResponse + { + $this->authorizeProject($request, $project); + + $project->delete(); + + return redirect()->route('projects.index')->with('status', 'Project deleted.'); + } + + private function authorizeProject(Request $request, Project $project): void + { + abort_unless( + $request->user()->projects()->where('projects.id', $project->id)->exists(), + 403, + ); + } +} diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php index 66028f2..aafca65 100644 --- a/resources/views/dashboard.blade.php +++ b/resources/views/dashboard.blade.php @@ -1,17 +1,144 @@ -

- {{ __('Dashboard') }} -

+

Dashboard

-
-
-
-
- {{ __("You're logged in!") }} +
+
+ + {{-- Summary Cards --}} +
+
+
Unresolved Issues
+
{{ number_format($unresolvedCount) }}
+
+
+
Events (24h)
+
{{ number_format($events24h) }}
+
+
+
Active Projects
+
{{ $mostActiveProjects->count() }}
+ + {{-- Events Over Time Chart --}} +
+
+

Events Over Time

+
+ @foreach([7, 14, 30] as $d) + + {{ $d }}d + + @endforeach +
+
+
+ +
+ +
+ + {{-- Most Active Projects --}} + @if($mostActiveProjects->isNotEmpty()) +
+
+

Most Active Projects

+
+ + + + + + + + + + + @foreach($mostActiveProjects as $p) + + + + + + + @endforeach + +
ProjectPlatformUnresolvedEvents (24h)
+ {{ $p->name }} + {{ $p->platform ?? '—' }}{{ $p->issues_count }}{{ $p->events_count }}
+
+ @else +
+ No projects yet. + Create your first project +
+ @endif +
diff --git a/resources/views/events/show.blade.php b/resources/views/events/show.blade.php new file mode 100644 index 0000000..c553638 --- /dev/null +++ b/resources/views/events/show.blade.php @@ -0,0 +1,254 @@ + + @php($currentProject = $project) + + +
+
+ +

Event {{ Str::limit($event->event_id, 20) }}

+
+
+ @if($previousEvent) + ← Older + @endif + @if($nextEvent) + Newer → + @endif +
+
+
+ +
+
+ + {{-- Event Meta --}} +
+
+
+ Level +
{{ ucfirst($event->level) }}
+
+
+ Environment +
{{ $event->environment ?? '—' }}
+
+
+ Release +
{{ $event->release ?? '—' }}
+
+
+ Timestamp +
{{ $event->timestamp->format('Y-m-d H:i:s') }}
+
+ @if($event->server_name) +
+ Server +
{{ $event->server_name }}
+
+ @endif + @if($event->platform) +
+ Platform +
{{ $event->platform }}
+
+ @endif + @if($event->sdk) +
+ SDK +
{{ $event->sdk['name'] ?? '' }} {{ $event->sdk['version'] ?? '' }}
+
+ @endif +
+ @if($event->message) +
+ Message +

{{ $event->message }}

+
+ @endif +
+ + {{-- Stacktrace --}} + @if(!empty($event->exception['values'])) +
+
+

Exception

+
+ @foreach($event->exception['values'] as $exc) +
+
+ {{ $exc['type'] ?? 'Exception' }} + @if(!empty($exc['value'])) + {{ $exc['value'] }} + @endif +
+ @if(!empty($exc['stacktrace']['frames'])) +
+ @foreach(array_reverse($exc['stacktrace']['frames']) as $i => $frame) + @php + $file = $frame['filename'] ?? $frame['abs_path'] ?? '?'; + $fn = $frame['function'] ?? null; + $lineno = $frame['lineno'] ?? null; + $inApp = $frame['in_app'] ?? false; + $context = $frame['context_line'] ?? null; + $preContext = $frame['pre_context'] ?? []; + $postContext = $frame['post_context'] ?? []; + @endphp +
+ +
+ @if($context || !empty($preContext) || !empty($postContext)) +
@foreach($preContext as $ci => $line)
+{{ ($lineno ?? 0) - count($preContext) + $ci }}  {{ $line }}
+@endforeach
+@if($context){{ $lineno }}  {{ $context }}@endif
+@foreach($postContext as $ci => $line)
+{{ ($lineno ?? 0) + $ci + 1 }}  {{ $line }}
+@endforeach
+ @else +

No source context available.

+ @endif +
+
+ @endforeach +
+ @endif +
+ @endforeach +
+ @endif + + {{-- Breadcrumbs --}} + @php + $crumbs = $event->breadcrumbs['values'] ?? $event->breadcrumbs ?? []; + if (!is_array($crumbs)) $crumbs = []; + @endphp + @if(count($crumbs) > 0) +
+
+

Breadcrumbs

+
+
+ @foreach($crumbs as $crumb) +
+ + {{ isset($crumb['timestamp']) ? \Carbon\Carbon::parse($crumb['timestamp'])->format('H:i:s') : '' }} + + {{ $crumb['category'] ?? $crumb['type'] ?? 'default' }} + {{ $crumb['message'] ?? json_encode($crumb['data'] ?? '') }} +
+ @endforeach +
+
+ @endif + + {{-- Tags --}} + @if(!empty($event->tags)) +
+

Tags

+
+ @foreach($event->tags as $key => $value) + + {{ $key }} + = + {{ $value }} + + @endforeach +
+
+ @endif + + {{-- Contexts --}} + @php + $contexts = $event->contexts ?? []; + $userCtx = $event->user_context; + $requestCtx = $event->request_context; + @endphp + @if(!empty($contexts) || $userCtx || $requestCtx) +
+ @if($requestCtx) +
+

Request

+
+ @foreach($requestCtx as $k => $v) +
+
{{ ucfirst($k) }}
+
+ @if(is_string($v) || is_numeric($v)) + {{ $v }} + @else +
{{ json_encode($v, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}
+ @endif +
+
+ @endforeach +
+
+ @endif + @if($userCtx) +
+

User

+
+ @foreach($userCtx as $k => $v) +
+
{{ ucfirst($k) }}
+
{{ is_string($v) ? $v : json_encode($v) }}
+
+ @endforeach +
+
+ @endif + @foreach($contexts as $ctxName => $ctxData) + @if(is_array($ctxData)) +
+

{{ ucfirst(str_replace('_', ' ', $ctxName)) }}

+
+ @foreach($ctxData as $k => $v) + @if($k !== 'type') +
+
{{ ucfirst(str_replace('_', ' ', $k)) }}
+
{{ is_string($v) || is_numeric($v) ? $v : json_encode($v) }}
+
+ @endif + @endforeach +
+
+ @endif + @endforeach +
+ @endif + + {{-- Raw JSON --}} +
+ +
+
{{ json_encode($event->raw_payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}
+
+
+ +
+
+
diff --git a/resources/views/issues/index.blade.php b/resources/views/issues/index.blade.php new file mode 100644 index 0000000..790ad73 --- /dev/null +++ b/resources/views/issues/index.blade.php @@ -0,0 +1,142 @@ + + @php($currentProject = $project) + + +
+

{{ $project->name }} — Issues

+
+
+ +
+
+ + @if(session('status')) +
+ {{ session('status') }} +
+ @endif + + {{-- Filters --}} +
+
+
+ + +
+
+ + +
+
+ + +
+ Filter + @if($search || $currentLevel || $currentStatus !== 'unresolved') + Reset + @endif +
+
+ + {{-- Bulk Actions --}} +
+ +
+ @csrf + + + + + +
+
+ + {{-- Issues Table --}} +
+ @if($issues->isEmpty()) +
+ No issues found. +
+ @else + + + + + + + + + + + + @foreach($issues as $issue) + + + + + + + + @endforeach + +
+ + StatusIssueEventsLast Seen
+ + + @php + $statusColors = [ + 'unresolved' => 'bg-red-100 text-red-800', + 'resolved' => 'bg-green-100 text-green-800', + 'ignored' => 'bg-yellow-100 text-yellow-800', + 'muted' => 'bg-gray-100 text-gray-600', + ]; + $levelColors = [ + 'fatal' => 'bg-red-600', + 'error' => 'bg-red-400', + 'warning' => 'bg-yellow-400', + 'info' => 'bg-blue-400', + 'debug' => 'bg-gray-400', + ]; + @endphp + + + {{ ucfirst($issue->status) }} + + + +
{{ $issue->title }}
+ @if($issue->culprit) +
{{ $issue->culprit }}
+ @endif +
+
+ {{ number_format($issue->times_seen) }} + + {{ $issue->last_seen_at->diffForHumans() }} +
+
+ {{ $issues->links() }} +
+ @endif +
+ +
+
+
diff --git a/resources/views/issues/show.blade.php b/resources/views/issues/show.blade.php new file mode 100644 index 0000000..ca7f2a9 --- /dev/null +++ b/resources/views/issues/show.blade.php @@ -0,0 +1,324 @@ + + @php($currentProject = $project) + + +
+
+ +

{{ $issue->title }}

+ @if($issue->culprit) +

{{ $issue->culprit }}

+ @endif +
+
+
+ @csrf + @method('PATCH') + @if($issue->status === 'unresolved') + + + @elseif($issue->status === 'resolved') + + @else + + + @endif +
+
+
+
+ +
+
+ + @if(session('status')) +
+ {{ session('status') }} +
+ @endif + + {{-- Issue Meta --}} + @php + $statusColors = [ + 'unresolved' => 'bg-red-100 text-red-800', + 'resolved' => 'bg-green-100 text-green-800', + 'ignored' => 'bg-yellow-100 text-yellow-800', + 'muted' => 'bg-gray-100 text-gray-600', + ]; + $levelColors = [ + 'fatal' => 'bg-red-100 text-red-800', + 'error' => 'bg-red-50 text-red-700', + 'warning' => 'bg-yellow-50 text-yellow-700', + 'info' => 'bg-blue-50 text-blue-700', + 'debug' => 'bg-gray-100 text-gray-600', + ]; + @endphp +
+
+
+ Status +
+ + {{ ucfirst($issue->status) }} + @if($issue->is_regression) + (regression) + @endif + +
+
+
+ Level +
+ + {{ ucfirst($issue->level) }} + +
+
+
+ Events +
{{ number_format($issue->times_seen) }}
+
+
+ First Seen +
{{ $issue->first_seen_at->diffForHumans() }}
+
+
+ Last Seen +
{{ $issue->last_seen_at->diffForHumans() }}
+
+
+
+ + @if($latestEvent) + {{-- Stacktrace --}} + @if(!empty($latestEvent->exception['values'])) +
+
+

Stacktrace

+
+ @foreach($latestEvent->exception['values'] as $exc) +
+
+ {{ $exc['type'] ?? 'Exception' }} + @if(!empty($exc['value'])) + {{ $exc['value'] }} + @endif +
+ @if(!empty($exc['stacktrace']['frames'])) +
+ @foreach(array_reverse($exc['stacktrace']['frames']) as $i => $frame) + @php + $file = $frame['filename'] ?? $frame['abs_path'] ?? '?'; + $fn = $frame['function'] ?? null; + $lineno = $frame['lineno'] ?? null; + $inApp = $frame['in_app'] ?? false; + $context = $frame['context_line'] ?? null; + $preContext = $frame['pre_context'] ?? []; + $postContext = $frame['post_context'] ?? []; + @endphp +
+ +
+ @if($context || !empty($preContext) || !empty($postContext)) +
@foreach($preContext as $ci => $line)
+{{ ($lineno ?? 0) - count($preContext) + $ci }}  {{ $line }}
+@endforeach
+@if($context){{ $lineno }}  {{ $context }}@endif
+@foreach($postContext as $ci => $line)
+{{ ($lineno ?? 0) + $ci + 1 }}  {{ $line }}
+@endforeach
+ @elseif(!empty($frame['vars'])) +
{{ json_encode($frame['vars'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}
+ @else +

No source context available.

+ @endif +
+
+ @endforeach +
+ @endif +
+ @endforeach +
+ @endif + + {{-- Breadcrumbs --}} + @if(!empty($latestEvent->breadcrumbs['values'] ?? $latestEvent->breadcrumbs)) + @php + $crumbs = $latestEvent->breadcrumbs['values'] ?? $latestEvent->breadcrumbs; + if (!is_array($crumbs)) $crumbs = []; + @endphp + @if(count($crumbs) > 0) +
+
+

Breadcrumbs

+
+
+ @foreach($crumbs as $crumb) +
+ + {{ isset($crumb['timestamp']) ? \Carbon\Carbon::parse($crumb['timestamp'])->format('H:i:s') : '' }} + + @php + $catColor = match($crumb['type'] ?? $crumb['category'] ?? '') { + 'error' => 'text-red-600', + 'warning' => 'text-yellow-600', + 'http', 'navigation' => 'text-blue-600', + default => 'text-gray-600', + }; + @endphp + {{ $crumb['category'] ?? $crumb['type'] ?? 'default' }} + {{ $crumb['message'] ?? json_encode($crumb['data'] ?? '') }} +
+ @endforeach +
+
+ @endif + @endif + + {{-- Tags --}} + @if(!empty($latestEvent->tags)) +
+

Tags

+
+ @foreach($latestEvent->tags as $key => $value) + + {{ $key }} + = + {{ $value }} + + @endforeach +
+
+ @endif + + {{-- Context Panels --}} + @php + $contexts = $latestEvent->contexts ?? []; + $userCtx = $latestEvent->user_context; + $requestCtx = $latestEvent->request_context; + @endphp + @if(!empty($contexts) || $userCtx || $requestCtx) +
+ @if($requestCtx) +
+

Request

+
+ @if(!empty($requestCtx['method'])) +
Method
{{ $requestCtx['method'] }}
+ @endif + @if(!empty($requestCtx['url'])) +
URL
{{ $requestCtx['url'] }}
+ @endif + @if(!empty($requestCtx['headers'])) +
+
Headers
+
{{ json_encode($requestCtx['headers'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}
+
+ @endif +
+
+ @endif + + @if($userCtx) +
+

User

+
+ @foreach($userCtx as $k => $v) +
+
{{ ucfirst($k) }}
+
{{ is_string($v) ? $v : json_encode($v) }}
+
+ @endforeach +
+
+ @endif + + @foreach($contexts as $ctxName => $ctxData) + @if(is_array($ctxData)) +
+

{{ ucfirst(str_replace('_', ' ', $ctxName)) }}

+
+ @foreach($ctxData as $k => $v) + @if($k !== 'type') +
+
{{ ucfirst(str_replace('_', ' ', $k)) }}
+
{{ is_string($v) || is_numeric($v) ? $v : json_encode($v) }}
+
+ @endif + @endforeach +
+
+ @endif + @endforeach +
+ @endif + + {{-- Extra Data --}} + @if(!empty($latestEvent->extra)) +
+

Extra Data

+
{{ json_encode($latestEvent->extra, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}
+
+ @endif + @endif + + {{-- Events List --}} +
+
+

Events

+
+ @if($events->isEmpty()) +
No events recorded.
+ @else + + + + + + + + + + + @foreach($events as $ev) + + + + + + + @endforeach + +
Event IDEnvironmentReleaseTimestamp
+ + {{ Str::limit($ev->event_id, 16) }} + + {{ $ev->environment ?? '—' }}{{ $ev->release ?? '—' }}{{ $ev->timestamp->diffForHumans() }}
+
+ {{ $events->links() }} +
+ @endif +
+ +
+
+
diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php index c2d3a65..63682c6 100644 --- a/resources/views/layouts/navigation.blade.php +++ b/resources/views/layouts/navigation.blade.php @@ -1,58 +1,81 @@