package handlers import ( "encoding/json" "net/http" "strconv" "github.com/go-chi/chi/v5" "omixlab.com/mosis-portal/internal/database" "omixlab.com/mosis-portal/internal/telemetry" ) // TelemetryHandler handles telemetry API requests type TelemetryHandler struct { db *database.DB telemetry *telemetry.Service } // NewTelemetryHandler creates a new telemetry handler func NewTelemetryHandler(db *database.DB, ts *telemetry.Service) *TelemetryHandler { return &TelemetryHandler{ db: db, telemetry: ts, } } // RecordEvents handles POST /v1/telemetry/events func (h *TelemetryHandler) RecordEvents(w http.ResponseWriter, r *http.Request) { var batch telemetry.EventBatch if err := json.NewDecoder(r.Body).Decode(&batch); err != nil { Error(w, http.StatusBadRequest, "invalid_body", "Invalid request body") return } if batch.AppID == "" { Error(w, http.StatusBadRequest, "missing_app_id", "App ID is required") return } if batch.DeviceID == "" { Error(w, http.StatusBadRequest, "missing_device_id", "Device ID is required") return } if len(batch.Events) == 0 { Error(w, http.StatusBadRequest, "no_events", "At least one event is required") return } count, err := h.telemetry.RecordEvents(r.Context(), &batch) if err != nil { Error(w, http.StatusInternalServerError, "record_error", err.Error()) return } JSON(w, http.StatusOK, map[string]interface{}{ "received": count, }) } // RecordCrash handles POST /v1/telemetry/crash func (h *TelemetryHandler) RecordCrash(w http.ResponseWriter, r *http.Request) { var report telemetry.CrashReport if err := json.NewDecoder(r.Body).Decode(&report); err != nil { Error(w, http.StatusBadRequest, "invalid_body", "Invalid request body") return } if report.AppID == "" { Error(w, http.StatusBadRequest, "missing_app_id", "App ID is required") return } if report.DeviceID == "" { Error(w, http.StatusBadRequest, "missing_device_id", "Device ID is required") return } if report.Crash.Type == "" { Error(w, http.StatusBadRequest, "missing_crash_type", "Crash type is required") return } groupID, err := h.telemetry.RecordCrash(r.Context(), &report) if err != nil { Error(w, http.StatusInternalServerError, "record_error", err.Error()) return } JSON(w, http.StatusOK, map[string]interface{}{ "id": groupID, }) } // GetAnalyticsOverview handles GET /v1/apps/:appID/analytics/overview func (h *TelemetryHandler) GetAnalyticsOverview(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "appID") // Verify app ownership developerID := getDeveloperID(r) app, err := h.db.GetApp(r.Context(), appID) if err != nil { Error(w, http.StatusNotFound, "not_found", "App not found") return } if app.DeveloperID != developerID { Error(w, http.StatusForbidden, "forbidden", "Access denied") return } days := 30 if d := r.URL.Query().Get("days"); d != "" { if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 && parsed <= 365 { days = parsed } } overview, err := h.telemetry.GetAnalyticsOverview(r.Context(), appID, days) if err != nil { Error(w, http.StatusInternalServerError, "query_error", err.Error()) return } JSON(w, http.StatusOK, overview) } // GetAnalyticsEvents handles GET /v1/apps/:appID/analytics/events func (h *TelemetryHandler) GetAnalyticsEvents(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "appID") // Verify app ownership developerID := getDeveloperID(r) app, err := h.db.GetApp(r.Context(), appID) if err != nil { Error(w, http.StatusNotFound, "not_found", "App not found") return } if app.DeveloperID != developerID { Error(w, http.StatusForbidden, "forbidden", "Access denied") return } eventType := r.URL.Query().Get("event_type") days := 30 if d := r.URL.Query().Get("days"); d != "" { if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 && parsed <= 365 { days = parsed } } stats, err := h.telemetry.GetDailyStats(r.Context(), appID, eventType, days) if err != nil { Error(w, http.StatusInternalServerError, "query_error", err.Error()) return } JSON(w, http.StatusOK, map[string]interface{}{ "data": stats, }) } // GetCrashes handles GET /v1/apps/:appID/crashes func (h *TelemetryHandler) GetCrashes(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "appID") // Verify app ownership developerID := getDeveloperID(r) app, err := h.db.GetApp(r.Context(), appID) if err != nil { Error(w, http.StatusNotFound, "not_found", "App not found") return } if app.DeveloperID != developerID { Error(w, http.StatusForbidden, "forbidden", "Access denied") return } status := r.URL.Query().Get("status") page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit < 1 || limit > 100 { limit = 20 } offset := (page - 1) * limit crashes, total, err := h.telemetry.GetCrashGroups(r.Context(), appID, status, limit, offset) if err != nil { Error(w, http.StatusInternalServerError, "query_error", err.Error()) return } JSON(w, http.StatusOK, map[string]interface{}{ "crashes": crashes, "pagination": map[string]int{ "page": page, "limit": limit, "total": total, "total_pages": (total + limit - 1) / limit, }, }) } // GetCrash handles GET /v1/apps/:appID/crashes/:crashID func (h *TelemetryHandler) GetCrash(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "appID") crashID := chi.URLParam(r, "crashID") // Verify app ownership developerID := getDeveloperID(r) app, err := h.db.GetApp(r.Context(), appID) if err != nil { Error(w, http.StatusNotFound, "not_found", "App not found") return } if app.DeveloperID != developerID { Error(w, http.StatusForbidden, "forbidden", "Access denied") return } crash, err := h.telemetry.GetCrashGroup(r.Context(), appID, crashID) if err != nil { Error(w, http.StatusNotFound, "not_found", "Crash group not found") return } JSON(w, http.StatusOK, crash) } // UpdateCrashStatus handles PATCH /v1/apps/:appID/crashes/:crashID func (h *TelemetryHandler) UpdateCrashStatus(w http.ResponseWriter, r *http.Request) { appID := chi.URLParam(r, "appID") crashID := chi.URLParam(r, "crashID") // Verify app ownership developerID := getDeveloperID(r) app, err := h.db.GetApp(r.Context(), appID) if err != nil { Error(w, http.StatusNotFound, "not_found", "App not found") return } if app.DeveloperID != developerID { Error(w, http.StatusForbidden, "forbidden", "Access denied") return } var req struct { Status string `json:"status"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { Error(w, http.StatusBadRequest, "invalid_body", "Invalid request body") return } if req.Status != "open" && req.Status != "resolved" && req.Status != "ignored" { Error(w, http.StatusBadRequest, "invalid_status", "Status must be open, resolved, or ignored") return } if err := h.telemetry.UpdateCrashGroupStatus(r.Context(), appID, crashID, req.Status); err != nil { Error(w, http.StatusInternalServerError, "update_error", err.Error()) return } crash, _ := h.telemetry.GetCrashGroup(r.Context(), appID, crashID) JSON(w, http.StatusOK, crash) }