This commit is contained in:
2026-08-31 15:11:52 +03:30
parent 236002a87c
commit 463b812e3e
63 changed files with 3254 additions and 1476 deletions
+183
View File
@@ -0,0 +1,183 @@
from datetime import timedelta
from django.contrib import admin
from django.db.models import Avg, Count, DurationField, ExpressionWrapper, F, Max, Q, Sum
from django.template.response import TemplateResponse
from django.utils import timezone
from unfold.admin import ModelAdmin
from apps.account.models import User
from .models import ApiCallLog, AppSession, ClickEvent, ClientErrorLog, PageView
# مانیتورینگِ کلیِ اپ توسط پنل ادمین: صفحه‌ی changelist مدلِ AppSession
# جایگزینِ یک داشبوردِ تجمیعی می‌شود (نه لیستِ خامِ ردیف‌ها) — برای دیدنِ
# لیستِ خام هر رکورد، لینکِ «نمای خام» بالای همان صفحه با ?raw=1 به همان
# changelist معمولیِ جنگو می‌رود.
_WINDOW_DAYS = 30
@admin.register(AppSession)
class AppSessionAdmin(ModelAdmin):
list_display = ("user", "platform", "started_at", "ended_at", "ip_address")
list_filter = ("platform",)
search_fields = ("user__phone", "ip_address")
date_hierarchy = "started_at"
def changelist_view(self, request, extra_context=None):
if request.GET.get("raw") == "1":
return super().changelist_view(request, extra_context)
return self._dashboard_view(request)
def _dashboard_view(self, request):
context = {
**self.admin_site.each_context(request),
"title": "مانیتورینگ اپ",
"opts": self.model._meta,
**self._build_dashboard_context(),
}
return TemplateResponse(request, "admin/telemetry/dashboard.html", context)
def _build_dashboard_context(self):
now = timezone.now()
cutoff = now - timedelta(days=_WINDOW_DAYS)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
sessions_qs = AppSession.objects.filter(started_at__gte=cutoff)
api_qs = ApiCallLog.objects.filter(created_at__gte=cutoff)
pageviews_qs = PageView.objects.filter(entered_at__gte=cutoff)
errors_qs = ClientErrorLog.objects.filter(created_at__gte=cutoff)
overview = {
"active_users_today": sessions_qs.filter(started_at__gte=today_start)
.values("user")
.distinct()
.count(),
"sessions_today": sessions_qs.filter(started_at__gte=today_start).count(),
"sessions_window": sessions_qs.count(),
"page_views_today": pageviews_qs.filter(entered_at__gte=today_start).count(),
"api_calls_today": api_qs.filter(created_at__gte=today_start).count(),
"errors_today": errors_qs.filter(created_at__gte=today_start).count(),
"avg_response_ms": round(api_qs.aggregate(v=Avg("duration_ms"))["v"] or 0, 1),
}
ended = sessions_qs.exclude(ended_at__isnull=True).annotate(
dur=ExpressionWrapper(F("ended_at") - F("started_at"), output_field=DurationField())
)
avg_duration = ended.aggregate(v=Avg("dur"))["v"]
overview["avg_session_minutes"] = round(avg_duration.total_seconds() / 60, 1) if avg_duration else 0
top_screens = list(
pageviews_qs.values("screen_name")
.annotate(count=Count("id"), avg_duration=Avg("duration_seconds"))
.order_by("-count")[:10]
)
max_screen_count = max((s["count"] for s in top_screens), default=1) or 1
for s in top_screens:
s["bar_percent"] = round(100 * s["count"] / max_screen_count)
s["avg_duration"] = round(s["avg_duration"], 1) if s["avg_duration"] else None
endpoints = list(
api_qs.values("path", "method")
.annotate(
count=Count("id"),
avg_ms=Avg("duration_ms"),
max_ms=Max("duration_ms"),
error_count=Count("id", filter=Q(status_code__gte=400)),
)
.order_by("-count")[:20]
)
for e in endpoints:
e["avg_ms"] = round(e["avg_ms"], 1) if e["avg_ms"] else 0
recent_errors = list(errors_qs.select_related("session__user")[:20])
active_user_ids = sessions_qs.values_list("user_id", flat=True).distinct()
user_rows = []
for user in User.objects.filter(id__in=active_user_ids):
user_sessions = sessions_qs.filter(user=user)
ended_user = user_sessions.exclude(ended_at__isnull=True).annotate(
dur=ExpressionWrapper(F("ended_at") - F("started_at"), output_field=DurationField())
)
total_dur = ended_user.aggregate(v=Sum("dur"))["v"]
ips = list(
user_sessions.exclude(ip_address__isnull=True)
.values_list("ip_address", flat=True)
.distinct()
)
top_screen = (
pageviews_qs.filter(session__user=user)
.values("screen_name")
.annotate(c=Count("id"))
.order_by("-c")
.first()
)
last_session = user_sessions.order_by("-started_at").first()
user_rows.append(
{
"user": user,
"session_count": user_sessions.count(),
"total_minutes": round(total_dur.total_seconds() / 60, 1) if total_dur else 0,
"page_view_count": pageviews_qs.filter(session__user=user).count(),
"api_call_count": api_qs.filter(user=user).count(),
"error_count": errors_qs.filter(session__user=user).count(),
"ips": ips,
"referral_count": User.objects.filter(
referred_by=user, referral_bonus_granted=True
).count(),
"top_screen": top_screen["screen_name"] if top_screen else "—",
"last_seen": last_session.started_at if last_session else None,
}
)
user_rows.sort(key=lambda r: r["total_minutes"], reverse=True)
overview_cards = [
("کاربران فعال امروز", overview["active_users_today"]),
("جلسه‌های امروز", overview["sessions_today"]),
(f"جلسه‌ها ({_WINDOW_DAYS} روز اخیر)", overview["sessions_window"]),
("میانگین طول جلسه (دقیقه)", overview["avg_session_minutes"]),
("صفحه‌بازدید امروز", overview["page_views_today"]),
("فراخوانی API امروز", overview["api_calls_today"]),
("میانگین زمان پاسخ (ms)", overview["avg_response_ms"]),
("خطای امروز", overview["errors_today"]),
]
return {
"overview": overview,
"overview_cards": overview_cards,
"top_screens": top_screens,
"endpoints": endpoints,
"recent_errors": recent_errors,
"user_rows": user_rows[:50],
"window_days": _WINDOW_DAYS,
}
@admin.register(ApiCallLog)
class ApiCallLogAdmin(ModelAdmin):
list_display = ("method", "path", "status_code", "duration_ms", "ip_address", "user", "created_at")
list_filter = ("method", "status_code")
search_fields = ("path", "user__phone", "ip_address")
date_hierarchy = "created_at"
@admin.register(PageView)
class PageViewAdmin(ModelAdmin):
list_display = ("screen_name", "session", "entered_at", "duration_seconds")
list_filter = ("screen_name",)
search_fields = ("screen_name",)
@admin.register(ClickEvent)
class ClickEventAdmin(ModelAdmin):
list_display = ("screen_name", "element", "session", "created_at")
list_filter = ("screen_name",)
search_fields = ("screen_name", "element")
@admin.register(ClientErrorLog)
class ClientErrorLogAdmin(ModelAdmin):
list_display = ("screen_name", "message", "session", "created_at")
list_filter = ("screen_name",)
search_fields = ("screen_name", "message")