2025-12-03 19:29:37 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"time"
|
|
|
|
|
|
2025-12-16 19:07:36 +00:00
|
|
|
apperrors "veza-backend-api/internal/errors"
|
2025-12-16 18:34:08 +00:00
|
|
|
"veza-backend-api/internal/services"
|
2025-12-24 13:47:12 +00:00
|
|
|
"veza-backend-api/internal/workers"
|
2025-12-16 18:34:08 +00:00
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
"github.com/gin-gonic/gin"
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"go.uber.org/zap"
|
2025-12-03 19:29:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// AnalyticsHandler gère les opérations d'analytics de lecture de tracks
|
|
|
|
|
type AnalyticsHandler struct {
|
|
|
|
|
analyticsService *services.AnalyticsService
|
2025-12-24 13:47:12 +00:00
|
|
|
jobWorker *workers.JobWorker
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
commonHandler *CommonHandler
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewAnalyticsHandler crée un nouveau handler d'analytics
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
func NewAnalyticsHandler(analyticsService *services.AnalyticsService, logger *zap.Logger) *AnalyticsHandler {
|
|
|
|
|
return &AnalyticsHandler{
|
|
|
|
|
analyticsService: analyticsService,
|
|
|
|
|
commonHandler: NewCommonHandler(logger),
|
|
|
|
|
}
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 13:47:12 +00:00
|
|
|
// SetJobWorker définit le JobWorker pour enregistrer des événements analytics
|
|
|
|
|
func (h *AnalyticsHandler) SetJobWorker(jobWorker *workers.JobWorker) {
|
|
|
|
|
h.jobWorker = jobWorker
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
// RecordPlayRequest représente la requête pour enregistrer une lecture
|
2025-12-16 18:34:08 +00:00
|
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique
|
2025-12-03 19:29:37 +00:00
|
|
|
type RecordPlayRequest struct {
|
2025-12-16 18:34:08 +00:00
|
|
|
Duration int `json:"duration" binding:"required,min=1" validate:"required,min=1"`
|
|
|
|
|
Device string `json:"device,omitempty" validate:"omitempty,max=100"`
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecordPlay gère l'enregistrement d'une lecture de track
|
|
|
|
|
func (h *AnalyticsHandler) RecordPlay(c *gin.Context) {
|
|
|
|
|
trackIDStr := c.Param("id")
|
|
|
|
|
if trackIDStr == "" {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("track id is required"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req RecordPlayRequest
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer user_id si authentifié (optionnel pour analytics anonymes)
|
|
|
|
|
var userID *uuid.UUID
|
|
|
|
|
if uid, ok := c.Get("user_id"); ok {
|
|
|
|
|
if uidUUID, ok := uid.(uuid.UUID); ok {
|
|
|
|
|
userID = &uidUUID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer IP address et device
|
|
|
|
|
ipAddress := c.ClientIP()
|
|
|
|
|
device := req.Device
|
|
|
|
|
if device == "" {
|
|
|
|
|
device = c.GetHeader("User-Agent")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = h.analyticsService.RecordPlay(c.Request.Context(), trackID, userID, req.Duration, device, ipAddress)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err.Error() == "track not found" {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("track"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to record play", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "play recorded"})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetTrackStats gère la récupération des statistiques d'un track
|
|
|
|
|
func (h *AnalyticsHandler) GetTrackStats(c *gin.Context) {
|
|
|
|
|
trackIDStr := c.Param("id")
|
|
|
|
|
if trackIDStr == "" {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("track id is required"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stats, err := h.analyticsService.GetTrackStats(c.Request.Context(), trackID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err.Error() == "track not found" {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("track"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get track stats", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"stats": stats})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetTopTracks gère la récupération des tracks les plus écoutés
|
|
|
|
|
func (h *AnalyticsHandler) GetTopTracks(c *gin.Context) {
|
|
|
|
|
// Parse limit
|
|
|
|
|
limit := 10
|
|
|
|
|
if limitStr := c.Query("limit"); limitStr != "" {
|
|
|
|
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
|
|
|
|
limit = l
|
|
|
|
|
} else {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid limit (must be between 1 and 100)"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse start_date (optionnel)
|
|
|
|
|
var startDate *time.Time
|
|
|
|
|
if startDateStr := c.Query("start_date"); startDateStr != "" {
|
|
|
|
|
parsed, err := time.Parse(time.RFC3339, startDateStr)
|
|
|
|
|
if err != nil {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid start_date format (use RFC3339)"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
startDate = &parsed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse end_date (optionnel)
|
|
|
|
|
var endDate *time.Time
|
|
|
|
|
if endDateStr := c.Query("end_date"); endDateStr != "" {
|
|
|
|
|
parsed, err := time.Parse(time.RFC3339, endDateStr)
|
|
|
|
|
if err != nil {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid end_date format (use RFC3339)"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
endDate = &parsed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
topTracks, err := h.analyticsService.GetTopTracks(c.Request.Context(), limit, startDate, endDate)
|
|
|
|
|
if err != nil {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get top tracks", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"tracks": topTracks})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetPlaysOverTime gère la récupération des lectures sur une période
|
|
|
|
|
func (h *AnalyticsHandler) GetPlaysOverTime(c *gin.Context) {
|
|
|
|
|
trackIDStr := c.Param("id")
|
|
|
|
|
if trackIDStr == "" {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("track id is required"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse start_date (optionnel, défaut: 30 jours)
|
|
|
|
|
startDate := time.Now().AddDate(0, 0, -30)
|
|
|
|
|
if startDateStr := c.Query("start_date"); startDateStr != "" {
|
|
|
|
|
parsed, err := time.Parse(time.RFC3339, startDateStr)
|
|
|
|
|
if err != nil {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid start_date format (use RFC3339)"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
startDate = parsed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse end_date (optionnel, défaut: maintenant)
|
|
|
|
|
endDate := time.Now()
|
|
|
|
|
if endDateStr := c.Query("end_date"); endDateStr != "" {
|
|
|
|
|
parsed, err := time.Parse(time.RFC3339, endDateStr)
|
|
|
|
|
if err != nil {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid end_date format (use RFC3339)"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
endDate = parsed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse interval (optionnel, défaut: day)
|
|
|
|
|
interval := c.DefaultQuery("interval", "day")
|
|
|
|
|
validIntervals := map[string]bool{"hour": true, "day": true, "week": true, "month": true}
|
|
|
|
|
if !validIntervals[interval] {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid interval (must be: hour, day, week, month)"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
points, err := h.analyticsService.GetPlaysOverTime(c.Request.Context(), trackID, startDate, endDate, interval)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err.Error() == "track not found" {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("track"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get plays over time", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"points": points})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetUserStats gère la récupération des statistiques d'un utilisateur
|
|
|
|
|
func (h *AnalyticsHandler) GetUserStats(c *gin.Context) {
|
|
|
|
|
userIDStr := c.Param("id")
|
|
|
|
|
if userIDStr == "" {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("user id is required"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userID, err := uuid.Parse(userIDStr)
|
|
|
|
|
if err != nil {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Vérifier que l'utilisateur peut accéder à ses propres stats
|
|
|
|
|
var authenticatedUserID *uuid.UUID
|
|
|
|
|
if uid, ok := c.Get("user_id"); ok {
|
|
|
|
|
if uidUUID, ok := uid.(uuid.UUID); ok {
|
|
|
|
|
authenticatedUserID = &uidUUID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if authenticatedUserID != nil && *authenticatedUserID != userID {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewForbiddenError("cannot access other user's stats"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stats, err := h.analyticsService.GetUserStats(c.Request.Context(), userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err.Error() == "user not found" {
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("user"))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 19:07:36 +00:00
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get user stats", err))
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"stats": stats})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
2025-12-24 13:47:12 +00:00
|
|
|
|
2025-12-24 13:48:28 +00:00
|
|
|
// GetTrackAnalyticsDashboard gère la récupération du dashboard d'analytics complet pour un track
|
|
|
|
|
// BE-API-036: GET /api/v1/analytics/tracks/:id returns comprehensive track analytics
|
|
|
|
|
// @Summary Get Track Analytics Dashboard
|
|
|
|
|
// @Description Get comprehensive analytics dashboard for a track
|
|
|
|
|
// @Tags Analytics
|
|
|
|
|
// @Accept json
|
|
|
|
|
// @Produce json
|
|
|
|
|
// @Security BearerAuth
|
|
|
|
|
// @Param id path string true "Track ID"
|
|
|
|
|
// @Success 200 {object} APIResponse{data=object{dashboard=object}}
|
|
|
|
|
// @Failure 400 {object} APIResponse "Validation Error"
|
|
|
|
|
// @Failure 404 {object} APIResponse "Track not found"
|
|
|
|
|
// @Failure 500 {object} APIResponse "Internal Error"
|
|
|
|
|
// @Router /analytics/tracks/{id} [get]
|
|
|
|
|
func (h *AnalyticsHandler) GetTrackAnalyticsDashboard(c *gin.Context) {
|
|
|
|
|
trackIDStr := c.Param("id")
|
|
|
|
|
if trackIDStr == "" {
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("track id is required"))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackID, err := uuid.Parse(trackIDStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer les statistiques de base
|
|
|
|
|
stats, err := h.analyticsService.GetTrackStats(c.Request.Context(), trackID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err.Error() == "track not found" {
|
|
|
|
|
RespondWithAppError(c, apperrors.NewNotFoundError("track"))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get track stats", err))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer les lectures sur une période (30 derniers jours)
|
|
|
|
|
startDate := time.Now().AddDate(0, 0, -30)
|
|
|
|
|
endDate := time.Now()
|
|
|
|
|
playsOverTime, err := h.analyticsService.GetPlaysOverTime(c.Request.Context(), trackID, startDate, endDate, "day")
|
|
|
|
|
if err != nil {
|
|
|
|
|
// Ne pas échouer si on ne peut pas récupérer les données temporelles
|
|
|
|
|
playsOverTime = []services.PlayTimePoint{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Construire le dashboard complet
|
|
|
|
|
dashboard := gin.H{
|
|
|
|
|
"track_id": trackID.String(),
|
|
|
|
|
"stats": gin.H{
|
|
|
|
|
"total_plays": stats.TotalPlays,
|
|
|
|
|
"unique_listeners": stats.UniqueListeners,
|
|
|
|
|
"average_duration": stats.AverageDuration,
|
|
|
|
|
"completion_rate": stats.CompletionRate,
|
|
|
|
|
},
|
|
|
|
|
"plays_over_time": playsOverTime,
|
|
|
|
|
"period": gin.H{
|
|
|
|
|
"start_date": startDate.Format(time.RFC3339),
|
|
|
|
|
"end_date": endDate.Format(time.RFC3339),
|
|
|
|
|
"days": 30,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
|
|
|
"dashboard": dashboard,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 13:47:12 +00:00
|
|
|
// RecordEventRequest représente la requête pour enregistrer un événement analytics personnalisé
|
|
|
|
|
// BE-API-035: POST /api/v1/analytics/events to record custom analytics events
|
|
|
|
|
type RecordEventRequest struct {
|
|
|
|
|
EventName string `json:"event_name" binding:"required,min=1,max=100" validate:"required,min=1,max=100"`
|
|
|
|
|
Payload map[string]interface{} `json:"payload,omitempty" validate:"omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecordEvent gère l'enregistrement d'un événement analytics personnalisé
|
|
|
|
|
// BE-API-035: Implement analytics events endpoint
|
|
|
|
|
// @Summary Record Analytics Event
|
|
|
|
|
// @Description Record a custom analytics event
|
|
|
|
|
// @Tags Analytics
|
|
|
|
|
// @Accept json
|
|
|
|
|
// @Produce json
|
|
|
|
|
// @Security BearerAuth
|
|
|
|
|
// @Param request body RecordEventRequest true "Event Data"
|
|
|
|
|
// @Success 200 {object} APIResponse{data=object{message=string}}
|
|
|
|
|
// @Failure 400 {object} APIResponse "Validation Error"
|
|
|
|
|
// @Failure 401 {object} APIResponse "Unauthorized"
|
|
|
|
|
// @Failure 500 {object} APIResponse "Internal Error"
|
|
|
|
|
// @Router /analytics/events [post]
|
|
|
|
|
func (h *AnalyticsHandler) RecordEvent(c *gin.Context) {
|
|
|
|
|
var req RecordEventRequest
|
|
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer user_id si authentifié (optionnel pour analytics anonymes)
|
|
|
|
|
var userID *uuid.UUID
|
|
|
|
|
if uid, ok := c.Get("user_id"); ok {
|
|
|
|
|
if uidUUID, ok := uid.(uuid.UUID); ok {
|
|
|
|
|
userID = &uidUUID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Vérifier que le JobWorker est disponible
|
|
|
|
|
if h.jobWorker == nil {
|
|
|
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "analytics service not available"))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enqueue l'événement analytics via le JobWorker
|
|
|
|
|
h.jobWorker.EnqueueAnalyticsJob(req.EventName, userID, req.Payload)
|
|
|
|
|
|
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
|
|
|
"message": "event recorded",
|
|
|
|
|
"event_name": req.EventName,
|
|
|
|
|
})
|
|
|
|
|
}
|