2025-12-03 19:29:37 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"math"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"veza-backend-api/internal/dto"
|
|
|
|
|
"veza-backend-api/internal/models"
|
|
|
|
|
"veza-backend-api/internal/services"
|
|
|
|
|
|
|
|
|
|
"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
|
|
|
"go.uber.org/zap"
|
2025-12-03 19:29:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// PlaybackAnalyticsHandler gère les requêtes pour les analytics de lecture
|
|
|
|
|
// T0358: Create Playback Analytics Endpoint
|
|
|
|
|
type PlaybackAnalyticsHandler struct {
|
|
|
|
|
analyticsService *services.PlaybackAnalyticsService
|
|
|
|
|
heatmapService *services.PlaybackHeatmapService
|
|
|
|
|
rateLimiter *services.PlaybackAnalyticsRateLimiter // T0389: Create Playback Analytics Rate Limiting
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewPlaybackAnalyticsHandler crée un nouveau handler d'analytics de lecture
|
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 NewPlaybackAnalyticsHandler(analyticsService *services.PlaybackAnalyticsService, logger *zap.Logger) *PlaybackAnalyticsHandler {
|
2025-12-03 19:29:37 +00:00
|
|
|
return &PlaybackAnalyticsHandler{
|
|
|
|
|
analyticsService: analyticsService,
|
|
|
|
|
heatmapService: nil,
|
|
|
|
|
rateLimiter: nil, // Rate limiter optionnel
|
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: NewCommonHandler(logger),
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewPlaybackAnalyticsHandlerWithRateLimiter crée un nouveau handler avec rate limiter
|
|
|
|
|
// T0389: Create Playback Analytics Rate Limiting
|
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 NewPlaybackAnalyticsHandlerWithRateLimiter(analyticsService *services.PlaybackAnalyticsService, rateLimiter *services.PlaybackAnalyticsRateLimiter, logger *zap.Logger) *PlaybackAnalyticsHandler {
|
2025-12-03 19:29:37 +00:00
|
|
|
return &PlaybackAnalyticsHandler{
|
|
|
|
|
analyticsService: analyticsService,
|
|
|
|
|
heatmapService: nil,
|
|
|
|
|
rateLimiter: rateLimiter,
|
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: NewCommonHandler(logger),
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewPlaybackAnalyticsHandlerWithHeatmap crée un nouveau handler avec service heatmap
|
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 NewPlaybackAnalyticsHandlerWithHeatmap(analyticsService *services.PlaybackAnalyticsService, heatmapService *services.PlaybackHeatmapService, logger *zap.Logger) *PlaybackAnalyticsHandler {
|
2025-12-03 19:29:37 +00:00
|
|
|
return &PlaybackAnalyticsHandler{
|
|
|
|
|
analyticsService: analyticsService,
|
|
|
|
|
heatmapService: heatmapService,
|
|
|
|
|
rateLimiter: nil,
|
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: NewCommonHandler(logger),
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewPlaybackAnalyticsHandlerFull crée un nouveau handler avec tous les services
|
|
|
|
|
// T0389: Create Playback Analytics Rate Limiting
|
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 NewPlaybackAnalyticsHandlerFull(analyticsService *services.PlaybackAnalyticsService, heatmapService *services.PlaybackHeatmapService, rateLimiter *services.PlaybackAnalyticsRateLimiter, logger *zap.Logger) *PlaybackAnalyticsHandler {
|
2025-12-03 19:29:37 +00:00
|
|
|
return &PlaybackAnalyticsHandler{
|
|
|
|
|
analyticsService: analyticsService,
|
|
|
|
|
heatmapService: heatmapService,
|
|
|
|
|
rateLimiter: rateLimiter,
|
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: NewCommonHandler(logger),
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecordAnalyticsRequest représente la requête pour enregistrer des analytics de lecture
|
|
|
|
|
// T0388: Create Playback Analytics Validation - Amélioré avec validation
|
|
|
|
|
type RecordAnalyticsRequest struct {
|
|
|
|
|
PlayTime int `json:"play_time" binding:"required,min=0"` // seconds
|
|
|
|
|
PauseCount int `json:"pause_count" binding:"min=0"` // optional, default 0
|
|
|
|
|
SeekCount int `json:"seek_count" binding:"min=0"` // optional, default 0
|
|
|
|
|
CompletionRate *float64 `json:"completion_rate,omitempty"` // optional, will be calculated if not provided
|
|
|
|
|
StartedAt time.Time `json:"started_at" binding:"required"` // ISO 8601 format
|
|
|
|
|
EndedAt *time.Time `json:"ended_at,omitempty"` // optional
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ValidationResult représente le résultat d'une validation
|
|
|
|
|
// T0388: Create Playback Analytics Validation
|
|
|
|
|
// GO-013: Utilise dto.ValidationError pour éviter les cycles d'import
|
|
|
|
|
type ValidationResult struct {
|
|
|
|
|
Valid bool
|
|
|
|
|
Errors []dto.ValidationError
|
|
|
|
|
Sanitized *RecordAnalyticsRequest
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecordAnalytics gère la requête POST /api/v1/tracks/:id/playback/analytics
|
|
|
|
|
// Enregistre les analytics de lecture pour un track
|
|
|
|
|
// T0358: Create Playback Analytics Endpoint
|
|
|
|
|
func (h *PlaybackAnalyticsHandler) RecordAnalytics(c *gin.Context) {
|
|
|
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
|
|
|
if userID == uuid.Nil {
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer l'ID du track depuis les paramètres de l'URL
|
|
|
|
|
trackIDStr := c.Param("id")
|
|
|
|
|
trackID, err := uuid.Parse(trackIDStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Valider et parser le body de la requête
|
|
|
|
|
var req RecordAnalyticsRequest
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// T0388: Create Playback Analytics Validation
|
|
|
|
|
// Valider et sanitizer les données
|
|
|
|
|
validationResult := h.validateAndSanitizeAnalyticsRequest(&req, trackID)
|
|
|
|
|
if !validationResult.Valid {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
|
|
|
"error": "Validation failed",
|
|
|
|
|
"errors": validationResult.Errors,
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Utiliser les données sanitizées
|
|
|
|
|
req = *validationResult.Sanitized
|
|
|
|
|
|
|
|
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
|
|
|
// Vérifier le rate limiting si activé
|
|
|
|
|
if h.rateLimiter != nil {
|
|
|
|
|
rateLimitResult, err := h.rateLimiter.CheckRateLimit(c.Request.Context(), userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check rate limit"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !rateLimitResult.Allowed {
|
|
|
|
|
// Ajouter les headers de rate limiting
|
|
|
|
|
c.Header("X-RateLimit-Remaining", "0")
|
|
|
|
|
c.Header("X-RateLimit-Retry-After", strconv.FormatInt(int64(rateLimitResult.RetryAfter.Seconds()), 10))
|
|
|
|
|
c.Header("X-RateLimit-Reason", rateLimitResult.Reason)
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
|
|
|
|
"error": "Rate limit exceeded",
|
|
|
|
|
"reason": rateLimitResult.Reason,
|
|
|
|
|
"retry_after": int(rateLimitResult.RetryAfter.Seconds()),
|
|
|
|
|
"quota_used": rateLimitResult.QuotaUsed,
|
|
|
|
|
"quota_limit": rateLimitResult.QuotaLimit,
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ajouter les headers de rate limiting
|
|
|
|
|
c.Header("X-RateLimit-Remaining", strconv.Itoa(rateLimitResult.Remaining))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Créer le modèle PlaybackAnalytics
|
|
|
|
|
analytics := &models.PlaybackAnalytics{
|
|
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
|
|
|
|
PlayTime: req.PlayTime,
|
|
|
|
|
PauseCount: req.PauseCount,
|
|
|
|
|
SeekCount: req.SeekCount,
|
|
|
|
|
StartedAt: req.StartedAt,
|
|
|
|
|
EndedAt: req.EndedAt,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Définir le completion_rate si fourni
|
|
|
|
|
if req.CompletionRate != nil {
|
|
|
|
|
analytics.CompletionRate = *req.CompletionRate
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enregistrer les analytics via le service
|
|
|
|
|
err = h.analyticsService.RecordPlayback(c.Request.Context(), analytics)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// Gérer les erreurs spécifiques
|
|
|
|
|
if err.Error() == "invalid track ID: 0" ||
|
|
|
|
|
err.Error() == "invalid user ID: 0" ||
|
|
|
|
|
err.Error()[:14] == "invalid play time" ||
|
|
|
|
|
err.Error()[:14] == "invalid pause" ||
|
|
|
|
|
err.Error()[:14] == "invalid seek" ||
|
|
|
|
|
err.Error()[:14] == "invalid completion" ||
|
|
|
|
|
err.Error() == "started_at is required" {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err.Error()[:13] == "track not found" {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
|
|
|
// Enregistrer la requête dans le rate limiter si activé
|
|
|
|
|
if h.rateLimiter != nil {
|
|
|
|
|
if err := h.rateLimiter.RecordRequest(c.Request.Context(), userID); err != nil {
|
|
|
|
|
// Logger l'erreur mais ne pas échouer la requête
|
|
|
|
|
// Le rate limiting est une fonctionnalité de protection, pas critique
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Retourner le succès
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
2025-12-03 19:29:37 +00:00
|
|
|
"status": "recorded",
|
|
|
|
|
"id": analytics.ID,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetQuotaInfo gère la requête GET /api/v1/playback/analytics/quota
|
|
|
|
|
// Retourne les informations de quota pour l'utilisateur actuel
|
|
|
|
|
// T0389: Create Playback Analytics Rate Limiting
|
|
|
|
|
func (h *PlaybackAnalyticsHandler) GetQuotaInfo(c *gin.Context) {
|
|
|
|
|
// Récupérer l'ID de l'utilisateur depuis le contexte
|
|
|
|
|
userID := c.MustGet("user_id").(uuid.UUID)
|
|
|
|
|
if userID == uuid.Nil {
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if h.rateLimiter == nil {
|
|
|
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "rate limiting not enabled"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
quotaInfo, err := h.rateLimiter.GetQuotaInfo(c.Request.Context(), userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get quota info"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
2025-12-03 19:29:37 +00:00
|
|
|
"quota": quotaInfo,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DashboardData représente les données du dashboard d'analytics
|
|
|
|
|
// T0363: Create Playback Analytics Dashboard Endpoint
|
|
|
|
|
type DashboardData struct {
|
|
|
|
|
Stats *services.PlaybackStats `json:"stats"`
|
|
|
|
|
Trends *TrendsData `json:"trends"`
|
|
|
|
|
TimeSeries []TimeSeriesPoint `json:"time_series"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TrendsData représente les tendances d'analytics
|
|
|
|
|
type TrendsData struct {
|
|
|
|
|
PlayTimeTrend float64 `json:"play_time_trend"` // % de changement sur 7 jours
|
|
|
|
|
CompletionTrend float64 `json:"completion_trend"` // % de changement sur 7 jours
|
|
|
|
|
SessionsTrend float64 `json:"sessions_trend"` // % de changement sur 7 jours
|
|
|
|
|
AveragePlayTime float64 `json:"average_play_time"` // Moyenne sur 7 jours
|
|
|
|
|
AverageCompletion float64 `json:"average_completion"` // Moyenne sur 7 jours
|
|
|
|
|
TotalSessions7Days int64 `json:"total_sessions_7days"` // Total sur 7 jours
|
|
|
|
|
TotalSessions30Days int64 `json:"total_sessions_30days"` // Total sur 30 jours
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TimeSeriesPoint représente un point dans une série temporelle
|
|
|
|
|
type TimeSeriesPoint struct {
|
|
|
|
|
Date string `json:"date"` // Format: YYYY-MM-DD
|
|
|
|
|
Sessions int64 `json:"sessions"`
|
|
|
|
|
TotalPlayTime int64 `json:"total_play_time"` // seconds
|
|
|
|
|
AveragePlayTime float64 `json:"average_play_time"` // seconds
|
|
|
|
|
AverageCompletion float64 `json:"average_completion"` // percentage
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetDashboard gère la requête GET /api/v1/tracks/:id/playback/dashboard
|
|
|
|
|
// Retourne les statistiques agrégées, graphiques et tendances pour un track
|
|
|
|
|
// T0363: Create Playback Analytics Dashboard Endpoint
|
|
|
|
|
func (h *PlaybackAnalyticsHandler) GetDashboard(c *gin.Context) {
|
|
|
|
|
// Récupérer l'ID du track depuis les paramètres de l'URL
|
|
|
|
|
trackIDStr := c.Param("id")
|
|
|
|
|
trackID, err := uuid.Parse(trackIDStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if trackID == uuid.Nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer les statistiques globales
|
|
|
|
|
stats, err := h.analyticsService.GetTrackStats(c.Request.Context(), trackID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
errMsg := err.Error()
|
|
|
|
|
if len(errMsg) >= 13 && errMsg[:13] == "track not found" {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": errMsg})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculer les tendances (comparaison 7 jours vs 14-7 jours)
|
|
|
|
|
trends, err := h.calculateTrends(c.Request.Context(), trackID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to calculate trends: " + err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculer les séries temporelles (30 derniers jours)
|
|
|
|
|
timeSeries, err := h.calculateTimeSeries(c.Request.Context(), trackID, 30)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to calculate time series: " + err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Construire la réponse
|
|
|
|
|
dashboard := DashboardData{
|
|
|
|
|
Stats: stats,
|
|
|
|
|
Trends: trends,
|
|
|
|
|
TimeSeries: timeSeries,
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
2025-12-03 19:29:37 +00:00
|
|
|
"dashboard": dashboard,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// calculateTrends calcule les tendances d'analytics
|
|
|
|
|
func (h *PlaybackAnalyticsHandler) calculateTrends(ctx context.Context, trackID uuid.UUID) (*TrendsData, error) {
|
|
|
|
|
now := time.Now()
|
|
|
|
|
sevenDaysAgo := now.AddDate(0, 0, -7)
|
|
|
|
|
fourteenDaysAgo := now.AddDate(0, 0, -14)
|
|
|
|
|
thirtyDaysAgo := now.AddDate(0, 0, -30)
|
|
|
|
|
|
|
|
|
|
// Statistiques sur les 7 derniers jours
|
|
|
|
|
stats7Days, err := h.getStatsForDateRange(ctx, trackID, sevenDaysAgo, now)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Statistiques sur les 7 jours précédents (14-7 jours)
|
|
|
|
|
statsPrev7Days, err := h.getStatsForDateRange(ctx, trackID, fourteenDaysAgo, sevenDaysAgo)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Statistiques sur les 30 derniers jours
|
|
|
|
|
stats30Days, err := h.getStatsForDateRange(ctx, trackID, thirtyDaysAgo, now)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trends := &TrendsData{
|
|
|
|
|
TotalSessions7Days: stats7Days.TotalSessions,
|
|
|
|
|
TotalSessions30Days: stats30Days.TotalSessions,
|
|
|
|
|
AveragePlayTime: stats7Days.AveragePlayTime,
|
|
|
|
|
AverageCompletion: stats7Days.AverageCompletion,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculer les tendances en pourcentage
|
|
|
|
|
if statsPrev7Days.TotalSessions > 0 {
|
|
|
|
|
// Tendance des sessions
|
|
|
|
|
trends.SessionsTrend = float64(stats7Days.TotalSessions-statsPrev7Days.TotalSessions) / float64(statsPrev7Days.TotalSessions) * 100.0
|
|
|
|
|
} else if stats7Days.TotalSessions > 0 {
|
|
|
|
|
trends.SessionsTrend = 100.0 // Nouvelle donnée
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if statsPrev7Days.AveragePlayTime > 0 {
|
|
|
|
|
// Tendance du temps de lecture
|
|
|
|
|
trends.PlayTimeTrend = (stats7Days.AveragePlayTime - statsPrev7Days.AveragePlayTime) / statsPrev7Days.AveragePlayTime * 100.0
|
|
|
|
|
} else if stats7Days.AveragePlayTime > 0 {
|
|
|
|
|
trends.PlayTimeTrend = 100.0 // Nouvelle donnée
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if statsPrev7Days.AverageCompletion > 0 {
|
|
|
|
|
// Tendance du taux de complétion
|
|
|
|
|
trends.CompletionTrend = (stats7Days.AverageCompletion - statsPrev7Days.AverageCompletion) / statsPrev7Days.AverageCompletion * 100.0
|
|
|
|
|
} else if stats7Days.AverageCompletion > 0 {
|
|
|
|
|
trends.CompletionTrend = 100.0 // Nouvelle donnée
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return trends, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getStatsForDateRange récupère les statistiques pour une plage de dates
|
|
|
|
|
func (h *PlaybackAnalyticsHandler) getStatsForDateRange(ctx context.Context, trackID uuid.UUID, startDate, endDate time.Time) (*services.PlaybackStats, error) {
|
|
|
|
|
sessions, err := h.analyticsService.GetSessionsByDateRange(ctx, trackID, startDate, endDate)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(sessions) == 0 {
|
|
|
|
|
return &services.PlaybackStats{}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var totalPlayTime int64
|
|
|
|
|
var totalPauses int64
|
|
|
|
|
var totalSeeks int64
|
|
|
|
|
var totalCompletion float64
|
|
|
|
|
|
|
|
|
|
for _, session := range sessions {
|
|
|
|
|
totalPlayTime += int64(session.PlayTime)
|
|
|
|
|
totalPauses += int64(session.PauseCount)
|
|
|
|
|
totalSeeks += int64(session.SeekCount)
|
|
|
|
|
totalCompletion += session.CompletionRate
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
totalSessions := int64(len(sessions))
|
|
|
|
|
avgPlayTime := float64(totalPlayTime) / float64(totalSessions)
|
|
|
|
|
avgPauses := float64(totalPauses) / float64(totalSessions)
|
|
|
|
|
avgSeeks := float64(totalSeeks) / float64(totalSessions)
|
|
|
|
|
avgCompletion := totalCompletion / float64(totalSessions)
|
|
|
|
|
|
|
|
|
|
// Compter les sessions complétées (>90%)
|
|
|
|
|
var completedSessions int64
|
|
|
|
|
for _, session := range sessions {
|
|
|
|
|
if session.CompletionRate >= 90 {
|
|
|
|
|
completedSessions++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
completionRate := float64(completedSessions) / float64(totalSessions) * 100.0
|
|
|
|
|
|
|
|
|
|
return &services.PlaybackStats{
|
|
|
|
|
TotalSessions: totalSessions,
|
|
|
|
|
TotalPlayTime: totalPlayTime,
|
|
|
|
|
AveragePlayTime: avgPlayTime,
|
|
|
|
|
TotalPauses: totalPauses,
|
|
|
|
|
AveragePauses: avgPauses,
|
|
|
|
|
TotalSeeks: totalSeeks,
|
|
|
|
|
AverageSeeks: avgSeeks,
|
|
|
|
|
AverageCompletion: avgCompletion,
|
|
|
|
|
CompletionRate: completionRate,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// calculateTimeSeries calcule les séries temporelles pour les N derniers jours
|
|
|
|
|
func (h *PlaybackAnalyticsHandler) calculateTimeSeries(ctx context.Context, trackID uuid.UUID, days int) ([]TimeSeriesPoint, error) {
|
|
|
|
|
now := time.Now()
|
|
|
|
|
startDate := now.AddDate(0, 0, -days)
|
|
|
|
|
|
|
|
|
|
// Récupérer toutes les sessions dans la plage
|
|
|
|
|
sessions, err := h.analyticsService.GetSessionsByDateRange(ctx, trackID, startDate, now)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Grouper par jour
|
|
|
|
|
dailyStats := make(map[string]*dailyStat)
|
|
|
|
|
for _, session := range sessions {
|
|
|
|
|
dateKey := session.CreatedAt.Format("2006-01-02")
|
|
|
|
|
if dailyStats[dateKey] == nil {
|
|
|
|
|
dailyStats[dateKey] = &dailyStat{}
|
|
|
|
|
}
|
|
|
|
|
stat := dailyStats[dateKey]
|
|
|
|
|
stat.sessions++
|
|
|
|
|
stat.totalPlayTime += int64(session.PlayTime)
|
|
|
|
|
stat.totalCompletion += session.CompletionRate
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Créer les points de série temporelle pour tous les jours
|
|
|
|
|
var timeSeries []TimeSeriesPoint
|
|
|
|
|
for i := days - 1; i >= 0; i-- {
|
|
|
|
|
date := now.AddDate(0, 0, -i)
|
|
|
|
|
dateKey := date.Format("2006-01-02")
|
|
|
|
|
|
|
|
|
|
stat := dailyStats[dateKey]
|
|
|
|
|
if stat == nil {
|
|
|
|
|
stat = &dailyStat{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var avgPlayTime float64
|
|
|
|
|
var avgCompletion float64
|
|
|
|
|
if stat.sessions > 0 {
|
|
|
|
|
avgPlayTime = float64(stat.totalPlayTime) / float64(stat.sessions)
|
|
|
|
|
avgCompletion = stat.totalCompletion / float64(stat.sessions)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
timeSeries = append(timeSeries, TimeSeriesPoint{
|
|
|
|
|
Date: dateKey,
|
|
|
|
|
Sessions: stat.sessions,
|
|
|
|
|
TotalPlayTime: stat.totalPlayTime,
|
|
|
|
|
AveragePlayTime: avgPlayTime,
|
|
|
|
|
AverageCompletion: avgCompletion,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return timeSeries, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// dailyStat représente les statistiques d'un jour
|
|
|
|
|
type dailyStat struct {
|
|
|
|
|
sessions int64
|
|
|
|
|
totalPlayTime int64
|
|
|
|
|
totalCompletion float64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SummaryData représente le résumé des analytics de lecture
|
|
|
|
|
// T0370: Create Playback Analytics Summary Endpoint
|
|
|
|
|
type SummaryData struct {
|
|
|
|
|
TotalPlays int64 `json:"total_plays"` // Nombre total de lectures
|
|
|
|
|
CompletionRate float64 `json:"completion_rate"` // Taux de complétion moyen (%)
|
|
|
|
|
AveragePlayTime float64 `json:"average_play_time"` // Temps de lecture moyen (secondes)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetSummary gère la requête GET /api/v1/tracks/:id/playback/summary
|
|
|
|
|
// Retourne un résumé des analytics de lecture pour un track
|
|
|
|
|
// T0370: Create Playback Analytics Summary Endpoint
|
|
|
|
|
func (h *PlaybackAnalyticsHandler) GetSummary(c *gin.Context) {
|
|
|
|
|
// Récupérer l'ID du track depuis les paramètres de l'URL
|
|
|
|
|
trackIDStr := c.Param("id")
|
|
|
|
|
trackID, err := uuid.Parse(trackIDStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if trackID == uuid.Nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer les statistiques via le service
|
|
|
|
|
stats, err := h.analyticsService.GetTrackStats(c.Request.Context(), trackID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
errMsg := err.Error()
|
|
|
|
|
if len(errMsg) >= 13 && errMsg[:13] == "track not found" {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": errMsg})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Construire le résumé
|
|
|
|
|
summary := SummaryData{
|
|
|
|
|
TotalPlays: stats.TotalSessions,
|
|
|
|
|
CompletionRate: stats.CompletionRate,
|
|
|
|
|
AveragePlayTime: stats.AveragePlayTime,
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
2025-12-03 19:29:37 +00:00
|
|
|
"summary": summary,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetHeatmap gère la requête GET /api/v1/tracks/:id/playback/heatmap
|
|
|
|
|
// Retourne les données de heatmap pour un track
|
|
|
|
|
// T0376: Create Playback Analytics Heatmap Generation
|
|
|
|
|
func (h *PlaybackAnalyticsHandler) GetHeatmap(c *gin.Context) {
|
|
|
|
|
if h.heatmapService == nil {
|
|
|
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "heatmap service not available"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer l'ID du track depuis les paramètres de l'URL
|
|
|
|
|
trackIDStr := c.Param("id")
|
|
|
|
|
trackID, err := uuid.Parse(trackIDStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if trackID == uuid.Nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer la taille de segment depuis les query params (optionnel, défaut: 5)
|
|
|
|
|
segmentSize := 5
|
|
|
|
|
if segmentSizeStr := c.Query("segment_size"); segmentSizeStr != "" {
|
|
|
|
|
if parsed, err := strconv.Atoi(segmentSizeStr); err == nil && parsed > 0 {
|
|
|
|
|
segmentSize = parsed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Générer la heatmap via le service
|
|
|
|
|
heatmap, err := h.heatmapService.GenerateHeatmap(c.Request.Context(), trackID, segmentSize)
|
|
|
|
|
if err != nil {
|
|
|
|
|
errMsg := err.Error()
|
|
|
|
|
if len(errMsg) >= 13 && errMsg[:13] == "track not found" {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": errMsg})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": errMsg})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
2025-12-03 19:29:37 +00:00
|
|
|
"heatmap": heatmap,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validateAndSanitizeAnalyticsRequest valide et sanitize une requête d'analytics
|
|
|
|
|
// T0388: Create Playback Analytics Validation
|
|
|
|
|
func (h *PlaybackAnalyticsHandler) validateAndSanitizeAnalyticsRequest(req *RecordAnalyticsRequest, trackID uuid.UUID) ValidationResult {
|
|
|
|
|
result := ValidationResult{
|
|
|
|
|
Valid: true,
|
|
|
|
|
Errors: make([]dto.ValidationError, 0),
|
|
|
|
|
Sanitized: &RecordAnalyticsRequest{},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copier les données pour la sanitization
|
|
|
|
|
sanitized := *req
|
|
|
|
|
|
|
|
|
|
// 1. Validation du schéma - PlayTime
|
|
|
|
|
if req.PlayTime < 0 {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "play_time",
|
|
|
|
|
Message: "play_time must be greater than or equal to 0",
|
|
|
|
|
Value: fmt.Sprintf("%d", req.PlayTime),
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// Limiter play_time à une valeur raisonnable (max 24 heures = 86400 secondes)
|
|
|
|
|
if req.PlayTime > 86400 {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "play_time",
|
|
|
|
|
Message: "play_time cannot exceed 86400 seconds (24 hours)",
|
|
|
|
|
Value: fmt.Sprintf("%d", req.PlayTime),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
sanitized.PlayTime = req.PlayTime
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Validation du schéma - PauseCount
|
|
|
|
|
if req.PauseCount < 0 {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "pause_count",
|
|
|
|
|
Message: "pause_count must be greater than or equal to 0",
|
|
|
|
|
Value: fmt.Sprintf("%d", req.PauseCount),
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// Limiter pause_count à une valeur raisonnable (max 1000)
|
|
|
|
|
if req.PauseCount > 1000 {
|
|
|
|
|
sanitized.PauseCount = 1000
|
|
|
|
|
} else {
|
|
|
|
|
sanitized.PauseCount = req.PauseCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Validation du schéma - SeekCount
|
|
|
|
|
if req.SeekCount < 0 {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "seek_count",
|
|
|
|
|
Message: "seek_count must be greater than or equal to 0",
|
|
|
|
|
Value: fmt.Sprintf("%d", req.SeekCount),
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// Limiter seek_count à une valeur raisonnable (max 1000)
|
|
|
|
|
if req.SeekCount > 1000 {
|
|
|
|
|
sanitized.SeekCount = 1000
|
|
|
|
|
} else {
|
|
|
|
|
sanitized.SeekCount = req.SeekCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Validation du schéma - CompletionRate
|
|
|
|
|
if req.CompletionRate != nil {
|
|
|
|
|
rate := *req.CompletionRate
|
|
|
|
|
if math.IsNaN(rate) || math.IsInf(rate, 0) {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "completion_rate",
|
|
|
|
|
Message: "completion_rate must be a valid number",
|
|
|
|
|
Value: fmt.Sprintf("%f", rate),
|
|
|
|
|
})
|
|
|
|
|
} else if rate < 0 || rate > 100 {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "completion_rate",
|
|
|
|
|
Message: "completion_rate must be between 0 and 100",
|
|
|
|
|
Value: fmt.Sprintf("%f", rate),
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// Arrondir à 2 décimales
|
|
|
|
|
roundedRate := math.Round(rate*100) / 100
|
|
|
|
|
sanitized.CompletionRate = &roundedRate
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 5. Validation du schéma - StartedAt
|
|
|
|
|
if req.StartedAt.IsZero() {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "started_at",
|
|
|
|
|
Message: "started_at is required",
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
now := time.Now()
|
|
|
|
|
// Vérifier que started_at n'est pas dans le futur (avec une marge de 1 minute pour les décalages d'horloge)
|
|
|
|
|
if req.StartedAt.After(now.Add(1 * time.Minute)) {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "started_at",
|
|
|
|
|
Message: "started_at cannot be in the future",
|
|
|
|
|
Value: req.StartedAt.Format(time.RFC3339),
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// Vérifier que started_at n'est pas trop ancien (max 30 jours)
|
|
|
|
|
thirtyDaysAgo := now.AddDate(0, 0, -30)
|
|
|
|
|
if req.StartedAt.Before(thirtyDaysAgo) {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "started_at",
|
|
|
|
|
Message: "started_at cannot be older than 30 days",
|
|
|
|
|
Value: req.StartedAt.Format(time.RFC3339),
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
sanitized.StartedAt = req.StartedAt
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 6. Validation du schéma - EndedAt
|
|
|
|
|
if req.EndedAt != nil {
|
|
|
|
|
endedAt := *req.EndedAt
|
|
|
|
|
if endedAt.IsZero() {
|
|
|
|
|
// Si ended_at est fourni mais est zero, le traiter comme nil
|
|
|
|
|
sanitized.EndedAt = nil
|
|
|
|
|
} else {
|
|
|
|
|
// Vérifier que ended_at n'est pas dans le futur
|
|
|
|
|
now := time.Now()
|
|
|
|
|
if endedAt.After(now.Add(1 * time.Minute)) {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "ended_at",
|
|
|
|
|
Message: "ended_at cannot be in the future",
|
|
|
|
|
Value: endedAt.Format(time.RFC3339),
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
sanitized.EndedAt = &endedAt
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 7. Vérification de cohérence - EndedAt doit être après StartedAt
|
|
|
|
|
if !req.StartedAt.IsZero() && req.EndedAt != nil && !req.EndedAt.IsZero() {
|
|
|
|
|
if req.EndedAt.Before(req.StartedAt) {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "ended_at",
|
|
|
|
|
Message: "ended_at must be after started_at",
|
|
|
|
|
Value: req.EndedAt.Format(time.RFC3339),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 8. Vérification de cohérence - PlayTime doit être cohérent avec les dates
|
|
|
|
|
if !req.StartedAt.IsZero() && req.EndedAt != nil && !req.EndedAt.IsZero() {
|
|
|
|
|
duration := req.EndedAt.Sub(req.StartedAt).Seconds()
|
|
|
|
|
// Le play_time ne devrait pas être significativement supérieur à la durée entre started_at et ended_at
|
|
|
|
|
// (avec une marge de 10% pour les pauses)
|
|
|
|
|
maxExpectedPlayTime := duration * 1.1
|
|
|
|
|
if float64(req.PlayTime) > maxExpectedPlayTime && maxExpectedPlayTime > 0 {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "play_time",
|
|
|
|
|
Message: fmt.Sprintf("play_time (%.0f seconds) is inconsistent with session duration (%.0f seconds)", float64(req.PlayTime), duration),
|
|
|
|
|
Value: fmt.Sprintf("%d", req.PlayTime),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 9. Vérification de cohérence - CompletionRate doit être cohérent avec PlayTime si fourni
|
|
|
|
|
// Cette vérification nécessite la durée du track, donc elle sera faite après la récupération du track
|
|
|
|
|
// Pour l'instant, on valide juste que le completion_rate est dans une plage raisonnable
|
|
|
|
|
|
|
|
|
|
// 10. Vérification de cohérence - PauseCount et SeekCount doivent être raisonnables par rapport à PlayTime
|
|
|
|
|
if req.PlayTime > 0 {
|
|
|
|
|
// Si play_time est très court (< 10 secondes), pause_count et seek_count devraient être faibles
|
|
|
|
|
if req.PlayTime < 10 {
|
|
|
|
|
if req.PauseCount > 5 {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "pause_count",
|
|
|
|
|
Message: "pause_count is too high for such a short play_time",
|
|
|
|
|
Value: fmt.Sprintf("%d", req.PauseCount),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if req.SeekCount > 10 {
|
|
|
|
|
result.Valid = false
|
|
|
|
|
result.Errors = append(result.Errors, dto.ValidationError{
|
|
|
|
|
Field: "seek_count",
|
|
|
|
|
Message: "seek_count is too high for such a short play_time",
|
|
|
|
|
Value: fmt.Sprintf("%d", req.SeekCount),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.Sanitized = &sanitized
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validateAnalyticsConsistencyWithTrack valide la cohérence des analytics avec le track
|
|
|
|
|
// T0388: Create Playback Analytics Validation
|
|
|
|
|
func (h *PlaybackAnalyticsHandler) validateAnalyticsConsistencyWithTrack(ctx context.Context, req *RecordAnalyticsRequest, trackID uuid.UUID) []dto.ValidationError {
|
|
|
|
|
errors := make([]dto.ValidationError, 0)
|
|
|
|
|
|
|
|
|
|
// Récupérer le track pour valider la cohérence
|
|
|
|
|
// Note: Cette validation nécessite un accès à la base de données
|
|
|
|
|
// Pour l'instant, on retourne une liste vide car la validation du track
|
|
|
|
|
// est déjà faite dans le service RecordPlayback
|
|
|
|
|
// Cette fonction peut être étendue pour des validations plus spécifiques
|
|
|
|
|
|
|
|
|
|
// Vérifier que completion_rate est cohérent avec play_time et track duration
|
|
|
|
|
// Cette vérification sera faite dans le service car elle nécessite la durée du track
|
|
|
|
|
|
|
|
|
|
return errors
|
|
|
|
|
}
|