204 lines
7.5 KiB
Python
204 lines
7.5 KiB
Python
"""
|
|
Commande Django pour agréger les statistiques de pages vues
|
|
et purger les données détaillées de plus d'un mois.
|
|
|
|
Workflow:
|
|
1. Pour chaque jour ayant des PageView de plus de 30 jours,
|
|
calculer les agrégats et créer/mettre à jour un DailyPageViewSummary.
|
|
2. Supprimer les PageView et UserSession de plus de 30 jours.
|
|
|
|
Usage:
|
|
python manage.py cleanup_analytics
|
|
python manage.py cleanup_analytics --days 60
|
|
python manage.py cleanup_analytics --dry-run
|
|
"""
|
|
|
|
from datetime import timedelta
|
|
|
|
from django.core.management.base import BaseCommand
|
|
from django.db.models import Count, Avg, Max, Q
|
|
from django.db.models.functions import TruncDate
|
|
from django.utils import timezone
|
|
|
|
from analytics.models import PageView, UserSession, DailyPageViewSummary
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = (
|
|
"Agrège les statistiques quotidiennes de pages vues dans "
|
|
"DailyPageViewSummary puis purge les enregistrements détaillés "
|
|
"de PageView et UserSession de plus de N jours (défaut: 30)."
|
|
)
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
"--days",
|
|
type=int,
|
|
default=30,
|
|
help="Nombre de jours de rétention des données détaillées (défaut: 30).",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Affiche ce qui serait fait sans modifier la base de données.",
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
retention_days = options["days"]
|
|
dry_run = options["dry_run"]
|
|
cutoff = timezone.now() - timedelta(days=retention_days)
|
|
cutoff_date = cutoff.date()
|
|
|
|
if dry_run:
|
|
self.stdout.write(self.style.WARNING("[DRY-RUN] Aucune modification ne sera effectuée.\n"))
|
|
|
|
self.stdout.write(
|
|
self.style.WARNING(
|
|
f"\n=== Nettoyage analytics — rétention {retention_days} jours "
|
|
f"(avant le {cutoff_date}) ===\n"
|
|
)
|
|
)
|
|
|
|
# ── 1. Agrégation ─────────────────────────────────────────────
|
|
old_page_views = PageView.objects.filter(timestamp__lt=cutoff)
|
|
total_old = old_page_views.count()
|
|
|
|
if total_old == 0:
|
|
self.stdout.write("Aucune PageView à agréger.\n")
|
|
else:
|
|
self.stdout.write(f"{total_old} PageView(s) à agréger et purger.\n")
|
|
self._aggregate_page_views(old_page_views, dry_run)
|
|
|
|
# ── 2. Purge des PageView ─────────────────────────────────────
|
|
if not dry_run and total_old > 0:
|
|
deleted_pv, _ = old_page_views.delete()
|
|
self.stdout.write(self.style.SUCCESS(f" ✓ {deleted_pv} PageView(s) supprimée(s).\n"))
|
|
|
|
# ── 3. Purge des UserSession inactives ────────────────────────
|
|
old_sessions = UserSession.objects.filter(
|
|
last_activity__lt=cutoff,
|
|
is_active=False,
|
|
)
|
|
total_sessions = old_sessions.count()
|
|
if total_sessions == 0:
|
|
self.stdout.write("Aucune UserSession inactive à purger.\n")
|
|
else:
|
|
if dry_run:
|
|
self.stdout.write(f" [DRY-RUN] {total_sessions} UserSession(s) inactives seraient supprimées.\n")
|
|
else:
|
|
deleted_us, _ = old_sessions.delete()
|
|
self.stdout.write(self.style.SUCCESS(f" ✓ {deleted_us} UserSession(s) inactives supprimées.\n"))
|
|
|
|
self.stdout.write(self.style.SUCCESS("\n=== Nettoyage terminé ===\n"))
|
|
|
|
# ──────────────────────────────────────────────────────────────────
|
|
# Méthodes internes
|
|
# ──────────────────────────────────────────────────────────────────
|
|
|
|
def _aggregate_page_views(self, queryset, dry_run):
|
|
"""
|
|
Calcule les résumés quotidiens à partir du queryset fourni
|
|
et crée / met à jour les DailyPageViewSummary correspondants.
|
|
"""
|
|
days = (
|
|
queryset
|
|
.annotate(day=TruncDate("timestamp"))
|
|
.values("day")
|
|
.annotate(count=Count("id"))
|
|
.order_by("day")
|
|
)
|
|
|
|
for entry in days:
|
|
day = entry["day"]
|
|
day_qs = queryset.filter(timestamp__date=day)
|
|
summary_data = self._compute_daily_summary(day, day_qs)
|
|
|
|
if dry_run:
|
|
self.stdout.write(
|
|
f" [DRY-RUN] {day} : {summary_data['total_page_views']} vues, "
|
|
f"{summary_data['unique_users']} utilisateurs uniques"
|
|
)
|
|
else:
|
|
DailyPageViewSummary.objects.update_or_create(
|
|
date=day,
|
|
defaults=summary_data,
|
|
)
|
|
self.stdout.write(
|
|
self.style.SUCCESS(
|
|
f" ✓ Résumé {day} : {summary_data['total_page_views']} vues, "
|
|
f"{summary_data['unique_users']} utilisateurs uniques"
|
|
)
|
|
)
|
|
|
|
def _compute_daily_summary(self, day, day_qs):
|
|
"""Calcule les métriques agrégées pour un jour donné."""
|
|
# Compteurs de base
|
|
total_page_views = day_qs.count()
|
|
unique_users = (
|
|
day_qs.filter(user__isnull=False)
|
|
.values("user")
|
|
.distinct()
|
|
.count()
|
|
)
|
|
unique_sessions = (
|
|
day_qs.exclude(session_key="")
|
|
.values("session_key")
|
|
.distinct()
|
|
.count()
|
|
)
|
|
unique_ips = (
|
|
day_qs.filter(ip_address__isnull=False)
|
|
.values("ip_address")
|
|
.distinct()
|
|
.count()
|
|
)
|
|
|
|
# Temps de réponse
|
|
response_stats = day_qs.filter(
|
|
response_time_ms__isnull=False
|
|
).aggregate(
|
|
avg=Avg("response_time_ms"),
|
|
mx=Max("response_time_ms"),
|
|
)
|
|
avg_response = response_stats["avg"]
|
|
max_response = response_stats["mx"]
|
|
|
|
# Top 20 pages
|
|
top_paths = dict(
|
|
day_qs.values_list("path")
|
|
.annotate(cnt=Count("id"))
|
|
.order_by("-cnt")[:20]
|
|
)
|
|
|
|
# Top 20 utilisateurs
|
|
top_users = dict(
|
|
day_qs.filter(user__isnull=False)
|
|
.values_list("user__username")
|
|
.annotate(cnt=Count("id"))
|
|
.order_by("-cnt")[:20]
|
|
)
|
|
|
|
# Répartition par méthode HTTP
|
|
method_counts = dict(
|
|
day_qs.values_list("method")
|
|
.annotate(cnt=Count("id"))
|
|
.order_by("-cnt")
|
|
)
|
|
|
|
# Sessions démarrées ce jour-là
|
|
total_sessions_started = UserSession.objects.filter(
|
|
login_time__date=day
|
|
).count()
|
|
|
|
return {
|
|
"total_page_views": total_page_views,
|
|
"unique_users": unique_users,
|
|
"unique_sessions": unique_sessions,
|
|
"unique_ips": unique_ips,
|
|
"avg_response_time_ms": round(avg_response, 2) if avg_response else None,
|
|
"max_response_time_ms": max_response,
|
|
"top_paths": top_paths,
|
|
"top_users": top_users,
|
|
"method_counts": method_counts,
|
|
"total_sessions_started": total_sessions_started,
|
|
}
|