add telemetry system with analytics and crash reporting (M08)

This commit is contained in:
2026-01-18 21:53:06 +01:00
parent fbcb5c9543
commit a5aa3cc9d7
6 changed files with 1484 additions and 3 deletions

View File

@@ -8,6 +8,7 @@ import (
"github.com/omixlab/mosis-portal/internal/database"
"github.com/omixlab/mosis-portal/internal/review"
"github.com/omixlab/mosis-portal/internal/storage"
"github.com/omixlab/mosis-portal/internal/telemetry"
)
// Handler handles web page requests
@@ -16,6 +17,7 @@ type Handler struct {
templates *Templates
store *storage.Storage
review *review.Service
telemetry *telemetry.Service
}
// NewHandler creates a new web handler
@@ -36,6 +38,11 @@ func (h *Handler) SetStorage(store *storage.Storage) {
h.store = store
}
// SetTelemetry sets the telemetry service for the handler
func (h *Handler) SetTelemetry(ts *telemetry.Service) {
h.telemetry = ts
}
// PageData is the base data structure for all pages
type PageData struct {
Title string
@@ -436,3 +443,141 @@ func (h *Handler) AdminValidate(w http.ResponseWriter, r *http.Request) {
// Render validation results partial
h.renderPartial(w, "validation_results", result)
}
// AppAnalytics renders the app analytics page
func (h *Handler) AppAnalytics(w http.ResponseWriter, r *http.Request) {
developer := getDeveloperFromContext(r)
if developer == nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
appID := chi.URLParam(r, "appID")
app, err := h.db.GetApp(r.Context(), appID)
if err != nil {
http.Error(w, "App not found", http.StatusNotFound)
return
}
// Verify ownership
if app.DeveloperID != developer.ID {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// Get days parameter
days := 30
if d := r.URL.Query().Get("days"); d != "" {
if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 && parsed <= 365 {
days = parsed
}
}
// Get analytics data
var overview *telemetry.AnalyticsOverview
var eventStats []telemetry.DailyStats
var crashes []*telemetry.CrashGroup
if h.telemetry != nil {
overview, _ = h.telemetry.GetAnalyticsOverview(r.Context(), appID, days)
eventStats, _ = h.telemetry.GetDailyStats(r.Context(), appID, "", days)
crashes, _, _ = h.telemetry.GetCrashGroups(r.Context(), appID, "open", 5, 0)
}
if overview == nil {
overview = &telemetry.AnalyticsOverview{}
}
data := struct {
PageData
App *database.App
Days int
Overview *telemetry.AnalyticsOverview
EventStats []telemetry.DailyStats
Crashes []*telemetry.CrashGroup
}{
PageData: PageData{
Title: app.Name + " - Analytics",
ActiveNav: "apps",
Developer: developer,
},
App: app,
Days: days,
Overview: overview,
EventStats: eventStats,
Crashes: crashes,
}
h.render(w, "app_analytics", data)
}
// AppCrashes renders the app crashes page
func (h *Handler) AppCrashes(w http.ResponseWriter, r *http.Request) {
developer := getDeveloperFromContext(r)
if developer == nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
appID := chi.URLParam(r, "appID")
app, err := h.db.GetApp(r.Context(), appID)
if err != nil {
http.Error(w, "App not found", http.StatusNotFound)
return
}
// Verify ownership
if app.DeveloperID != developer.ID {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// Get status filter
status := r.URL.Query().Get("status")
if status == "" {
status = "open"
}
// Pagination
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
limit := 20
offset := (page - 1) * limit
// Get crashes
var crashes []*telemetry.CrashGroup
var total int
if h.telemetry != nil {
crashes, total, _ = h.telemetry.GetCrashGroups(r.Context(), appID, status, limit, offset)
}
data := struct {
PageData
App *database.App
Status string
Crashes []*telemetry.CrashGroup
Pagination struct {
Page int
Limit int
Total int
TotalPages int
}
}{
PageData: PageData{
Title: app.Name + " - Crashes",
ActiveNav: "apps",
Developer: developer,
},
App: app,
Status: status,
Crashes: crashes,
}
data.Pagination.Page = page
data.Pagination.Limit = limit
data.Pagination.Total = total
data.Pagination.TotalPages = (total + limit - 1) / limit
h.render(w, "app_crashes", data)
}