loko/streetup/analytics/models.py
2026-07-22 14:48:40 +02:00

260 lines
7.7 KiB
Python

"""
Modèles pour le monitoring de l'utilisation de l'application.
Ce module fournit des modèles pour tracker:
- Les visites de pages (PageView)
- Les sessions utilisateurs (UserSession)
- Les résumés quotidiens (DailyPageViewSummary)
- Les statistiques d'utilisation
"""
from django.db import models, transaction
from django.contrib.auth.models import User
from django.conf import settings
from django.utils import timezone
class PageView(models.Model):
"""
Enregistre chaque visite de page par un utilisateur.
Permet de tracker les pages visitées et la fréquence d'utilisation.
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name="Utilisateur",
related_name="page_views"
)
session_key = models.CharField(
max_length=40,
db_index=True,
verbose_name="Clé de session"
)
path = models.CharField(
max_length=2048,
verbose_name="Chemin URL",
db_index=True
)
full_url = models.CharField(
max_length=4096,
blank=True,
null=True,
verbose_name="URL complète"
)
method = models.CharField(
max_length=10,
default='GET',
verbose_name="Méthode HTTP"
)
timestamp = models.DateTimeField(
auto_now_add=True,
verbose_name="Date et heure",
db_index=True
)
ip_address = models.GenericIPAddressField(
null=True,
blank=True,
verbose_name="Adresse IP"
)
user_agent = models.TextField(
blank=True,
null=True,
verbose_name="User Agent"
)
referer = models.CharField(
max_length=2048,
blank=True,
null=True,
verbose_name="Page précédente"
)
response_time_ms = models.IntegerField(
null=True,
blank=True,
verbose_name="Temps de réponse (ms)",
help_text="Durée de traitement de la requête en millisecondes"
)
class Meta:
verbose_name = "Vue de page"
verbose_name_plural = "Vues de pages"
ordering = ["-timestamp"]
indexes = [
models.Index(fields=['user', 'timestamp']),
models.Index(fields=['session_key', 'timestamp']),
models.Index(fields=['path', 'timestamp']),
models.Index(fields=['timestamp']),
]
def __str__(self):
user_str = self.user.username if self.user else "Anonyme"
return f"{user_str} - {self.path} - {self.timestamp.strftime('%Y-%m-%d %H:%M:%S')}"
class UserSession(models.Model):
"""
Enrichit les informations de session Django avec des métadonnées supplémentaires.
Permet de tracker les sessions actives et l'historique de connexion.
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
verbose_name="Utilisateur",
related_name="analytics_sessions"
)
session_key = models.CharField(
max_length=40,
unique=True,
db_index=True,
verbose_name="Clé de session"
)
login_time = models.DateTimeField(
verbose_name="Heure de connexion",
db_index=True
)
last_activity = models.DateTimeField(
verbose_name="Dernière activité",
db_index=True
)
logout_time = models.DateTimeField(
null=True,
blank=True,
verbose_name="Heure de déconnexion"
)
ip_address = models.GenericIPAddressField(
null=True,
blank=True,
verbose_name="Adresse IP"
)
user_agent = models.TextField(
blank=True,
null=True,
verbose_name="User Agent"
)
page_views_count = models.IntegerField(
default=0,
verbose_name="Nombre de pages vues"
)
is_active = models.BooleanField(
default=True,
verbose_name="Session active",
db_index=True
)
class Meta:
verbose_name = "Session utilisateur"
verbose_name_plural = "Sessions utilisateurs"
ordering = ["-login_time"]
indexes = [
models.Index(fields=['user', 'login_time']),
models.Index(fields=['is_active', 'last_activity']),
]
def __str__(self):
status = "Active" if self.is_active else "Terminée"
return f"{self.user.username} - {self.login_time.strftime('%Y-%m-%d %H:%M')} - {status}"
@property
def duration(self):
"""Retourne la durée de la session."""
end_time = self.logout_time if self.logout_time else self.last_activity
return end_time - self.login_time
@property
def duration_seconds(self):
"""Retourne la durée de la session en secondes."""
return self.duration.total_seconds()
def mark_inactive(self):
"""Marque la session comme inactive de manière atomique."""
with transaction.atomic():
# Utiliser select_for_update pour éviter les race conditions
session = UserSession.objects.select_for_update().filter(pk=self.pk).first()
if session and session.is_active:
session.is_active = False
if not session.logout_time:
session.logout_time = timezone.now()
session.save(update_fields=['is_active', 'logout_time'])
# Mettre à jour l'instance actuelle
self.is_active = session.is_active
self.logout_time = session.logout_time
class DailyPageViewSummary(models.Model):
"""
Résumé quotidien des statistiques de pages vues.
Ce modèle stocke les agrégats par jour pour conserver des statistiques
d'utilisation même après la purge des données détaillées de PageView.
"""
date = models.DateField(
unique=True,
db_index=True,
verbose_name="Date"
)
total_page_views = models.IntegerField(
default=0,
verbose_name="Total pages vues"
)
unique_users = models.IntegerField(
default=0,
verbose_name="Utilisateurs uniques"
)
unique_sessions = models.IntegerField(
default=0,
verbose_name="Sessions uniques"
)
unique_ips = models.IntegerField(
default=0,
verbose_name="IPs uniques"
)
avg_response_time_ms = models.FloatField(
null=True,
blank=True,
verbose_name="Temps de réponse moyen (ms)"
)
max_response_time_ms = models.IntegerField(
null=True,
blank=True,
verbose_name="Temps de réponse max (ms)"
)
top_paths = models.JSONField(
default=dict,
blank=True,
verbose_name="Top pages visitées",
help_text="Les 20 pages les plus visitées avec leur nombre de vues"
)
top_users = models.JSONField(
default=dict,
blank=True,
verbose_name="Top utilisateurs",
help_text="Les 20 utilisateurs les plus actifs avec leur nombre de vues"
)
method_counts = models.JSONField(
default=dict,
blank=True,
verbose_name="Répartition par méthode HTTP",
help_text="Nombre de requêtes par méthode HTTP (GET, POST, ...)"
)
total_sessions_started = models.IntegerField(
default=0,
verbose_name="Sessions démarrées"
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="Créé le"
)
updated_at = models.DateTimeField(
auto_now=True,
verbose_name="Mis à jour le"
)
class Meta:
verbose_name = "Résumé quotidien"
verbose_name_plural = "Résumés quotidiens"
ordering = ["-date"]
def __str__(self):
return f"{self.date} — {self.total_page_views} vues, {self.unique_users} utilisateurs"