from django.conf import settings from django.db import models from django.utils.translation import gettext_lazy as _ # مانیتورینگ اپ: هر فراخوانیِ API خودکار (سمت سرور، بدون نیاز به تغییر اپ # فلاتر) لاگ می‌شود؛ رویدادهای سمت کلاینت (صفحه‌بازدید/کلیک/خطا) از یک # اندپوینتِ دسته‌ای ثبت می‌شوند که اپ فلاتر دوره‌ای می‌فرستد. class ApiCallLog(models.Model): """هر درخواستِ API — خودکار توسط ApiCallLoggingMiddleware ثبت می‌شود.""" user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="api_calls", verbose_name=_("کاربر"), ) method = models.CharField(max_length=10, verbose_name=_("متد")) path = models.CharField(max_length=255, verbose_name=_("مسیر")) status_code = models.PositiveSmallIntegerField(verbose_name=_("کد وضعیت")) duration_ms = models.FloatField(verbose_name=_("زمان پاسخ (میلی‌ثانیه)")) ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name=_("آی‌پی")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("زمان")) class Meta: verbose_name = _("لاگ فراخوانی API") verbose_name_plural = _("لاگ‌های فراخوانی API") ordering = ["-created_at"] indexes = [ models.Index(fields=["user", "created_at"]), models.Index(fields=["path", "created_at"]), ] def __str__(self): return f"{self.method} {self.path} ({self.status_code})" class AppSession(models.Model): """یک دوره‌ی «باز بودنِ اپ» — از سمت کلاینت (اپ فلاتر) با یک UUID شروع می‌شود، با رویدادِ session_end یا انقضای بی‌فعالیت بسته می‌شود.""" id = models.CharField(max_length=36, primary_key=True) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="app_sessions", verbose_name=_("کاربر") ) platform = models.CharField(max_length=30, blank=True, verbose_name=_("پلتفرم")) app_version = models.CharField(max_length=30, blank=True, verbose_name=_("نسخه‌ی اپ")) ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name=_("آی‌پی")) started_at = models.DateTimeField(verbose_name=_("شروع")) ended_at = models.DateTimeField(null=True, blank=True, verbose_name=_("پایان")) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = _("جلسه‌ی استفاده از اپ") verbose_name_plural = _("جلسه‌های استفاده از اپ") ordering = ["-started_at"] indexes = [models.Index(fields=["user", "started_at"])] def __str__(self): return f"{self.user} — {self.started_at:%Y-%m-%d %H:%M}" @property def duration_seconds(self) -> float | None: if not self.ended_at: return None return (self.ended_at - self.started_at).total_seconds() class PageView(models.Model): """یک بازدید از یک صفحه/مسیر توی اپ — با ورود و مدت‌زمانِ ماندن.""" session = models.ForeignKey(AppSession, on_delete=models.CASCADE, related_name="page_views") screen_name = models.CharField(max_length=150, verbose_name=_("نام صفحه")) entered_at = models.DateTimeField(verbose_name=_("زمان ورود")) duration_seconds = models.FloatField(null=True, blank=True, verbose_name=_("مدت ماندن (ثانیه)")) class Meta: verbose_name = _("بازدید صفحه") verbose_name_plural = _("بازدیدهای صفحه") ordering = ["entered_at"] indexes = [models.Index(fields=["screen_name"])] def __str__(self): return self.screen_name class ClickEvent(models.Model): """یک لمس/کلیک — مختصات نسبی (۰ تا ۱) نسبت به عرض/ارتفاع صفحه، برای این‌که مستقل از سایز گوشی قابل‌مقایسه باشد.""" session = models.ForeignKey(AppSession, on_delete=models.CASCADE, related_name="clicks") screen_name = models.CharField(max_length=150, blank=True, verbose_name=_("نام صفحه")) element = models.CharField(max_length=150, blank=True, verbose_name=_("برچسبِ عنصر (اختیاری)")) x = models.FloatField(null=True, blank=True) y = models.FloatField(null=True, blank=True) created_at = models.DateTimeField(verbose_name=_("زمان")) class Meta: verbose_name = _("رویداد کلیک") verbose_name_plural = _("رویدادهای کلیک") ordering = ["-created_at"] indexes = [models.Index(fields=["screen_name"])] class ClientErrorLog(models.Model): """خطای سمت کلاینت (uncaught exception توی اپ فلاتر) — «کجا مشکل خورده».""" session = models.ForeignKey( AppSession, on_delete=models.SET_NULL, null=True, blank=True, related_name="errors" ) screen_name = models.CharField(max_length=150, blank=True, verbose_name=_("نام صفحه")) message = models.TextField(verbose_name=_("پیام خطا")) stack_trace = models.TextField(blank=True, verbose_name=_("Stack trace")) created_at = models.DateTimeField(verbose_name=_("زمان")) class Meta: verbose_name = _("خطای سمت کاربر") verbose_name_plural = _("خطاهای سمت کاربر") ordering = ["-created_at"]