add telemetry system with analytics and crash reporting (M08)
This commit is contained in:
228
portal/internal/web/templates/pages/app_analytics.html
Normal file
228
portal/internal/web/templates/pages/app_analytics.html
Normal file
@@ -0,0 +1,228 @@
|
||||
{{define "content"}}
|
||||
<div class="mb-6">
|
||||
<a href="/dashboard" class="inline-flex items-center text-sm text-gray-500 hover:text-gray-700">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- App Header -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-16 h-16 bg-gray-100 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">{{.App.Name}}</h1>
|
||||
<p class="text-gray-500">{{.App.PackageID}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<select id="days-select" class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
hx-get="/apps/{{.App.ID}}/analytics"
|
||||
hx-trigger="change"
|
||||
hx-target="#analytics-content"
|
||||
hx-select="#analytics-content"
|
||||
hx-include="this">
|
||||
<option value="7" {{if eq .Days 7}}selected{{end}}>Last 7 days</option>
|
||||
<option value="30" {{if eq .Days 30}}selected{{end}}>Last 30 days</option>
|
||||
<option value="90" {{if eq .Days 90}}selected{{end}}>Last 90 days</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="border-b border-gray-200 mb-6">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
<a href="/apps/{{.App.ID}}" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
|
||||
Overview
|
||||
</a>
|
||||
<a href="/apps/{{.App.ID}}/versions" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
|
||||
Versions
|
||||
</a>
|
||||
<a href="/apps/{{.App.ID}}/analytics" class="border-indigo-500 text-indigo-600 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
|
||||
Analytics
|
||||
</a>
|
||||
<a href="/apps/{{.App.ID}}/settings" class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm">
|
||||
Settings
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Analytics Content -->
|
||||
<div id="analytics-content">
|
||||
<!-- Overview Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-gray-500">Daily Active Users</p>
|
||||
<p class="mt-1 text-3xl font-semibold text-gray-900">{{.Overview.DAU}}</p>
|
||||
</div>
|
||||
<div class="{{if ge .Overview.DAUChange 0.0}}text-green-500{{else}}text-red-500{{end}}">
|
||||
{{if ge .Overview.DAUChange 0.0}}
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
|
||||
</svg>
|
||||
{{else}}
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6"/>
|
||||
</svg>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-sm {{if ge .Overview.DAUChange 0.0}}text-green-600{{else}}text-red-600{{end}}">
|
||||
{{if ge .Overview.DAUChange 0.0}}+{{end}}{{printf "%.1f" .Overview.DAUChange}}% from previous period
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-gray-500">Total Sessions</p>
|
||||
<p class="mt-1 text-3xl font-semibold text-gray-900">{{.Overview.TotalSessions}}</p>
|
||||
</div>
|
||||
<div class="text-indigo-500">
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-gray-500">Crash-Free Rate</p>
|
||||
<p class="mt-1 text-3xl font-semibold text-gray-900">{{printf "%.1f" .Overview.CrashFreeRate}}%</p>
|
||||
</div>
|
||||
<div class="{{if ge .Overview.CrashFreeRate 99.0}}text-green-500{{else if ge .Overview.CrashFreeRate 95.0}}text-yellow-500{{else}}text-red-500{{end}}">
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-gray-500">Total Crashes</p>
|
||||
<p class="mt-1 text-3xl font-semibold text-gray-900">{{.Overview.TotalCrashes}}</p>
|
||||
</div>
|
||||
<div class="{{if eq .Overview.TotalCrashes 0}}text-green-500{{else}}text-red-500{{end}}">
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{{if gt .Overview.TotalCrashes 0}}
|
||||
<a href="/apps/{{.App.ID}}/crashes" class="mt-2 text-sm text-indigo-600 hover:text-indigo-700">
|
||||
View crash reports →
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<!-- DAU Chart -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Daily Active Users</h3>
|
||||
<div id="dau-chart" class="h-64"
|
||||
hx-get="/apps/{{.App.ID}}/partials/chart-dau?days={{.Days}}"
|
||||
hx-trigger="load">
|
||||
<div class="flex items-center justify-center h-full text-gray-400">
|
||||
<svg class="animate-spin h-8 w-8" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sessions Chart -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Sessions</h3>
|
||||
<div id="sessions-chart" class="h-64"
|
||||
hx-get="/apps/{{.App.ID}}/partials/chart-sessions?days={{.Days}}"
|
||||
hx-trigger="load">
|
||||
<div class="flex items-center justify-center h-full text-gray-400">
|
||||
<svg class="animate-spin h-8 w-8" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Types -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Event Distribution</h3>
|
||||
{{if .EventStats}}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Event Type</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Count</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Unique Devices</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{{range .EventStats}}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{.EventType}}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">{{.Count}}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">{{.UniqueDevices}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="text-gray-500 text-center py-8">No events recorded yet.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Recent Crashes -->
|
||||
{{if .Crashes}}
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Recent Crashes</h3>
|
||||
<a href="/apps/{{.App.ID}}/crashes" class="text-sm text-indigo-600 hover:text-indigo-700">
|
||||
View all →
|
||||
</a>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{{range .Crashes}}
|
||||
<div class="flex items-start p-4 bg-red-50 border border-red-100 rounded-lg">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="w-5 h-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm font-medium text-red-800">{{.CrashType}}: {{.Message}}</p>
|
||||
<div class="mt-1 flex items-center space-x-4 text-xs text-red-600">
|
||||
<span>{{.OccurrenceCount}} occurrences</span>
|
||||
<span>Last seen: {{.LastSeen}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ml-4 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {{if eq .Status "open"}}bg-red-100 text-red-800{{else if eq .Status "resolved"}}bg-green-100 text-green-800{{else}}bg-gray-100 text-gray-800{{end}}">
|
||||
{{.Status}}
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
148
portal/internal/web/templates/pages/app_crashes.html
Normal file
148
portal/internal/web/templates/pages/app_crashes.html
Normal file
@@ -0,0 +1,148 @@
|
||||
{{define "content"}}
|
||||
<div class="mb-6">
|
||||
<a href="/apps/{{.App.ID}}/analytics" class="inline-flex items-center text-sm text-gray-500 hover:text-gray-700">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
Back to Analytics
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-12 h-12 bg-red-100 rounded-xl flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Crash Reports</h1>
|
||||
<p class="text-gray-500">{{.App.Name}}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Filter -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="/apps/{{.App.ID}}/crashes?status=open"
|
||||
class="px-4 py-2 text-sm font-medium rounded-lg {{if eq .Status "open"}}bg-red-100 text-red-700{{else}}text-gray-500 hover:bg-gray-100{{end}}">
|
||||
Open
|
||||
</a>
|
||||
<a href="/apps/{{.App.ID}}/crashes?status=resolved"
|
||||
class="px-4 py-2 text-sm font-medium rounded-lg {{if eq .Status "resolved"}}bg-green-100 text-green-700{{else}}text-gray-500 hover:bg-gray-100{{end}}">
|
||||
Resolved
|
||||
</a>
|
||||
<a href="/apps/{{.App.ID}}/crashes?status=ignored"
|
||||
class="px-4 py-2 text-sm font-medium rounded-lg {{if eq .Status "ignored"}}bg-gray-200 text-gray-700{{else}}text-gray-500 hover:bg-gray-100{{end}}">
|
||||
Ignored
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Crashes List -->
|
||||
{{if .Crashes}}
|
||||
<div class="space-y-4">
|
||||
{{range .Crashes}}
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:border-gray-300 transition-colors">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2 mb-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
{{.CrashType}}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">{{.OccurrenceCount}} occurrences</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">{{.Message}}</h3>
|
||||
{{if .SampleStackTrace}}
|
||||
<pre class="mt-3 p-3 bg-gray-50 rounded-lg text-xs text-gray-600 overflow-x-auto max-h-32">{{.SampleStackTrace}}</pre>
|
||||
{{end}}
|
||||
<div class="mt-3 flex items-center space-x-4 text-xs text-gray-500">
|
||||
<span>First seen: {{.FirstSeen}}</span>
|
||||
<span>Last seen: {{.LastSeen}}</span>
|
||||
{{if .AffectedVersions}}
|
||||
<span>Versions: {{range $i, $v := .AffectedVersions}}{{if $i}}, {{end}}{{$v}}{{end}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-6 flex flex-col items-end space-y-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded text-xs font-medium {{if eq .Status "open"}}bg-red-100 text-red-800{{else if eq .Status "resolved"}}bg-green-100 text-green-800{{else}}bg-gray-100 text-gray-800{{end}}">
|
||||
{{.Status}}
|
||||
</span>
|
||||
<div class="flex space-x-2">
|
||||
{{if eq .Status "open"}}
|
||||
<button hx-post="/apps/{{$.App.ID}}/crashes/{{.ID}}/resolve"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="closest div.bg-white"
|
||||
class="text-xs text-green-600 hover:text-green-700 font-medium">
|
||||
Mark Resolved
|
||||
</button>
|
||||
<button hx-post="/apps/{{$.App.ID}}/crashes/{{.ID}}/ignore"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="closest div.bg-white"
|
||||
class="text-xs text-gray-500 hover:text-gray-700 font-medium">
|
||||
Ignore
|
||||
</button>
|
||||
{{else if eq .Status "resolved"}}
|
||||
<button hx-post="/apps/{{$.App.ID}}/crashes/{{.ID}}/reopen"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="closest div.bg-white"
|
||||
class="text-xs text-red-600 hover:text-red-700 font-medium">
|
||||
Reopen
|
||||
</button>
|
||||
{{else}}
|
||||
<button hx-post="/apps/{{$.App.ID}}/crashes/{{.ID}}/reopen"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="closest div.bg-white"
|
||||
class="text-xs text-red-600 hover:text-red-700 font-medium">
|
||||
Reopen
|
||||
</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{{if gt .Pagination.TotalPages 1}}
|
||||
<div class="mt-6 flex items-center justify-between">
|
||||
<div class="text-sm text-gray-500">
|
||||
Showing {{add (mul (sub .Pagination.Page 1) .Pagination.Limit) 1}} to {{min (mul .Pagination.Page .Pagination.Limit) .Pagination.Total}} of {{.Pagination.Total}} crashes
|
||||
</div>
|
||||
<nav class="flex space-x-2">
|
||||
{{if gt .Pagination.Page 1}}
|
||||
<a href="/apps/{{.App.ID}}/crashes?status={{.Status}}&page={{sub .Pagination.Page 1}}"
|
||||
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Previous
|
||||
</a>
|
||||
{{end}}
|
||||
{{if lt .Pagination.Page .Pagination.TotalPages}}
|
||||
<a href="/apps/{{.App.ID}}/crashes?status={{.Status}}&page={{add .Pagination.Page 1}}"
|
||||
class="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||
Next
|
||||
</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-12 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">No crashes</h3>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
{{if eq .Status "open"}}
|
||||
No open crash reports. Your app is running smoothly!
|
||||
{{else if eq .Status "resolved"}}
|
||||
No resolved crashes to show.
|
||||
{{else}}
|
||||
No ignored crashes.
|
||||
{{end}}
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user